This commit is contained in:
IamGroot60 2026-05-29 03:06:55 -04:00 committed by GitHub
commit 34a113142f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 183 additions and 10 deletions

View file

@ -21,6 +21,7 @@ folder scan.
from __future__ import annotations
import re
import shutil
import time
import uuid
@ -92,29 +93,114 @@ def quality_score(title: str, quality_guess) -> int:
return _QUALITY_SCORE.get(quality_guess(title) or '', 0)
def pick_best_album_release(candidates, quality_guess) -> Optional[object]:
# Words that decorate an album title in a release name but aren't part of
# the album's identity — stripped before computing title relevance so e.g.
# 'Heroes (2017 Remaster)' and a 'David Bowie - Heroes - ... 2017' release
# still match on the core token 'heroes'.
_ALBUM_TITLE_NOISE = {
'remaster', 'remastered', 'remasters', 'edition', 'deluxe', 'expanded',
'anniversary', 'special', 'platinum', 'collectors', 'collector',
'bonus', 'version', 'mono', 'stereo', 'reissue', 'the',
}
# Minimum fraction of the album's core title tokens that must appear in a
# release title for it to be considered the same album. Below this we refuse
# the candidate — downloading a different (often more-popular) album is far
# worse than failing the bundle and falling back to per-track search.
_ALBUM_TITLE_RELEVANCE_FLOOR = 0.55
def _normalize_album_text(text: Any) -> str:
lowered = str(text or '').lower()
lowered = re.sub(r"['`]", '', lowered) # drop apostrophes/quotes
lowered = re.sub(r'[^a-z0-9]+', ' ', lowered) # everything else → space
return re.sub(r'\s+', ' ', lowered).strip()
def _album_core_tokens(album_name: Any) -> list:
"""Significant title tokens — digits (years) and edition/remaster noise
removed. Falls back to the raw tokens if stripping leaves nothing."""
tokens = _normalize_album_text(album_name).split()
core = [t for t in tokens if not t.isdigit() and t not in _ALBUM_TITLE_NOISE]
return core or tokens
def album_title_relevance(release_title: Any, album_name: Any) -> float:
"""How well a release title matches the requested album (0.01.0).
Token-coverage based: the fraction of the album's core tokens present in
the release title, with a bonus when the full core phrase appears as a
substring. Robust to the codec/year/group noise that pads release names
('Artist-Album-24-192-WEB-FLAC-REMASTERED-2017-GROUP')."""
core = _album_core_tokens(album_name)
if not core:
return 0.0
release_norm = _normalize_album_text(release_title)
release_tokens = set(release_norm.split())
present = sum(1 for token in core if token in release_tokens)
coverage = present / len(core)
phrase = ' '.join(core)
if phrase and phrase in release_norm:
coverage = max(coverage, 0.9)
return coverage
def pick_best_album_release(
candidates, quality_guess, album_name: str = '', artist_name: str = '',
) -> Optional[object]:
"""Pick the single best torrent / NZB for an album-bundle download.
Heuristic, in priority order:
1. Reasonable album-ish size (40 MB 3 GB) drops single-track
1. Title relevance the release must actually be the requested album.
Prowlarr/indexers return broad fuzzy matches (a 'Heroes' search also
returns 'Scary Monsters'), and ranking purely by popularity then
grabs the wrong, more-popular album. When ``album_name`` is supplied
we drop candidates whose title doesn't cover the album's core tokens
(``_ALBUM_TITLE_RELEVANCE_FLOOR``), and refuse rather than download a
mismatch if none qualify. Soulseek is unaffected it uses the
title/artist/coverage-aware album pre-flight instead of this picker.
2. Reasonable album-ish size (40 MB 3 GB) drops single-track
releases that snuck in and quarantines suspicious giants.
2. Higher seeders > lower (dead torrents = dead downloads).
3. Higher seeders > lower (dead torrents = dead downloads).
Usenet releases use ``grabs`` as a popularity proxy when
seeders is None.
3. Higher quality (FLAC > AAC > MP3) inferred from title.
4. Larger size as tiebreaker (often = higher bitrate).
4. Higher quality (FLAC > AAC > MP3) inferred from title.
5. Larger size as tiebreaker (often = higher bitrate).
"""
if not candidates:
return None
sized = [c for c in candidates
pool = list(candidates)
# 1. Title relevance gate (only when we know the target album).
if album_name:
relevant = [
c for c in pool
if album_title_relevance(getattr(c, 'title', '') or '', album_name)
>= _ALBUM_TITLE_RELEVANCE_FLOOR
]
if not relevant:
logger.warning(
"[album_bundle] No candidate title matched album %r (checked %d) "
"— refusing to grab a mismatched release",
album_name, len(pool),
)
return None
pool = relevant
# 2. Size sanity.
sized = [c for c in pool
if ALBUM_PICK_MIN_BYTES <= (c.size or 0) <= ALBUM_PICK_MAX_BYTES]
pool = sized or list(candidates)
pool = sized or pool
if not pool:
return None
def _score(c) -> tuple:
seeders = c.seeders if c.seeders is not None else (c.grabs or 0)
return (seeders, quality_score(c.title or '', quality_guess), c.size or 0)
# Relevance bucket first so a strong title match always beats a
# weakly-matching but more-popular release.
relevance = album_title_relevance(getattr(c, 'title', '') or '', album_name) if album_name else 0.0
return (round(relevance, 2), seeders, quality_score(c.title or '', quality_guess), c.size or 0)
return max(pool, key=_score)
@ -416,6 +502,7 @@ def copy_audio_files_atomically(
__all__ = [
"ALBUM_PICK_MIN_BYTES",
"ALBUM_PICK_MAX_BYTES",
"album_title_relevance",
"DEFAULT_POLL_INTERVAL_SECONDS",
"DEFAULT_POLL_TIMEOUT_SECONDS",
"DEFAULT_TRANSIENT_MISS_THRESHOLD",

View file

@ -488,7 +488,10 @@ class TorrentDownloadPlugin(DownloadSourcePlugin):
result['error'] = f'No torrent results found for "{query}"'
return result
picked = pick_best_album_release(candidates, _guess_quality_from_title)
picked = pick_best_album_release(
candidates, _guess_quality_from_title,
album_name=album_name, artist_name=artist_name,
)
if picked is None:
result['error'] = 'No suitable torrent candidate after filtering'
return result

View file

@ -416,7 +416,10 @@ class UsenetDownloadPlugin(DownloadSourcePlugin):
result['error'] = f'No usenet results found for "{query}"'
return result
picked = pick_best_album_release(candidates, _guess_quality_from_title)
picked = pick_best_album_release(
candidates, _guess_quality_from_title,
album_name=album_name, artist_name=artist_name,
)
if picked is None:
result['error'] = 'No suitable NZB candidate after filtering'
return result

View file

@ -22,6 +22,7 @@ import pytest
from core.download_plugins.album_bundle import (
ALBUM_PICK_MAX_BYTES,
ALBUM_PICK_MIN_BYTES,
album_title_relevance,
DEFAULT_POLL_INTERVAL_SECONDS,
DEFAULT_POLL_TIMEOUT_SECONDS,
atomic_copy_to_staging,
@ -114,6 +115,85 @@ def test_picker_rejects_oversized_box_sets() -> None:
assert pick_best_album_release([sane, box], _flac_quality_guess) is sane
def test_picker_rejects_wrong_album_by_title() -> None:
"""Regression for #730: a 'Heroes' request returned 'Scary Monsters'
because the picker ranked purely by grabs and that release was far more
popular. With the album name supplied, title relevance must gate first
so the actual requested album wins despite ~16x fewer grabs."""
scary = _Release(
title='David Bowie-Scary Monsters And Super Creeps-24-192-WEB-FLAC-REMASTERED-2017-OBZEN',
size=2_000_000_000, seeders=None, grabs=15698,
)
heroes = _Release(
title='David Bowie-Heroes-24-192-WEB-FLAC-REMASTERED-2017-OBZEN',
size=1_000_000_000, seeders=None, grabs=960,
)
picked = pick_best_album_release(
[scary, heroes], _flac_quality_guess,
album_name='"Heroes" (2017 Remaster)', artist_name='David Bowie',
)
assert picked is heroes
def test_picker_returns_none_when_no_candidate_matches_album() -> None:
"""If nothing resembles the requested album, refuse rather than grab a
mismatched (popular) release failing the bundle lets the per-track
flow take over instead of importing the wrong album."""
scary = _Release(
title='David Bowie-Scary Monsters And Super Creeps-FLAC-2017',
size=2_000_000_000, seeders=None, grabs=15698,
)
assert pick_best_album_release(
[scary], _flac_quality_guess, album_name='"Heroes" (2017 Remaster)',
) is None
def test_picker_without_album_name_keeps_legacy_popularity_ranking() -> None:
"""Back-compat: with no album name (older callers), the relevance gate
is skipped and the most-popular album-sized release still wins."""
a = _Release(title='Whatever A [FLAC]', size=200_000_000, seeders=None, grabs=10)
b = _Release(title='Whatever B [FLAC]', size=200_000_000, seeders=None, grabs=999)
assert pick_best_album_release([a, b], _flac_quality_guess) is b
def test_picker_prefers_stronger_title_match_over_grabs() -> None:
"""A multi-token album: a release covering all core tokens beats a
more-popular release that only partially matches."""
exact = _Release(
title='Scary Monsters And Super Creeps [FLAC]',
size=300_000_000, seeders=None, grabs=5,
)
partial = _Release(
title='Scary Monsters (single) [FLAC]',
size=300_000_000, seeders=None, grabs=9000,
)
picked = pick_best_album_release(
[partial, exact], _flac_quality_guess,
album_name='Scary Monsters and Super Creeps',
)
assert picked is exact
def test_album_title_relevance_scoring() -> None:
# Exact / noise-padded release name still matches on the core token.
assert album_title_relevance(
'David Bowie-Heroes-24-192-WEB-FLAC-REMASTERED-2017-OBZEN',
'"Heroes" (2017 Remaster)',
) >= 0.9
# Different album → no shared core tokens.
assert album_title_relevance(
'David Bowie-Scary Monsters And Super Creeps-FLAC-2017',
'"Heroes" (2017 Remaster)',
) == 0.0
# Multi-token album fully covered.
assert album_title_relevance(
'VA - Scary Monsters And Super Creeps (Remastered) FLAC',
'Scary Monsters and Super Creeps',
) >= 0.9
# Empty album name is unscoreable.
assert album_title_relevance('Anything', '') == 0.0
# ---------------------------------------------------------------------------
# quality_score
# ---------------------------------------------------------------------------