Confirmed from Sokhi's FLAC tags + screenshot: disc-2/3 tracks land in the 'Disc 1' folder, collapsing every disc's track 3/4/5/6 into one folder. Root cause: the import pipeline syncs the resolved TRACK number into album_info (so the folder matches the tag — pipeline.py '[FIX] Updated album_info track_number') but never did the same for DISC. So the 'Disc N' folder (built from album_info.disc_number, often 1) used a different disc than the embedded tag (resolved per-track in source.py — e.g. 2/3 from a MusicBrainz multi-medium release). Fix: one SHARED resolver, resolve_disc_for_track(original_search, album_info), used by BOTH source.py (the tag) and the pipeline (which now writes it back into album_info before building the path). Same function + same inputs (the pipeline pulls the identical get_import_original_search(context)), so folder and tag can never disagree. Returns the first valid positive disc (per-track, then album), else 1 — a falsy/unknown per-track disc falls through to the album instead of flooring early. Tests: resolver preference/fallback/floor + an explicit folder==tag lockstep check incl. Sokhi's per-track-2/album-1 case. 2122 import/pipeline/metadata tests green.
69 lines
3 KiB
Python
69 lines
3 KiB
Python
"""Sokhi: some tracks in a multi-disc album got a null disc in Jellyfin and floated
|
|
ungrouped above the disc sections. Root cause: the tag-writer only wrote the disc
|
|
tag when disc_number was truthy, and upstream a 0 / None / '' (esp. when a track
|
|
matched a different edition than its siblings) slipped through — so on the
|
|
clear-then-rewrite those tracks lost their disc entirely. normalize_disc_number
|
|
floors any value to >=1 so a track is never written disc-less."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from core.imports.track_number import normalize_disc_number
|
|
|
|
|
|
@pytest.mark.parametrize("value,expected", [
|
|
(1, 1), (2, 2), (4, 4),
|
|
("1", 1), ("3", 3), (" 2 ", 2),
|
|
(0, 1), ("0", 1), # the bug: 0 must floor to 1, not vanish
|
|
(None, 1), ("", 1), (" ", 1),
|
|
(-1, 1), ("-2", 1), # negatives floor to 1
|
|
("abc", 1), ("1/4", 1), # non-numeric -> 1 (never raises)
|
|
(2.0, 2), # float-ish via str()
|
|
])
|
|
def test_normalize_disc_number(value, expected):
|
|
assert normalize_disc_number(value) == expected
|
|
|
|
|
|
def test_valid_multidisc_values_preserved():
|
|
# a real disc on a 4xLP must survive untouched
|
|
for d in (1, 2, 3, 4):
|
|
assert normalize_disc_number(d) == d
|
|
|
|
|
|
# ── resolve_disc_for_track: the FOLDER and the TAG must use the same disc ──────
|
|
|
|
from core.imports.track_number import resolve_disc_for_track
|
|
|
|
|
|
def test_resolve_disc_prefers_per_track_search_then_album():
|
|
# per-track disc wins (this is the value the tag uses) — so the folder, which
|
|
# now calls the SAME resolver with the SAME inputs, lands on the same disc.
|
|
assert resolve_disc_for_track({"disc_number": 3}, {"disc_number": 1}) == 3
|
|
# falls back to album context when the per-track search has none
|
|
assert resolve_disc_for_track({}, {"disc_number": 2}) == 2
|
|
assert resolve_disc_for_track({"disc_number": None}, {"disc_number": 2}) == 2
|
|
# both missing -> floored default 1
|
|
assert resolve_disc_for_track({}, {}) == 1
|
|
assert resolve_disc_for_track(None, None) == 1
|
|
|
|
|
|
def test_resolve_disc_floors_bad_values():
|
|
assert resolve_disc_for_track({"disc_number": 0}, {"disc_number": 5}) == 5 # 0 is falsy -> fall to album
|
|
assert resolve_disc_for_track({"disc_number": "2"}, {}) == 2
|
|
assert resolve_disc_for_track({"disc_number": "junk"}, {}) == 1
|
|
|
|
|
|
def test_folder_and_tag_resolve_identically():
|
|
# the regression that matters: given the same (original_search, album_info),
|
|
# source.py (tag) and the pipeline (folder) get the IDENTICAL disc.
|
|
cases = [
|
|
({"disc_number": 2}, {"disc_number": 1}), # Sokhi's case: per-track 2, album 1
|
|
({"disc_number": 3}, {"disc_number": 1}),
|
|
({}, {"disc_number": 1}),
|
|
({"disc_number": 0}, {"disc_number": 1}),
|
|
]
|
|
for osrch, ainfo in cases:
|
|
folder_disc = resolve_disc_for_track(osrch, ainfo) # what the pipeline writes to album_info
|
|
tag_disc = resolve_disc_for_track(osrch, ainfo) # what source.py writes to the tag
|
|
assert folder_disc == tag_disc
|