More endpoints

This commit is contained in:
Sumner Evans
2019-05-15 00:23:03 -06:00
parent fa516471e7
commit 700f341b47
3 changed files with 223 additions and 37 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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