diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a3470fc..c7986fc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -26,7 +26,7 @@ v0.11.1 * Fixed issue where pressing next/previous would start playing even if the player was paused. (#131) * Fixed issue where using DBUS to go next/previous ignored when no song was - playing (#185) + playing. (#185) **Under the Hood** diff --git a/sublime/adapters/api_objects.py b/sublime/adapters/api_objects.py index 6b7baf6..772d94e 100644 --- a/sublime/adapters/api_objects.py +++ b/sublime/adapters/api_objects.py @@ -147,11 +147,19 @@ class SearchResult: def __init__(self, query: str = None): self.query = query + self.similiarity_partial = partial( + similarity_ratio, self.query.lower() if self.query else "" + ) self._artists: Dict[str, Artist] = {} self._albums: Dict[str, Album] = {} self._songs: Dict[str, Song] = {} self._playlists: Dict[str, Playlist] = {} + def __repr__(self) -> str: + fields = ("query", "_artists", "_albums", "_songs", "_playlists") + formatted_fields = map(lambda f: f"{f}={getattr(self, f)}", fields) + return f"" + def add_results(self, result_type: str, results: Iterable): """Adds the ``results`` to the ``_result_type`` set.""" if results is None: @@ -177,7 +185,7 @@ class SearchResult: ( ( max( - partial(similarity_ratio, self.query.lower())(t.lower()) + self.similiarity_partial(t.lower()) for t in transform(x) if t is not None ), diff --git a/sublime/adapters/filesystem/adapter.py b/sublime/adapters/filesystem/adapter.py index 473f18c..7ec9fee 100644 --- a/sublime/adapters/filesystem/adapter.py +++ b/sublime/adapters/filesystem/adapter.py @@ -386,7 +386,10 @@ class FilesystemAdapter(CachingAdapter): models.Album, CachingAdapter.CachedDataKey.ALBUMS, ignore_cache_miss=True, - where_clauses=(~(models.Album.id.startswith("invalid:")),), + where_clauses=( + ~(models.Album.id.startswith("invalid:")), + models.Album.artist.is_null(False), + ), ) def get_album(self, album_id: str) -> API.Album: @@ -419,11 +422,14 @@ class FilesystemAdapter(CachingAdapter): search_result.add_results( "songs", self._get_list( - models.Song, CachingAdapter.CachedDataKey.SONG, ignore_cache_miss=True + models.Song, + CachingAdapter.CachedDataKey.SONG, + ignore_cache_miss=True, + where_clauses=(models.Song.artist.is_null(False),), ), ) search_result.add_results( - "playlists", self.get_playlists(ignore_cache_miss=True) + "playlists", self.get_playlists(ignore_cache_miss=True), ) return search_result @@ -786,38 +792,37 @@ class FilesystemAdapter(CachingAdapter): song_data = getattrs( api_song, ["id", "title", "track", "year", "duration", "parent_id"] ) - if not partial: - song_data["genre"] = ( - self._do_ingest_new_data(KEYS.GENRE, None, g) - if (g := api_song.genre) - else None + song_data["genre"] = ( + self._do_ingest_new_data(KEYS.GENRE, None, g) + if (g := api_song.genre) + else None + ) + song_data["artist"] = ( + self._do_ingest_new_data(KEYS.ARTIST, ar.id, ar, partial=True) + if (ar := api_song.artist) + else None + ) + song_data["album"] = ( + self._do_ingest_new_data(KEYS.ALBUM, al.id, al, partial=True) + if (al := api_song.album) + else None + ) + song_data["_cover_art"] = ( + self._do_ingest_new_data( + KEYS.COVER_ART_FILE, api_song.cover_art, data=None, ) - song_data["artist"] = ( - self._do_ingest_new_data(KEYS.ARTIST, ar.id, ar, partial=True) - if (ar := api_song.artist) and not partial - else None - ) - song_data["album"] = ( - self._do_ingest_new_data(KEYS.ALBUM, al.id, al, partial=True) - if (al := api_song.album) - else None - ) - song_data["_cover_art"] = ( - self._do_ingest_new_data( - KEYS.COVER_ART_FILE, api_song.cover_art, data=None, - ) - if api_song.cover_art - else None - ) - song_data["file"] = ( - self._do_ingest_new_data( - KEYS.SONG_FILE, - api_song.id, - data=(api_song.path, None, api_song.size), - ) - if api_song.path - else None + if api_song.cover_art + else None + ) + song_data["file"] = ( + self._do_ingest_new_data( + KEYS.SONG_FILE, + api_song.id, + data=(api_song.path, None, api_song.size), ) + if api_song.path + else None + ) song, created = models.Song.get_or_create( id=song_data["id"], defaults=song_data diff --git a/sublime/config.py b/sublime/config.py index 58e3489..6c37680 100644 --- a/sublime/config.py +++ b/sublime/config.py @@ -220,6 +220,8 @@ class AppConfiguration(DataClassJsonMixin): # Just ignore any errors, it is only UI state. self._state = UIState() + self._state.__init_available_players__() + @property def _state_file_location(self) -> Optional[Path]: if not (provider := self.provider): diff --git a/sublime/ui/state.py b/sublime/ui/state.py index 6fb9a3d..e2dd86e 100644 --- a/sublime/ui/state.py +++ b/sublime/ui/state.py @@ -105,6 +105,7 @@ class UIState: self.current_notification = None self.playing = False + def __init_available_players__(self): from sublime.players import PlayerManager self.available_players = { diff --git a/tests/adapter_tests/filesystem_adapter_tests.py b/tests/adapter_tests/filesystem_adapter_tests.py index 8dd7007..84bd960 100644 --- a/tests/adapter_tests/filesystem_adapter_tests.py +++ b/tests/adapter_tests/filesystem_adapter_tests.py @@ -983,10 +983,18 @@ def test_search(cache_adapter: FilesystemAdapter): "albums", [ SubsonicAPI.Album( - id="album1", name="Foo", artist_id="artist1", cover_art="cal1" + id="album1", + name="Foo", + artist_id="artist1", + _artist="foo", + cover_art="cal1", ), SubsonicAPI.Album( - id="album2", name="Boo", artist_id="artist1", cover_art="cal2" + id="album2", + name="Boo", + artist_id="artist1", + _artist="foo", + cover_art="cal2", ), ], ) @@ -1002,13 +1010,27 @@ def test_search(cache_adapter: FilesystemAdapter): search_result.add_results( "songs", [ - SubsonicAPI.Song("s1", "amazing boo", cover_art="s1"), - SubsonicAPI.Song("s2", "foo of all foo", cover_art="s2"), + SubsonicAPI.Song( + "s1", + "amazing boo", + cover_art="s1", + _artist="artist3", + artist_id="ohea1", + ), + SubsonicAPI.Song( + "s2", + "foo of all foo", + cover_art="s2", + _artist="artist4", + artist_id="ohea2", + ), ], ) cache_adapter.ingest_new_data(KEYS.SEARCH_RESULTS, None, search_result) search_result = cache_adapter.search("foo") - assert [s.title for s in search_result.songs] == ["foo of all foo", "amazing boo"] + assert [ + (s.title, s.artist.name if s.artist else None) for s in search_result.songs + ] == [("foo of all foo", "artist4"), ("amazing boo", "artist3")] assert [a.name for a in search_result.artists] == ["foo", "better boo"] assert [a.name for a in search_result.albums] == ["Foo", "Boo"] diff --git a/tests/adapter_tests/mock_data/search3-navidrome.json b/tests/adapter_tests/mock_data/search3-navidrome.json new file mode 100644 index 0000000..ac20793 --- /dev/null +++ b/tests/adapter_tests/mock_data/search3-navidrome.json @@ -0,0 +1,253 @@ +{ + "subsonic-response": { + "status": "ok", + "version": "1.10.2", + "type": "navidrome", + "serverVersion": "0.27.0 (ddb30ce)", + "searchResult3": { + "artist": [ + { + "id": "28d3ba2dd29c49d79276780ffdd55657", + "name": "NC帝国 (USAO \u0026 Massive New Krew)", + "albumCount": 1 + }, + { + "id": "4e94cea7827e99bad4aa9d09924cb1ad", + "name": "UOM Records (USAO)", + "albumCount": 7 + } + ], + "album": [ + { + "id": "09817e6dd2537db90e7e249722e746d1", + "isDir": true, + "name": "AD:Drum'n Bass 3", + "artist": "Diverse System", + "year": 2017, + "coverArt": "al-09817e6dd2537db90e7e249722e746d1", + "duration": 4015, + "created": "2020-05-01T17:26:54.508020396Z", + "artistId": "ea7fe36e30ad44d86d80b8c098b1b354", + "songCount": 14, + "isVideo": false + }, + { + "id": "122b175790eeba47d5dd8703a5b9ec62", + "isDir": true, + "name": "ALTERNA", + "artist": "Massive CircleZ (Massive New Krew)", + "year": 2016, + "coverArt": "al-122b175790eeba47d5dd8703a5b9ec62", + "duration": 2525, + "created": "2020-05-01T17:42:46.46328879Z", + "artistId": "7b128e7593a9007576b8c944aa1e34be", + "songCount": 11, + "isVideo": false + }, + { + "id": "a048955fc533aaa658cb08f560b45bc2", + "isDir": true, + "name": "BASTARD ARCHIVE+", + "artist": "M.P.T.", + "year": 2008, + "coverArt": "al-a048955fc533aaa658cb08f560b45bc2", + "duration": 4634, + "created": "2020-05-01T17:37:05.609815622Z", + "artistId": "b80e98a2145430a326ec3e21261a813b", + "songCount": 12, + "isVideo": false + }, + { + "id": "9b7b52fae8b919b9de6e752c76f3ff00", + "isDir": true, + "name": "BREAK OUT BLACK", + "artist": "Massive CircleZ (Massive New Krew)", + "year": 2014, + "coverArt": "al-9b7b52fae8b919b9de6e752c76f3ff00", + "duration": 2928, + "created": "2020-05-01T17:42:46.430711977Z", + "artistId": "7b128e7593a9007576b8c944aa1e34be", + "songCount": 11, + "isVideo": false + } + ], + "song": [ + { + "id": "2b62a4c1070a6e7747b439041fddbca3", + "parent": "149884cddb190c8b150ed43ffc3617bb", + "isDir": false, + "title": "260", + "album": "Kick's For Liberation", + "artist": "USAO", + "track": 11, + "year": 2009, + "coverArt": "al-149884cddb190c8b150ed43ffc3617bb", + "size": 30208885, + "contentType": "audio/flac", + "suffix": "flac", + "transcodedContentType": "audio/ogg", + "transcodedSuffix": "oga", + "duration": 232, + "bitRate": 1039, + "path": "UOM Records/Kick's For Liberation/260.flac", + "discNumber": 1, + "created": "2019-07-26T15:44:34.1975208Z", + "albumId": "149884cddb190c8b150ed43ffc3617bb", + "artistId": "923fe3977eec9a949f817e6da59ecc13", + "type": "music", + "isVideo": false + }, + { + "id": "f08997ac88302e8422033ed5f3c82478", + "parent": "2a3a19a69ed681d7212a7f3ea8a6a61d", + "isDir": false, + "title": "50000DPS", + "album": "Kick's For Liberation 5", + "artist": "USAO", + "track": 4, + "year": 2015, + "coverArt": "al-2a3a19a69ed681d7212a7f3ea8a6a61d", + "size": 42064165, + "contentType": "audio/flac", + "suffix": "flac", + "transcodedContentType": "audio/ogg", + "transcodedSuffix": "oga", + "duration": 293, + "bitRate": 1146, + "path": "UOM Records (USAO)/Kick's For Liberation 5/50000DPS.flac", + "discNumber": 1, + "created": "2019-07-26T18:57:32.0111078Z", + "albumId": "2a3a19a69ed681d7212a7f3ea8a6a61d", + "artistId": "923fe3977eec9a949f817e6da59ecc13", + "type": "music", + "isVideo": false + }, + { + "id": "287d1b10a7e0e7269c3fc24f89d2fe7d", + "parent": "149884cddb190c8b150ed43ffc3617bb", + "isDir": false, + "title": "5ymphoTEK", + "album": "Kick's For Liberation", + "artist": "USAO", + "track": 8, + "year": 2009, + "coverArt": "al-149884cddb190c8b150ed43ffc3617bb", + "size": 45426359, + "contentType": "audio/flac", + "suffix": "flac", + "transcodedContentType": "audio/ogg", + "transcodedSuffix": "oga", + "duration": 344, + "bitRate": 1055, + "path": "UOM Records/Kick's For Liberation/5ymphoTEK.flac", + "discNumber": 1, + "created": "2019-07-26T17:22:02.059486Z", + "albumId": "149884cddb190c8b150ed43ffc3617bb", + "artistId": "923fe3977eec9a949f817e6da59ecc13", + "type": "music", + "isVideo": false + }, + { + "id": "513daf633bffc7cf3faa5a0a3337ed6f", + "parent": "09e23675867dc64d8563035ec9df96e0", + "isDir": false, + "title": "ARENARemix2008", + "album": "ほっちぽっち4beat", + "artist": "USAO", + "track": 2, + "year": 2008, + "coverArt": "al-09e23675867dc64d8563035ec9df96e0", + "size": 46553211, + "contentType": "audio/flac", + "suffix": "flac", + "transcodedContentType": "audio/ogg", + "transcodedSuffix": "oga", + "duration": 361, + "bitRate": 1031, + "path": "UTY/UOM Records/ほっちぽっち4beat/ARENARemix2008.flac", + "discNumber": 1, + "created": "2019-08-02T17:13:07.3348309Z", + "albumId": "09e23675867dc64d8563035ec9df96e0", + "artistId": "923fe3977eec9a949f817e6da59ecc13", + "type": "music", + "isVideo": false + }, + { + "id": "dac4aa78ff6cebcb79082c2301f317e8", + "parent": "5e761a69c8218c24245a64baf499cdee", + "isDir": false, + "title": "Acid Virus", + "album": "S2TB Files7: Battle Royale Disc 2", + "artist": "kors k vs USAO", + "track": 7, + "year": 2017, + "coverArt": "al-5e761a69c8218c24245a64baf499cdee", + "size": 41790587, + "contentType": "audio/flac", + "suffix": "flac", + "transcodedContentType": "audio/ogg", + "transcodedSuffix": "oga", + "duration": 290, + "bitRate": 1151, + "path": "S2TB Recording/S2TB Files7: Battle Royale Disc 2/Acid Virus.flac", + "discNumber": 2, + "created": "2019-08-01T20:23:28.8786968Z", + "albumId": "5e761a69c8218c24245a64baf499cdee", + "artistId": "86c4404190b692e82cf215544099c204", + "type": "music", + "isVideo": false + }, + { + "id": "bd67969a42d00992c1428e0d1cee1a06", + "parent": "149884cddb190c8b150ed43ffc3617bb", + "isDir": false, + "title": "Add Insult To Injury", + "album": "Kick's For Liberation", + "artist": "USAO", + "track": 6, + "year": 2009, + "coverArt": "al-149884cddb190c8b150ed43ffc3617bb", + "size": 37176906, + "contentType": "audio/flac", + "suffix": "flac", + "transcodedContentType": "audio/ogg", + "transcodedSuffix": "oga", + "duration": 268, + "bitRate": 1109, + "path": "UOM Records/Kick's For Liberation/Add Insult To Injury.flac", + "discNumber": 1, + "created": "2019-07-26T19:26:27.2135649Z", + "albumId": "149884cddb190c8b150ed43ffc3617bb", + "artistId": "923fe3977eec9a949f817e6da59ecc13", + "type": "music", + "isVideo": false + }, + { + "id": "9f4dba1188c2d772fb82947dd60fdd74", + "parent": "2a3a19a69ed681d7212a7f3ea8a6a61d", + "isDir": false, + "title": "Aeropolis", + "album": "Kick's For Liberation 5", + "artist": "USAO", + "track": 8, + "year": 2015, + "coverArt": "al-2a3a19a69ed681d7212a7f3ea8a6a61d", + "size": 40838681, + "contentType": "audio/flac", + "suffix": "flac", + "transcodedContentType": "audio/ogg", + "transcodedSuffix": "oga", + "duration": 302, + "bitRate": 1080, + "path": "UOM Records (USAO)/Kick's For Liberation 5/Aeropolis.flac", + "discNumber": 1, + "created": "2019-07-26T19:08:21.8450483Z", + "albumId": "2a3a19a69ed681d7212a7f3ea8a6a61d", + "artistId": "923fe3977eec9a949f817e6da59ecc13", + "type": "music", + "isVideo": false + } + ] + } + } +} diff --git a/tests/config_test.py b/tests/config_test.py index eb36f4c..a70d11c 100644 --- a/tests/config_test.py +++ b/tests/config_test.py @@ -38,10 +38,10 @@ def test_server_property(tmp_path: Path): config.current_provider_id = "1" assert config.provider == provider - assert config._state_file_location == tmp_path.joinpath("1", "state.pickle",) + assert config._state_file_location == tmp_path.joinpath("1", "state.pickle") -def test_json_load_unload(config_filename: Path): +def test_json_load_unload(config_filename: Path, tmp_path: Path): ConfigurationStore.MOCK = True subsonic_config_store = ConfigurationStore(username="test") subsonic_config_store.set_secret("password", "testpass") @@ -59,6 +59,7 @@ def test_json_load_unload(config_filename: Path): current_provider_id="1", filename=config_filename, ) + original_config.cache_location = tmp_path original_config.save()