Finished implementing server

This commit is contained in:
Sumner Evans
2019-06-22 10:45:41 -06:00
parent 36d6193fa2
commit 0a48c756bd
2 changed files with 574 additions and 12 deletions

View File

@@ -17,6 +17,6 @@ def main():
print(server.ping())
# print(server.search2('The Band Perry'))
stream = server.stream(740)
stream = server.download(740)
# app = LibremsonicApp()
# app.run(sys.argv)

View File

@@ -1,6 +1,6 @@
import math
from deprecated import deprecated
from typing import Any, Optional, Dict, List, Union, Iterator
from typing import Any, Optional, Dict, List, Union, Iterator, cast
from datetime import datetime
import requests
@@ -13,31 +13,48 @@ from .api_objects import (
ArtistInfo,
ArtistsID3,
ArtistWithAlbumsID3,
Bookmarks,
Child,
Directory,
Genres,
Indexes,
InternetRadioStations,
License,
Lyrics,
MusicFolders,
NowPlaying,
Playlists,
PlaylistWithSongs,
PlayQueue,
Response,
ScanStatus,
SearchResult,
SearchResult2,
SearchResult3,
Shares,
Starred,
Starred2,
Songs,
User,
Users,
VideoInfo,
)
class Server:
"""Defines a *Sonic server."""
"""
Defines a *Sonic server.
Notes:
* The ``hls`` endpoint is not supported.
* The ``getCaptions`` endpoint is not supported
* None of the podcast endpoints are supported.
* The ``jukeboxControl`` endpoint is not supported.
* None of the chat message endpoints are supported.
"""
def __init__(self, name: str, hostname: str, username: str, password: str):
# TODO handle these optionals better.
self.name: str = name
self.hostname: str = hostname
self.username: str = username
@@ -59,7 +76,11 @@ class Server:
def _subsonic_error_to_exception(self, error):
return Exception(f'{error.code}: {error.message}')
def _post(self, url, **params) -> Response:
def _post(
self,
url: str,
**params: Union[None, str, datetime, int, List[int]],
) -> Response:
"""
Make a post to a *Sonic REST API. Handle all types of errors including
*Sonic ``<error>`` responses.
@@ -69,6 +90,12 @@ class Server:
:raises Exception: needs some work TODO
"""
params = {**self._get_params(), **params}
# Deal with datetime parameters (convert to milliseconds since 1970)
for k, v in params.items():
if type(v) == datetime:
params[k] = int(cast(datetime, v).timestamp() * 1000)
result = requests.post(url, data=params)
# TODO make better
if result.status_code != 200:
@@ -148,8 +175,7 @@ class Server:
:param music_folder_id: If specified, only return artists in the music
folder with the given ID. See ``getMusicFolders``.
:param if_modified_since: If specified, only return a result if the
artist collection has changed since the given time (in milliseconds
since 1 Jan 1970).
artist collection has changed since the given time.
"""
result = self._post(self._make_url('getIndexes'),
musicFolderId=music_folder_id,
@@ -711,10 +737,10 @@ class Server:
:param comment: The playlist comment.
:param public: ``true`` if the playlist should be visible to all users,
``false`` otherwise.
:param song_id_to_add: Add this song with this ID to the playlist.
Multiple parameters allowed.
:param song_id_to_remove: Remove the song at this position in the
playlist. Multiple parameters allowed.
:param song_id_to_add: Add this song with this/these ID(s) to the
playlist. Can be a single ID or a list of IDs.
:param song_id_to_remove: Remove the song at this/these position(s) in
the playlist. Can be a single ID or a list of IDs.
"""
return self._post(
self._make_url('updatePlaylist'),
@@ -791,4 +817,540 @@ class Server:
Obtained by calls to ``getMusicDirectory``.
"""
# TODO make this a decent object
return self._post(self._make_url('stream'), id=id)
return self._post(self._make_url('download'), id=id)
def get_cover_art(self, id: int, size: str = None):
"""
Returns the cover art image in binary form.
:param id: The ID of a song, album or artist.
:param size: If specified, scale image to this size.
"""
return self._post(self._make_url('getCoverArt'), id=id, size=size)
def get_lyrics(self, artist: str = None, title: str = None) -> Lyrics:
"""
Searches for and returns lyrics for a given song.
:param artist: The artist name.
:param title: The song title.
"""
result = self._post(
self._make_url('getLyrics'),
artist=artist,
title=title,
)
return result.lyrics
def get_avatar(self, username: str):
"""
Returns the avatar (personal image) for a user.
:param username: the user in question.
"""
return self._post(self._make_url('getAvatar'), username=username)
def star(
self,
id: Union[int, List[int]] = None,
album_id: Union[int, List[int]] = None,
artist_id: Union[int, List[int]] = None,
) -> Response:
"""
Attaches a star to a song, album or artist.
:param id: The ID(s) of the file(s) (song(s)) or folder(s)
(album(s)/artist(s)) to star. Can be a single ID or a list of IDs.
:param album_id: The ID(s) of an album/albums to star. Use this rather
than ``id`` if the client accesses the media collection according
to ID3 tags rather than file structure. Can be a single ID or a
list of IDs.
:param artist_id: The ID(s) of an artist/artists to star. Use this
rather than ``id`` if the client accesses the media collection
according to ID3 tags rather than file structure. Can be a single
ID or a list of IDs.
"""
return self._post(
self._make_url('star'),
id=id,
albumId=album_id,
artistId=artist_id,
)
def unstar(
self,
id: Union[int, List[int]] = None,
album_id: Union[int, List[int]] = None,
artist_id: Union[int, List[int]] = None,
) -> Response:
"""
Removes the star from a song, album or artist.
:param id: The ID(s) of the file(s) (song(s)) or folder(s)
(album(s)/artist(s)) to star. Can be a single ID or a list of IDs.
:param album_id: The ID(s) of an album/albums to star. Use this rather
than ``id`` if the client accesses the media collection according
to ID3 tags rather than file structure. Can be a single ID or a
list of IDs.
:param artist_id: The ID(s) of an artist/artists to star. Use this
rather than ``id`` if the client accesses the media collection
according to ID3 tags rather than file structure. Can be a single
ID or a list of IDs.
"""
return self._post(
self._make_url('unstar'),
id=id,
albumId=album_id,
artistId=artist_id,
)
def set_rating(self, id: int, rating: int) -> Response:
"""
Sets the rating for a music file.
:param id: A string which uniquely identifies the file (song) or folder
(album/artist) to rate.
:param rating: The rating between 1 and 5 (inclusive), or 0 to remove
the rating.
"""
return self._post(self._make_url('setRating'), id=id, rating=rating)
def scrobble(
self,
id: int,
time: datetime = None,
submission: bool = True,
) -> Response:
"""
Registers the local playback of one or more media files. Typically used
when playing media that is cached on the client. This operation
includes the following:
* "Scrobbles" the media files on last.fm if the user has configured
his/her last.fm credentials on the Subsonic server (Settings >
Personal).
* Updates the play count and last played timestamp for the media
files. (Since 1.11.0)
* Makes the media files appear in the "Now playing" page in the web
app, and appear in the list of songs returned by
``getNowPlaying`` (Since 1.11.0)
Since 1.8.0 you may specify multiple id (and optionally time)
parameters to scrobble multiple files.
:param id: The ID of the file to scrobble.
:param time: (Since 1.8.0) The time at which the song was listened to.
:param submission: Whether this is a "submission" or a "now playing"
notification.
"""
return self._post(
self._make_url('scrobble'),
id=id,
time=time,
submission=submission,
)
def get_shares(self) -> Shares:
"""
Returns information about shared media this user is allowed to manage.
Takes no extra parameters.
"""
result = self._post(self._make_url('getShares'))
return result.shares
def create_share(
self,
id: Union[int, List[int]],
description: str = None,
expires: datetime = None,
) -> Shares:
"""
Creates a public URL that can be used by anyone to stream music or
video from the Subsonic server. The URL is short and suitable for
posting on Facebook, Twitter etc. Note: The user must be authorized to
share (see Settings > Users > User is allowed to share files with
anyone).
:param id: ID(s) of (a) song(s), album(s) or video(s) to share. Can be
a single ID or a list of IDs.
:param description: A user-defined description that will be displayed
to people visiting the shared media.
:param expires: The time at which the share expires.
"""
result = self._post(
self._make_url('createShare'),
id=id,
description=description,
expires=expires,
)
return result.shares
def update_share(
self,
id: int,
description: str = None,
expires: datetime = None,
) -> Response:
"""
Updates the description and/or expiration date for an existing share.
:param id: ID of the share to update.
:param description: A user-defined description that will be displayed
to people visiting the shared media.
:param expires: The time at which the share expires.
"""
return self._post(
self._make_url('updateShare'),
id=id,
description=description,
expires=expires,
)
def delete_share(self, id: int) -> Response:
"""
Deletes an existing share.
:param id: ID of the share to delete.
"""
return self._post(self._make_url('deleteShare'), id=id)
def get_internet_radio_stations(self) -> InternetRadioStations:
"""
Returns all internet radio stations. Takes no extra parameters.
"""
result = self._post(self._make_url('getInternetRadioStations'))
return result.internetRadioStations
def create_internet_radio_station(
self,
stream_url: str,
name: str,
homepage_url: str = None,
) -> Response:
"""
Adds a new internet radio station. Only users with admin privileges are
allowed to call this method.
:param stream_url: The stream URL for the station.
:param name: The user-defined name for the station.
:param homepage_url: The home page URL for the station.
"""
return self._post(
self._make_url('createInternetRadioStation'),
streamUrl=stream_url,
name=name,
homepageUrl=homepage_url,
)
def update_internet_radio_station(
self,
id: int,
stream_url: str,
name: str,
homepage_url: str = None,
) -> Response:
"""
Updates an existing internet radio station. Only users with admin
privileges are allowed to call this method.
:param id: The ID for the station.
:param stream_url: The stream URL for the station.
:param name: The user-defined name for the station.
:param homepage_url: The home page URL for the station.
"""
return self._post(
self._make_url('updateInternetRadioStation'),
id=id,
streamUrl=stream_url,
name=name,
homepageUrl=homepage_url,
)
def delete_internet_radio_station(self, id: int) -> Response:
"""
Deletes an existing internet radio station. Only users with admin
privileges are allowed to call this method.
:param id: The ID for the station.
"""
return self._post(self._make_url('deleteInternetRadioStation'), id=id)
def get_user(self, username: str) -> User:
"""
Get details about a given user, including which authorization roles and
folder access it has. Can be used to enable/disable certain features in
the client, such as jukebox control.
:param username: The name of the user to retrieve. You can only
retrieve your own user unless you have admin privileges.
"""
result = self._post(self._make_url('getUser'), username=username)
return result.user
def get_users(self) -> Users:
"""
Get details about all users, including which authorization roles and
folder access they have. Only users with admin privileges are allowed
to call this method.
"""
result = self._post(self._make_url('getUsers'))
return result.users
def create_user(
self,
username: str,
password: str,
email: str,
ldap_authenticated: bool = False,
admin_role: bool = False,
settings_role: bool = True,
stream_role: bool = True,
jukebox_role: bool = False,
download_role: bool = False,
upload_role: bool = False,
playlist_role: bool = False,
covert_art_role: bool = False,
comment_role: bool = False,
podcast_role: bool = False,
share_role: bool = False,
video_conversion_role: bool = False,
music_folder_id: Union[int, List[int]] = None,
) -> Response:
"""
Creates a new Subsonic user.
:param username: The name of the new user.
:param password: The password of the new user, either in clear text or
hex-encoded.
:param email: The email address of the new user.
:param ldap_authenticated: Whether the user is authenicated in LDAP.
:param admin_role: Whether the user is administrator.
:param settings_role: Whether the user is allowed to change personal
settings and password.
:param stream_role: Whether the user is allowed to play files.
:param jukebox_role: Whether the user is allowed to play files in
jukebox mode.
:param download_role: Whether the user is allowed to download files.
:param upload_role: Whether the user is allowed to upload files.
:param playlist_role: Whether the user is allowed to create and delete
playlists. Since 1.8.0, changing this role has no effect.
:param covert_art_role: Whether the user is allowed to change cover art
and tags.
:param comment_role: Whether the user is allowed to create and edit
comments and ratings.
:param podcast_role: Whether the user is allowed to administrate
Podcasts.
:param share_role: (Since 1.8.0) Whether the user is allowed to share
files with anyone.
:param video_conversion_role: (Since 1.15.0) Whether the user is
allowed to start video conversions.
:param music_folder_id: (Since 1.12.0) IDs of the music folders the
user is allowed access to. Can be a single ID or a list of IDs.
"""
return self._post(
self._make_url('createUser'),
username=username,
password=password,
email=email,
ldapAuthenticated=ldap_authenticated,
adminRole=admin_role,
settingsRole=settings_role,
streamRole=stream_role,
jukeboxRole=jukebox_role,
downloadRole=download_role,
uploadRole=upload_role,
playlistRole=playlist_role,
coverArtRole=covert_art_role,
commentRole=comment_role,
podcastRole=podcast_role,
shareRole=share_role,
videoConversionRole=video_conversion_role,
musicFolderId=music_folder_id,
)
def update_user(
self,
username: str,
password: str = None,
email: str = None,
ldap_authenticated: bool = False,
admin_role: bool = False,
settings_role: bool = True,
stream_role: bool = True,
jukebox_role: bool = False,
download_role: bool = False,
upload_role: bool = False,
playlist_role: bool = False,
covert_art_role: bool = False,
comment_role: bool = False,
podcast_role: bool = False,
share_role: bool = False,
video_conversion_role: bool = False,
music_folder_id: Union[int, List[int]] = None,
) -> Response:
"""
Modifies an existing Subsonic user.
:param username: The name of the user.
:param password: The password of the user, either in clear text or
hex-encoded.
:param email: The email address of the user.
:param ldap_authenticated: Whether the user is authenicated in LDAP.
:param admin_role: Whether the user is administrator.
:param settings_role: Whether the user is allowed to change personal
settings and password.
:param stream_role: Whether the user is allowed to play files.
:param jukebox_role: Whether the user is allowed to play files in
jukebox mode.
:param download_role: Whether the user is allowed to download files.
:param upload_role: Whether the user is allowed to upload files.
:param playlist_role: Whether the user is allowed to create and delete
playlists. Since 1.8.0, changing this role has no effect.
:param covert_art_role: Whether the user is allowed to change cover art
and tags.
:param comment_role: Whether the user is allowed to create and edit
comments and ratings.
:param podcast_role: Whether the user is allowed to administrate
Podcasts.
:param share_role: (Since 1.8.0) Whether the user is allowed to share
files with anyone.
:param video_conversion_role: (Since 1.15.0) Whether the user is
allowed to start video conversions.
:param music_folder_id: (Since 1.12.0) IDs of the music folders the
user is allowed access to. Can be a single ID or a list of IDs.
"""
return self._post(
self._make_url('updateUser'),
username=username,
password=password,
email=email,
ldapAuthenticated=ldap_authenticated,
adminRole=admin_role,
settingsRole=settings_role,
streamRole=stream_role,
jukeboxRole=jukebox_role,
downloadRole=download_role,
uploadRole=upload_role,
playlistRole=playlist_role,
coverArtRole=covert_art_role,
commentRole=comment_role,
podcastRole=podcast_role,
shareRole=share_role,
videoConversionRole=video_conversion_role,
musicFolderId=music_folder_id,
)
def delete_user(self, username: str) -> Response:
"""
Deletes an existing Subsonic user.
:param username: The name of the new user.
"""
return self._post(self._make_url('deleteUser'), username=username)
def change_password(self, username: str, password: str) -> Response:
"""
Changes the password of an existing Subsonic user. You can only change
your own password unless you have admin privileges.
:param username: The name of the user which should change its password.
:param password: The new password of the new user, either in clear text
of hex-encoded.
"""
return self._post(
self._make_url('changePassword'),
username=username,
password=password,
)
def get_bookmarks(self) -> Bookmarks:
"""
Returns all bookmarks for this user. A bookmark is a position within a
certain media file.
"""
result = self._post(self._make_url('getBookmarks'))
return result.bookmarks
def create_bookmarks(
self,
id: int,
position: int,
comment: str = None,
) -> Response:
"""
Creates or updates a bookmark (a position within a media file).
Bookmarks are personal and not visible to other users.
:param id: ID of the media file to bookmark. If a bookmark already
exists for this file it will be overwritten.
:param position: The position (in milliseconds) within the media file.
:param comment: A user-defined comment.
"""
return self._post(
self._make_url('createBookmark'),
id=id,
position=position,
comment=comment,
)
def delete_bookmark(self, id: int) -> Response:
"""
Deletes the bookmark for a given file.
:param id: ID of the media file for which to delete the bookmark. Other
users' bookmarks are not affected.
"""
return self._post(self._make_url('deleteBookmark'), id=id)
def get_play_queue(self) -> Optional[PlayQueue]:
"""
Returns the state of the play queue for this user (as set by
``savePlayQueue``). This includes the tracks in the play queue, the
currently playing track, and the position within this track. Typically
used to allow a user to move between different clients/apps while
retaining the same play queue (for instance when listening to an audio
book).
"""
result = self._post(self._make_url('getPlayQueue'))
return result.playQueue
def savePlayQueue(
self,
id: Union[int, List[int]],
current: int = None,
position: int = None,
) -> Response:
"""
Saves the state of the play queue for this user. This includes the
tracks in the play queue, the currently playing track, and the position
within this track. Typically used to allow a user to move between
different clients/apps while retaining the same play queue (for
instance when listening to an audio book).
:param id: ID(s) of a/the song(s) in the play queue. Can be either a
single ID or a list of IDs.
:param current: The Id of the current playing song.
:param position: The position in milliseconds within the currently
playing song.
"""
return self._post(
self._make_url('savePlayQueue'),
id=id,
current=current,
position=position,
)
def get_scan_status(self) -> ScanStatus:
"""
Returns the current status for media library scanning. Takes no extra
parameters.
"""
result = self._post(self._make_url('getScanStatus'))
return result.scanStatus
def start_scan(self) -> ScanStatus:
"""
Initiates a rescan of the media libraries. Takes no extra parameters.
"""
result = self._post(self._make_url('startScan'))
return result.scanStatus