Added some tests for the playlist details endpoint

This commit is contained in:
Sumner Evans
2020-04-20 10:02:19 -06:00
parent b740729ca0
commit b14d20b4cd
6 changed files with 185 additions and 57 deletions

View File

@@ -3,14 +3,54 @@ Defines the objects that are returned by adapter methods.
"""
import abc
from datetime import datetime, timedelta
from enum import Enum
from typing import Optional, Sequence
class MediaType(Enum):
MUSIC = 'music'
PODCAST = 'podcast'
AUDIOBOOK = 'audiobook'
VIDEO = 'video'
class Song(abc.ABC):
id: str
title: str
value: Optional[str]
parent: Optional[str]
album: Optional[str]
artist: Optional[str]
track: Optional[int]
year: Optional[int]
genre: Optional[str]
cover_art: Optional[str]
size: Optional[int]
content_type: Optional[str]
suffix: Optional[str]
transcoded_content_type: Optional[str]
transcoded_suffix: Optional[str]
duration: Optional[int]
bit_rate: Optional[int]
path: Optional[str]
is_video: Optional[bool]
user_rating: Optional[int]
average_rating: Optional[float]
play_count: Optional[int]
disc_number: Optional[int]
created: Optional[datetime]
starred: Optional[datetime]
album_id: Optional[str]
artist_id: Optional[str]
type: Optional[MediaType]
bookmark_position: Optional[int]
original_width: Optional[int]
original_height: Optional[int]
# TODO trim down
class Playlist(abc.ABC):
# TODO trim down
id: str
name: str
song_count: Optional[int]
@@ -24,6 +64,7 @@ class Playlist(abc.ABC):
class PlaylistDetails(abc.ABC):
# TODO trim down
id: str
name: str
song_count: int

View File

@@ -117,6 +117,7 @@ class SubsonicAdapter(Adapter):
def _get_json(
self,
url: str,
timeout: Union[float, Tuple[float, float], None] = None,
**params: Union[None, str, datetime, int, Sequence[int]],
) -> Response:
"""
@@ -126,7 +127,7 @@ class SubsonicAdapter(Adapter):
:returns: a dictionary of the subsonic response.
:raises Exception: needs some work TODO
"""
result = self._get(url, **params)
result = self._get(url, timeout=timeout, **params)
subsonic_response = result.json().get('subsonic-response')
# TODO (#122): make better
@@ -185,12 +186,5 @@ class SubsonicAdapter(Adapter):
self._make_url('getPlaylist'),
id=playlist_id,
).playlist
print(result)
assert result, f'Error getting playlist {playlist_id}'
return result
# assert result
# result['duration'] = result.get('duration') or sum(
# s.get('duration') or 0 for s in result['entry'])
# result['songCount'] = result.get('songCount') or len(result['entry'])
# songs = [Song(id=s['id']) for s in result['entry']]
# del result['entry']
# return API.PlaylistDetails(**self._to_snake_case(result), songs=songs)

View File

@@ -13,22 +13,18 @@ from marshmallow import fields
from .. import api_objects as SublimeAPI
datetime_metadata = config(
encoder=datetime.isoformat,
decoder=lambda d: parser.parse(d) if d else None,
mm_field=fields.DateTime(format='iso'),
)
timedelta_metadata = config(
encoder=datetime.isoformat,
decoder=lambda s: timedelta(seconds=s) if s else None,
mm_field=fields.TimeDelta(),
)
dataclasses_json.cfg.global_config.encoders[datetime] = datetime.isoformat
dataclasses_json.cfg.global_config.decoders[datetime] = parser.parse
dataclasses_json.cfg.global_config.encoders[timedelta] = (
timedelta.total_seconds)
dataclasses_json.cfg.global_config.decoders[timedelta] = lambda s: timedelta(
seconds=s)
@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass(frozen=True)
class Child(SublimeAPI.Song, DataClassJsonMixin):
class Child(SublimeAPI.Song):
id: str
title: str
value: Optional[str] = None
parent: Optional[str] = None
@@ -37,7 +33,7 @@ class Child(SublimeAPI.Song, DataClassJsonMixin):
track: Optional[int] = None
year: Optional[int] = None
genre: Optional[str] = None
coverArt: Optional[str] = None
cover_art: Optional[str] = None
size: Optional[int] = None
content_type: Optional[str] = None
suffix: Optional[str] = None
@@ -46,16 +42,16 @@ class Child(SublimeAPI.Song, DataClassJsonMixin):
duration: Optional[int] = None
bit_rate: Optional[int] = None
path: Optional[str] = None
isVideo: Optional[bool] = None
# userRating: Optional[UserRating] = None
# averageRating: Optional[AverageRating] = 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
albumId: Optional[str] = None
artistId: Optional[str] = None
# type_: Optional[MediaType] = 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
@@ -67,12 +63,9 @@ class Playlist(SublimeAPI.Playlist):
id: str
name: str
song_count: Optional[int] = None
duration: Optional[timedelta] = field(
default=None, metadata=timedelta_metadata)
created: Optional[datetime] = field(
default=None, metadata=datetime_metadata)
changed: Optional[datetime] = field(
default=None, metadata=datetime_metadata)
duration: Optional[timedelta] = None
created: Optional[datetime] = None
changed: Optional[datetime] = None
comment: Optional[str] = None
owner: Optional[str] = None
public: Optional[bool] = None
@@ -82,16 +75,22 @@ class Playlist(SublimeAPI.Playlist):
@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class PlaylistWithSongs(SublimeAPI.PlaylistDetails):
duration: timedelta = field(
default_factory=timedelta, metadata=timedelta_metadata)
songs: List[SublimeAPI.Song] = field(
default_factory=list,
metadata=config(field_name='entry'),
)
created: Optional[datetime] = field(
default=None, metadata=datetime_metadata)
changed: Optional[datetime] = field(
default=None, metadata=datetime_metadata)
id: str
name: str
songs: List[Child] = field(metadata=config(field_name='entry'))
song_count: int = field(default=0)
duration: timedelta = field(default=timedelta())
created: Optional[datetime] = None
changed: Optional[datetime] = None
comment: Optional[str] = None
owner: Optional[str] = None
public: Optional[bool] = None
cover_art: Optional[str] = None
def __post_init__(self):
self.song_count = self.song_count or len(self.songs)
self.duration = self.duration or timedelta(
seconds=sum(s.duration for s in self.songs))
@dataclass(frozen=True)
@@ -107,4 +106,3 @@ class Response(DataClassJsonMixin):
song: Optional[Child] = None
playlists: Optional[Playlists] = None
playlist: Optional[PlaylistWithSongs] = None
value: Optional[str] = None

View File

@@ -8,9 +8,9 @@ from fuzzywuzzy import process
from gi.repository import Gdk, Gio, GLib, GObject, Gtk, Pango
from sublime.adapters import AdapterManager
from sublime.adapters.api_objects import Playlist, PlaylistDetails
from sublime.cache_manager import CacheManager
from sublime.config import AppConfiguration
from sublime.server.api_objects import PlaylistWithSongs
from sublime.ui import util
from sublime.ui.common import (
EditFormDialog,
@@ -188,7 +188,7 @@ class PlaylistList(Gtk.Box):
)
def update_list(
self,
playlists: List[PlaylistWithSongs],
playlists: List[Playlist],
app_config: AppConfiguration,
force: bool = False,
order_token: int = None,
@@ -468,7 +468,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
)
def update_playlist_view(
self,
playlist: PlaylistWithSongs,
playlist: PlaylistDetails,
app_config: AppConfiguration = None,
force: bool = False,
order_token: int = None,
@@ -495,7 +495,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
# Update the artwork.
self.update_playlist_artwork(
playlist.coverArt,
playlist.cover_art,
order_token=order_token,
)
@@ -512,7 +512,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
song.artist,
util.format_song_duration(song.duration),
song.id,
] for song in (playlist.entry or [])
] for song in playlist.songs
]
util.diff_song_store(self.playlist_song_store, new_store)
@@ -757,14 +757,14 @@ class PlaylistDetailPanel(Gtk.Overlay):
@util.async_callback(lambda *a, **k: CacheManager.get_playlist(*a, **k))
def _update_playlist_order(
self,
playlist: PlaylistWithSongs,
playlist: PlaylistDetails,
app_config: AppConfiguration,
**kwargs,
):
self.playlist_view_loading_box.show_all()
update_playlist_future = CacheManager.update_playlist(
playlist_id=playlist.id,
song_index_to_remove=list(range(playlist.songCount)),
song_index_to_remove=list(range(playlist.song_count)),
song_id_to_add=[s[-1] for s in self.playlist_song_store],
)
@@ -776,7 +776,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
order_token=self.update_playlist_view_order_token,
)))
def _format_stats(self, playlist: PlaylistWithSongs) -> str:
def _format_stats(self, playlist: PlaylistDetails) -> str:
created_date = playlist.created.strftime('%B %d, %Y')
lines = [
util.dot_join(
@@ -785,8 +785,8 @@ class PlaylistDetailPanel(Gtk.Overlay):
),
util.dot_join(
'{} {}'.format(
playlist.songCount,
util.pluralize("song", playlist.songCount)),
playlist.song_count,
util.pluralize("song", playlist.song_count)),
util.format_sequence_duration(playlist.duration),
),
]

View File

@@ -1,8 +1,9 @@
import functools
import re
from concurrent.futures import Future
from datetime import timedelta
from typing import (
Any, Callable, cast, Iterable, List, Match, Optional, Tuple, Union)
Any, Callable, cast, Iterable, List, Match, Optional,Tuple, Union,)
import gi
from deepdiff import DeepDiff
@@ -50,7 +51,7 @@ def pluralize(
return string
def format_sequence_duration(duration_secs: int) -> str:
def format_sequence_duration(duration_secs: Union[int, timedelta]) -> str:
"""
Formats duration in English.
@@ -61,6 +62,9 @@ def format_sequence_duration(duration_secs: int) -> str:
>>> format_sequence_duration(60 * 60 + 120)
'1 hour, 2 minutes'
"""
# TODO remove int compatibility eventually
if isinstance(duration_secs, timedelta):
duration_secs = int(duration_secs.total_seconds())
duration_mins = (duration_secs // 60) % 60
duration_hrs = duration_secs // 60 // 60
duration_secs = duration_secs % 60

View File

@@ -138,3 +138,94 @@ def test_get_playlists(subsonic_adapter: SubsonicAdapter):
),
]
assert subsonic_adapter.get_playlists() == expected
# When playlists is null, expect an empty list.
subsonic_adapter._set_mock_data(mock_json())
assert subsonic_adapter.get_playlists() == []
def test_get_playlist_details(subsonic_adapter: SubsonicAdapter):
playlist = {
"id":
"6",
"name":
"Playlist 1",
"comment":
"Foo",
"owner":
"test",
"public":
True,
"songCount":
2,
"duration":
952,
"created":
"2020-03-27T05:39:43.327Z",
"changed":
"2020-03-27T05:44:37.275Z",
"coverArt":
"pl-6",
"entry": [
{
"id": "202",
"parent": "318",
"isDir": False,
"title": "What a Beautiful Name",
"album": "What a Beautiful Name - Single",
"artist": "Hillsong Worship",
"track": 1,
"year": 2016,
"genre": "Christian & Gospel",
"coverArt": "318",
"size": 8381640,
"contentType": "audio/mp4",
"suffix": "m4a",
"transcodedContentType": "audio/mpeg",
"transcodedSuffix": "mp3",
"duration": 238,
"bitRate": 256,
"path":
"Hillsong Worship/What a Beautiful Name - Single/01 What a Beautiful Name.m4a",
"isVideo": False,
"playCount": 20,
"discNumber": 1,
"created": "2020-03-27T05:17:07.000Z",
"albumId": "48",
"artistId": "38",
"type": "music"
},
{
"id": "203",
"parent": "319",
"isDir": False,
"title": "Great Are You Lord",
"album": "Great Are You Lord EP",
"artist": "one sonic society",
"track": 1,
"year": 2016,
"genre": "Christian & Gospel",
"coverArt": "319",
"size": 8245813,
"contentType": "audio/mp4",
"suffix": "m4a",
"transcodedContentType": "audio/mpeg",
"transcodedSuffix": "mp3",
"duration": 232,
"bitRate": 256,
"path":
"one sonic society/Great Are You Lord EP/01 Great Are You Lord.m4a",
"isVideo": False,
"playCount": 18,
"discNumber": 1,
"created": "2020-03-27T05:32:42.000Z",
"albumId": "10",
"artistId": "8",
"type": "music"
},
],
}
subsonic_adapter._set_mock_data(mock_json(playlist=playlist))
playlist_details = subsonic_adapter.get_playlist_details('6')
assert len(playlist_details.songs) == 2