More endpoints
This commit is contained in:
@@ -17,12 +17,16 @@ def main():
|
|||||||
password=sys.argv[2])
|
password=sys.argv[2])
|
||||||
|
|
||||||
# print(server.ping())
|
# print(server.ping())
|
||||||
print(server.get_license())
|
# print(server.get_license())
|
||||||
print(server.get_music_folders())
|
# print(server.get_music_folders())
|
||||||
# print(server.get_indexes())
|
# print(server.get_indexes())
|
||||||
# print()
|
# print(server.get_music_directory(581))
|
||||||
# print(server.get_music_directory(599))
|
|
||||||
# print(server.get_genres())
|
# 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 = MainWindow()
|
||||||
# win.connect("destroy", Gtk.main_quit)
|
# win.connect("destroy", Gtk.main_quit)
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import inspect
|
|
||||||
import typing
|
import typing
|
||||||
from typing import Dict, List, Any, Type
|
from typing import Dict, List, Any, Type
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -10,10 +9,17 @@ def _from_json(cls, data):
|
|||||||
Approach for deserialization here:
|
Approach for deserialization here:
|
||||||
https://stackoverflow.com/a/40639688/2319844
|
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__', {})
|
annotations: Dict[str, Type] = getattr(cls, '__annotations__', {})
|
||||||
|
|
||||||
# Handle lists of objects.
|
# Handle primitive of objects
|
||||||
if cls == str:
|
if data is None:
|
||||||
|
instance = None
|
||||||
|
elif cls == str:
|
||||||
instance = data
|
instance = data
|
||||||
elif cls == int:
|
elif cls == int:
|
||||||
instance = int(data)
|
instance = int(data)
|
||||||
@@ -21,17 +27,19 @@ def _from_json(cls, data):
|
|||||||
instance = bool(data)
|
instance = bool(data)
|
||||||
elif cls == datetime:
|
elif cls == datetime:
|
||||||
instance = parser.parse(data)
|
instance = parser.parse(data)
|
||||||
|
|
||||||
|
# Handle generics. List[*], Dict[*, *] in particular.
|
||||||
elif type(cls) == typing._GenericAlias:
|
elif type(cls) == typing._GenericAlias:
|
||||||
# Having to use this because things changed in Python 3.7.
|
# 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 class_name == 'List':
|
||||||
if cls._name == 'List':
|
|
||||||
list_type = cls.__args__[0]
|
list_type = cls.__args__[0]
|
||||||
instance: List[list_type] = list()
|
instance: List[list_type] = list()
|
||||||
for value in data:
|
for value in data:
|
||||||
instance.append(_from_json(list_type, value))
|
instance.append(_from_json(list_type, value))
|
||||||
|
|
||||||
elif cls._name == 'Dict':
|
elif class_name == 'Dict':
|
||||||
key_type, val_type = cls.__args__
|
key_type, val_type = cls.__args__
|
||||||
instance: Dict[key_type, val_type] = dict()
|
instance: Dict[key_type, val_type] = dict()
|
||||||
for key, value in data.items():
|
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.
|
# all of the sub-elements, recursively calling from_json on them.
|
||||||
else:
|
else:
|
||||||
instance: cls = cls()
|
instance: cls = cls()
|
||||||
for name, value in data.items():
|
for field, field_type in annotations.items():
|
||||||
field_type = annotations.get(name)
|
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.
|
# # Sometimes there are extraneous values, ignore them.
|
||||||
if field_type:
|
# if field_type:
|
||||||
setattr(instance, name, _from_json(field_type, value))
|
# setattr(instance, name, _from_json(field_type, value))
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
@@ -65,7 +76,7 @@ class APIObject:
|
|||||||
return getattr(self, field, default)
|
return getattr(self, field, default)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
annotations: Dict[str, Any] = self.__annotations__
|
annotations: Dict[str, Any] = self.get('__annotations__', {})
|
||||||
typename = type(self).__name__
|
typename = type(self).__name__
|
||||||
fieldstr = ' '.join([
|
fieldstr = ' '.join([
|
||||||
f'{field}={getattr(self, field)!r}'
|
f'{field}={getattr(self, field)!r}'
|
||||||
@@ -94,9 +105,129 @@ class MusicFolder(APIObject):
|
|||||||
name: str
|
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):
|
class SubsonicResponse(APIObject):
|
||||||
status: str
|
status: str
|
||||||
version: str
|
version: str
|
||||||
license: License
|
license: License
|
||||||
error: SubsonicError
|
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
|
||||||
|
@@ -1,16 +1,23 @@
|
|||||||
import requests
|
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:
|
class Server:
|
||||||
"""Defines a *Sonic server."""
|
"""Defines a *Sonic server."""
|
||||||
|
|
||||||
def __init__(self, name=None, hostname=None, username=None, password=None):
|
def __init__(self,
|
||||||
self.name = name
|
name: str = None,
|
||||||
self.hostname = hostname
|
hostname: str = None,
|
||||||
self.username = username
|
username: str = None,
|
||||||
self.password = password
|
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):
|
def _get_params(self):
|
||||||
return dict(
|
return dict(
|
||||||
@@ -21,13 +28,18 @@ class Server:
|
|||||||
v='1.15.0',
|
v='1.15.0',
|
||||||
)
|
)
|
||||||
|
|
||||||
def _make_url(self, endpoint):
|
def _make_url(self, endpoint: str) -> str:
|
||||||
return f'{self.hostname}/rest/{endpoint}.view'
|
return f'{self.hostname}/rest/{endpoint}.view'
|
||||||
|
|
||||||
def _post(self, url, **params):
|
def _post(self, url, **params) -> SubsonicResponse:
|
||||||
params = {**self._get_params(), **params}
|
params = {**self._get_params(), **params}
|
||||||
result = requests.post(url, data=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']
|
subsonic_response = result.json()['subsonic-response']
|
||||||
|
|
||||||
# TODO make better
|
# TODO make better
|
||||||
if not subsonic_response:
|
if not subsonic_response:
|
||||||
raise Exception('Fail!')
|
raise Exception('Fail!')
|
||||||
@@ -49,20 +61,59 @@ class Server:
|
|||||||
result = self._post(self._make_url('getLicense'))
|
result = self._post(self._make_url('getLicense'))
|
||||||
return result.license
|
return result.license
|
||||||
|
|
||||||
def get_music_folders(self):
|
def get_music_folders(self) -> List[MusicFolder]:
|
||||||
result = self._post(self._make_url('getMusicFolders'))
|
result = self._post(self._make_url('getMusicFolders'))
|
||||||
# The Airsonic API implementation of this is dumb. It gives totally the
|
return result.musicFolders.musicFolder
|
||||||
# wrong answer so we have to go in to the 'musicFolder' key here.
|
|
||||||
return result.musicFolders['musicFolder']
|
|
||||||
|
|
||||||
def get_indexes(self):
|
def get_indexes(self,
|
||||||
result = self._post(self._make_url('getIndexes'))
|
music_folder_id: int = None,
|
||||||
return result
|
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):
|
def get_music_directory(self, dir_id) -> Directory:
|
||||||
result = self._post(self._make_url('getIndexes'), id=str(dir_id))
|
result = self._post(self._make_url('getMusicDirectory'),
|
||||||
return result
|
id=str(dir_id))
|
||||||
|
return result.directory
|
||||||
|
|
||||||
def get_genres(self):
|
def get_genres(self) -> List[Genre]:
|
||||||
result = self._post(self._make_url('getGenres'))
|
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
|
||||||
|
Reference in New Issue
Block a user