Multi-disc albums: never write a disc-less track (floor disc to >=1)
Sokhi: some tracks in a multi-disc album showed up with a null disc in Jellyfin and floated ungrouped above the disc sections (tracks 3/9/15). Mechanism: the tag writer only wrote the disc tag when disc_number was truthy, and enrichment CLEARS all tags before rewriting — so a track whose disc came back 0 / None / '' lost its disc entirely. Those falsy values slipped through because source.py defaulted with 'is not None' (a literal 0 passed) and context.py's or-chain can yield None; this happens especially when a track resolves to a different edition than its siblings. Fix: normalize_disc_number() floors any value to >=1, and enrichment now writes the disc tag UNCONDITIONALLY (like the track number) so a track is never disc-less. source.py uses the same floor so the metadata dict (and the 'Disc N' folder org) stays consistent. Valid multi-disc values are preserved untouched. Tests: normalize floors 0/None/''/negatives/non-numeric -> 1, preserves 1..4 and tolerates '2.0'. 1406 enrich/metadata/track-number tests green, ruff clean. NOTE: this fixes the SYMPTOM (never ungrouped). The deeper cause — a track matching a DIFFERENT edition/release than its album siblings (the Persona-box-set mismatch in the sample file; the canonical-version problem) — is separate and still open.
This commit is contained in:
parent
1ad80d77a6
commit
c11a742e58
4 changed files with 68 additions and 7 deletions
|
|
@ -159,3 +159,24 @@ def resolve_track_number(
|
|||
# value the pre-fix resolver would have used. A correctly-named file
|
||||
# with a stale/wrong embedded tag is therefore never regressed.
|
||||
return _coerce_positive(embedded_track_number)
|
||||
|
||||
|
||||
def normalize_disc_number(value) -> int:
|
||||
"""Coerce a disc value to a positive int, defaulting to 1.
|
||||
|
||||
Every track in a multi-disc album MUST carry a disc number, or Jellyfin/Plex
|
||||
leave the disc-less ones floating ungrouped above the disc sections (Sokhi's
|
||||
"tracks 3/9/15 at the top"). Upstream sources can hand back 0, None, '', or a
|
||||
non-numeric string for some tracks — especially when a track resolved to a
|
||||
different edition than its siblings — and the tag-writer only wrote the disc
|
||||
tag when it was truthy, so those tracks lost it entirely on the clear-then-
|
||||
rewrite. Flooring to >=1 here means a track is never written disc-less.
|
||||
"""
|
||||
try:
|
||||
n = int(value) # int, float, or clean int-string
|
||||
except (TypeError, ValueError):
|
||||
try:
|
||||
n = int(float(str(value).strip())) # tolerate "2.0"
|
||||
except (TypeError, ValueError):
|
||||
return 1
|
||||
return n if n >= 1 else 1
|
||||
|
|
|
|||
|
|
@ -131,6 +131,14 @@ def enhance_file_metadata(file_path: str, context: dict, artist: dict, album_inf
|
|||
track_num_str = format_track_number_tag(
|
||||
metadata.get('track_number'), metadata.get('total_tracks')
|
||||
)
|
||||
# Disc number is written UNCONDITIONALLY (floored to >=1), like the
|
||||
# track number above. The old code only wrote it when truthy, so a
|
||||
# track whose disc came back 0/None/'' (e.g. matched to a different
|
||||
# edition) lost its disc tag on the clear-then-rewrite and floated
|
||||
# ungrouped above the disc sections in Jellyfin/Plex (Sokhi).
|
||||
from core.imports.track_number import normalize_disc_number
|
||||
_disc_num = normalize_disc_number(metadata.get('disc_number'))
|
||||
disc_num_str = str(_disc_num)
|
||||
write_multi = cfg.get("metadata_enhancement.tags.write_multi_artist", False)
|
||||
artists_list = metadata.get("_artists_list", [])
|
||||
|
||||
|
|
@ -162,8 +170,7 @@ def enhance_file_metadata(file_path: str, context: dict, artist: dict, album_inf
|
|||
if metadata.get("genre"):
|
||||
audio_file.tags.add(symbols.TCON(encoding=3, text=[metadata["genre"]]))
|
||||
audio_file.tags.add(symbols.TRCK(encoding=3, text=[track_num_str]))
|
||||
if metadata.get("disc_number"):
|
||||
audio_file.tags.add(symbols.TPOS(encoding=3, text=[str(metadata["disc_number"])]))
|
||||
audio_file.tags.add(symbols.TPOS(encoding=3, text=[disc_num_str]))
|
||||
elif is_vorbis_like(audio_file, symbols):
|
||||
if metadata.get("title"):
|
||||
audio_file["title"] = [metadata["title"]]
|
||||
|
|
@ -180,8 +187,7 @@ def enhance_file_metadata(file_path: str, context: dict, artist: dict, album_inf
|
|||
if metadata.get("genre"):
|
||||
audio_file["genre"] = [metadata["genre"]]
|
||||
audio_file["tracknumber"] = [track_num_str]
|
||||
if metadata.get("disc_number"):
|
||||
audio_file["discnumber"] = [str(metadata["disc_number"])]
|
||||
audio_file["discnumber"] = [disc_num_str]
|
||||
elif isinstance(audio_file, symbols.MP4):
|
||||
if metadata.get("title"):
|
||||
audio_file["\xa9nam"] = [metadata["title"]]
|
||||
|
|
@ -198,8 +204,7 @@ def enhance_file_metadata(file_path: str, context: dict, artist: dict, album_inf
|
|||
audio_file["trkn"] = [format_track_number_tuple(
|
||||
metadata.get("track_number"), metadata.get("total_tracks")
|
||||
)]
|
||||
if metadata.get("disc_number"):
|
||||
audio_file["disk"] = [(metadata["disc_number"], 0)]
|
||||
audio_file["disk"] = [(_disc_num, 0)]
|
||||
|
||||
embed_source_ids(audio_file, metadata, context, runtime=runtime)
|
||||
|
||||
|
|
|
|||
|
|
@ -1092,7 +1092,11 @@ def extract_source_metadata(context: dict, artist: dict, album_info: dict) -> di
|
|||
disc_num = original_search.get("disc_number")
|
||||
if disc_num is None and album_info:
|
||||
disc_num = album_info.get("disc_number")
|
||||
metadata["disc_number"] = disc_num if disc_num is not None else 1
|
||||
# Floor to >=1: a 0 / '' / non-numeric disc must not slip through (the old
|
||||
# `is not None` let a literal 0 past, which then read as disc-less downstream
|
||||
# and ungrouped the track in Jellyfin/Plex). Also feeds the "Disc N" folder org.
|
||||
from core.imports.track_number import normalize_disc_number
|
||||
metadata["disc_number"] = normalize_disc_number(disc_num)
|
||||
|
||||
if album_ctx and album_ctx.get("release_date"):
|
||||
release_date = _normalize_release_date_tag(album_ctx.get("release_date"))
|
||||
|
|
|
|||
31
tests/imports/test_normalize_disc_number.py
Normal file
31
tests/imports/test_normalize_disc_number.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"""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
|
||||
Loading…
Reference in a new issue