From 220f991fae31c53c9901435ad72a8b373893f573 Mon Sep 17 00:00:00 2001 From: Your GitHub Name Date: Thu, 16 Jul 2026 12:34:03 -0700 Subject: [PATCH] fix: prefer topic channel for album artist --- app/tests/test_ytdl_utils.py | 34 ++++++++++++++++++++++++++++++++++ app/ytdl.py | 16 ++++++++++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/app/tests/test_ytdl_utils.py b/app/tests/test_ytdl_utils.py index 73b40fe..72c716e 100644 --- a/app/tests/test_ytdl_utils.py +++ b/app/tests/test_ytdl_utils.py @@ -87,6 +87,40 @@ class AlbumArtistPostProcessorTests(unittest.TestCase): self.assertEqual(result['album_artist'], 'ScHoolboy Q') + def test_uses_topic_channel_artist_for_joint_album(self): + info = { + 'album': 'Watch the Throne', + 'artists': ['JAY-Z', 'Kanye West'], + 'channel': 'JAY-Z & Kanye West - Topic', + } + + _, result = self.postprocessor.run(info) + + self.assertEqual(result['album_artist'], 'JAY-Z & Kanye West') + + def test_uses_topic_uploader_and_strips_suffix_for_compilation(self): + info = { + 'album': 'Compilation', + 'artist': 'Track Artist', + 'channel': 'Regular Channel', + 'uploader': 'Various Artists - Topic', + } + + _, result = self.postprocessor.run(info) + + self.assertEqual(result['album_artist'], 'Various Artists') + + def test_regular_channel_falls_back_to_main_artist(self): + info = { + 'album': 'Album', + 'artist': 'Track Artist', + 'channel': 'Label Channel', + } + + _, result = self.postprocessor.run(info) + + self.assertEqual(result['album_artist'], 'Track Artist') + def test_preserves_explicit_various_artists(self): info = { 'album': 'Revenge of the Dreamers III', diff --git a/app/ytdl.py b/app/ytdl.py index daeb594..5c13b51 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -54,7 +54,9 @@ _LIVE_PROBE_MAX_FAILURES = 5 class _AlbumArtistPostProcessor(PostProcessor): - """Fill missing album-artist metadata from yt-dlp's main track artist.""" + """Fill missing album-artist metadata from yt-dlp's album-level signals.""" + + _TOPIC_SUFFIX = ' - Topic' @staticmethod def _has_value(value: Any) -> bool: @@ -76,13 +78,23 @@ class _AlbumArtistPostProcessor(PostProcessor): return candidate.split(' ยท ', 1)[0].strip() return None + @classmethod + def _topic_artist(cls, info) -> Optional[str]: + for field in ('channel', 'uploader'): + value = info.get(field) + if not isinstance(value, str) or not value.endswith(cls._TOPIC_SUFFIX): + continue + if artist := value[:-len(cls._TOPIC_SUFFIX)].strip(): + return artist + return None + def run(self, info): if not self._has_value(info.get('album')): return [], info if self._has_value(info.get('album_artist')) or self._has_value(info.get('album_artists')): return [], info - if artist := self._main_artist(info): + if artist := self._topic_artist(info) or self._main_artist(info): info['album_artist'] = artist return [], info