Added some tests for the playlist details endpoint
This commit is contained in:
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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),
|
||||
),
|
||||
]
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user