Cache songs on get playlist details
This commit is contained in:
@@ -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()
|
||||||
|
@@ -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):
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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')
|
||||||
|
@@ -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(
|
||||||
(
|
(
|
||||||
|
Reference in New Issue
Block a user