From 700f341b47108af6b7680efd18ac494e80cca753 Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Wed, 15 May 2019 00:23:03 -0600 Subject: [PATCH] More endpoints --- libremsonic/__main__.py | 12 ++- libremsonic/server/api_objects.py | 157 +++++++++++++++++++++++++++--- libremsonic/server/server.py | 91 +++++++++++++---- 3 files changed, 223 insertions(+), 37 deletions(-) diff --git a/libremsonic/__main__.py b/libremsonic/__main__.py index c3ee2eb..bccde9b 100644 --- a/libremsonic/__main__.py +++ b/libremsonic/__main__.py @@ -17,12 +17,16 @@ def main(): password=sys.argv[2]) # print(server.ping()) - print(server.get_license()) - print(server.get_music_folders()) + # print(server.get_license()) + # print(server.get_music_folders()) # print(server.get_indexes()) - # print() - # print(server.get_music_directory(599)) + # print(server.get_music_directory(581)) # print(server.get_genres()) + # print(server.get_artists()) + # print(server.get_artist(20)) + # print(server.get_album(31)) + # print(server.get_song(203)) + print(server.get_artist_info(20)) # win = MainWindow() # win.connect("destroy", Gtk.main_quit) diff --git a/libremsonic/server/api_objects.py b/libremsonic/server/api_objects.py index 6019f4a..59db37a 100644 --- a/libremsonic/server/api_objects.py +++ b/libremsonic/server/api_objects.py @@ -1,4 +1,3 @@ -import inspect import typing from typing import Dict, List, Any, Type from datetime import datetime @@ -10,10 +9,17 @@ def _from_json(cls, data): Approach for deserialization here: https://stackoverflow.com/a/40639688/2319844 """ + # If it's a forward reference, evaluate it to figure out the actual + # type. + if isinstance(cls, typing.ForwardRef): + cls = cls._evaluate(globals(), locals()) + annotations: Dict[str, Type] = getattr(cls, '__annotations__', {}) - # Handle lists of objects. - if cls == str: + # Handle primitive of objects + if data is None: + instance = None + elif cls == str: instance = data elif cls == int: instance = int(data) @@ -21,17 +27,19 @@ def _from_json(cls, data): instance = bool(data) elif cls == datetime: instance = parser.parse(data) + + # Handle generics. List[*], Dict[*, *] in particular. elif type(cls) == typing._GenericAlias: # Having to use this because things changed in Python 3.7. + class_name = cls._name - # No idea what the heck this is, but let's go with it. - if cls._name == 'List': + if class_name == 'List': list_type = cls.__args__[0] instance: List[list_type] = list() for value in data: instance.append(_from_json(list_type, value)) - elif cls._name == 'Dict': + elif class_name == 'Dict': key_type, val_type = cls.__args__ instance: Dict[key_type, val_type] = dict() for key, value in data.items(): @@ -46,12 +54,15 @@ def _from_json(cls, data): # all of the sub-elements, recursively calling from_json on them. else: instance: cls = cls() - for name, value in data.items(): - field_type = annotations.get(name) + for field, field_type in annotations.items(): + value = data.get(field) + setattr(instance, field, _from_json(field_type, value)) + # for name, value in data.items(): + # field_type = annotations.get(name) - # Sometimes there are extraneous values, ignore them. - if field_type: - setattr(instance, name, _from_json(field_type, value)) + # # Sometimes there are extraneous values, ignore them. + # if field_type: + # setattr(instance, name, _from_json(field_type, value)) return instance @@ -65,7 +76,7 @@ class APIObject: return getattr(self, field, default) def __repr__(self): - annotations: Dict[str, Any] = self.__annotations__ + annotations: Dict[str, Any] = self.get('__annotations__', {}) typename = type(self).__name__ fieldstr = ' '.join([ f'{field}={getattr(self, field)!r}' @@ -94,9 +105,129 @@ class MusicFolder(APIObject): name: str +class File(APIObject): + id: int + parent: int + title: str + isDir: bool + album: str + artist: str + track: str + year: str + genre: str + coverArt: int + size: int + contentType: str + isVideo: bool + transcodedSuffix: str + transcodedContentType: str + suffix: str + duration: int + bitRate: int + path: str + playCount: int + created: datetime + + +class Album(APIObject): + id: int + name: str + artist: str + artistId: int + coverArt: str + songCount: int + duration: int + created: datetime + year: str + genre: str + + song: List[File] + + +class Artist(APIObject): + id: int + name: str + coverArt: str + albumCount: int + album: List[Album] + + +class Shortcut(APIObject): + id: int + name: str + + +class Index(APIObject): + name: str + artist: List[Artist] + + +class Indexes(APIObject): + lastModified: int + ignoredArticles: str + index: List[Index] + shortcut: List[Shortcut] + child: List[File] + + +class Directory(APIObject): + id: int + parent: str + name: str + playCount: int + child: List[File] + + +class Genre(APIObject): + songCount: int + albumCount: int + vvalue: str + + +class MusicFolders(APIObject): + musicFolder: List[MusicFolder] + + +class Genres(APIObject): + genre: List[Genre] + + +class Artists(APIObject): + index: List[Index] + + +class Videos(APIObject): + video: List[File] + + +class VideoInfo(APIObject): + # TODO implement when I have videos + pass + + +class ArtistInfo(APIObject): + biography: str + musicBrainzId: str + lastFmUrl: str + smallImageUrl: str + mediumImageUrl: str + largeImageUrl: str + similarArtist: List[Artist] + + class SubsonicResponse(APIObject): status: str version: str license: License error: SubsonicError - musicFolders: Dict[str, List[MusicFolder]] + musicFolders: MusicFolders + indexes: Indexes + directory: Directory + genres: Genres + artists: Artists + artist: Artist + album: Album + song: File + videos: Videos + videoInfo: VideoInfo + artistInfo: ArtistInfo diff --git a/libremsonic/server/server.py b/libremsonic/server/server.py index 3f2b1f1..7836569 100644 --- a/libremsonic/server/server.py +++ b/libremsonic/server/server.py @@ -1,16 +1,23 @@ import requests +from typing import List, Optional -from .api_objects import SubsonicResponse, License +from .api_objects import (SubsonicResponse, License, MusicFolder, Indexes, + ArtistInfo, VideoInfo, File, Album, Artist, Artists, + Directory, Genre) class Server: """Defines a *Sonic server.""" - def __init__(self, name=None, hostname=None, username=None, password=None): - self.name = name - self.hostname = hostname - self.username = username - self.password = password + def __init__(self, + name: str = None, + hostname: str = None, + username: str = None, + password: str = None): + self.name: Optional[str] = name + self.hostname: Optional[str] = hostname + self.username: Optional[str] = username + self.password: Optional[str] = password def _get_params(self): return dict( @@ -21,13 +28,18 @@ class Server: v='1.15.0', ) - def _make_url(self, endpoint): + def _make_url(self, endpoint: str) -> str: return f'{self.hostname}/rest/{endpoint}.view' - def _post(self, url, **params): + def _post(self, url, **params) -> SubsonicResponse: params = {**self._get_params(), **params} result = requests.post(url, data=params) + # TODO make better + if result.status_code != 200: + raise Exception(f'Fail! {result.status_code}') + subsonic_response = result.json()['subsonic-response'] + # TODO make better if not subsonic_response: raise Exception('Fail!') @@ -49,20 +61,59 @@ class Server: result = self._post(self._make_url('getLicense')) return result.license - def get_music_folders(self): + def get_music_folders(self) -> List[MusicFolder]: result = self._post(self._make_url('getMusicFolders')) - # The Airsonic API implementation of this is dumb. It gives totally the - # wrong answer so we have to go in to the 'musicFolder' key here. - return result.musicFolders['musicFolder'] + return result.musicFolders.musicFolder - def get_indexes(self): - result = self._post(self._make_url('getIndexes')) - return result + def get_indexes(self, + music_folder_id: int = None, + if_modified_since: int = None) -> Indexes: + result = self._post(self._make_url('getIndexes'), + musicFolderId=music_folder_id, + ifModifiedSince=if_modified_since) + return result.indexes - def get_music_directory(self, dir_id): - result = self._post(self._make_url('getIndexes'), id=str(dir_id)) - return result + def get_music_directory(self, dir_id) -> Directory: + result = self._post(self._make_url('getMusicDirectory'), + id=str(dir_id)) + return result.directory - def get_genres(self): + def get_genres(self) -> List[Genre]: result = self._post(self._make_url('getGenres')) - return result + return result.genres.genre + + def get_artists(self, music_folder_id: int = None) -> Artists: + result = self._post(self._make_url('getArtists'), + musicFolderId=music_folder_id) + return result.artists + + def get_artist(self, artist_id: int) -> Artist: + result = self._post(self._make_url('getArtist'), id=artist_id) + return result.artist + + def get_album(self, album_id: int) -> Album: + result = self._post(self._make_url('getAlbum'), id=album_id) + return result.album + + def get_song(self, song_id: int) -> File: + result = self._post(self._make_url('getSong'), id=song_id) + return result.song + + def get_videos(self) -> Optional[List[File]]: + result = self._post(self._make_url('getVideos')) + return result.videos.video + + def get_video_info(self, video_id: int) -> Optional[VideoInfo]: + result = self._post(self._make_url('getVideoInfo'), id=video_id) + return result.videoInfo + + def get_artist_info(self, + artist_id: int, + count: int = None, + include_not_present: bool = None + ) -> Optional[ArtistInfo]: + result = self._post(self._make_url('getArtistInfo'), + id=artist_id, + count=count, + includeNotPresent=include_not_present) + return result.artistInfo