Got browse caching working

This commit is contained in:
Sumner Evans
2020-05-14 22:49:30 -06:00
parent 8f07d1ec48
commit 8017aac704
24 changed files with 900 additions and 388 deletions

View File

@@ -3,7 +3,7 @@ from time import sleep
import pytest
from sublime.adapters import AdapterManager, Result
from sublime.adapters import AdapterManager, Result, SearchResult
from sublime.config import AppConfiguration, ServerConfiguration
@@ -114,3 +114,22 @@ def test_get_song_details(adapter_manager: AdapterManager):
# assert 0
# TODO
pass
def test_search(adapter_manager: AdapterManager):
# TODO
return
results = []
# TODO ingest data
def search_callback(result: SearchResult):
results.append((result.artists, result.albums, result.songs, result.playlists))
AdapterManager.search("ohea", search_callback=search_callback).result()
# TODO test getting results from the server and updating using that
while len(results) < 1:
sleep(0.1)
assert len(results) == 1

View File

@@ -1,3 +1,4 @@
import json
import shutil
from dataclasses import asdict
from datetime import timedelta
@@ -27,8 +28,8 @@ MOCK_SONG_FILE2_HASH = "c32597c724e2e484dbf5856930b2e5bb80de13b7"
MOCK_SUBSONIC_SONGS = [
SubsonicAPI.Song(
"2",
"Song 2",
_parent="foo",
title="Song 2",
parent_id="d1",
_album="foo",
album_id="a1",
_artist="cool",
@@ -40,8 +41,8 @@ MOCK_SUBSONIC_SONGS = [
),
SubsonicAPI.Song(
"1",
"Song 1",
_parent="foo",
title="Song 1",
parent_id="d1",
_album="foo",
album_id="a1",
_artist="foo",
@@ -53,8 +54,8 @@ MOCK_SUBSONIC_SONGS = [
),
SubsonicAPI.Song(
"1",
"Song 1",
_parent="foo",
title="Song 1",
parent_id="d1",
_album="foo",
album_id="a1",
_artist="foo",
@@ -66,6 +67,8 @@ MOCK_SUBSONIC_SONGS = [
),
]
KEYS = FilesystemAdapter.CachedDataKey
@pytest.fixture
def adapter(tmp_path: Path):
@@ -100,7 +103,7 @@ def verify_songs(
assert len(actual_songs) == len(expected_songs)
for actual, song in zip(actual_songs, expected_songs):
for k, v in asdict(song).items():
if k in ("_genre", "_album", "_artist", "_parent", "album_id", "artist_id"):
if k in ("_genre", "_album", "_artist", "album_id", "artist_id"):
continue
print(k, "->", v) # noqa: T001
@@ -110,8 +113,6 @@ def verify_songs(
assert ("a1", "foo") == (actual_value.id, actual_value.name)
elif k == "genre":
assert v["name"] == actual_value.name
elif k == "parent":
assert "foo" == actual_value.id
elif k == "artist":
assert (v["id"], v["name"]) == (actual_value.id, actual_value.name)
else:
@@ -123,7 +124,7 @@ def test_caching_get_playlists(cache_adapter: FilesystemAdapter):
cache_adapter.get_playlists()
# Ingest an empty list (for example, no playlists added yet to server).
cache_adapter.ingest_new_data(FilesystemAdapter.CachedDataKey.PLAYLISTS, (), [])
cache_adapter.ingest_new_data(KEYS.PLAYLISTS, (), [])
# After the first cache miss of get_playlists, even if an empty list is
# returned, the next one should not be a cache miss.
@@ -131,7 +132,7 @@ def test_caching_get_playlists(cache_adapter: FilesystemAdapter):
# Ingest two playlists.
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.PLAYLISTS,
KEYS.PLAYLISTS,
(),
[
SubsonicAPI.Playlist("1", "test1", comment="comment"),
@@ -150,7 +151,7 @@ def test_caching_get_playlists(cache_adapter: FilesystemAdapter):
# Ingest a new playlist list with one of them deleted.
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.PLAYLISTS,
KEYS.PLAYLISTS,
(),
[
SubsonicAPI.Playlist("1", "test1", comment="comment"),
@@ -186,7 +187,7 @@ def test_caching_get_playlist_details(cache_adapter: FilesystemAdapter):
# Simulate the playlist being retrieved from Subsonic.
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.PLAYLIST_DETAILS,
KEYS.PLAYLIST_DETAILS,
("1",),
SubsonicAPI.PlaylistWithSongs("1", "test1", songs=MOCK_SUBSONIC_SONGS[:2]),
)
@@ -200,7 +201,7 @@ def test_caching_get_playlist_details(cache_adapter: FilesystemAdapter):
# "Force refresh" the playlist and add a new song (duplicate).
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.PLAYLIST_DETAILS,
KEYS.PLAYLIST_DETAILS,
("1",),
SubsonicAPI.PlaylistWithSongs("1", "foo", songs=MOCK_SUBSONIC_SONGS),
)
@@ -231,7 +232,7 @@ def test_no_caching_get_playlist_details(adapter: FilesystemAdapter):
def test_caching_get_playlist_then_details(cache_adapter: FilesystemAdapter):
# Ingest a list of playlists (like the sidebar, without songs)
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.PLAYLISTS,
KEYS.PLAYLISTS,
(),
[SubsonicAPI.Playlist("1", "test1"), SubsonicAPI.Playlist("2", "test2")],
)
@@ -248,13 +249,11 @@ def test_caching_get_playlist_then_details(cache_adapter: FilesystemAdapter):
# Simulate getting playlist details for id=1, then id=2
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.PLAYLIST_DETAILS,
("1",),
SubsonicAPI.PlaylistWithSongs("1", "test1"),
KEYS.PLAYLIST_DETAILS, ("1",), SubsonicAPI.PlaylistWithSongs("1", "test1"),
)
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.PLAYLIST_DETAILS,
KEYS.PLAYLIST_DETAILS,
("2",),
SubsonicAPI.PlaylistWithSongs("2", "test2", songs=MOCK_SUBSONIC_SONGS),
)
@@ -272,7 +271,7 @@ def test_cache_cover_art(cache_adapter: FilesystemAdapter):
# After ingesting the data, reading from the cache should give the exact same file.
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.COVER_ART_FILE, ("pl_test1",), MOCK_ALBUM_ART,
KEYS.COVER_ART_FILE, ("pl_test1",), MOCK_ALBUM_ART,
)
with open(cache_adapter.get_cover_art_uri("pl_test1", "file"), "wb+") as cached:
with open(MOCK_ALBUM_ART, "wb+") as expected:
@@ -281,32 +280,28 @@ def test_cache_cover_art(cache_adapter: FilesystemAdapter):
def test_invalidate_playlist(cache_adapter: FilesystemAdapter):
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.PLAYLISTS,
KEYS.PLAYLISTS,
(),
[SubsonicAPI.Playlist("1", "test1"), SubsonicAPI.Playlist("2", "test2")],
)
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.COVER_ART_FILE, ("pl_test1",), MOCK_ALBUM_ART,
KEYS.COVER_ART_FILE, ("pl_test1",), MOCK_ALBUM_ART,
)
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.PLAYLIST_DETAILS,
KEYS.PLAYLIST_DETAILS,
("2",),
SubsonicAPI.PlaylistWithSongs("2", "test2", cover_art="pl_2", songs=[]),
)
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.COVER_ART_FILE, ("pl_2",), MOCK_ALBUM_ART2,
KEYS.COVER_ART_FILE, ("pl_2",), MOCK_ALBUM_ART2,
)
stale_uri_1 = cache_adapter.get_cover_art_uri("pl_test1", "file")
stale_uri_2 = cache_adapter.get_cover_art_uri("pl_2", "file")
cache_adapter.invalidate_data(FilesystemAdapter.CachedDataKey.PLAYLISTS, ())
cache_adapter.invalidate_data(
FilesystemAdapter.CachedDataKey.PLAYLIST_DETAILS, ("2",)
)
cache_adapter.invalidate_data(
FilesystemAdapter.CachedDataKey.COVER_ART_FILE, ("pl_test1",)
)
cache_adapter.invalidate_data(KEYS.PLAYLISTS, ())
cache_adapter.invalidate_data(KEYS.PLAYLIST_DETAILS, ("2",))
cache_adapter.invalidate_data(KEYS.COVER_ART_FILE, ("pl_test1",))
# After invalidating the data, it should cache miss, but still have the old, stale,
# data.
@@ -341,21 +336,16 @@ def test_invalidate_playlist(cache_adapter: FilesystemAdapter):
def test_invalidate_song_file(cache_adapter: FilesystemAdapter):
CACHE_KEYS = FilesystemAdapter.CachedDataKey
cache_adapter.ingest_new_data(KEYS.SONG, ("2",), MOCK_SUBSONIC_SONGS[0])
cache_adapter.ingest_new_data(KEYS.SONG, ("1",), MOCK_SUBSONIC_SONGS[1])
cache_adapter.ingest_new_data(
CACHE_KEYS.SONG_DETAILS, ("2",), MOCK_SUBSONIC_SONGS[0]
KEYS.COVER_ART_FILE, ("s1", "song"), MOCK_ALBUM_ART,
)
cache_adapter.ingest_new_data(
CACHE_KEYS.SONG_DETAILS, ("1",), MOCK_SUBSONIC_SONGS[1]
)
cache_adapter.ingest_new_data(
CACHE_KEYS.COVER_ART_FILE, ("s1", "song"), MOCK_ALBUM_ART,
)
cache_adapter.ingest_new_data(CACHE_KEYS.SONG_FILE, ("1",), MOCK_SONG_FILE)
cache_adapter.ingest_new_data(CACHE_KEYS.SONG_FILE, ("2",), MOCK_SONG_FILE2)
cache_adapter.ingest_new_data(KEYS.SONG_FILE, ("1",), MOCK_SONG_FILE)
cache_adapter.ingest_new_data(KEYS.SONG_FILE, ("2",), MOCK_SONG_FILE2)
cache_adapter.invalidate_data(CACHE_KEYS.SONG_FILE, ("1",))
cache_adapter.invalidate_data(CACHE_KEYS.COVER_ART_FILE, ("s1", "song"))
cache_adapter.invalidate_data(KEYS.SONG_FILE, ("1",))
cache_adapter.invalidate_data(KEYS.COVER_ART_FILE, ("s1", "song"))
with pytest.raises(CacheMissError):
cache_adapter.get_song_uri("1", "file")
@@ -369,21 +359,21 @@ def test_invalidate_song_file(cache_adapter: FilesystemAdapter):
def test_delete_playlists(cache_adapter: FilesystemAdapter):
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.PLAYLIST_DETAILS,
KEYS.PLAYLIST_DETAILS,
("1",),
SubsonicAPI.PlaylistWithSongs("1", "test1", cover_art="pl_1", songs=[]),
)
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.PLAYLIST_DETAILS,
KEYS.PLAYLIST_DETAILS,
("2",),
SubsonicAPI.PlaylistWithSongs("2", "test1", cover_art="pl_2", songs=[]),
)
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.COVER_ART_FILE, ("pl_1",), MOCK_ALBUM_ART,
KEYS.COVER_ART_FILE, ("pl_1",), MOCK_ALBUM_ART,
)
# Deleting a playlist should get rid of it entirely.
cache_adapter.delete_data(FilesystemAdapter.CachedDataKey.PLAYLIST_DETAILS, ("2",))
cache_adapter.delete_data(KEYS.PLAYLIST_DETAILS, ("2",))
try:
cache_adapter.get_playlist_details("2")
assert 0, "DID NOT raise CacheMissError"
@@ -391,7 +381,7 @@ def test_delete_playlists(cache_adapter: FilesystemAdapter):
assert e.partial_data is None
# Deleting a playlist with associated cover art should get rid the cover art too.
cache_adapter.delete_data(FilesystemAdapter.CachedDataKey.PLAYLIST_DETAILS, ("1",))
cache_adapter.delete_data(KEYS.PLAYLIST_DETAILS, ("1",))
try:
cache_adapter.get_cover_art_uri("pl_1", "file")
assert 0, "DID NOT raise CacheMissError"
@@ -410,21 +400,17 @@ def test_delete_playlists(cache_adapter: FilesystemAdapter):
def test_delete_song_data(cache_adapter: FilesystemAdapter):
cache_adapter.ingest_new_data(KEYS.SONG, ("1",), MOCK_SUBSONIC_SONGS[1])
cache_adapter.ingest_new_data(KEYS.SONG_FILE, ("1",), MOCK_SONG_FILE)
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.SONG_DETAILS, ("1",), MOCK_SUBSONIC_SONGS[1]
)
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.SONG_FILE, ("1",), MOCK_SONG_FILE
)
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.COVER_ART_FILE, ("s1",), MOCK_ALBUM_ART,
KEYS.COVER_ART_FILE, ("s1",), MOCK_ALBUM_ART,
)
music_file_path = cache_adapter.get_song_uri("1", "file")
cover_art_path = cache_adapter.get_cover_art_uri("s1", "file")
cache_adapter.delete_data(FilesystemAdapter.CachedDataKey.SONG_FILE, ("1",))
cache_adapter.delete_data(FilesystemAdapter.CachedDataKey.COVER_ART_FILE, ("s1",))
cache_adapter.delete_data(KEYS.SONG_FILE, ("1",))
cache_adapter.delete_data(KEYS.COVER_ART_FILE, ("s1",))
assert not Path(music_file_path).exists()
assert not Path(cover_art_path).exists()
@@ -446,12 +432,8 @@ def test_caching_get_genres(cache_adapter: FilesystemAdapter):
with pytest.raises(CacheMissError):
cache_adapter.get_genres()
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.SONG_DETAILS, ("2",), MOCK_SUBSONIC_SONGS[0]
)
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.SONG_DETAILS, ("1",), MOCK_SUBSONIC_SONGS[1]
)
cache_adapter.ingest_new_data(KEYS.SONG, ("2",), MOCK_SUBSONIC_SONGS[0])
cache_adapter.ingest_new_data(KEYS.SONG, ("1",), MOCK_SUBSONIC_SONGS[1])
# Getting genres now should look at what's on the songs. This sould cache miss, but
# still give some data.
@@ -463,7 +445,7 @@ def test_caching_get_genres(cache_adapter: FilesystemAdapter):
# After we actually ingest the actual list, it should be returned instead.
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.GENRES,
KEYS.GENRES,
(),
[
SubsonicAPI.Genre("Bar", 10, 20),
@@ -479,9 +461,7 @@ def test_caching_get_song_details(cache_adapter: FilesystemAdapter):
cache_adapter.get_song_details("1")
# Simulate the song details being retrieved from Subsonic.
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.SONG_DETAILS, ("1",), MOCK_SUBSONIC_SONGS[1]
)
cache_adapter.ingest_new_data(KEYS.SONG, ("1",), MOCK_SUBSONIC_SONGS[1])
song = cache_adapter.get_song_details("1")
assert song.id == "1"
@@ -489,19 +469,19 @@ def test_caching_get_song_details(cache_adapter: FilesystemAdapter):
assert song.album
assert (song.album.id, song.album.name) == ("a1", "foo")
assert song.artist and song.artist.name == "foo"
assert song.parent.id == "foo"
assert song.parent_id == "d1"
assert song.duration == timedelta(seconds=10.2)
assert song.path == "foo/song1.mp3"
assert song.genre and song.genre.name == "Foo"
# "Force refresh" the song details
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.SONG_DETAILS,
KEYS.SONG,
("1",),
SubsonicAPI.Song(
"1",
"Song 1",
_parent="bar",
title="Song 1",
parent_id="bar",
_album="bar",
album_id="a2",
_artist="bar",
@@ -518,7 +498,7 @@ def test_caching_get_song_details(cache_adapter: FilesystemAdapter):
assert song.album
assert (song.album.id, song.album.name) == ("a2", "bar")
assert song.artist and song.artist.name == "bar"
assert song.parent.id == "bar"
assert song.parent_id == "bar"
assert song.duration == timedelta(seconds=10.2)
assert song.path == "bar/song1.mp3"
assert song.genre and song.genre.name == "Bar"
@@ -529,12 +509,12 @@ def test_caching_get_song_details(cache_adapter: FilesystemAdapter):
def test_caching_less_info(cache_adapter: FilesystemAdapter):
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.SONG_DETAILS,
KEYS.SONG,
("1",),
SubsonicAPI.Song(
"1",
"Song 1",
_parent="bar",
title="Song 1",
parent_id="bar",
_album="bar",
album_id="a2",
_artist="bar",
@@ -545,12 +525,12 @@ def test_caching_less_info(cache_adapter: FilesystemAdapter):
),
)
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.SONG_DETAILS,
KEYS.SONG,
("1",),
SubsonicAPI.Song(
"1",
"Song 1",
_parent="bar",
title="Song 1",
parent_id="bar",
duration=timedelta(seconds=10.2),
path="bar/song1.mp3",
),
@@ -568,7 +548,7 @@ def test_caching_get_artists(cache_adapter: FilesystemAdapter):
# Ingest artists.
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.ARTISTS,
KEYS.ARTISTS,
(),
[
SubsonicAPI.ArtistAndArtistInfo("1", "test1", album_count=3, albums=[]),
@@ -583,7 +563,7 @@ def test_caching_get_artists(cache_adapter: FilesystemAdapter):
# Ingest a new artists list with one of them deleted.
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.ARTISTS,
KEYS.ARTISTS,
(),
[
SubsonicAPI.ArtistAndArtistInfo("1", "test1", album_count=3),
@@ -603,16 +583,12 @@ def test_caching_get_ignored_articles(cache_adapter: FilesystemAdapter):
cache_adapter.get_ignored_articles()
# Ingest ignored_articles.
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.IGNORED_ARTICLES, (), {"Foo", "Bar"}
)
cache_adapter.ingest_new_data(KEYS.IGNORED_ARTICLES, (), {"Foo", "Bar"})
artists = cache_adapter.get_ignored_articles()
assert {"Foo", "Bar"} == artists
# Ingest a new artists list with one of them deleted.
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.IGNORED_ARTICLES, (), {"Foo", "Baz"}
)
cache_adapter.ingest_new_data(KEYS.IGNORED_ARTICLES, (), {"Foo", "Baz"})
artists = cache_adapter.get_ignored_articles()
assert {"Foo", "Baz"} == artists
@@ -623,7 +599,7 @@ def test_caching_get_artist(cache_adapter: FilesystemAdapter):
# Simulate the artist details being retrieved from Subsonic.
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.ARTIST,
KEYS.ARTIST,
("1",),
SubsonicAPI.ArtistAndArtistInfo(
"1",
@@ -658,7 +634,7 @@ def test_caching_get_artist(cache_adapter: FilesystemAdapter):
# Simulate "force refreshing" the artist details being retrieved from Subsonic.
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.ARTIST,
KEYS.ARTIST,
("1",),
SubsonicAPI.ArtistAndArtistInfo(
"1",
@@ -704,7 +680,7 @@ def test_caching_get_album(cache_adapter: FilesystemAdapter):
# Simulate the artist details being retrieved from Subsonic.
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.ALBUM,
KEYS.ALBUM,
("a1",),
SubsonicAPI.Album(
"a1",
@@ -739,7 +715,7 @@ def test_caching_get_album(cache_adapter: FilesystemAdapter):
def test_caching_invalidate_artist(cache_adapter: FilesystemAdapter):
# Simulate the artist details being retrieved from Subsonic.
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.ARTIST,
KEYS.ARTIST,
("artist1",),
SubsonicAPI.ArtistAndArtistInfo(
"artist1",
@@ -759,23 +735,23 @@ def test_caching_invalidate_artist(cache_adapter: FilesystemAdapter):
),
)
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.ALBUM,
KEYS.ALBUM,
("1",),
SubsonicAPI.Album("1", "Foo", artist_id="artist1", cover_art="1"),
)
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.ALBUM,
KEYS.ALBUM,
("2",),
SubsonicAPI.Album("2", "Bar", artist_id="artist1", cover_art="2"),
)
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.COVER_ART_FILE, ("image",), MOCK_ALBUM_ART3,
KEYS.COVER_ART_FILE, ("image",), MOCK_ALBUM_ART3,
)
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.COVER_ART_FILE, ("1",), MOCK_ALBUM_ART,
KEYS.COVER_ART_FILE, ("1",), MOCK_ALBUM_ART,
)
cache_adapter.ingest_new_data(
FilesystemAdapter.CachedDataKey.COVER_ART_FILE, ("2",), MOCK_ALBUM_ART2,
KEYS.COVER_ART_FILE, ("2",), MOCK_ALBUM_ART2,
)
stale_artist = cache_adapter.get_artist("artist1")
@@ -785,7 +761,7 @@ def test_caching_invalidate_artist(cache_adapter: FilesystemAdapter):
stale_cover_art_1 = cache_adapter.get_cover_art_uri("1", "file")
stale_cover_art_2 = cache_adapter.get_cover_art_uri("2", "file")
cache_adapter.invalidate_data(FilesystemAdapter.CachedDataKey.ARTIST, ("artist1",))
cache_adapter.invalidate_data(KEYS.ARTIST, ("artist1",))
# Test the cascade of cache invalidations.
try:
@@ -829,3 +805,45 @@ def test_caching_invalidate_artist(cache_adapter: FilesystemAdapter):
except CacheMissError as e:
assert e.partial_data
assert e.partial_data == stale_cover_art_2
def test_get_music_directory(cache_adapter: FilesystemAdapter):
dir_id = "d1"
with pytest.raises(CacheMissError):
cache_adapter.get_directory(dir_id)
# Simulate the directory details being retrieved from Subsonic.
cache_adapter.ingest_new_data(
KEYS.DIRECTORY,
(dir_id,),
SubsonicAPI.Directory(
dir_id,
title="foo",
parent_id=None,
_children=[json.loads(s.to_json()) for s in MOCK_SUBSONIC_SONGS[:2]]
+ [
{
"id": "542",
"parent": dir_id,
"isDir": True,
"title": "Crash My Party",
}
],
),
)
directory = cache_adapter.get_directory(dir_id)
assert directory and directory.id == dir_id
assert directory.name == "foo"
assert directory.parent_id == "root"
dir_child, *song_children = directory.children
verify_songs(song_children, MOCK_SUBSONIC_SONGS[:2])
assert dir_child.id == "542"
assert dir_child.parent_id
assert dir_child.name == "Crash My Party"
def test_search(adapter: FilesystemAdapter):
# TODO
pass

View File

@@ -0,0 +1,55 @@
{
"subsonic-response": {
"status": "ok",
"version": "1.15.0",
"indexes": {
"lastModified": 1588577415000,
"ignoredArticles": "The El La Los Las Le Les",
"index": [
{
"name": "A",
"artist": [
{
"id": "73",
"name": "The Afters"
},
{
"id": "100",
"name": "Adele"
},
{
"id": "120",
"name": "Austin French"
}
]
},
{
"name": "B",
"artist": [
{
"id": "93",
"name": "The Band Perry"
},
{
"id": "41",
"name": "Basshunter"
}
]
},
{
"name": "X-Z",
"artist": [
{
"id": "155",
"name": "Zac Brown Band"
},
{
"id": "25",
"name": "Zach Williams"
}
]
}
]
}
}
}

View File

@@ -0,0 +1,24 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"directory" : {
"id" : "60",
"name" : "Luke Bryan",
"playCount" : 0,
"child" : [ {
"id" : "542",
"parent" : "60",
"isDir" : true,
"title" : "Crash My Party",
"album" : "Crash My Party",
"artist" : "Luke Bryan",
"year" : 2013,
"genre" : "Country",
"coverArt" : "542",
"playCount" : 48,
"created" : "2020-03-27T05:27:57.000Z"
} ]
}
}
}

View File

@@ -0,0 +1,235 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"searchResult3" : {
"artist" : [ {
"id" : "25",
"name" : "Zach Williams",
"coverArt" : "ar-25",
"albumCount" : 1
}, {
"id" : "154",
"name" : "Zac Brown Band",
"coverArt" : "ar-154",
"albumCount" : 3
} ],
"album" : [ {
"id" : "31",
"name" : "Chain Breaker",
"artist" : "Zach Williams",
"artistId" : "25",
"coverArt" : "al-31",
"songCount" : 1,
"duration" : 196,
"created" : "2020-03-27T05:32:31.000Z",
"year" : 2016,
"genre" : "Christian & Gospel"
}, {
"id" : "235",
"name" : "The Foundation",
"artist" : "Zac Brown Band",
"artistId" : "154",
"coverArt" : "al-235",
"songCount" : 3,
"duration" : 675,
"created" : "2020-03-27T05:32:15.000Z",
"year" : 2008,
"genre" : "Country"
}, {
"id" : "236",
"name" : "Uncaged",
"artist" : "Zac Brown Band",
"artistId" : "154",
"coverArt" : "al-236",
"songCount" : 2,
"duration" : 602,
"created" : "2020-03-27T05:32:24.000Z",
"year" : 2012,
"genre" : "Country"
}, {
"id" : "237",
"name" : "You Get What You Give",
"artist" : "Zac Brown Band",
"artistId" : "154",
"coverArt" : "al-237",
"songCount" : 1,
"duration" : 273,
"created" : "2020-03-27T05:32:28.000Z",
"year" : 2010,
"genre" : "Country"
} ],
"song" : [ {
"id" : "246",
"parent" : "360",
"isDir" : false,
"title" : "Chain Breaker",
"album" : "Chain Breaker",
"artist" : "Zach Williams",
"track" : 1,
"year" : 2016,
"genre" : "Christian & Gospel",
"coverArt" : "360",
"size" : 7038712,
"contentType" : "audio/mp4",
"suffix" : "m4a",
"transcodedContentType" : "audio/mpeg",
"transcodedSuffix" : "mp3",
"duration" : 196,
"bitRate" : 256,
"path" : "Zach Williams/Chain Breaker/01 Chain Breaker.m4a",
"isVideo" : false,
"playCount" : 8,
"discNumber" : 1,
"created" : "2020-03-27T05:32:31.000Z",
"albumId" : "31",
"artistId" : "25",
"type" : "music"
}, {
"id" : "737",
"parent" : "738",
"isDir" : false,
"title" : "Highway 20 Ride",
"album" : "The Foundation",
"artist" : "Zac Brown Band",
"track" : 9,
"year" : 2008,
"genre" : "Country",
"coverArt" : "738",
"size" : 7843278,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 229,
"bitRate" : 263,
"path" : "Zac Brown Band/The Foundation/09 - Highway 20 Ride.mp3",
"isVideo" : false,
"playCount" : 8,
"discNumber" : 1,
"created" : "2020-03-27T05:32:15.000Z",
"albumId" : "235",
"artistId" : "154",
"type" : "music"
}, {
"id" : "743",
"parent" : "738",
"isDir" : false,
"title" : "Chicken Fried",
"album" : "The Foundation",
"artist" : "Zac Brown Band",
"track" : 6,
"year" : 2008,
"genre" : "Country",
"coverArt" : "738",
"size" : 8420335,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 238,
"bitRate" : 272,
"path" : "Zac Brown Band/The Foundation/06 - Chicken Fried.mp3",
"isVideo" : false,
"playCount" : 9,
"discNumber" : 1,
"created" : "2020-03-27T05:32:11.000Z",
"albumId" : "235",
"artistId" : "154",
"type" : "music"
}, {
"id" : "744",
"parent" : "738",
"isDir" : false,
"title" : "Whatever It Is",
"album" : "The Foundation",
"artist" : "Zac Brown Band",
"track" : 2,
"year" : 2008,
"genre" : "Country",
"coverArt" : "738",
"size" : 7313167,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 208,
"bitRate" : 269,
"path" : "Zac Brown Band/The Foundation/02 - Whatever It Is.mp3",
"isVideo" : false,
"playCount" : 17,
"discNumber" : 1,
"created" : "2020-03-27T05:32:08.000Z",
"albumId" : "235",
"artistId" : "154",
"type" : "music"
}, {
"id" : "739",
"parent" : "740",
"isDir" : false,
"title" : "Sweet Annie",
"album" : "Uncaged",
"artist" : "Zac Brown Band",
"track" : 6,
"year" : 2012,
"genre" : "Country",
"coverArt" : "740",
"size" : 9523591,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 278,
"bitRate" : 265,
"path" : "Zac Brown Band/Uncaged/06 - Sweet Annie.mp3",
"isVideo" : false,
"playCount" : 10,
"discNumber" : 1,
"created" : "2020-03-27T05:32:24.000Z",
"albumId" : "236",
"artistId" : "154",
"type" : "music"
}, {
"id" : "745",
"parent" : "740",
"isDir" : false,
"title" : "Goodbye In Her Eyes",
"album" : "Uncaged",
"artist" : "Zac Brown Band",
"track" : 3,
"year" : 2012,
"genre" : "Country",
"coverArt" : "740",
"size" : 11111186,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 324,
"bitRate" : 267,
"path" : "Zac Brown Band/Uncaged/03 - Goodbye In Her Eyes.mp3",
"isVideo" : false,
"playCount" : 7,
"discNumber" : 1,
"created" : "2020-03-27T05:32:20.000Z",
"albumId" : "236",
"artistId" : "154",
"type" : "music"
}, {
"id" : "741",
"parent" : "742",
"isDir" : false,
"title" : "Colder Weather",
"album" : "You Get What You Give",
"artist" : "Zac Brown Band",
"track" : 8,
"year" : 2010,
"genre" : "Country",
"coverArt" : "742",
"size" : 9088683,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 273,
"bitRate" : 255,
"path" : "Zac Brown Band/You Get What You Give/08 - Colder Weather.mp3",
"isVideo" : false,
"playCount" : 8,
"discNumber" : 1,
"created" : "2020-03-27T05:32:28.000Z",
"albumId" : "237",
"artistId" : "154",
"type" : "music"
} ]
}
}
}

View File

@@ -58,6 +58,7 @@ def mock_data_files(
num_files += 1
yield file, iter(parts)
# Make sure that is at least one test file
assert num_files > 0
@@ -158,7 +159,7 @@ def test_get_playlist_details(adapter: SubsonicAdapter):
# Make sure that at least the first song got decoded properly.
assert playlist_details.songs[0] == SubsonicAPI.Song(
id="202",
_parent="318",
parent_id="318",
title="What a Beautiful Name",
_album="What a Beautiful Name - Single",
album_id="48",
@@ -201,7 +202,7 @@ def test_create_playlist(adapter: SubsonicAdapter):
songs=[
SubsonicAPI.Song(
id="202",
_parent="318",
parent_id="318",
title="What a Beautiful Name",
_album="What a Beautiful Name - Single",
album_id="48",
@@ -263,8 +264,8 @@ def test_get_song_details(adapter: SubsonicAdapter):
"544",
timedelta(seconds=203),
)
assert song.path.endswith("Sweet Caroline.mp3")
assert song.parent and song.parent.id == "544"
assert song.path and song.path.endswith("Sweet Caroline.mp3")
assert song.parent_id == "544"
assert song.artist
assert (song.artist.id, song.artist.name) == ("60", "Neil Diamond")
assert song.album
@@ -425,3 +426,48 @@ def test_get_album(adapter: SubsonicAdapter):
"Nothing Like You",
"Better Together",
]
def test_get_music_directory(adapter: SubsonicAdapter):
for filename, data in mock_data_files("get_music_directory"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
directory = adapter.get_directory("3")
assert directory.id == "60"
assert directory.name == "Luke Bryan"
assert directory.parent_id == "root"
assert directory.children and len(directory.children) == 1
child = directory.children[0]
assert isinstance(child, SubsonicAPI.Directory)
assert child.id == "542"
assert child.name == "Crash My Party"
assert child.parent_id == "60"
for filename, data in mock_data_files("get_indexes"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
directory = adapter.get_directory("root")
assert directory.id == "root"
assert directory.parent_id is None
assert len(directory.children) == 7
child = directory.children[0]
assert isinstance(child, SubsonicAPI.Directory)
assert child.id == "73"
assert child.name == "The Afters"
assert child.parent_id == "root"
def test_search(adapter: SubsonicAdapter):
for filename, data in mock_data_files("search3"):
logging.info(filename)
logging.debug(data)
adapter._set_mock_data(data)
search_results = adapter.search("3")
assert len(search_results._songs) == 7
assert len(search_results._artists) == 2
assert len(search_results._albums) == 4