Cache songs on get playlist details

This commit is contained in:
Sumner Evans
2020-04-22 01:51:29 -06:00
parent c90b52f493
commit 934eff06c5
6 changed files with 79 additions and 13 deletions

View File

@@ -39,7 +39,9 @@ class FilesystemAdapter(CachingAdapter):
database_filename = data_directory.joinpath('cache.db') database_filename = data_directory.joinpath('cache.db')
models.database.init(database_filename) models.database.init(database_filename)
models.database.connect() models.database.connect()
models.database.create_tables(models.ALL_TABLES)
with models.database.atomic():
models.database.create_tables(models.ALL_TABLES)
def shutdown(self): def shutdown(self):
logging.info('Shutdown complete') logging.info('Shutdown complete')
@@ -86,6 +88,7 @@ class FilesystemAdapter(CachingAdapter):
# Data Ingestion Methods # Data Ingestion Methods
# ========================================================================= # =========================================================================
@models.database.atomic()
def ingest_new_data( def ingest_new_data(
self, self,
function: 'CachingAdapter.FunctionNames', function: 'CachingAdapter.FunctionNames',
@@ -105,7 +108,22 @@ class FilesystemAdapter(CachingAdapter):
asdict, data)).on_conflict_replace().execute() asdict, data)).on_conflict_replace().execute()
elif function == CachingAdapter.FunctionNames.GET_PLAYLIST_DETAILS: elif function == CachingAdapter.FunctionNames.GET_PLAYLIST_DETAILS:
playlist_data = asdict(data) playlist_data = asdict(data)
# TODO deal with the songs playlist, created = models.Playlist.get_or_create(
del playlist_data['songs'] id=playlist_data['id'],
models.Playlist.insert( defaults=playlist_data,
playlist_data).on_conflict_replace().execute() )
# Handle the songs.
f = ('id', 'title', 'duration')
playlist.songs = [
models.Song.create(
**dict(filter(lambda kv: kv[0] in f, s.items())))
for s in playlist_data['songs']
]
# Update the values if the playlist already existed.
if not created:
for k, v in playlist_data.items():
setattr(playlist, k, v)
playlist.save()

View File

@@ -24,10 +24,10 @@ database = SqliteDatabase(None)
# ============================================================================= # =============================================================================
class DurationField(IntegerField): class DurationField(IntegerField):
def db_value(self, value: timedelta) -> Optional[int]: def db_value(self, value: timedelta) -> Optional[int]:
return value.microseconds if value else None return value.total_seconds() if value else None
def python_value(self, value: Optional[int]) -> Optional[timedelta]: def python_value(self, value: Optional[int]) -> Optional[timedelta]:
return timedelta(microseconds=value) if value else None return timedelta(seconds=value) if value else None
class CacheConstantsField(TextField): class CacheConstantsField(TextField):
@@ -53,6 +53,37 @@ class CoverArt(BaseModel):
class Song(BaseModel): class Song(BaseModel):
id = TextField(unique=True, primary_key=True) id = TextField(unique=True, primary_key=True)
title = TextField()
duration = DurationField()
# parent: Optional[str] = None
# album: Optional[str] = None
# artist: Optional[str] = None
# track: Optional[int] = None
# year: Optional[int] = None
# genre: Optional[str] = None
# cover_art: Optional[str] = None
# size: Optional[int] = None
# content_type: Optional[str] = None
# suffix: Optional[str] = None
# transcoded_content_type: Optional[str] = None
# transcoded_suffix: Optional[str] = None
# duration: Optional[int] = None
# bit_rate: Optional[int] = None
# path: Optional[str] = None
# is_video: Optional[bool] = None
# user_rating: Optional[int] = None
# average_rating: Optional[float] = None
# play_count: Optional[int] = None
# disc_number: Optional[int] = None
# created: Optional[datetime] = None
# starred: Optional[datetime] = None
# album_id: Optional[str] = None
# artist_id: Optional[str] = None
# type: Optional[SublimeAPI.MediaType] = None
# bookmark_position: Optional[int] = None
# original_width: Optional[int] = None
# original_height: Optional[int] = None
class CacheInfo(BaseModel): class CacheInfo(BaseModel):

View File

@@ -187,5 +187,6 @@ class SubsonicAdapter(Adapter):
self._make_url('getPlaylist'), self._make_url('getPlaylist'),
id=playlist_id, id=playlist_id,
).playlist ).playlist
# TODO better error
assert result, f'Error getting playlist {playlist_id}' assert result, f'Error getting playlist {playlist_id}'
return result return result

View File

@@ -5,6 +5,8 @@ These are the API objects that are returned by Subsonic.
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import List, Optional from typing import List, Optional
import operator
from functools import reduce
import dataclasses_json import dataclasses_json
from dataclasses_json import ( from dataclasses_json import (
@@ -34,7 +36,6 @@ for type_, translation_function in extra_translation_map.items():
class Child(SublimeAPI.Song): class Child(SublimeAPI.Song):
id: str id: str
title: str title: str
value: Optional[str] = None
parent: Optional[str] = None parent: Optional[str] = None
album: Optional[str] = None album: Optional[str] = None
artist: Optional[str] = None artist: Optional[str] = None
@@ -47,7 +48,7 @@ class Child(SublimeAPI.Song):
suffix: Optional[str] = None suffix: Optional[str] = None
transcoded_content_type: Optional[str] = None transcoded_content_type: Optional[str] = None
transcoded_suffix: Optional[str] = None transcoded_suffix: Optional[str] = None
duration: Optional[int] = None duration: Optional[timedelta] = None
bit_rate: Optional[int] = None bit_rate: Optional[int] = None
path: Optional[str] = None path: Optional[str] = None
is_video: Optional[bool] = None is_video: Optional[bool] = None
@@ -99,7 +100,7 @@ class PlaylistWithSongs(SublimeAPI.PlaylistDetails):
def __post_init__(self): def __post_init__(self):
self.song_count = self.song_count or len(self.songs) self.song_count = self.song_count or len(self.songs)
self.duration = self.duration or timedelta( self.duration = self.duration or timedelta(
seconds=sum(s.duration for s in self.songs)) seconds=sum(s.duration.total_seconds() if s.duration else 0 for s in self.songs))
@dataclass @dataclass

View File

@@ -98,12 +98,27 @@ def test_caching_get_playlist_details(
# Ingest an empty list (for example, no playlists added yet to server). # Ingest an empty list (for example, no playlists added yet to server).
cache_adapter.ingest_new_data( cache_adapter.ingest_new_data(
FilesystemAdapter.FunctionNames.GET_PLAYLIST_DETAILS, ('1', ), FilesystemAdapter.FunctionNames.GET_PLAYLIST_DETAILS,
SubsonicAPI.PlaylistWithSongs('1', 'test1', songs=[])) ('1', ),
SubsonicAPI.PlaylistWithSongs(
'1',
'test1',
songs=[
SubsonicAPI.Child(
'1', 'Song 1', duration=timedelta(seconds=10.2)),
SubsonicAPI.Child(
'2', 'Song 2', duration=timedelta(seconds=20.8)),
],
),
)
playlist = cache_adapter.get_playlist_details('1') playlist = cache_adapter.get_playlist_details('1')
assert playlist.id == '1' assert playlist.id == '1'
assert playlist.name == 'test1' assert playlist.name == 'test1'
assert playlist.song_count == 2
assert playlist.duration == timedelta(seconds=31)
assert (playlist.songs[0].id, playlist.songs[0].title) == ('1', 'Song 1')
assert (playlist.songs[1].id, playlist.songs[1].title) == ('2', 'Song 2')
with pytest.raises(CacheMissError): with pytest.raises(CacheMissError):
cache_adapter.get_playlist_details('2') cache_adapter.get_playlist_details('2')

View File

@@ -159,7 +159,7 @@ def test_get_playlist_details(adapter: SubsonicAdapter):
suffix="m4a", suffix="m4a",
transcoded_content_type="audio/mpeg", transcoded_content_type="audio/mpeg",
transcoded_suffix="mp3", transcoded_suffix="mp3",
duration=238, duration=timedelta(seconds=238),
bit_rate=256, bit_rate=256,
path='/'.join( path='/'.join(
( (