import unittest from types import SimpleNamespace import inspect class FakeResponse: def __init__(self, payload): self.payload = payload def raise_for_status(self): return None def json(self): return self.payload class DeferredSongInfoTests(unittest.TestCase): def test_deferred_module_avoids_py39_str_prefix_suffix_methods(self): import musicdl.catalogsync.deferred as deferred source = inspect.getsource(deferred) self.assertNotIn(".removesuffix(", source) self.assertNotIn(".removeprefix(", source) def test_extract_playlist_id_from_html_path(self): from musicdl.catalogsync.deferred import _extract_playlist_id_from_url playlist_id = _extract_playlist_id_from_url("https://www.kuwo.cn/playlist_detail/3671258656.html") self.assertEqual("3671258656", playlist_id) def test_build_kuwo_raw_track_song_infos_strips_music_prefix(self): from musicdl.catalogsync.deferred import build_kuwo_raw_track_song_infos client = SimpleNamespace( source="KuwoMusicClient", _constructuniqueworkdir=lambda keyword: f"/tmp/{keyword}", _removeduplicates=lambda song_infos: list(song_infos), ) song_infos = build_kuwo_raw_track_song_infos( client, raw_tracks=[ { "MUSICRID": "MUSIC_123456", "SONGNAME": "Song A", "ARTIST": "Singer A", "ALBUM": "Album A", "DURATION": 180, } ], playlist_name="Kuwo Playlist", ) self.assertEqual(1, len(song_infos)) self.assertEqual("123456", song_infos[0].identifier) def test_build_deferred_song_info_marks_snapshot_as_deferred(self): from musicdl.catalogsync.deferred import build_deferred_song_info song_info = build_deferred_song_info( source="NeteaseMusicClient", raw_search_result={"id": 101, "name": "Song A"}, identifier="101", song_name="Song A", singers="Singer A", album="Album A", duration_s=215, cover_url="https://example.com/a.jpg", ext="flac", ) self.assertEqual("NeteaseMusicClient", song_info.source) self.assertEqual("101", song_info.identifier) self.assertEqual("Song A", song_info.song_name) self.assertEqual("Singer A", song_info.singers) self.assertEqual("Album A", song_info.album) self.assertEqual("flac", song_info.ext) self.assertFalse(song_info.with_valid_download_url) self.assertTrue(song_info.raw_data["deferred_search"]) self.assertEqual(101, song_info.raw_data["search"]["id"]) def test_build_netease_playlist_song_infos_fetches_missing_track_details(self): from musicdl.catalogsync.deferred import build_netease_playlist_song_infos class FakeClient: source = "NeteaseMusicClient" def __init__(self): self.calls = [] def post(self, url, data=None, **kwargs): self.calls.append((url, data)) if url.endswith("/api/v6/playlist/detail"): return FakeResponse( { "playlist": { "name": "Test Playlist", "trackIds": [{"id": 101}, {"id": 102}], "tracks": [ { "id": 101, "name": "Song A", "dt": 215000, "ar": [{"name": "Singer A"}], "al": {"name": "Album A", "picUrl": "https://example.com/a.jpg"}, "sq": {"size": 1}, } ], } } ) if url.endswith("/api/v3/song/detail"): return FakeResponse( { "songs": [ { "id": 102, "name": "Song B", "dt": 186000, "ar": [{"name": "Singer B"}], "al": {"name": "Album B", "picUrl": "https://example.com/b.jpg"}, "h": {"size": 1}, } ] } ) raise AssertionError(f"Unexpected URL: {url}") def _constructuniqueworkdir(self, keyword): return f"/tmp/{keyword}" def _removeduplicates(self, song_infos): return list(song_infos) client = FakeClient() song_infos = build_netease_playlist_song_infos(client, "https://music.163.com/#/playlist?id=999") self.assertEqual(2, len(song_infos)) self.assertEqual(["101", "102"], [song_info.identifier for song_info in song_infos]) self.assertEqual(["Song A", "Song B"], [song_info.song_name for song_info in song_infos]) self.assertTrue(all(song_info.raw_data["deferred_search"] for song_info in song_infos)) self.assertEqual("flac", song_infos[0].ext) self.assertEqual("mp3", song_infos[1].ext) self.assertTrue(any(url.endswith("/api/v3/song/detail") for url, _ in client.calls)) def test_build_qq_raw_track_song_infos_keeps_tracks_without_direct_download_urls(self): from musicdl.catalogsync.deferred import build_qq_raw_track_song_infos client = SimpleNamespace( source="QQMusicClient", _constructuniqueworkdir=lambda keyword: f"/tmp/{keyword}", _removeduplicates=lambda song_infos: list(song_infos), ) song_infos = build_qq_raw_track_song_infos( client, raw_tracks=[ { "songmid": "mid-a", "songname": "Song A", "interval": 210, "singer": [{"name": "Singer A"}], "albumname": "Album A", "albummid": "album-a", "sizeflac": 1024, }, { "songmid": "mid-b", "songname": "Song B", "interval": 180, "singer": [{"name": "Singer B"}], "albumname": "Album B", "albummid": "album-b", "size320": 512, }, ], playlist_name="QQ Playlist", ) self.assertEqual(2, len(song_infos)) self.assertEqual(["mid-a", "mid-b"], [song_info.identifier for song_info in song_infos]) self.assertEqual(["Song A", "Song B"], [song_info.song_name for song_info in song_infos]) self.assertEqual(["flac", "mp3"], [song_info.ext for song_info in song_infos]) self.assertTrue(all(song_info.raw_data["deferred_search"] for song_info in song_infos))