Closes #253: fixed ingestion of search results; restricted search results to just ones that won't blow up

This commit is contained in:
Sumner Evans
2020-07-31 21:51:59 -06:00
parent 0f0b8bb5b9
commit a95dff751f
8 changed files with 334 additions and 42 deletions

View File

@@ -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**

View File

@@ -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"<SearchResult {' '.join(formatted_fields)}>"
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
),

View File

@@ -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

View File

@@ -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):

View File

@@ -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 = {

View File

@@ -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"]

View File

@@ -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": "UTYUOM 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
}
]
}
}
}

View File

@@ -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()