From 0bea332aed379b03f64cec8b8f8eed683cfc3211 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Sun, 24 May 2026 13:59:44 -0700 Subject: [PATCH] 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. --- core/downloads/staging.py | 44 +++++++++++++++++++---- tests/downloads/test_downloads_staging.py | 43 ++++++++++++++++++++++ web_server.py | 9 +++++ 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/core/downloads/staging.py b/core/downloads/staging.py index f7ca8906..d1d4e3c3 100644 --- a/core/downloads/staging.py +++ b/core/downloads/staging.py @@ -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 diff --git a/tests/downloads/test_downloads_staging.py b/tests/downloads/test_downloads_staging.py index 70e86b6b..097c3323 100644 --- a/tests/downloads/test_downloads_staging.py +++ b/tests/downloads/test_downloads_staging.py @@ -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' diff --git a/web_server.py b/web_server.py index 993cda4a..de2344c9 100644 --- a/web_server.py +++ b/web_server.py @@ -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, })