Preserve album bundle track numbers

Keep album-bundle staging from replacing known per-track album numbers with the filename parser's default when staged files do not expose a real track number. Carry staging tag numbers through the cache, fall back to task metadata for private release staging, and cap hybrid album batches to one worker when Soulseek is first in the source order.
This commit is contained in:
Broque Thomas 2026-05-24 13:59:44 -07:00
parent a3ba79a9ce
commit 0bea332aed
3 changed files with 89 additions and 7 deletions

View file

@ -38,8 +38,6 @@ import re
from dataclasses import dataclass
from typing import Any, Callable
from core.imports.filename import extract_track_number_from_filename
# `shutil` and `SequenceMatcher` are imported inline inside try_staging_match()
# to keep the lift byte-identical with the original web_server.py function body.
@ -61,6 +59,25 @@ def _coerce_positive_int(value: Any, default: int = 0) -> int:
return coerced if coerced > 0 else default
def _extract_explicit_track_number(filename: str) -> int:
"""Extract a track number only when the filename visibly carries one."""
basename = os.path.splitext(os.path.basename(str(filename or '')))[0].strip()
if not basename:
return 0
match = re.match(r"^\d[\-\.](\d{1,2})\s*[\-\.]\s*", basename)
if match:
num = int(match.group(1))
return num if 1 <= num <= 99 else 0
match = re.match(r"^\(?(\d{1,3})\)?\s*[\-\.)\]]\s*", basename)
if match:
num = int(match.group(1))
return num if 1 <= num <= 999 else 0
return 0
def _staging_title_variants(title: Any, normalize: Callable[[str], str]) -> list[str]:
"""Return conservative title variants for release-file matching.
@ -320,12 +337,24 @@ def try_staging_match(task_id, batch_id, track, deps: StagingDeps):
file_track_number = (
_coerce_positive_int(best_match.get('track_number'), 0) or
extract_track_number_from_filename(best_match.get('full_path', ''))
_extract_explicit_track_number(best_match.get('full_path', ''))
)
file_disc_number = _coerce_positive_int(best_match.get('disc_number'), 1)
file_disc_number = _coerce_positive_int(best_match.get('disc_number'), 0)
if _private_album_bundle_staging:
track_number = file_track_number
disc_number = file_disc_number
track_number = (
file_track_number or
_coerce_positive_int(track_info.get('track_number'), 0) or
_coerce_positive_int(track_info.get('trackNumber'), 0) or
_coerce_positive_int(getattr(track, 'track_number', 0), 0) or
1
)
disc_number = (
file_disc_number or
_coerce_positive_int(track_info.get('disc_number'), 0) or
_coerce_positive_int(track_info.get('discNumber'), 0) or
_coerce_positive_int(getattr(track, 'disc_number', 0), 0) or
1
)
else:
track_number = (
_coerce_positive_int(track_info.get('track_number'), 0) or
@ -337,7 +366,8 @@ def try_staging_match(task_id, batch_id, track, deps: StagingDeps):
_coerce_positive_int(track_info.get('disc_number'), 0) or
_coerce_positive_int(track_info.get('discNumber'), 0) or
_coerce_positive_int(getattr(track, 'disc_number', 0), 0) or
file_disc_number
file_disc_number or
1
)
track_info['track_number'] = track_number
track_info['disc_number'] = disc_number

View file

@ -357,6 +357,49 @@ def test_private_album_bundle_staging_overrides_default_track_info_number(tmp_pa
assert ctx['original_search_result']['filename'] == str(src_file)
def test_private_album_bundle_staging_keeps_task_number_when_file_has_no_number(tmp_path):
"""Private release staging must not turn every unnumbered release file into track 1."""
src_file = tmp_path / 'staging' / 'Katy Perry - Firework.flac'
src_file.parent.mkdir()
src_file.touch()
def get_batch_field(_batch_id, field):
if field == 'album_bundle_source':
return 'soulseek'
if field == 'album_bundle_private_staging':
return True
return None
deps = _build_deps(
transfer_path=str(tmp_path / 'transfer'),
staging_files=[
{
'full_path': str(src_file),
'title': 'Firework',
'artist': 'Katy Perry',
},
],
get_batch_field=get_batch_field,
)
_seed_task('t6d', track_info={
'_is_explicit_album_download': True,
'_explicit_album_context': {'id': 'alb', 'name': 'Teenage Dream: The Complete Confection'},
'_explicit_artist_context': {'id': 'art', 'name': 'Katy Perry'},
'track_number': 4,
'disc_number': 1,
})
ds.try_staging_match(
't6d', 'b1',
_Track(name='Firework', artists=['Katy Perry']),
deps,
)
ctx = matched_downloads_context['staging_t6d']
assert ctx['track_info']['track_number'] == 4
assert ctx['original_search_result']['track_number'] == 4
def test_staging_title_match_accepts_feature_suffix_from_release_file(tmp_path):
"""Album releases can include featured artists in filenames."""
src_file = tmp_path / 'staging' / '05-kendrick_lamar-money_trees_(feat._jay_rock).flac'

View file

@ -1068,6 +1068,13 @@ def _get_batch_max_concurrent(is_album=False, source=None):
mode = config_manager.get('download_source.mode', 'soulseek')
if mode == 'soulseek':
return 1
if mode == 'hybrid':
hybrid_order = config_manager.get('download_source.hybrid_order', []) or []
if isinstance(hybrid_order, str):
hybrid_order = [hybrid_order]
first_source = next((str(s).strip().lower() for s in hybrid_order if str(s).strip()), '')
if first_source == 'soulseek':
return 1
return _get_max_concurrent()
# --- Session Download Statistics ---
@ -16531,6 +16538,8 @@ def _get_staging_file_cache(batch_id):
'title': meta['title'] or '',
'artist': meta['albumartist'] or meta['artist'] or '',
'album': meta['album'] or '',
'track_number': meta.get('track_number'),
'disc_number': meta.get('disc_number'),
'extension': ext,
})