From 45783bb0a7047b376a7cab3f4bb9e1bea5886b9e Mon Sep 17 00:00:00 2001 From: Kutu <87788540+kutu-dev@users.noreply.github.com> Date: Sat, 1 Jun 2024 12:19:58 +0200 Subject: [PATCH] Feat/add docstrings (#43) * Add docstrings for exception handling * Add docstrings for the bookmarks endpoints * Add docstrings for the browsing endpoints * Add chat, internet radio and jukebox endpoints docstrings * Add lists endpoints docstrings * Add media retrieval endpoints docstrings * Add media annotation endpoints docstrings * Add podcast endpoints docstrings * Add searching and sharing endpoints docstrings * Add system endpoints and Subsonic object docstrings * Add user management endpoints docstrings * Add docstrings for album models * Add docstrings for artist, artist index, bookmark, chat message, contributor, over art and genre models * Add docstrings for internet radio station, jukebox, lyrics, music directory and music folder models and the base model class * Add docstrings for jukebox, now playing entry, play queue, playlist, podcast, replay gain, scan status, search result, share, song and starred contentmodels * Add docstrings for jukebox, now playing entry, play queue, playlist, podcast, replay gain, scan status, search result, share, song and starred contentmodels * Update TODO.md --------- Co-authored-by: Kutu --- README.md | 6 +- TODO.md | 7 + pyproject.toml | 12 +- src/knuckles/__init__.py | 4 +- src/knuckles/_api.py | 4 +- src/knuckles/_bookmarks.py | 132 +++--- src/knuckles/_browsing.py | 334 ++++++++++----- src/knuckles/_chat.py | 25 +- src/knuckles/_internet_radio.py | 111 ++--- src/knuckles/_jukebox.py | 143 ++++--- src/knuckles/_lists.py | 405 +++++++++++++++++- src/knuckles/_media_annotation.py | 178 ++++---- src/knuckles/_media_library_scanning.py | 21 +- src/knuckles/_media_retrieval.py | 258 ++++++----- src/knuckles/_playlists.py | 122 +++--- src/knuckles/_podcast.py | 152 ++++--- src/knuckles/_searching.py | 83 +++- src/knuckles/_sharing.py | 82 ++-- src/knuckles/_subsonic.py | 53 ++- src/knuckles/_system.py | 43 +- src/knuckles/_user_management.py | 133 ++++-- src/knuckles/exceptions.py | 129 ++++-- src/knuckles/models/_album.py | 178 ++++---- src/knuckles/models/_artist.py | 103 +++-- .../models/{_index.py => _artist_index.py} | 12 +- src/knuckles/models/_bookmark.py | 47 +- src/knuckles/models/_chat_message.py | 23 +- src/knuckles/models/_contributor.py | 11 +- src/knuckles/models/_cover_art.py | 12 +- src/knuckles/models/_genre.py | 16 +- .../models/_internet_radio_station.py | 55 ++- src/knuckles/models/_jukebox.py | 145 ++++--- src/knuckles/models/_lyrics.py | 8 + src/knuckles/models/_model.py | 5 + src/knuckles/models/_music_directory.py | 18 + src/knuckles/models/_music_folder.py | 29 +- src/knuckles/models/_now_playing_entry.py | 12 + src/knuckles/models/_play_queue.py | 25 +- src/knuckles/models/_playlist.py | 117 +++-- src/knuckles/models/_podcast.py | 159 +++---- src/knuckles/models/_replay_gain.py | 15 + src/knuckles/models/_scan_status.py | 16 +- src/knuckles/models/_search_result.py | 11 + src/knuckles/models/_share.py | 86 ++-- src/knuckles/models/_song.py | 198 +++++---- src/knuckles/models/_starred_content.py | 11 + src/knuckles/models/_system.py | 66 ++- src/knuckles/models/_user.py | 86 ++-- src/knuckles/models/_video.py | 116 ++++- tests/api/test_browsing.py | 6 +- tests/api/test_chat.py | 2 +- ...est_code_errors.py => test_error_codes.py} | 18 +- tests/api/test_jukebox_control.py | 4 +- tests/api/test_podcast.py | 16 +- tests/models/test_channel.py | 6 +- tests/models/test_episode.py | 6 +- tests/models/test_jukebox.py | 6 +- 57 files changed, 2589 insertions(+), 1492 deletions(-) create mode 100644 TODO.md rename src/knuckles/models/{_index.py => _artist_index.py} (61%) rename tests/api/{test_code_errors.py => test_error_codes.py} (77%) diff --git a/README.md b/README.md index 69e5575..4557417 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,12 @@ # Knuckles -> A [OpenSubsonic](https://opensubsonic.netlify.app/) API wrapper for Python. +> A [OpenSubsonic](https://opensubsonic.netlify.app/) API wrapper for Python 3.11.0+. ## Compatiblity Knuckles **only** works with servers compatible with the REST API version 1.4.0 onwards (Subsonic 4.2+). -It follows strictly the [OpenSubsonic API Spec](https://opensubsonic.netlify.app/docs/opensubsonic-api/), being fully retro-compatible with the original [Subsonic API](https://subsonic.org/pages/api.jsp). +It follows strictly the [OpenSubsonic API Spec](https://opensubsonic.netlify.app/docs/opensubsonic-api/), it being fully retro-compatible with the original [Subsonic API](https://subsonic.org/pages/api.jsp). ### Quickstart ... ## Acknowledgements Created with :heart: by [Jorge "Kutu" Dobón Blanco](https://dobon.dev). - -::: knuckles.models._user diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..9fdbcf2 --- /dev/null +++ b/TODO.md @@ -0,0 +1,7 @@ +# TODO + +- [ ] Add `justfile`. +- [ ] Remove `pre-commit`. + - [ ] Custom git-hooks? +- [ ] Document in contributing why attributes are double typed. +- [ ] Add general documentation and tutorials. diff --git a/pyproject.toml b/pyproject.toml index 4b00f99..8f393b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,12 +22,12 @@ tests = [ "responses>=0.23.1", ] docs = [ - "mkdocs==1.5.3", - "mkdocs-material==9.5.18", - "mkdocstrings[python]==0.24.3", - "mkdocs-gen-files==0.5.0", - "pymdown-extensions==10.8.0", - "mkdocs-literate-nav==0.6.1" + "mkdocs>=1.5.3", + "mkdocs-material>=9.5.18", + "mkdocstrings[python]>=0.24.3", + "mkdocs-gen-files>=0.5.0", + "pymdown-extensions>=10.8.0", + "mkdocs-literate-nav>=0.6.1" ] [build-system] diff --git a/src/knuckles/__init__.py b/src/knuckles/__init__.py index 668b7ba..f99d74e 100644 --- a/src/knuckles/__init__.py +++ b/src/knuckles/__init__.py @@ -3,12 +3,12 @@ from ._subsonic import Subsonic from .models._album import Album, AlbumInfo, Disc, RecordLabel, ReleaseDate from .models._artist import Artist, ArtistInfo +from .models._artist_index import ArtistIndex from .models._bookmark import Bookmark from .models._chat_message import ChatMessage from .models._contributor import Contributor from .models._cover_art import CoverArt from .models._genre import Genre, ItemGenre -from .models._index import Index from .models._internet_radio_station import InternetRadioStation from .models._jukebox import Jukebox from .models._lyrics import Lyrics @@ -45,7 +45,7 @@ "CoverArt", "ItemGenre", "Genre", - "Index", + "ArtistIndex", "InternetRadioStation", "Jukebox", "Lyrics", diff --git a/src/knuckles/_api.py b/src/knuckles/_api.py index aa43f63..77a1f52 100644 --- a/src/knuckles/_api.py +++ b/src/knuckles/_api.py @@ -9,7 +9,7 @@ from requests import Response from requests.models import PreparedRequest -from .exceptions import CODE_ERROR_EXCEPTIONS, get_code_error_exception +from .exceptions import ERROR_CODE_EXCEPTION, get_error_code_exception class RequestMethod(Enum): @@ -182,7 +182,7 @@ def json_request( json_response: dict[str, Any] = response.json()["subsonic-response"] if json_response["status"] == "failed": - code_error: CODE_ERROR_EXCEPTIONS = get_code_error_exception( + code_error: ERROR_CODE_EXCEPTION = get_error_code_exception( json_response["error"]["code"] ) diff --git a/src/knuckles/_bookmarks.py b/src/knuckles/_bookmarks.py index e6c41fe..15d5a84 100644 --- a/src/knuckles/_bookmarks.py +++ b/src/knuckles/_bookmarks.py @@ -9,9 +9,9 @@ class Bookmarks: - """Class that contains all the methods needed to interact - with the browsing calls in the Subsonic API. - + """Class that contains all the methods needed to interact with the + [bookmark endpoints](https://opensubsonic.netlify.app/ + categories/bookmarks/) in the Subsonic API. """ def __init__(self, api: Api, subsonic: "Subsonic") -> None: @@ -19,93 +19,98 @@ def __init__(self, api: Api, subsonic: "Subsonic") -> None: self.subsonic = subsonic def get_bookmarks(self) -> list[Bookmark]: - """Calls the "getBookmarks" endpoints of the API. + """Get all the bookmarks created by the authenticated user. - :return: A list with all the bookmarks given by the server. - :rtype: list[Bookmark] + Returns: + A list containing all the bookmarks for the authenticated user. """ response = self.api.json_request("getBookmarks")["bookmarks"]["bookmark"] return [Bookmark(self.subsonic, **bookmark) for bookmark in response] - def get_bookmark(self, id_: str) -> Bookmark | None: - """Using the "getBookmarks" endpoint iterates over all the bookmarks - and find the one with the same ID. + def get_bookmark(self, bookmark_id: str) -> Bookmark | None: + """Get all the info of a bookmark given its ID. - :param id_: The ID of the song of the bookmark to find. - :type id_: str - :return: The found bookmark or None if no one is found. - :rtype: Bookmark | None + Args: + bookmark_id: The id of the bookmark to get. + + Returns: + A object that contains all the info of the requested bookmark. """ bookmarks = self.get_bookmarks() for bookmark in bookmarks: - if bookmark.song.id == id_: + if bookmark.song.id == bookmark_id: return bookmark return None def create_bookmark( - self, id_: str, position: int, comment: str | None = None + self, song_or_video_id: str, position: int, comment: str | None = None ) -> Bookmark: - """Calls the "createBookmark" endpoint of the API. - - :param id_: The ID of the song of the bookmark. - :type id_: str - :param position: The position in seconds of the bookmark. - :type position: int - :param comment: The comment of the bookmark, defaults to None. - :type comment: str | None, optional - :return: The new created share. - :rtype: Bookmark + """Creates a new bookmark for the authenticated user. + + Args: + song_or_video_id: The ID of the song or video to bookmark. + position: A position in milliseconds to be indicated with the song + or video. + comment: A comment to be attached with the song or video. + + Returns: + An object that contains all the info of the new created + bookmark. """ self.api.json_request( - "createBookmark", {"id": id_, "position": position, "comment": comment} + "createBookmark", + {"id": song_or_video_id, "position": position, "comment": comment}, ) # Fake the song structure given by in the API. - return Bookmark(self.subsonic, {"id": id_}, position=position, comment=comment) + return Bookmark( + self.subsonic, {"id": song_or_video_id}, position=position, comment=comment + ) def update_bookmark( - self, id_: str, position: int, comment: str | None = None + self, song_or_video_id: str, position: int, comment: str | None = None ) -> Bookmark: - """Method that internally calls the create_bookmark method - as creating and updating a bookmark uses the same endpoint. Useful for having - more self-descriptive code. - - :param id_: The ID of the song of the bookmark. - :type id_: str - :param position: The position in seconds of the bookmark. - :type position: int - :param comment: The comment of the bookmark, defaults to None. - :type comment: str | None, optional - :return: A Bookmark object with all the updated info. - :rtype: Bookmark + """Updates a bookmark for the authenticated user. + + Args: + song_or_video_id: The ID of the song or video to update its + bookmark. + position: A position in milliseconds to be indicated with the song + or video. + comment: A comment to be attached with the song or video. + Returns: + An object that contains all the info of the new created + bookmark. """ - return self.create_bookmark(id_, position, comment) + return self.create_bookmark(song_or_video_id, position, comment) - def delete_bookmark(self, id_: str) -> "Subsonic": - """Calls the "deleteBookmark" endpoint of the API. + def delete_bookmark(self, song_or_video_id: str) -> "Subsonic": + """Deletes a bookmark for the authenticated user. - :param id_: The ID of the song of the bookmark to delete. - :type id_: str - :return: The object itself to allow method chaining. - :rtype: Subsonic + Args: + song_or_video_id: The ID of the song or video to delete its + bookmark. + Returns: + The Subsonic object where this method was called to allow + method chaining. """ - - self.api.json_request("deleteBookmark", {"id": id_}) + self.api.json_request("deleteBookmark", {"id": song_or_video_id}) return self.subsonic def get_play_queue(self) -> PlayQueue: - """Calls the "getPlayQueue" endpoint of the API. + """Get the play queue of the authenticated user. - :return: The play queue of the authenticated user. - :rtype: PlayQueue + Returns: + An object that contains all the info of the + play queue of the user. """ response = self.api.json_request("getPlayQueue")["playQueue"] @@ -118,18 +123,17 @@ def save_play_queue( current_song_id: str | None = None, position: int | None = None, ) -> PlayQueue: - """Calls the "savePlayQueue" endpoint of the API. - - :param song_ids: A list with all the IDs of the songs to add. - :type song_ids: list[str] - :param current_song_id: The ID of the current song in the queue, - defaults to None. - :type current_song_id: str | None, optional - :param position: The position in seconds of the current song, - defaults to None. - :type position: int | None, optional - :return: The new saved play queue. - :rtype: PlayQueue + """Saves a new play queue for the authenticated user. + + Args: + song_ids: A list with all the songs to add to the queue. + current_song_id: The ID of the current playing song. + position: A position in milliseconds of where the current song + playback it at. + + Returns: + An object that contains all the info of the new + saved play queue. """ self.api.json_request( diff --git a/src/knuckles/_browsing.py b/src/knuckles/_browsing.py index 25afead..bb8fb34 100644 --- a/src/knuckles/_browsing.py +++ b/src/knuckles/_browsing.py @@ -3,8 +3,8 @@ from ._api import Api from .models._album import Album, AlbumInfo from .models._artist import Artist, ArtistInfo +from .models._artist_index import ArtistIndex from .models._genre import Genre -from .models._index import Index from .models._music_directory import MusicDirectory from .models._music_folder import MusicFolder from .models._song import Song @@ -15,9 +15,9 @@ class Browsing: - """Class that contains all the methods needed to interact - with the browsing calls in the Subsonic API. - + """Class that contains all the methods needed to interact with the + [browsing endpoints](https://opensubsonic.netlify.app/categories/browsing) + in the Subsonic API. """ def __init__(self, api: Api, subsonic: "Subsonic") -> None: @@ -25,10 +25,11 @@ def __init__(self, api: Api, subsonic: "Subsonic") -> None: self.subsonic = subsonic def get_music_folders(self) -> list[MusicFolder]: - """Calls the "getMusicFolders" endpoint of the API. + """Get all the top level music folders. - :return: A list with all the received music folders. - :rtype: list[MusicFolder] + Returns: + A list that contains all the info about all the available + music folders. """ response = self.api.json_request("getMusicFolders")["musicFolders"][ @@ -37,33 +38,36 @@ def get_music_folders(self) -> list[MusicFolder]: return [MusicFolder(self.subsonic, **music_folder) for music_folder in response] - def get_music_folder(self, id_: str) -> MusicFolder | None: - """Get a desired music folder. + def get_music_folder(self, music_folder_id: str) -> MusicFolder | None: + """Get the info of a music folder. - :param id_: The id of the music folder to get. - :type id_: str - :return: A music folder object that correspond with the given id - or None if is no music folder is found. - :rtype: Genre | None + Args: + music_folder_id: The ID of the music folder to get. + + Returns: + An object that contains all the info about the + requested music folder, or None if it wasn't found. """ music_folders = self.get_music_folders() for music_folder in music_folders: - if music_folder.id == id_: + if music_folder.id == music_folder_id: return music_folder return None - def get_indexes(self, music_folder_id: str, modified_since: int) -> Index: - response = self.api.json_request( - "getIndexes", - {"musicFolderId": music_folder_id, "ifModifiedSince": modified_since}, - )["indexes"] + def get_music_directory(self, music_directory_id: str) -> MusicDirectory: + """Get the info of a music directory. - return Index(subsonic=self.subsonic, **response) + Args: + music_directory_id: The ID of the music directory to get its info. + + Returns: + An object that holds all the info about the requested music + directory. + """ - def get_music_directory(self, music_directory_id: str) -> MusicDirectory: response = self.api.json_request( "getMusicDirectory", {"id": music_directory_id} )["directory"] @@ -71,42 +75,44 @@ def get_music_directory(self, music_directory_id: str) -> MusicDirectory: return MusicDirectory(subsonic=self.subsonic, **response) def get_genres(self) -> list[Genre]: - """Calls the "getGenres" endpoint of the API. + """Get all the available genres in the server. - :return: A list will all the registered genres. - :rtype: list[Genre] + Returns: + A list with all the registered genres in the server. """ response = self.api.json_request("getGenres")["genres"]["genre"] return [Genre(self.subsonic, **genre) for genre in response] - def get_genre(self, name: str) -> Genre | None: - """Get a desired genre. + def get_genre(self, genre_name: str) -> Genre | None: + """Get all the info of a genre. + + Args: + genre_name: The name of the genre to get its info. - :param name: The name of the genre to get. - :type name: str - :return: A genre object that correspond with the given name - or None if is no genre is found. - :rtype: Genre | None + Returns: + An object that contains all the info + about the requested genre. """ genres = self.get_genres() for genre in genres: - if genre.value == name: + if genre.value == genre_name: return genre return None def get_artists(self, music_folder_id: str | None = None) -> list[Artist]: - """Calls the "getArtists" endpoint of the API. + """Get all the registered artists in the server. - :param music_folder_id: Only return artists in the music folder - with the given ID. - :type music_folder_id: str | None - :return: A list with all the artists. - :rtype: list[Artist] + Args: + music_folder_id: A music folder ID to reduce the scope of the + artists to return. + + Returns: + A list with all the info about all the received artists. """ response = self.api.json_request( @@ -122,80 +128,127 @@ def get_artists(self, music_folder_id: str | None = None) -> list[Artist]: return artists - def get_artist(self, id_: str) -> Artist: - """Calls the "getArtist" endpoint of the API. + def get_artist(self, artist_id: str) -> Artist: + """Get all the info about an artist. + + Args: + artist_id: The ID of the artist to get its info. - :param id_: The ID of the artist to get. - :type id_: str - :return: An object with all the information - that the server has given about the album. - :rtype: Artist + Returns: + An object that contains all the info about + the requested artist. """ - response = self.api.json_request("getArtist", {"id": id_})["artist"] + response = self.api.json_request("getArtist", {"id": artist_id})["artist"] return Artist(self.subsonic, **response) - def get_album(self, id_: str) -> Album: - """Calls the "getAlbum" endpoint of the API. + def get_artists_indexed( + self, music_folder_id: str, modified_since: int + ) -> ArtistIndex: + """Get all the registered artist indexed alphabetically. + + Args: + music_folder_id: A music folder ID to reduce the scope + where the artist should be from. + modified_since: Time in milliseconds since the artist have changed + its collection. - :param id_: The ID of the album to get. - :type id_: str - :return: An object with all the information - that the server has given about the album. - :rtype: Album + Returns: + An object containt all the artist alphabetically indexed. """ - response = self.api.json_request("getAlbum", {"id": id_})["album"] + response = self.api.json_request( + "getIndexes", + {"musicFolderId": music_folder_id, "ifModifiedSince": modified_since}, + )["indexes"] + + return ArtistIndex(subsonic=self.subsonic, **response) + + def get_album(self, album_id: str) -> Album: + """Get all the info about an album. + + Args: + album_id: The ID of the album to get its info. + + Returns: + An object that contains all the info about + the requested album. + """ + + response = self.api.json_request("getAlbum", {"id": album_id})["album"] return Album(self.subsonic, **response) - def get_album_info_non_id3(self, id_: str) -> AlbumInfo: - """Calls to the "getAlbumInfo2" endpoint of the API. + def get_album_info_non_id3(self, album_id: str) -> AlbumInfo: + """Get all the extra info about an album. Not organized according + ID3 tags. - :param id_: The ID of the album to get its info. - :type id_: str - :return: An object with all the extra info given by the server about the album. - :rtype: AlbumInfo + Args: + album_id: The ID of the album to get its extra info. + + Returns: + An object that contains all the extra info about + the requested album. """ - response = self.api.json_request("getAlbumInfo", {"id": id_})["albumInfo"] + response = self.api.json_request("getAlbumInfo", {"id": album_id})["albumInfo"] + + return AlbumInfo(self.subsonic, album_id, **response) - return AlbumInfo(self.subsonic, id_, **response) + def get_album_info(self, album_id: str) -> AlbumInfo: + """Get all the extra info about an album. - def get_album_info(self, id_: str) -> AlbumInfo: - """Calls to the "getAlbumInfo2" endpoint of the API. + Args: + album_id: The ID of the album to get its extra info. - :param id_: The ID of the album to get its info. - :type id_: str - :return: An object with all the extra info given by the server about the album. - :rtype: AlbumInfo + Returns: + An object that contains all the extra info about + the requested album. """ - response = self.api.json_request("getAlbumInfo2", {"id": id_})["albumInfo"] + response = self.api.json_request("getAlbumInfo2", {"id": album_id})["albumInfo"] + + return AlbumInfo(self.subsonic, album_id, **response) - return AlbumInfo(self.subsonic, id_, **response) + def get_song(self, song_id: str) -> Song: + """Get all the info about a song. - def get_song(self, id_: str) -> Song: - """Calls to the "getSong" endpoint of the API. + Args: + song_id: The ID of the song to get its info. - :param id_: The ID of the song to get. - :type id_: str - :return: An object with all the information - that the server has given about the song. - :rtype: Song + Returns: + An object that contains all the info + about the requested song. """ - response = self.api.json_request("getSong", {"id": id_})["song"] + response = self.api.json_request("getSong", {"id": song_id})["song"] return Song(self.subsonic, **response) def get_videos(self) -> list[Video]: + """Get all the registered videos in the server. + + Returns: + A list with all the info about al the videos + available in the server. + """ + response = self.api.json_request("getVideos")["videos"]["video"] return [Video(self.subsonic, **video) for video in response] def get_video(self, video_id: str) -> Video | None: + """Get all the info about a video. + + Args: + video_id: The ID of the video to get its info. + + Returns: + An object that contains all the info about + the requested video. + """ + videos = self.get_videos() for video in videos: @@ -205,6 +258,16 @@ def get_video(self, video_id: str) -> Video | None: return None def get_video_info(self, video_id: str) -> VideoInfo: + """Get all the extra info about a video. + + Args: + video_id: The ID of the video to get its extra info. + + Returns: + An object that holds all the extra info about + the requested video. + """ + response = self.api.json_request("getVideoInfo", {"id": video_id})["videoInfo"] return VideoInfo(self.subsonic, video_id=video_id, **response) @@ -212,70 +275,119 @@ def get_video_info(self, video_id: str) -> VideoInfo: def get_artist_info_non_id3( self, artist_id: str, - count: int | None = None, - include_not_present: bool | None = None, + max_similar_artists: int | None = None, + include_similar_artists_not_present: bool | None = None, ) -> ArtistInfo: - """Calls the "getArtistInfo" endpoint of the API. - - :param artist_id: The id of the artist to get its info - :type artist_id: - :param count: - :type count: - :param include_not_present: - :type include_not_present: - :return: - :rtype: + """Get all the extra info about an artist. Not organized according + ID3 tags. + + Args: + artist_id: The ID of the artist to get its extra info. + max_similar_artists: The max number of similar artists to + return. + include_similar_artists_not_present: Include similar artists + that are not present in any the media library. + + Returns: + An object that contains all the extra info about + the requested artist. """ response = self.api.json_request( "getArtistInfo", - {"id": artist_id, "count": count, "includeNotPresent": include_not_present}, + { + "id": artist_id, + "count": max_similar_artists, + "includeNotPresent": include_similar_artists_not_present, + }, )["artistInfo"] return ArtistInfo(self.subsonic, artist_id, **response) def get_artist_info( self, - id_: str, - count: int | None = None, - include_not_present: bool | None = None, + artist_id: str, + max_similar_artists: int | None = None, + include_similar_artists_not_present: bool | None = None, ) -> ArtistInfo: - """Calls the "getArtistInfo" endpoint of the API. - - :param id_: The id of the artist to get its info - :type id_: - :param count: - :type count: - :param include_not_present: - :type include_not_present: - :return: - :rtype: + """Get all the extra info about an artist. + + Args: + artist_id: The ID of the artist to get its extra info. + max_similar_artists: The max number of similar artists to + return. + include_similar_artists_not_present: Include similar artists + that are not present in any the media library. + + Returns: + An object that contains all the extra info about + the requested artist. """ response = self.api.json_request( "getArtistInfo2", - {"id": id_, "count": count, "includeNotPresent": include_not_present}, + { + "id": artist_id, + "count": max_similar_artists, + "includeNotPresent": include_similar_artists_not_present, + }, )["artistInfo2"] - return ArtistInfo(self.subsonic, id_, **response) + return ArtistInfo(self.subsonic, artist_id, **response) def get_similar_songs_non_id3( - self, song_id: str, count: int | None = None + self, song_id: str, song_count: int | None = None ) -> list[Song]: + """Get similar songs to the given one. Not organized according + ID3 tags. + + Args: + song_id: The ID of the song to get similar songs. + song_count: The number of songs to return. + + Returns: + A list that contains all the songs that are similar + to the given one. + """ + response = self.api.json_request( - "getSimilarSongs", {"id": song_id, "count": count} + "getSimilarSongs", {"id": song_id, "count": song_count} )["similarSongs"]["song"] return [Song(subsonic=self.subsonic, **song) for song in response] - def get_similar_songs(self, song_id: str, count: int | None = None) -> list[Song]: + def get_similar_songs( + self, song_id: str, song_count: int | None = None + ) -> list[Song]: + """Get similar songs to the given one. + + Args: + song_id: The ID of the song to get similar songs. + song_count: The number of songs to return. + + Returns: + A list that contains all the songs that are similar + to the given one. + """ + response = self.api.json_request( - "getSimilarSongs2", {"id": song_id, "count": count} + "getSimilarSongs2", {"id": song_id, "count": song_count} )["similarSongs2"]["song"] return [Song(subsonic=self.subsonic, **song) for song in response] def get_top_songs(self, artist_name: str, max_num_of_songs: int) -> list[Song]: + """Get the top rated songs in the server. + + Args: + artist_name: Limit the ranked songs to the ones created by the + given artist. + max_num_of_songs: The max number of songs to return. + + Returns: + A list that contains the top rated songs of the server. + """ + response = self.api.json_request( "getTopSongs", {"artist": artist_name, "count": max_num_of_songs} )["topSongs"]["song"] diff --git a/src/knuckles/_chat.py b/src/knuckles/_chat.py index 5af7cd1..08472f6 100644 --- a/src/knuckles/_chat.py +++ b/src/knuckles/_chat.py @@ -8,9 +8,9 @@ class Chat: - """Class that contains all the methods needed to interact - with the chat calls in the Subsonic API. - + """Class that contains all the methods needed to interact with the + [chat endpoints](https://opensubsonic.netlify.app/categories/chat) + in the Subsonic API. """ def __init__(self, api: Api, subsonic: "Subsonic") -> None: @@ -18,23 +18,24 @@ def __init__(self, api: Api, subsonic: "Subsonic") -> None: self.subsonic = subsonic def add_chat_message(self, message: str) -> "Subsonic": - """Calls to the "addChatMessage" endpoint of the API: + """Add chat message. - :param message: The message to send. - :type message: str - :return: The object itself to allow method chaining. - :rtype: Self - """ + Args: + message: The message content to add. + Returns: + The Subsonic object where this method was called to allow + method chaining. + """ self.api.json_request("addChatMessage", {"message": message}) return self.subsonic def get_chat_messages(self) -> list[ChatMessage]: - """Calls to the "getChatMessages" endpoint of the API. + """Get all send chat messages. - :return: A list of ChatMessage objects. - :rtype: list[ChatMessage] + Returns: + A list with all the messages info. """ response: list[dict[str, Any]] = self.api.json_request("getChatMessages")[ diff --git a/src/knuckles/_internet_radio.py b/src/knuckles/_internet_radio.py index 2f2342a..695b12a 100644 --- a/src/knuckles/_internet_radio.py +++ b/src/knuckles/_internet_radio.py @@ -8,9 +8,9 @@ class InternetRadio: - """Class that contains all the methods needed to interact - with the internet radio calls and actions in the Subsonic API. - + """Class that contains all the methods needed to interact with the + [internet radio endpoints](https://opensubsonic.netlify.app/ + categories/internet-radio) in the Subsonic API. """ def __init__(self, api: Api, subsonic: "Subsonic") -> None: @@ -22,10 +22,10 @@ def __init__(self, api: Api, subsonic: "Subsonic") -> None: def get_internet_radio_stations( self, ) -> list[InternetRadioStation]: - """Calls the "getInternetRadioStation" endpoint of the API. + """Get all the internet radio stations available in the server. - :return: A list with all the internet radio stations. - :rtype: list[InternetRadioStation] + Returns: + A list with all the reported internet radio stations. """ response = self.api.json_request("getInternetRadioStations")[ @@ -34,20 +34,24 @@ def get_internet_radio_stations( return [InternetRadioStation(self.subsonic, **station) for station in response] - def get_internet_radio_station(self, id_: str) -> InternetRadioStation | None: - """Using the "getInternetRadioStation" endpoint iterates over all the stations - and find the one with the same ID. + def get_internet_radio_station( + self, internet_radio_station_id: str + ) -> InternetRadioStation | None: + """Get all the info related with a internet radio station. - :param id_: The ID of the station to find. - :type id_: str - :return: The found internet radio station or None if no one is found. - :rtype: InternetRadioStation | None + Args: + internet_radio_station_id: The ID of the internet radio station + to get its info. + + Returns: + An object that contains all the info about the requested + internet radio station. """ stations = self.get_internet_radio_stations() for station in stations: - if station.id == id_: + if station.id == internet_radio_station_id: return station return None @@ -55,16 +59,18 @@ def get_internet_radio_station(self, id_: str) -> InternetRadioStation | None: def create_internet_radio_station( self, stream_url: str, name: str, homepage_url: str | None = None ) -> "Subsonic": - """Calls the "createInternetRadioStation" endpoint of the API. - - :param stream_url: The stream url of the station. - :type stream_url: str - :param name: The name of the station. - :type name: str - :param homepage_url: The url of the homepage of the station, defaults to None. - :type homepage_url: str | None, optional - :return: The object itself to allow method chaining. - :rtype: Subsonic + """Create a new internet radio station. + + Args: + stream_url: The URL of the stream to be added to the + internet radio station. + name: The name of the new created internet radio station. + homepage_url: An URL for the homepage of the internet + radio station. + + Returns: + An object that holds all the data about the new created + internet radio station. """ self.api.json_request( @@ -75,27 +81,30 @@ def create_internet_radio_station( return self.subsonic def update_internet_radio_station( - self, id_: str, stream_url: str, name: str, homepage_url: str | None = None + self, + internet_radio_station_id: str, + stream_url: str, + name: str, + homepage_url: str | None = None, ) -> "Subsonic": - """Calls the "updateInternetRadioStation" endpoint ot the API. - - :param id_: The ID of the station to update. - :type id_: str - :param stream_url: The new steam url of the station. - :type stream_url: str - :param name: The new name of the station. - :type name: str - :param homepage_url: The new url of the homepage of the station, - defaults to None. - :type homepage_url: str | None, optional - :return: The object itself to allow method chaining. - :rtype: Subsonic + """Update the data of an internet radio station. + + Args: + internet_radio_station_id: The ID of the internet radio station + to edit its data. + stream_url: A new stream URL for the internet radio station. + name: a new name for the internet radio station. + homepage_url: A new homepage URL for the internet radio + station. + + Returns: + An object that holds all the data about the new updated + internet radio station. """ - self.api.json_request( "updateInternetRadioStation", { - "id": id_, + "id": internet_radio_station_id, "streamUrl": stream_url, "name": name, "homepageUrl": homepage_url, @@ -104,15 +113,21 @@ def update_internet_radio_station( return self.subsonic - def delete_internet_radio_station(self, id_: str) -> "Subsonic": - """Calls the "deleteInternetRadioStation" endpoint of the API. + def delete_internet_radio_station( + self, internet_radio_station_id: str + ) -> "Subsonic": + """Delete an internet radio station. + + Args: + internet_radio_station_id: The ID of the internet radio station + to delete. - :param id_: The ID of the station to delete - :type id_: str - :return: The object itself to allow method chaining. - :rtype: Subsonic + Returns: + The Subsonic object where this method was called to allow + method chaining. """ - - self.api.json_request("deleteInternetRadioStation", {"id": id_}) + self.api.json_request( + "deleteInternetRadioStation", {"id": internet_radio_station_id} + ) return self.subsonic diff --git a/src/knuckles/_jukebox.py b/src/knuckles/_jukebox.py index 14659f7..7f19bfe 100644 --- a/src/knuckles/_jukebox.py +++ b/src/knuckles/_jukebox.py @@ -8,9 +8,9 @@ class JukeboxControl: - """Class that contains all the methods needed to interact - with the jukebox calls and actions in the Subsonic API. - + """Class that contains all the methods needed to interact with the + [jukebox control endpoint](https://opensubsonic.netlify.app/ + categories/jukebox) in the Subsonic API. """ def __init__(self, api: Api, subsonic: "Subsonic") -> None: @@ -20,10 +20,12 @@ def __init__(self, api: Api, subsonic: "Subsonic") -> None: self.subsonic = subsonic def get(self) -> Jukebox: - """Calls the "jukeboxControl" endpoint of the API with the action "get". + """Get all the info related with the current playlist of + the jukebox. - :return: An object with all the given information about the jukebox. - :rtype: Jukebox + Returns: + An object that holds all the info related with + the playlist of the jukebox. """ response = self.api.json_request("jukeboxControl", {"action": "get"})[ @@ -33,11 +35,12 @@ def get(self) -> Jukebox: return Jukebox(self.subsonic, **response) def status(self) -> Jukebox: - """Calls the "jukeboxControl" endpoint of the API with the action "status". + """Get all the info related with the current state of + the jukebox. - :return: An object with all the given information about the jukebox. - Except the jukebox playlist. - :rtype: Jukebox + Returns: + An object that holds all the info related with + the scate of the jukebox. """ response = self.api.json_request("jukeboxControl", {"action": "status"})[ @@ -46,28 +49,32 @@ def status(self) -> Jukebox: return Jukebox(self.subsonic, **response) - def set(self, id_: str) -> Jukebox: - """Calls the "jukeboxControl" endpoint of the API with the action "set". + def set(self, songs_ids: list[str]) -> Jukebox: + """Set the song playlist for the jukebox. - :param id_: The ID of a song to set it in the jukebox. - :type id_: str - :return: An object with all the given information about the jukebox. - :rtype: Jukebox + Args: + songs_ids: A list of song IDs to set the jukebox playlist. + + Returns: + An object that contains the updated jukebox status + and playlist. """ response = self.api.json_request( - "jukeboxControl", {"action": "set", "id": id_} + "jukeboxControl", {"action": "set", "id": songs_ids} )["jukeboxStatus"] # Preset the song list as this call changes it in a predictable way - return Jukebox(self.subsonic, **response, entry=[{"id": id_}]) + return Jukebox( + self.subsonic, **response, entry=[{"id": song_id} for song_id in songs_ids] + ) def start(self) -> Jukebox: - """Calls the "jukeboxControl" endpoint of the API with the action "start". + """Start the playback of the current song in the jukebox playlist. - :return: An object with all the given information about the jukebox. - Except the jukebox playlist. - :rtype: Jukebox + Returns: + An object that contains the updated jukebox status + and playlist. """ response = self.api.json_request("jukeboxControl", {"action": "start"})[ @@ -77,11 +84,11 @@ def start(self) -> Jukebox: return Jukebox(self.subsonic, **response) def stop(self) -> Jukebox: - """Calls the "jukeboxControl" endpoint of the API with the action "stop". + """Stop the playback of the current song in the jukebox playlist. - :return: An object with all the given information about the jukebox. - Except the jukebox playlist. - :rtype: Jukebox + Returns: + An object that contains the updated jukebox status + and playlist. """ response = self.api.json_request("jukeboxControl", {"action": "stop"})[ @@ -91,15 +98,15 @@ def stop(self) -> Jukebox: return Jukebox(self.subsonic, **response) def skip(self, index: int, offset: float = 0) -> Jukebox: - """Calls the "jukeboxControl" endpoint of the API with the action "skip". - - :param index: The index in the jukebox playlist to skip to. - :type index: int - :param offset: Start playing this many seconds into the track, defaults to 0 - :type offset: float, optional - :return: An object with all the given information about the jukebox. - Except the jukebox playlist. - :rtype: Jukebox + """Skip the playback of the current song in the jukebox playlist. + + Args: + index: The index of the song to skip to. + offset: The offset of seconds to start playing the next song. + + Returns: + An object that contains the updated jukebox status + and playlist. """ response = self.api.json_request( @@ -108,30 +115,30 @@ def skip(self, index: int, offset: float = 0) -> Jukebox: return Jukebox(self.subsonic, **response) - def add(self, id_: str) -> Jukebox: - """Calls the "jukeboxControl" endpoint of the API with the action "add". + def add(self, songs_ids: list[str]) -> Jukebox: + """Add songs to the jukebox playlist. - :param id_: The ID of a song to add it in the jukebox. - :type id_: str - :return: An object with all the given information about the jukebox. - Except the jukebox playlist. - :rtype: Jukebox + Args: + songs_ids: A list of song IDs to add to the jukebox playlist. + + Returns: + An object that contains the updated jukebox status + and playlist. """ response = self.api.json_request( - "jukeboxControl", {"action": "add", "id": id_} + "jukeboxControl", {"action": "add", "id": songs_ids} )["jukeboxStatus"] return Jukebox(self.subsonic, **response) def clear(self) -> Jukebox: - """Calls the "jukeboxControl" endpoint of the API with the action "clear". + """Clear the playlist of the jukebox. - :return: An object with all the given information about the jukebox. - Except the jukebox playlist. - :rtype: Jukebox + Returns: + An object that contains the updated jukebox status + and playlist. """ - response = self.api.json_request("jukeboxControl", {"action": "clear"})[ "jukeboxStatus" ] @@ -139,13 +146,14 @@ def clear(self) -> Jukebox: return Jukebox(self.subsonic, **response) def remove(self, index: int) -> Jukebox: - """Calls the "jukeboxControl" endpoint of the API with the action "remove". + """Remove a song from the playlist of the jukebox. - :param index: The index in the jukebox playlist for the song to remove. - :type index: int - :return: An object with all the given information about the jukebox. - Except the jukebox playlist. - :rtype: Jukebox + Args: + index: The index of the song to remove from the playlist. + + Returns: + An object that contains the updated jukebox status + and playlist. """ response = self.api.json_request( @@ -155,11 +163,11 @@ def remove(self, index: int) -> Jukebox: return Jukebox(self.subsonic, **response) def shuffle(self) -> Jukebox: - """Calls the "jukeboxControl" endpoint of the API with the action "shuffle". + """Shuffle all the songs in the playlist of the jukebox. - :return: An object with all the given information about the jukebox. - Except the jukebox playlist. - :rtype: Jukebox + Returns: + An object that contains the updated jukebox status + and playlist. """ response = self.api.json_request("jukeboxControl", {"action": "shuffle"})[ @@ -169,14 +177,17 @@ def shuffle(self) -> Jukebox: return Jukebox(self.subsonic, **response) def set_gain(self, gain: float) -> Jukebox: - """Calls the "jukeboxControl" endpoint of the API with the action "setGain". - - :param gain: A number between 0 and 1 (inclusive) to set the gain. - :type gain: float - :raises ValueError: Raised if the gain argument isn't between the valid range. - :return: An object with all the given information about the jukebox. - Except the jukebox playlist. - :rtype: Jukebox + """Set the gain of the playback of the jukebox. + + Args: + gain: A number between 0 and 1 (inclusive) to be set as the gain. + + Raises: + ValueError: Raised if the given gain is not between 0 and 1. + + Returns: + An object that contains the updated jukebox status + and playlist. """ if not 1 > gain > 0: diff --git a/src/knuckles/_lists.py b/src/knuckles/_lists.py index ec2eb8b..171eaa6 100644 --- a/src/knuckles/_lists.py +++ b/src/knuckles/_lists.py @@ -11,6 +11,11 @@ class Lists: + """Class that contains all the methods needed to interact with the + [lists endpoints](https://opensubsonic.netlify.app/categories/lists) + in the Subsonic API. + """ + def __init__(self, api: Api, subsonic: "Subsonic") -> None: self.api = api @@ -19,17 +24,34 @@ def __init__(self, api: Api, subsonic: "Subsonic") -> None: def _get_album_list_generic( self, - type: str, + list_type: str, num_of_albums: int | None = None, album_list_offset: int | None = None, music_folder_id: str | None = None, id3: bool = True, **extra_params: Any, ) -> list[Album]: + """Make a GET requests to the "getAlbumList", used because this + endpoint can generate a lot of different types of list. + + Args: + list_type: The name of the type of list to request to the server. + num_of_albums: The number of albums to be in the list. + album_list_offset: The number of album to offset in the list, + useful for pagination. + music_folder_id: The ID of a music folder to list where the album + are from. + id3: If the request should be send to the ID3 or non-ID3 version + of the endpoint. + + Returns: + A list with all the info about the received albums. + """ + response = self.api.json_request( "getAlbumList2" if id3 else "getAlbumList", { - "type": type, + "type": list_type, "size": num_of_albums, "offset": album_list_offset, "musicFolderId": music_folder_id, @@ -45,6 +67,20 @@ def get_album_list_random_non_id3( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get a random list of albums from the server. Not organized + according ID3 tags. + + Args: + num_of_albums: The number of albums to be in the list. + album_list_offset: The number of album to offset in the list, + useful for pagination. + music_folder_id: The ID of a music folder to list where the album + are from. + + Returns: + A list that contains the info about random albums. + """ + return self._get_album_list_generic( "random", num_of_albums, album_list_offset, music_folder_id, False ) @@ -55,6 +91,21 @@ def get_album_list_newest_non_id3( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get a list of albums from the server organized from + the newest added to the oldest. Not organized according ID3 tags. + + Args: + num_of_albums: The number of albums to be in the list. + album_list_offset: The number of album to offset in the list, + useful for pagination. + music_folder_id: The ID of a music folder to list where the album + are from. + + Returns: + A list that contains the info about the albums + organized from newest to oldest. + """ + return self._get_album_list_generic( "newest", num_of_albums, album_list_offset, music_folder_id, False ) @@ -65,6 +116,21 @@ def get_album_list_highest_non_id3( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get a list of albums from the server organized from + the highest rated to the lowest ones. Not organized according ID3 tags. + + Args: + num_of_albums: The number of albums to be in the list. + album_list_offset: The number of album to offset in the list, + useful for pagination. + music_folder_id: The ID of a music folder to list where the album + are from. + + Returns: + A list that contains the info about the albums + organized from the highest rated to the lowest ones. + """ + return self._get_album_list_generic( "highest", num_of_albums, album_list_offset, music_folder_id, False ) @@ -75,6 +141,22 @@ def get_album_list_frequent_non_id3( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get a list of albums from the server organized from + the most frequent listened to the least. + Not organized according ID3 tags. + + args: + num_of_albums: the number of albums to be in the list. + album_list_offset: the number of album to offset in the list, + useful for pagination. + music_folder_id: the id of a music folder to list where the album + are from. + + Returns: + A list that contains the info about the albums + organized from the most frequent listened to the least. + """ + return self._get_album_list_generic( "frequent", num_of_albums, album_list_offset, music_folder_id, False ) @@ -85,6 +167,22 @@ def get_album_list_recent_non_id3( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get a list of albums from the server organized from + the most recent listened to the least. + not organized according id3 tags. + + args: + num_of_albums: the number of albums to be in the list. + album_list_offset: the number of album to offset in the list, + useful for pagination. + music_folder_id: the id of a music folder to list where the album + are from. + + Returns: + A list that contains the info about the albums + organized from the most recent listened to the least. + """ + return self._get_album_list_generic( "recent", num_of_albums, album_list_offset, music_folder_id, False ) @@ -95,6 +193,21 @@ def get_album_list_alphabetical_by_name_non_id3( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get a list of albums from the server organized alphabetically + by their names. Not organized according ID3 tags. + + args: + num_of_albums: the number of albums to be in the list. + album_list_offset: the number of album to offset in the list, + useful for pagination. + music_folder_id: the id of a music folder to list where the album + are from. + + Returns: + A list that contains the info about the albums + organized alphabetically by their names. + """ + return self._get_album_list_generic( "alphabeticalByName", num_of_albums, @@ -109,6 +222,21 @@ def get_album_list_alphabetical_by_artist_non_id3( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get a list of albums from the server organized alphabetically + by their artist name. Not organized according ID3 tags. + + args: + num_of_albums: the number of albums to be in the list. + album_list_offset: the number of album to offset in the list, + useful for pagination. + music_folder_id: the id of a music folder to list where the album + are from. + + Returns: + A list that contains the info about the albums + organized alphabetically by their artist name. + """ + return self._get_album_list_generic( "alphabeticalByArtist", num_of_albums, @@ -123,6 +251,21 @@ def get_album_list_starred_non_id3( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get a list of the albums that have been starred by + the authenticated user. Not organized according ID3 tags. + + args: + num_of_albums: the number of albums to be in the list. + album_list_offset: the number of album to offset in the list, + useful for pagination. + music_folder_id: the id of a music folder to list where the album + are from. + + Returns: + A list that contains the info about the albums + starred by the user. + """ + return self._get_album_list_generic( "starred", num_of_albums, album_list_offset, music_folder_id, False ) @@ -135,6 +278,25 @@ def get_album_list_by_year_non_id3( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get all the album registered by the server that were created between + the given year range. + + Args: + from_year: The minimum year of the range where the albums + were created. + to_year: The maximum year of the range where the albums + were created. + num_of_albums: the number of albums to be in the list. + album_list_offset: the number of album to offset in the list, + useful for pagination. + music_folder_id: the id of a music folder to list where the album + are from. + + Returns: + A list that contains the info about the albums + that where released in the given year range. + """ + return self._get_album_list_generic( "byYear", num_of_albums, @@ -152,6 +314,23 @@ def get_album_list_by_genre_non_id3( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get all the albums that are tagged with the given genre. + Not organized according ID3 tags. + + Args: + genre_name: The name of the genre that all the albums + must be tagged with. + num_of_albums: the number of albums to be in the list. + album_list_offset: the number of album to offset in the list, + useful for pagination. + music_folder_id: the id of a music folder to list where the album + are from. + + Returns: + A list that contains the info about the albums + that are tagged with the given album. + """ + return self._get_album_list_generic( "byGenre", num_of_albums, @@ -167,6 +346,19 @@ def get_album_list_random( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get a random list of albums from the server. + + Args: + num_of_albums: The number of albums to be in the list. + album_list_offset: The number of album to offset in the list, + useful for pagination. + music_folder_id: The ID of a music folder to list where the album + are from. + + Returns: + A list that contains the info about random albums. + """ + return self._get_album_list_generic( "random", num_of_albums, album_list_offset, music_folder_id ) @@ -177,6 +369,21 @@ def get_album_list_newest( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get a list of albums from the server organized from + the newest added to the oldest. Not organized according ID3 tags. + + Args: + num_of_albums: The number of albums to be in the list. + album_list_offset: The number of album to offset in the list, + useful for pagination. + music_folder_id: The ID of a music folder to list where the album + are from. + + Returns: + A list that contains the info about the albums + organized from newest to oldest. + """ + return self._get_album_list_generic( "newest", num_of_albums, album_list_offset, music_folder_id ) @@ -187,6 +394,21 @@ def get_album_list_highest( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get a list of albums from the server organized from + the highest rated to the lowest ones. Not organized according ID3 tags. + + Args: + num_of_albums: The number of albums to be in the list. + album_list_offset: The number of album to offset in the list, + useful for pagination. + music_folder_id: The ID of a music folder to list where the album + are from. + + Returns: + A list that contains the info about the albums + organized from the highest rated to the lowest ones. + """ + return self._get_album_list_generic( "highest", num_of_albums, album_list_offset, music_folder_id ) @@ -197,6 +419,22 @@ def get_album_list_frequent( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get a list of albums from the server organized from + the most frequent listened to the least. + Not organized according ID3 tags. + + args: + num_of_albums: the number of albums to be in the list. + album_list_offset: the number of album to offset in the list, + useful for pagination. + music_folder_id: the id of a music folder to list where the album + are from. + + Returns: + A list that contains the info about the albums + organized from the most frequent listened to the least. + """ + return self._get_album_list_generic( "frequent", num_of_albums, album_list_offset, music_folder_id ) @@ -207,6 +445,22 @@ def get_album_list_recent( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get a list of albums from the server organized from + the most recent listened to the least. + not organized according id3 tags. + + args: + num_of_albums: the number of albums to be in the list. + album_list_offset: the number of album to offset in the list, + useful for pagination. + music_folder_id: the id of a music folder to list where the album + are from. + + Returns: + A list that contains the info about the albums + organized from the most recent listened to the least. + """ + return self._get_album_list_generic( "recent", num_of_albums, album_list_offset, music_folder_id ) @@ -217,6 +471,21 @@ def get_album_list_alphabetical_by_name( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get a list of albums from the server organized alphabetically + by their names. Not organized according ID3 tags. + + args: + num_of_albums: the number of albums to be in the list. + album_list_offset: the number of album to offset in the list, + useful for pagination. + music_folder_id: the id of a music folder to list where the album + are from. + + Returns: + A list that contains the info about the albums + organized alphabetically by their names. + """ + return self._get_album_list_generic( "alphabeticalByName", num_of_albums, album_list_offset, music_folder_id ) @@ -227,6 +496,21 @@ def get_album_list_alphabetical_by_artist( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get a list of albums from the server organized alphabetically + by their artist name. Not organized according ID3 tags. + + args: + num_of_albums: the number of albums to be in the list. + album_list_offset: the number of album to offset in the list, + useful for pagination. + music_folder_id: the id of a music folder to list where the album + are from. + + Returns: + A list that contains the info about the albums + organized alphabetically by their artist name. + """ + return self._get_album_list_generic( "alphabeticalByArtist", num_of_albums, album_list_offset, music_folder_id ) @@ -237,6 +521,21 @@ def get_album_list_starred( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get a list of the albums that have been starred by + the authenticated user. Not organized according ID3 tags. + + args: + num_of_albums: the number of albums to be in the list. + album_list_offset: the number of album to offset in the list, + useful for pagination. + music_folder_id: the id of a music folder to list where the album + are from. + + Returns: + A list that contains the info about the albums + starred by the user. + """ + return self._get_album_list_generic( "starred", num_of_albums, album_list_offset, music_folder_id ) @@ -249,6 +548,25 @@ def get_album_list_by_year( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get all the album registered by the server that were created between + the given year range. + + Args: + from_year: The minimum year of the range where the albums + were created. + to_year: The maximum year of the range where the albums + were created. + num_of_albums: the number of albums to be in the list. + album_list_offset: the number of album to offset in the list, + useful for pagination. + music_folder_id: the id of a music folder to list where the album + are from. + + Returns: + A list that contains the info about the albums + that where released in the given year range. + """ + return self._get_album_list_generic( "byYear", num_of_albums, @@ -265,6 +583,23 @@ def get_album_list_by_genre( album_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Album]: + """Get all the albums that are tagged with the given genre. Not organized + according ID3 tags. + + Args: + genre_name: The name of the genre that all the albums must be tagged + with. + num_of_albums: the number of albums to be in the list. + album_list_offset: the number of album to offset in the list, + useful for pagination. + music_folder_id: the id of a music folder to list where the album + are from. + + Returns: + A list that contains the info about the albums + that are tagged with the given album. + """ + return self._get_album_list_generic( "byGenre", num_of_albums, @@ -281,6 +616,24 @@ def get_random_songs( to_year: int | None = None, music_folder_id: str | None = None, ) -> list[Song]: + """Get random songs registered in the server. + + Args: + num_of_songs: The number of songs to return. + genre_name: The genre that the songs must + have it tagged on them. + from_year: The minimum year where the songs + were released. + to_year: The maximum year where the songs + were released. + music_folder_id: An ID of a music folder + to limit where the songs should be from. + + Returns: + A list that contains all the info about + that were randomly selected by the server. + """ + response = self.api.json_request( "getRandomSongs", { @@ -301,6 +654,23 @@ def get_songs_by_genre( song_list_offset: int | None = None, music_folder_id: str | None = None, ) -> list[Song]: + """Get all the songs tagged with the given genre. + + Args: + genre_name: The name of the genre that all the songs + must be tagged with. + num_of_songs: The number of songs that the list + should have. + song_list_offset: the number of songs to offset in the list, + useful for pagination. + music_folder_id: An ID of a music folder where all the songs + should be from. + + Returns: + A list that contains all the info about + that are tagged with the given genre. + """ + response = self.api.json_request( "getSongsByGenre", { @@ -314,11 +684,30 @@ def get_songs_by_genre( return [Song(subsonic=self.subsonic, **song) for song in response] def get_now_playing(self) -> list[NowPlayingEntry]: + """Get the songs that are currently playing by all the users. + + Returns: + A list that holds all the info about all the + song that are current playing by all the users. + """ + response = self.api.json_request("getNowPlaying")["nowPlaying"]["entry"] return [NowPlayingEntry(subsonic=self.subsonic, **entry) for entry in response] def get_starred_non_id3(self, music_folder_id: str | None = None) -> StarredContent: + """Get all the songs, albums and artists starred by the authenticated + user. Not organized according ID3 tags. + + Args: + music_folder_id: An ID of a music folder where all the songs + albums, and artists should be from. + + Returns: + An object that holds all the info about all the starred + songs, albums and artists by the user. + """ + response = self.api.json_request( "getStarred", {"musicFolderId": music_folder_id} )["starred"] @@ -326,6 +715,18 @@ def get_starred_non_id3(self, music_folder_id: str | None = None) -> StarredCont return StarredContent(subsonic=self.subsonic, **response) def get_starred(self, music_folder_id: str | None = None) -> StarredContent: + """Get all the songs, albums and artists starred by the authenticated + user. + + Args: + music_folder_id: An ID of a music folder where all the songs + albums, and artists should be from. + + Returns: + An object that holds all the info about all the starred + songs, albums and artists by the user. + """ + response = self.api.json_request( "getStarred2", {"musicFolderId": music_folder_id} )["starred2"] diff --git a/src/knuckles/_media_annotation.py b/src/knuckles/_media_annotation.py index 17d1062..048fd61 100644 --- a/src/knuckles/_media_annotation.py +++ b/src/knuckles/_media_annotation.py @@ -9,105 +9,121 @@ class MediaAnnotation: - """Class that contains all the methods needed to interact - with the media annotation calls in the Subsonic API. - + """Class that contains all the methods needed to interact with the + [media annotations endpoints](https://opensubsonic.netlify.app/ + categories/media-annotation/) in the Subsonic API. """ def __init__(self, api: Api, subsonic: "Subsonic") -> None: self.api = api self.subsonic = subsonic - def star_song(self, id_: str) -> "Subsonic": - """Calls the "star" endpoint of the API. + def star_song(self, song_id: str) -> "Subsonic": + """Star a song from the server. - :param id_: The ID of a song to star. - :type id_: str - :return: The object itself to allow method chaining. - :rtype: Subsonic + Args: + song_id: The ID of the song to star. + + Returns: + The Subsonic object where this method was called to allow + method chaining. """ - self.api.json_request("star", {"id": id_}) + self.api.json_request("star", {"id": song_id}) return self.subsonic - def star_album(self, id_: str) -> "Subsonic": - """Calls the "star" endpoint of the API. + def star_album(self, album_id: str) -> "Subsonic": + """Star an album from the server. + + Args: + album_id: The ID of the album to star. - :param id_: The ID of an album to star. - :type id_: str - :return: The object itself to allow method chaining. - :rtype: Subsonic + Returns: + The Subsonic object where this method was called to allow + method chaining. """ - self.api.json_request("star", {"albumId": id_}) + self.api.json_request("star", {"albumId": album_id}) return self.subsonic - def star_artist(self, id_: str) -> "Subsonic": - """Calls the "star" endpoint of the API. + def star_artist(self, artist_id: str) -> "Subsonic": + """Star an artist from the server. + + Args: + artist_id: The ID of the artist to star. - :param id_: The ID of an artist to star. - :type id_: str - :return: The object itself to allow method chaining. - :rtype: Subsonic + Returns: + The Subsonic object where this method was called to allow + method chaining. """ - self.api.json_request("star", {"artistId": id_}) + self.api.json_request("star", {"artistId": artist_id}) return self.subsonic - def unstar_song(self, id_: str) -> "Subsonic": - """Calls the "unstar" endpoint of the API. + def unstar_song(self, song_id: str) -> "Subsonic": + """Unstar a song from the server. - :param id_: The ID of a song to unstar. - :type id_: str - :return: The object itself to allow method chaining. - :rtype: Subsonic + Args: + song_id: The ID of the song to unstar. + + Returns: + The Subsonic object where this method was called to allow + method chaining. """ - self.api.json_request("unstar", {"id": id_}) + self.api.json_request("unstar", {"id": song_id}) return self.subsonic - def unstar_album(self, id_: str) -> "Subsonic": - """Calls the "unstar" endpoint of the API. + def unstar_album(self, album_id: str) -> "Subsonic": + """Unstar an album from the server. + + Args: + album_id: The ID of the album to unstar. - :param id_: The ID of an album to unstar. - :type id_: str - :return: The object itself to allow method chaining. - :rtype: Subsonic + Returns: + The Subsonic object where this method was called to allow + method chaining. """ - self.api.json_request("unstar", {"albumId": id_}) + self.api.json_request("unstar", {"albumId": album_id}) return self.subsonic - def unstar_artist(self, id_: str) -> "Subsonic": - """Calls the "unstar" endpoint of the API. + def unstar_artist(self, artist_id: str) -> "Subsonic": + """Unstar an artist from the server. + + Args: + artist_id: The ID of the artist to unstar. - :param id_: The ID of an artist to unstar. - :type id_: str - :return: The object itself to allow method chaining. - :rtype: Subsonic + Returns: + The Subsonic object where this method was called to allow + method chaining. """ - self.api.json_request("unstar", {"artistId": id_}) + self.api.json_request("unstar", {"artistId": artist_id}) return self.subsonic - def set_rating(self, id_: str, rating: int) -> "Subsonic": - """Calls to the "setRating" endpoint of the API. - - :param id_: The ID of a song to set its rating. - :type id_: str - :param rating: The rating to set. It should be a number - between 1 and 5 (inclusive). - :type rating: int - :raises InvalidRatingNumber: Raised if the given rating number - isn't in the valid range. - :return: The object itself to allow method chaining. - :rtype: Subsonic + def set_rating(self, song_id: str, rating: int) -> "Subsonic": + """The the rating of a song. + + Args: + song_id: The ID of the song to set its rating. + rating: The rating between 1 and 5 (inclusive) to set + the rating of the song to. + + Raises: + InvalidRatingNumber: Raised when a number that is not + between 1 and 5 (inclusive) has been pass in into + the `rating` parameter. + + Returns: + The Subsonic object where this method was called to allow + method chaining. """ if rating not in range(1, 6): @@ -118,45 +134,49 @@ def set_rating(self, id_: str, rating: int) -> "Subsonic": ) ) - self.api.json_request("setRating", {"id": id_, "rating": rating}) + self.api.json_request("setRating", {"id": song_id, "rating": rating}) return self.subsonic - def remove_rating(self, id_: str) -> "Subsonic": - """Calls the "setRating" endpoint of the API with a rating of 0. + def remove_rating(self, song_id: str) -> "Subsonic": + """Remove the rating entry of a song. - :param id_: The ID of a song to set its rating. - :type id_: str - :return: The object itself to allow method chaining. - :rtype: Subsonic + Args: + song_id: The ID of the song which entry should + be removed. + + Returns: + The Subsonic object where this method was called to allow + method chaining. """ - self.api.json_request("setRating", {"id": id_, "rating": 0}) + self.api.json_request("setRating", {"id": song_id, "rating": 0}) return self.subsonic def scrobble( - self, id_: list[str], time: list[datetime], submission: bool = True + self, song_id: list[str], time: list[datetime], submission: bool = True ) -> "Subsonic": - """Calls to the "scrobble" endpoint of the API - - :param id_: The list of song IDs to scrobble. - :type id_: list[str] - :param time: The time at which the song was listened to. - :type time: datetime - :param submission: If the scrobble is a submission - or a "now playing" notification, defaults to True. - :type submission: bool, optional - :return: The object itself to allow method chaining. - :rtype: Subsonic + """Scrobble (register) that some song have been locally played or + is being played. + + Args: + song_id: The ID of the song to scrobble. + time: How many times in second the song has been listened. + submission: If true it will be registered that the song **was + played**, if false the song will be scrobble as + **now playing**. + + Returns: + The Subsonic object where this method was called to allow + method chaining. """ - self.api.json_request( "scrobble", # Multiply by 1000 because the API uses # milliseconds instead of seconds for UNIX time { - "id": id_, + "id": song_id, "time": [int(seconds.timestamp()) * 1000 for seconds in time], "submission": submission, }, diff --git a/src/knuckles/_media_library_scanning.py b/src/knuckles/_media_library_scanning.py index 009c3b7..c3d1436 100644 --- a/src/knuckles/_media_library_scanning.py +++ b/src/knuckles/_media_library_scanning.py @@ -8,9 +8,10 @@ class MediaLibraryScanning: - """Class that contains all the methods needed to interact - with the media library scanning calls in the Subsonic API. - + """Class that contains all the methods needed to interact with the + [media library scanning endpoints](https://opensubsonic.netlify.app/ + categories/media-library-scanning/) + in the Subsonic API. """ def __init__(self, api: Api, subsonic: "Subsonic") -> None: @@ -20,10 +21,11 @@ def __init__(self, api: Api, subsonic: "Subsonic") -> None: self.subsonic = subsonic def get_scan_status(self) -> ScanStatus: - """Calls to the "getScanStatus" endpoint of the API. + """Get the status of the scanning of the library. - :return: An object with the information about the status of the scan. - :rtype: ScanStatus + Returns: + An object that holds all the info about the + current state of the scanning of the library. """ response = self.api.json_request("getScanStatus")["scanStatus"] @@ -31,10 +33,11 @@ def get_scan_status(self) -> ScanStatus: return ScanStatus(self.subsonic, **response) def start_scan(self) -> ScanStatus: - """Calls to the "scanStatus" endpoint of the API. + """Request to the server to start a scanning of the library. - :return: An object with the information about the status of the scan. - :rtype: ScanStatus + Returns: + An object that holds all the info about the + current state of the scanning of the library. """ response = self.api.json_request("startScan")["scanStatus"] diff --git a/src/knuckles/_media_retrieval.py b/src/knuckles/_media_retrieval.py index 54a685b..2125168 100644 --- a/src/knuckles/_media_retrieval.py +++ b/src/knuckles/_media_retrieval.py @@ -18,9 +18,9 @@ class SubtitlesFileFormat(Enum): class MediaRetrieval: - """Class that contains all the methods needed to interact - with the media retrieval calls in the Subsonic API. - + """Class that contains all the methods needed to interact with the + [media retrieval endpoints](https://opensubsonic.netlify.app/ + categories/media-retrieval/) in the Subsonic API. """ def __init__(self, api: Api, subsonic: "Subsonic") -> None: @@ -29,14 +29,17 @@ def __init__(self, api: Api, subsonic: "Subsonic") -> None: @staticmethod def _download_file(response: Response, downloaded_file_path: Path) -> Path: - """Downloads a file attached to a Response object. - - :param response: The response to get the download binary data. - :type response: Response - :param downloaded_file_path: The file path to save the downloaded file. - :type downloaded_file_path: Path - :return: The same path given in downloaded_file_path. - :rtype: Path + """Download to the local filesystem the binary file data attached to a + `requests` Response object. + Doesn't check if the Response object is valid for file downloading. + + Args: + response: The response object to get the file from. + downloaded_file_path: A path where the file to download should + be saved. + + Returns: + The path where the file was finally saved. """ response.raise_for_status() @@ -54,6 +57,26 @@ def _handle_download( file_or_directory_path: Path, determinate_filename: Callable[[Response], str], ) -> Path: + """Download the file attached with the given `requests` Response + object, if the given path is a directory then the file will be + downloaded inside of it, if its a valid file path it will be downloaded + using this exact filename. + + In case of not being a directory then a custom callback to determine + the name of the file to be created. + + + Args: + response: The response object to get the file from. + file_or_directory_path: The directory or filename where the file + should be saved to. + determinate_filename: The callback to be used to determine the + filename in case the given path points to a directory. + + Returns: + The path where the file was finally saved. + """ + if not file_or_directory_path.is_dir(): return cls._download_file(response, file_or_directory_path) @@ -63,44 +86,46 @@ def _handle_download( def stream( self, - id_: str, + song_or_video_id: str, max_bitrate_rate: int | None = None, - format_: str | None = None, + stream_format: str | None = None, time_offset: int | None = None, size: str | None = None, estimate_content_length: bool | None = None, converted: bool | None = None, ) -> str: - """Returns a valid url for streaming the requested song or video - - :param id_: The id of the song or video to stream - :type id_: str - :param max_bitrate_rate: A limit for the stream bitrate - :type max_bitrate_rate: int | None - :param format_: The file format of preference to be used in the stream. - :type format_: str | None - :param time_offset: Only applicable to video streaming. - An offset in seconds from where the video should start. - :type time_offset: int | None - :param size: Only applicable to video streaming. - The resolution for the streamed video, in the format of "WIDTHHxHEIGHT". - :type size: str | None - :param estimate_content_length: If the response should set a - Content-Length HTTP header with an estimation of the duration of the media. - :type estimate_content_length: bool | None - :param converted: Only applicable to video streaming. - Try to retrieve from the server an optimize video in MP4 if it's available. - :type converted: bool | None - :return A url that points to the given song in the stream endpoint - :rtype str + """Get the URL required to stream a song or video. + + Args: + song_or_video_id: The ID of the song or video to get its + steam URL + max_bitrate_rate: The max bitrate the stream should have. + stream_format: The format the song or video should be. + **Warning**: The available formats are dependant of the + server implementation. The only secure format is "raw", + which disabled transcoding at all. + time_offset: An offset where the stream should start. It may + not work with video, depending of the server configuration. + size: The maximum resolution of the streaming in the format `WxH`, + only works with video streaming. + estimate_content_length: When set to true the response with have + the `Content-Length` HTTP header set to a estimated duration + for the streamed song or video. + converted: If set to true the server will try to stream a + transcoded version in `MP4`. Only works with video + streaming. + + Returns: + An URL with all the needed parameters to start a streaming + using a GET request. """ return self.subsonic.api.generate_url( "stream", { - "id": id_, + "id": song_or_video_id, "maxBitRate": max_bitrate_rate, - "format": format_, + "format": stream_format, "timeOffset": time_offset, "size": size, "estimateContentLength": estimate_content_length, @@ -108,20 +133,21 @@ def stream( }, ) - def download(self, id_: str, file_or_directory_path: Path) -> Path: - """Calls the "download" endpoint of the API. - - :param id_: The id of the song or video to download. - :type id_: str - :param file_or_directory_path: If a directory path is passed the file will be - inside of it with the default filename given by the API, - if not the file will be saved directly in the given path. - :type file_or_directory_path: Path - :return The path of the downloaded file - :rtype Path + def download(self, song_or_video_id: str, file_or_directory_path: Path) -> Path: + """Download a song or video from the server. + + Args: + song_or_video_id: The ID of the song or video to download. + file_or_directory_path: The path where the downloaded file should + be saved. If the given path is a directory then the file will + be downloaded inside of it, if its a valid file path it will be + downloaded using this exact filename. + + Returns: + The path where the song or video was finally saved. """ - response = self.api.raw_request("download", {"id": id_}) + response = self.api.raw_request("download", {"id": song_or_video_id}) def determinate_filename(file_response: Response) -> str: filename = ( @@ -146,48 +172,54 @@ def determinate_filename(file_response: Response) -> str: def hls( self, - id_: str, + song_or_video_id: str, custom_bitrates: list[str] | None = None, audio_track_id: str | None = None, ) -> str: - """Returns a valid url for streaming the requested song with hls.m3u8 - - :param id_: The id of the song to stream. - :type id_: str - :param custom_bitrates: A list of bitrates to be added to the hls playlist - for video streaming, the resolution can also be specified with - this format: "BITRATE@WIDTHxHEIGHT". - :type custom_bitrates: list[str] | None - :param audio_track_id: The id of the audio track to be used - if the playlist is for a video. - :type audio_track_id: str | None - :return A url that points to the given song in the hls.m3u8 endpoint - :rtype str + """Get the URL required to stream a song or video with hls.m3u8. + + Args: + song_or_video_id: The ID of the song or video to stream. + custom_bitrates: The bitrate that the server should try to + limit the stream to. If more that one is specified the + server will create a `variant playlist`, suitable for adaptive + bitrate streaming. + audio_track_id: The ID of an audio track to be added to the stream + if video is being streamed. + + Returns: + An URL with all the needed parameters to start a streaming + with hls.m3u8 using a GET request. """ return self.subsonic.api.generate_url( "hls.m3u8", - {"id": id_, "bitRate": custom_bitrates, "audioTrack": audio_track_id}, + { + "id": song_or_video_id, + "bitRate": custom_bitrates, + "audioTrack": audio_track_id, + }, ) def get_captions( self, - id_: str, + caption_id: str, file_or_directory_path: Path, subtitles_file_format: SubtitlesFileFormat = SubtitlesFileFormat.VTT, ) -> Path: - """Calls the "getCaptions" endpoint of the API. - - :param id_: The ID of the video to get the captions - :type id_: str - :param file_or_directory_path: If a directory path is passed the file will be - inside of it with the default filename given by the API, - if not the file will be saved directly in the given path. - :type file_or_directory_path: Path - :param subtitles_file_format: The preferred captions file format. - :type subtitles_file_format: SubtitlesFileFormat - :return: The path of the downloaded captions file. - :rtype: + """Download a video caption file from the server. + + Args: + caption_id: The ID of the caption to download. + file_or_directory_path: The path where the downloaded file should + be saved. If the given path is a directory then the file will + be downloaded inside of it, if its a valid file path it will be + downloaded using this exact filename. + subtitles_file_format: The format that the subtitle file should + have. + + Returns: + The path where the captions was finally saved. """ # Check if the given file format is a valid one @@ -195,7 +227,7 @@ def get_captions( response = self.api.raw_request( "getCaptions", - {"id": id_, "format": subtitles_file_format.value}, + {"id": caption_id, "format": subtitles_file_format.value}, ) def determinate_filename(file_response: Response) -> str: @@ -208,38 +240,40 @@ def determinate_filename(file_response: Response) -> str: else: file_extension = guess_extension(mime_type) - return id_ + file_extension if file_extension else id_ + return caption_id + file_extension if file_extension else caption_id return self._handle_download( response, file_or_directory_path, determinate_filename ) def get_cover_art( - self, id_: str, file_or_directory_path: Path, size: int | None = None + self, cover_art_id: str, file_or_directory_path: Path, size: int | None = None ) -> Path: - """Calls the "getCoverArt" endpoint of the API. - - :param id_: The id of the cover art to download. - :type id_: str - :param file_or_directory_path: If a directory path is passed the file will be - inside of it with the filename being the name of the user and - a guessed file extension, if not the file will be saved - directly in the given path. - :type file_or_directory_path: Path - :param size: The size of the image to be scale to in a square. - :type size: int - :return Returns the given path - :rtype Path + """Download the cover art from the server. + + Args: + cover_art_id: The ID of the cover art to download. + file_or_directory_path: The path where the downloaded file should + be saved. If the given path is a directory then the file will + be downloaded inside of it, if its a valid file path it will be + downloaded using this exact filename. + size: The width in pixels that the image should have, + the cover arts are always squares. + + Returns: + The path where the captions was finally saved. """ - response = self.api.raw_request("getCoverArt", {"id": id_, "size": size}) + response = self.api.raw_request( + "getCoverArt", {"id": cover_art_id, "size": size} + ) def determinate_filename(file_response: Response) -> str: file_extension = guess_extension( file_response.headers["content-type"].partition(";")[0].strip() ) - return id_ + file_extension if file_extension else id_ + return cover_art_id + file_extension if file_extension else cover_art_id return self._handle_download( response, file_or_directory_path, determinate_filename @@ -248,6 +282,18 @@ def determinate_filename(file_response: Response) -> str: def get_lyrics( self, artist_name: str | None = None, song_title: str | None = None ) -> Lyrics: + """Get the lyrics of a song. + + Args: + artist_name: The name of the artist that made the song to get its + lyrics from. + song_title: The title of the song to get its lyrics from. + + Returns: + An object that contains all the info about the requested + lyrics. + """ + response = self.api.json_request( "getLyrics", {"artist": artist_name, "title": song_title} )["lyrics"] @@ -255,17 +301,17 @@ def get_lyrics( return Lyrics(subsonic=self.subsonic, **response) def get_avatar(self, username: str, file_or_directory_path: Path) -> Path: - """Calls the "getAvatar" endpoint of the API. - - :param username: The username of the profile picture to download. - :type username: str - :param file_or_directory_path: If a directory path is passed the file will be - inside of it with the filename being the name of the user and - a guessed file extension, if not the file will be saved - directly in the given path. - :type file_or_directory_path: Path - :return Returns the given path - :rtype Path + """Download the avatar image of a user from the server. + + Args: + username: The username of the user to get its avatar from. + file_or_directory_path: The path where the downloaded file should + be saved. If the given path is a directory then the file will + be downloaded inside of it, if its a valid file path it will be + downloaded using this exact filename. + + Returns: + The path where the avatar image was finally saved. """ response = self.api.raw_request("getAvatar", {"username": username}) diff --git a/src/knuckles/_playlists.py b/src/knuckles/_playlists.py index 0d78592..47f60f1 100644 --- a/src/knuckles/_playlists.py +++ b/src/knuckles/_playlists.py @@ -8,9 +8,9 @@ class Playlists: - """Class that contains all the methods needed to interact - with the playlists calls and actions in the Subsonic API. - + """Class that contains all the methods needed to interact with the + [playlists endpoints](https://opensubsonic.netlify.app/ + categories/playlists/) in the Subsonic API. """ def __init__(self, api: Api, subsonic: "Subsonic") -> None: @@ -20,14 +20,17 @@ def __init__(self, api: Api, subsonic: "Subsonic") -> None: self.subsonic = subsonic def get_playlists(self, username: str | None = None) -> list[Playlist]: - """Calls to the "getPlaylists" endpoint of the API. + """Get all the playlists available to the authenticated user. - :param username: The user to get its playlist, - if None gets the playlist of the authenticated user, defaults to None. - :type username: str | None, optional - :return: A list with all the playlist of the desired user. - :rtype: list[Playlist] + Args: + username: The username of another user if is wanted to get the + playlists they can access. + + Returns: + A list that holds all the info about all the playlist + that the user can play. """ + response = self.api.json_request( "getPlaylists", {"username": username} if username else {}, @@ -37,15 +40,18 @@ def get_playlists(self, username: str | None = None) -> list[Playlist]: return playlists - def get_playlist(self, id_: str) -> Playlist: - """Calls to the "getPlaylist" endpoint of the API. + def get_playlist(self, playlist_id: str) -> Playlist: + """Get all the info about a playlist available for the authenticated + user. + + Args: + playlist_id: The ID of the playlist to get its info. - :param id_: The ID of the playlist to get. - :type id_: str - :return: The requested playlist. - :rtype: Playlist + Returns: + An object that holds all the info about the requested playlist. """ - response = self.api.json_request("getPlaylist", {"id": id_})["playlist"] + + response = self.api.json_request("getPlaylist", {"id": playlist_id})["playlist"] return Playlist(self.subsonic, **response) @@ -56,23 +62,19 @@ def create_playlist( public: bool | None = None, song_ids: list[str] | None = None, ) -> Playlist: - """Calls the "createPlaylist" endpoint of the API. - - The Subsonic API only allows to set a name and a list of songs when creating - a playlist. To allow more initial customization (comment and public) - this method calls the "updatePlaylist" endpoint internally. - - :param name: The name of the new playlist. - :type name: str - :param comment: A comment to append to the playlist, defaults to None. - :type comment: str | None, optional - :param public: If the playlist should be public of private, defaults to None. - :type public: bool | None, optional - :param song_ids: A list of songs to add to the playlist, defaults to None. - :type song_ids: list[str] | None, optional - :return: The new created playlist. - :rtype: Playlist + """Create a new playlist for the authenticated user. + + Args: + name: The name of the playlist to be created. + comment: A comment to be added to the new created playlist. + public: If the song should be public or not. + song_ids: A list of ID of the songs that should be included + with the playlist. + + Returns: + An object that holds all the info about the new created playlist. """ + response = self.api.json_request( "createPlaylist", {"name": name, "songId": song_ids} )["playlist"] @@ -91,36 +93,33 @@ def create_playlist( def update_playlist( self, - id_: str, + playlist_id: str, name: str | None = None, comment: str | None = None, public: bool | None = None, song_ids_to_add: list[str] | None = None, song_indexes_to_remove: list[int] | None = None, ) -> Playlist: - """Calls the "updatePlaylist" endpoint of the API. - - :param id_: The ID of the playlist to update. - :type id_: str - :param name: A new name for the playlist, defaults to None. - :type name: str | None, optional - :param comment: A new comment for the playlist, defaults to None. - :type comment: str | None, optional - :param public: A new public state for the playlist, defaults to None. - :type public: bool | None, optional - :param song_ids_to_add: A list of IDs of songs to add to the playlist, - defaults to None. - :type song_ids_to_add: list[str] | None, optional - :param song_indexes_to_remove: A list of indexes of songs to remove - in the playlist, defaults to None. - :type song_indexes_to_remove: list[int] | None, optional - :return: The updated version of the playlist. - :rtype: Playlist + """Update the info of a playlist. + + Args: + playlist_id: The ID of the playlist to update its info. + name: A new name for the playlist. + comment: A new comment for the playlist. + public: Change if the playlist should be public or private. + song_ids_to_add: A list of IDs of new songs to be added to the + playlist. + song_indexes_to_remove: A list in indexes of songs that should + be removed from the playlist. + + Returns: + An object that holds all the info about the updated playlist. """ + self.api.json_request( "updatePlaylist", { - "playlistId": id_, + "playlistId": playlist_id, "name": name, "comment": comment, "public": public, @@ -130,17 +129,20 @@ def update_playlist( ) return Playlist( - self.subsonic, id=id_, name=name, comment=comment, public=public + self.subsonic, id=playlist_id, name=name, comment=comment, public=public ) - def delete_playlist(self, id_: str) -> "Subsonic": - """Calls the "deletePlaylist" endpoint of the API. + def delete_playlist(self, playlist_id: str) -> "Subsonic": + """Delete a playlist. - :param id_: The ID of the song to remove. - :type id_: str - :return: The object itself to allow method chaining. - :rtype: Subsonic + Args: + playlist_id: The ID of the playlist to remove. + + Returns: + The Subsonic object where this method was called to allow + method chaining. """ - self.api.json_request("deletePlaylist", {"id": id_}) + + self.api.json_request("deletePlaylist", {"id": playlist_id}) return self.subsonic diff --git a/src/knuckles/_podcast.py b/src/knuckles/_podcast.py index cc9df2b..44b8f98 100644 --- a/src/knuckles/_podcast.py +++ b/src/knuckles/_podcast.py @@ -8,9 +8,9 @@ class Podcast: - """Class that contains all the methods needed to interact - with the podcast calls and actions in the Subsonic API. - + """Class that contains all the methods needed to interact with the + [podcast endpoints](https://opensubsonic.netlify.app/ + categories/podcast/) in the Subsonic API. """ def __init__(self, api: Api, subsonic: "Subsonic") -> None: @@ -19,14 +19,17 @@ def __init__(self, api: Api, subsonic: "Subsonic") -> None: # Only to pass it to the models self.subsonic = subsonic - def get_podcasts(self, with_episodes: bool = True) -> list[Channel]: - """Calls the "getPodcasts" endpoint of the API. + def get_podcast_channels(self, with_episodes: bool = True) -> list[Channel]: + """Get all the info about all the available podcasts channels in the + server. - :param with_episodes: If the channels should also have - all the episodes inside of them, defaults to True. - :type with_episodes: bool, optional - :return: A list with all the podcast channels in the sever. - :rtype: list[Channel] + Args: + with_episodes: If the server should also return all the info + about each episode of each podcast channel + + Returns: + An list that hold all the info about all the available podcasts + channels. """ response = self.api.json_request( @@ -35,32 +38,37 @@ def get_podcasts(self, with_episodes: bool = True) -> list[Channel]: return [Channel(self.subsonic, **channel) for channel in response] - def get_podcast(self, id_: str, with_episodes: bool | None = None) -> Channel: - """Calls the "getPodcasts" endpoint of the API with a specific ID - to only return the desired podcast channel. - - :param id_: The ID of the channel to get. - :type id_: str - :param with_episodes: If the channels should also have - all the episodes inside of them, defaults to True. - :type with_episodes: bool, optional - :return: The requested podcast channel. - :rtype: Channel + def get_podcast_channel( + self, podcast_channel_id: str, with_episodes: bool | None = None + ) -> Channel: + """Get all the info about a podcast channel. + + Args: + podcast_channel_id: The ID of the podcast channel to get its info. + with_episodes: If the server should also return all the info + about each episode of the podcast channel. + + Returns: + An object that hold all the info about the requested podcast + channel. """ response = self.api.json_request( - "getPodcasts", {"id": id_, "includeEpisodes": with_episodes} + "getPodcasts", {"id": podcast_channel_id, "includeEpisodes": with_episodes} )["podcasts"][0] return Channel(self.subsonic, **response) - def get_newest_podcasts(self, number_max_episodes: int) -> list[Episode]: - """Calls the "getNewestPodcasts" endpoint of the API. + def get_newest_podcast_episodes(self, number_max_episodes: int) -> list[Episode]: + """Get all the info about the newest released podcast episodes. - :param number_max_episodes: The max number of episodes to return. - :type number_max_episodes: int - :return: The list with the new episodes. - :rtype: list[Episode] + Args: + number_max_episodes: The max number of episodes that the server + should return. + + Returns: + A list that holds all the info about all the newest released + episodes. """ response = self.api.json_request( @@ -69,18 +77,18 @@ def get_newest_podcasts(self, number_max_episodes: int) -> list[Episode]: return [Episode(self.subsonic, **episode) for episode in response] - def get_episode(self, id_: str) -> Episode | None: - """Calls the "getPodcasts" endpoints of the API and search through - all the episodes to find the one with the same ID of the provided ID. + def get_podcast_episode(self, episode_id: str) -> Episode | None: + """Get all the info about a podcast episode. + + Args: + episode_id: The ID of the podcast episode to get its info. - :param id_: The provided episode ID. - :type id_: str - :return: The episode with the same ID - or None if no episode with the ID are found. - :rtype: Episode | None + Returns: + An object that holds all the info about the requested podcast + episode. """ - channels = self.get_podcasts() + channels = self.get_podcast_channels() # Flatten the list of episodes inside the list of channels list_of_episodes = [ @@ -91,16 +99,17 @@ def get_episode(self, id_: str) -> Episode | None: ] for episode in list_of_episodes: - if episode.id == id_: + if episode.id == episode_id: return episode return None def refresh_podcasts(self) -> "Subsonic": - """Calls the "refreshPodcast" method of the API. + """Request the server to search for new podcast episodes. - :return: The object itself to allow method chaining. - :rtype: Subsonic + Returns: + The Subsonic object where this method was called to allow + method chaining. """ self.api.json_request("refreshPodcasts") @@ -108,53 +117,62 @@ def refresh_podcasts(self) -> "Subsonic": return self.subsonic def create_podcast_channel(self, url: str) -> "Subsonic": - """Calls the "createPodcastChannel endpoint of the API." + """Create a new podcast channel - :param url: The url of the new podcast. - :type url: str - :return: The object itself to allow method chaining. - :rtype: Subsonic + Args: + url: The URL of the podcast to add. + + Returns: + The Subsonic object where this method was called to allow + method chaining. """ self.api.json_request("createPodcastChannel", {"url": url}) return self.subsonic - def delete_podcast_channel(self, id_: str) -> "Subsonic": - """Calls the "deletePodcastChannel" endpoint of the API. + def delete_podcast_channel(self, podcast_channel_id: str) -> "Subsonic": + """Delete a podcast channel. + + Args: + podcast_channel_id: The ID of the podcast channel to delete. - :param id_: The ID of the channel to delete. - :type id_: str - :return: The object itself to allow method chaining. - :rtype: Subsonic + Returns: + The Subsonic object where this method was called to allow + method chaining. """ - self.api.json_request("deletePodcastChannel", {"id": id_}) + self.api.json_request("deletePodcastChannel", {"id": podcast_channel_id}) return self.subsonic - def download_podcast_episode(self, id_: str) -> "Subsonic": - """Calls the "downloadPodcastEpisode" endpoint of the API. + def download_podcast_episode(self, podcast_episode_id: str) -> "Subsonic": + """Download a podcast episode to the server. - :param id_: The ID of the episode to download. - :type id_: str - :return: The object itself to allow method chaining. - :rtype: Subsonic + Args: + podcast_episode_id: The ID of the podcast episode to download to + the server. + + Returns: + The Subsonic object where this method was called to allow + method chaining. """ - self.api.json_request("downloadPodcastEpisode", {"id": id_}) + self.api.json_request("downloadPodcastEpisode", {"id": podcast_episode_id}) return self.subsonic - def delete_podcast_episode(self, id_: str) -> "Subsonic": - """Calls the "deletePodcastEpisode" endpoint of the API. + def delete_podcast_episode(self, podcast_episode_id: str) -> "Subsonic": + """Delete a podcast episode from the server. + + Args: + podcast_episode_id: The ID of the podcast episode to delete. - :param id_: The ID of the episode to delete. - :type id_: str - :return: The object itself to allow method chaining. - :rtype: Subsonic + Returns: + The Subsonic object where this method was called to allow + method chaining. """ - self.api.json_request("deletePodcastEpisode", {"id": id_}) + self.api.json_request("deletePodcastEpisode", {"id": podcast_episode_id}) return self.subsonic diff --git a/src/knuckles/_searching.py b/src/knuckles/_searching.py index 64e077e..f6adb2c 100644 --- a/src/knuckles/_searching.py +++ b/src/knuckles/_searching.py @@ -11,9 +11,9 @@ class Searching: - """Class that contains all the methods needed to interact - with the searching calls and actions in the Subsonic API. - + """Class that contains all the methods needed to interact with the + [bookmark endpoints](https://opensubsonic.netlify.app/ + categories/searching/) in the Subsonic API. """ def __init__(self, api: Api, subsonic: "Subsonic") -> None: @@ -34,6 +34,32 @@ def _generic_search( music_folder_id: str | None = None, id3: bool = True, ) -> SearchResult: + """Direct method to call the "search2" and "search3" endpoints, + abstracting all the parameters of them. + + Args: + query: The query string to be send to the server. + song_count: The numbers of songs that the server + should return. + song_offset: The number of songs to offset in the list, + useful for pagination. + album_count: The numbers of albums that the server + should return. + album_offset: The number of album to offset in the list, + useful for pagination. + artist_count: The numbers of artists that the server + should return. + artist_offset: The number of artists to offset in the list, + useful for pagination. + music_folder_id: An ID of a music folder to limit where the + songs, albums and artists should come from. + id3: If the ID3 organized endpoint should be called or not. + + Returns: + An object that contains all the info about the found songs, + albums and artists received with the given query. + """ + response = self.api.json_request( "search3" if id3 else "search2", { @@ -82,6 +108,31 @@ def search( artist_offset: int | None = None, music_folder_id: str | None = None, ) -> SearchResult: + """Search and find all the songs, albums and artists that + whose title match the given query. + + Args: + query: The query string to be send to the server. + song_count: The numbers of songs that the server + should return. + song_offset: The number of songs to offset in the list, + useful for pagination. + album_count: The numbers of albums that the server + should return. + album_offset: The number of album to offset in the list, + useful for pagination. + artist_count: The numbers of artists that the server + should return. + artist_offset: The number of artists to offset in the list, + useful for pagination. + music_folder_id: An ID of a music folder to limit where the + songs, albums and artists should come from. + + Returns: + An object that contains all the info about the found songs, + albums and artists received with the given query. + """ + return self._generic_search( query, song_count, @@ -103,6 +154,32 @@ def search_non_id3( artist_offset: int | None = None, music_folder_id: str | None = None, ) -> SearchResult: + """Search and find all the songs, albums and artists that + whose title match the given query. Not organized according + ID3 tags. + + Args: + query: The query string to be send to the server. + song_count: The numbers of songs that the server + should return. + song_offset: The number of songs to offset in the list, + useful for pagination. + album_count: The numbers of albums that the server + should return. + album_offset: The number of album to offset in the list, + useful for pagination. + artist_count: The numbers of artists that the server + should return. + artist_offset: The number of artists to offset in the list, + useful for pagination. + music_folder_id: An ID of a music folder to limit where the + songs, albums and artists should come from. + + Returns: + An object that contains all the info about the found songs, + albums and artists received with the given query. + """ + return self._generic_search( query, song_count, diff --git a/src/knuckles/_sharing.py b/src/knuckles/_sharing.py index f4619b8..c46e7ae 100644 --- a/src/knuckles/_sharing.py +++ b/src/knuckles/_sharing.py @@ -9,9 +9,9 @@ class Sharing: - """Class that contains all the methods needed to interact - with the sharing calls and actions in the Subsonic API. - + """Class that contains all the methods needed to interact with the + [sharing endpoints](https://opensubsonic.netlify.app/ + categories/sharing/) in the Subsonic API. """ def __init__(self, api: Api, subsonic: "Subsonic") -> None: @@ -21,30 +21,32 @@ def __init__(self, api: Api, subsonic: "Subsonic") -> None: self.subsonic = subsonic def get_shares(self) -> list[Share]: - """Calls the "getShares" endpoint of the API. + """Get all the shares manageable by the authenticated user. - :return: A list with all the shares given by the server. - :rtype: list[Share] + Returns: + A list that holds all the info about all the shares + manageable by the user. """ response = self.api.json_request("getShares")["shares"]["share"] return [Share(self.subsonic, **share) for share in response] - def get_share(self, id_: str) -> Share | None: - """Using the "getShares" endpoint iterates over all the shares - and find the one with the same ID. + def get_share(self, share_id: str) -> Share | None: + """Get all the info about a share. - :param id_: The ID of the share to find. - :type id_: str - :return: The found share or None if no one is found. - :rtype: Share | None + Args: + share_id: The ID of the share to get its info. + + Returns: + An object that holds all the info about the requested + share. """ shares = self.get_shares() for share in shares: - if share.id == id_: + if share.id == share_id: return share return None @@ -55,16 +57,17 @@ def create_share( description: str | None = None, expires: datetime | None = None, ) -> Share: - """Calls the "createShare" endpoint of the API. - - :param songs_ids: A list with IDs of songs to add to the new share. - :type songs_ids: list[str] - :param description: A description for the share, defaults to None. - :type description: str | None, optional - :param expires: The time when the share should expire, defaults to None. - :type expires: datetime | None, optional - :return: The new created share. - :rtype: Share + """Create a new share. + + Args: + songs_ids: A list that holds the IDs of all the songs + that the share can give access to. + description: A description to be added with the share. + expires: A timestamp that marks when the share should + be invalidated. + + Returns: + An object that holds all the info about the requested share. """ response = self.api.json_request( @@ -84,16 +87,15 @@ def update_share( new_description: str | None = None, new_expires: datetime | None = None, ) -> Share: - """Calls the "updateShare" endpoint of the API. - - :param share_id: The ID of the share to update. - :type share_id: str - :param new_description: A new description for the share, defaults to None. - :type new_description: str | None, optional - :param new_expires: A new expiry date fot the share, defaults to None. - :type new_expires: datetime | None, optional - :return: A Share object with all the updated info. - :rtype: Share + """Update the info of a share. + + Args: + share_id: The ID of the share to update. + new_description: A new description to be added to the share. + new_expires: A new expire timestamp for the share. + + Returns: + An object that holds all the new updated info for the share. """ self.api.json_request( @@ -114,12 +116,14 @@ def update_share( return updated_share def delete_share(self, share_id: str) -> "Subsonic": - """Calls the "deleteShare" endpoint of the API. + """Delete a share from the server. + + Args: + share_id: The ID of the server to delete. - :param share_id: The ID of the share to delete. - :type share_id: str - :return: The object itself to allow method chaining. - :rtype: Subsonic + Returns: + The Subsonic object where this method was called to allow + method chaining. """ self.api.json_request("deleteShare", {"id": share_id}) diff --git a/src/knuckles/_subsonic.py b/src/knuckles/_subsonic.py index 52bdade..8dc7ec3 100644 --- a/src/knuckles/_subsonic.py +++ b/src/knuckles/_subsonic.py @@ -17,7 +17,40 @@ class Subsonic: - """Container of all the methods to interact with the OpenSubsonic API""" + """Object that holds all the other helper objects to interact + with the OpenSubsonic REST API. + + Inside this object there are helper object that holds all the methods + used to access the REST API. The methods are split following the + [categories listed in the OpenSubsonic REST API Spec](https://opensubsonic. + netlify.app/categories/). + + Attributes: + api: Helper object used to directly access the REST API of the given + server. + system: Helper object used to access all system related endpoints. + browsing: Helper object used to access all system related endpoints. + lists: Helper object used to access all lists related endpoints. + searching: Helper object used to access all searching related + endpoints. + playlists: Helper object used to access playlists related endpoints. + media_retrieval: Helper object used to access all media retrieval + related endpoints. + media_annotation: Helper object used to access all media + annotation related endpoints. + sharing: Helper object used to access all sharing related endpoints. + podcast: Helper object used to access all podcast related endpoints. + jukebox: Helper object used to access all jukebox related endpoints. + internet_radio: Helper object used to access all internet radio + related endpoints. + chat: Helper object used to access all chat related endpoints. + user_management: Helper object used to access all user management + related endpoints. + bookmarks: Helper object used to access all bookmarks related + endpoints. + media_library_scanning: Helper object used to access all media + library scanning related endpoints. + """ def __init__( self, @@ -29,16 +62,20 @@ def __init__( use_token: bool = True, request_method: RequestMethod = RequestMethod.POST, ) -> None: - """FF + """Construction method of the Subsonic object used to + interact with the OpenSubsonic REST API. Args: url: The URL of the Subsonic server to connect to. - user: The name of the user - password: asd - client: sad - use_https: d a - use_token: as - request_method: as + user: The name of the user to authenticate. + password: The password of the user to authenticate. + client: A unique name of the client to report to the + server. + use_https: If the requests should be use of HTTPS. + use_token: If the authentication should be made + using a salted token or in plain text. + request_method: If the requests should be made + using a GET verb or a POST verb. """ self.api = Api( diff --git a/src/knuckles/_system.py b/src/knuckles/_system.py index a5ba25a..3e1ffba 100644 --- a/src/knuckles/_system.py +++ b/src/knuckles/_system.py @@ -13,9 +13,9 @@ class OpenSubsonicExtension(NamedTuple): class System: - """Class that contains all the methods needed to interact - with the systems calls in the Subsonic API. - + """Class that contains all the methods needed to interact with the + [system endpoints](https://opensubsonic.netlify.app/ + categories/system/) in the Subsonic API. """ def __init__(self, api: Api, subsonic: "Subsonic") -> None: @@ -25,12 +25,10 @@ def __init__(self, api: Api, subsonic: "Subsonic") -> None: self.subsonic = subsonic def ping(self) -> SubsonicResponse: - """Calls to the "ping" endpoint of the API. + """Make a ping to the server. - Useful to test the status of the server. - - :return: An object with all the data received from the server. - :rtype: SubsonicResponse + Returns: + An object that holds all the info returned by the server. """ response = self.api.json_request("ping") @@ -38,10 +36,11 @@ def ping(self) -> SubsonicResponse: return SubsonicResponse(self.subsonic, **response) def get_license(self) -> License: - """Calls to the "getLicense" endpoint of the API. + """Get the current status of the license of the server. - :return: An object with all the information about the status of the license. - :rtype: License + Returns: + An object that contains all the info about the status + of the license of the server. """ response = self.api.json_request("getLicense")["license"] @@ -49,6 +48,14 @@ def get_license(self) -> License: return License(self.subsonic, **response) def get_open_subsonic_extensions(self) -> list[OpenSubsonicExtension]: + """Get all the available OpenSubsonic REST API extensions for the + connected server. + + Returns: + A list that contains all the info about all the available + extensions in the connected server. + """ + response = self.api.json_request("getOpenSubsonicExtensions")[ "openSubsonicExtensions" ] @@ -59,6 +66,20 @@ def get_open_subsonic_extensions(self) -> list[OpenSubsonicExtension]: def check_open_subsonic_extension( self, extension_name: str, extension_version: int ) -> bool: + """Check if a OpenSubonic REST API extension is available on the + connected server. + + Args: + extension_name: The name of the extension to check if its + available. + extension_version: The version of the extension to check if + its available. + + Returns: + If the given extension at the given version is available on + the connected server or not. + """ + extensions = self.get_open_subsonic_extensions() for extension in extensions: diff --git a/src/knuckles/_user_management.py b/src/knuckles/_user_management.py index 43df403..d415be0 100644 --- a/src/knuckles/_user_management.py +++ b/src/knuckles/_user_management.py @@ -8,9 +8,9 @@ class UserManagement: - """Class that contains all the methods needed to interact - with the user management calls in the Subsonic API. - + """Class that contains all the methods needed to interact with the + [user management endpoints](https://opensubsonic.netlify.app/ + categories/user-management/) in the Subsonic API. """ def __init__(self, api: Api, subsonic: "Subsonic") -> None: @@ -18,14 +18,14 @@ def __init__(self, api: Api, subsonic: "Subsonic") -> None: self.subsonic = subsonic def get_user(self, username: str) -> User: - """Calls the "getUser" endpoint of the API. + """Get all the info about a user. - :param username: The username of the user to get. - :type username: str - :return: A User object will all the data of the requested user. - :rtype: User - """ + Args: + username: The username of the user to get its info. + Returns: + An object that holds all the info about the requested user. + """ request = self.api.json_request("getUser", {"username": username})["user"] return User( @@ -51,10 +51,11 @@ def get_user(self, username: str) -> User: ) def get_users(self) -> list[User]: - """Calls the "getUsers" endpoint of the API. + """Get all the users registered in the server. - :return: A list of User objects. - :rtype: list[User] + Returns: + A list that holds all the info about all the available + users in the server. """ request = self.api.json_request("getUsers")["users"]["user"] @@ -108,6 +109,44 @@ def create_user( music_folder_id: list[str] | None = None, max_bit_rate: int | None = None, ) -> User: + """Create a new user in the server. + + Args: + username: The username of the user to create. + password: The password of the user to create. + email: The email of the user to create. + ldap_authenticated: If the user is authenticated in a LDAP server. + admin_role: If the user should be an administrator. + settings_role: If the user is allowed to change its + personal settings and password. + stream_role: If the user should be allowed to stream songs and + videos. + jukebox_role: If the user should be able to play songs in the + jukebox. + download_role: If the user should be able to download files from + the server. + upload_role: If the user should be allowed to upload files to + the server. + playlist_role: If the user should be able to create and delete + playlists. + cover_art_role: If the user should be allowed to change cover + art and tags of songs. + comment_role: If the user is allowed to create and edit + comments and ratings. + podcast_role: If the user should be allowed to administrate + podcasts. + share_role: If the user should be able to create share links. + video_conversion_role: If the use should be allowed to + start video conversion in the server. + music_folder_id: A list of IDs where the used should have access + to. If no one is specified all of them will be accessible. + max_bit_rate: The max bitrate that the user should be able to + stream. + + Returns: + An object that holds all the info about the new created user. + """ + self.api.json_request( "createUser", { @@ -178,15 +217,42 @@ def update_user( music_folder_id: list[str] | None = None, max_bit_rate: int | None = None, ) -> User: - """Calls the "updateUser" endpoint of the API. + """Update the info of a user. - The user to update with the new data will be - selected with the username property of the User object. + Args: + username: The username of the user to update. + password: The password of the user to update. + email: The email of the user to update. + ldap_authenticated: If the user is authenticated in a LDAP server. + admin_role: If the user should be an administrator. + settings_role: If the user is allowed to change its + personal settings and password. + stream_role: If the user should be allowed to stream songs and + videos. + jukebox_role: If the user should be able to play songs in the + jukebox. + download_role: If the user should be able to download files from + the server. + upload_role: If the user should be allowed to upload files to + the server. + playlist_role: If the user should be able to create and delete + playlists. + cover_art_role: If the user should be allowed to change cover + art and tags of songs. + comment_role: If the user is allowed to create and edit + comments and ratings. + podcast_role: If the user should be allowed to administrate + podcasts. + share_role: If the user should be able to create share links. + video_conversion_role: If the use should be allowed to + start video conversion in the server. + music_folder_id: A list of IDs where the used should have access + to. If no one is specified all of them will be accessible. + max_bit_rate: The max bitrate that the user should be able to + stream. - :param updated_data_user: A user object with the updated data. - :type updated_data_user: User - :return: The object itself to allow method chaining. - :rtype: User + Returns: + An object that holds all the info about the update user. """ self.api.json_request( @@ -238,12 +304,14 @@ def update_user( return updated_user def delete_user(self, username: str) -> "Subsonic": - """Calls the "deleteUser" endpoint of the API. + """Delete a user from the server. + + Args: + username: The username of the user to delete. - :param username: The username of the user to delete. - :type username: str - :return: The object itself to allow method chaining. - :rtype: Subsonic + Returns: + The Subsonic object where this method was called to allow + method chaining. """ self.api.json_request("deleteUser", {"username": username}) @@ -251,14 +319,15 @@ def delete_user(self, username: str) -> "Subsonic": return self.subsonic def change_password(self, username: str, new_password: str) -> "Subsonic": - """Calls the "changePassword" endpoint of the API. - - :param username: The username of the user to change its password. - :type username: str - :param new_password: The new password of the user - :type new_password: str - :return: The object itself to allow method chaining. - :rtype: Self + """Change the password of a user. + + Args: + username: The username of the user to change its password. + new_password: The new password for the user. + + Returns: + The Subsonic object where this method was called to allow + method chaining. """ self.api.json_request( diff --git a/src/knuckles/exceptions.py b/src/knuckles/exceptions.py index 0400ceb..35828bb 100644 --- a/src/knuckles/exceptions.py +++ b/src/knuckles/exceptions.py @@ -2,14 +2,21 @@ class MissingRequiredProperty(Exception): + """Raised when a property required to call a method is missing.""" + pass class InvalidRatingNumber(ValueError): + """Raised when input an invalid rating weight in a method of the API.""" + pass class ResourceNotFound(Exception): + """Raised when a resource could not be retrieve to + generate a model using a previous one.""" + def __init__( self, message: str = ( @@ -21,92 +28,142 @@ def __init__( class ShareInvalidSongList(ValueError): + """Raised when a method in a share is called with an invalid song list.""" + pass -class CodeError0(Exception): +class ErrorCode0(Exception): + """Raised when the server returns an error code 0, + it being a generic error. + """ + pass -class CodeError10(Exception): +class ErrorCode10(Exception): + """Raised when the server returns an error code 10, + meaning that a parameter for the requested endpoint is missing. + Should never be raised because Knuckles takes care for enforcing mandatory + parameters, if you have encountered this exception the server may have + broke the OpenSubsonic API. + + If you suspect that this is an issue caused by Knuckles itself, + please report it to upstream. + """ + pass -class CodeError20(Exception): +class ErrorCode20(Exception): + """Raised when the server returns an error code 20, + meaning that the client has a lower RESP API version than the server. + Should never be raised given that Knuckles supports up to the latest + Subsonic REST API version. + """ + pass -class CodeError30(Exception): +class ErrorCode30(Exception): + """Raised when the server returns an error code 30, + meaning that the server has a lower RESP API version than the client. + """ + pass -class CodeError40(Exception): +class ErrorCode40(Exception): + """Raised when the server returns an error code 40, + meaning that the given user doesn't exists or the password is incorrect. + """ + pass -class CodeError41(Exception): +class ErrorCode41(Exception): + """Raised when the server returns an error code 42, + meaning that token authentication is not available. + """ + pass -class CodeError50(Exception): +class ErrorCode50(Exception): + """Raised when the server returns an error code 50, + meaning that the authenticated user is no authorized + for the requested action. + """ + pass -class CodeError60(Exception): +class ErrorCode60(Exception): + """Raised when the server return an error code 60, + meaning that the Subsonic trial period has ended. + """ + pass -class CodeError70(Exception): +class ErrorCode70(Exception): + """Raised when the server returns an error code 70, + meaning that the requested data wasn't found. + """ + pass class UnknownErrorCode(Exception): + """Raised when the server returns an error code that doesn't + have a specific core error exception. + """ + pass -CODE_ERROR_EXCEPTIONS = Type[ - CodeError0 - | CodeError10 - | CodeError20 - | CodeError30 - | CodeError40 - | CodeError41 - | CodeError50 - | CodeError60 - | CodeError70 +ERROR_CODE_EXCEPTION = Type[ + ErrorCode0 + | ErrorCode10 + | ErrorCode20 + | ErrorCode30 + | ErrorCode40 + | ErrorCode41 + | ErrorCode50 + | ErrorCode60 + | ErrorCode70 | UnknownErrorCode ] -def get_code_error_exception( +def get_error_code_exception( error_code: int, -) -> CODE_ERROR_EXCEPTIONS: - """With a given code error returns the corresponding exception. +) -> ERROR_CODE_EXCEPTION: + """Converts a numeric error code to its corresponding error code exception - :param error_code: The error code. - :type error_code: int - :return: The associated exception with the error code. - :rtype: CODE_ERROR_EXCEPTIONS - """ + Args: + error_code: The number of the error to get its exception. + Returns: The exception of the given error code + """ match error_code: case 0: - return CodeError0 + return ErrorCode0 case 10: - return CodeError10 + return ErrorCode10 case 20: - return CodeError20 + return ErrorCode20 case 30: - return CodeError30 + return ErrorCode30 case 40: - return CodeError40 + return ErrorCode40 case 41: - return CodeError41 + return ErrorCode41 case 50: - return CodeError50 + return ErrorCode50 case 60: - return CodeError60 + return ErrorCode60 case 70: - return CodeError70 + return ErrorCode70 case _: return UnknownErrorCode diff --git a/src/knuckles/models/_album.py b/src/knuckles/models/_album.py index e9376bf..372be2d 100644 --- a/src/knuckles/models/_album.py +++ b/src/knuckles/models/_album.py @@ -1,8 +1,9 @@ from typing import TYPE_CHECKING, Any +from dateutil import parser + # Avoid circular import error import knuckles.models._song as song_model_module -from dateutil import parser from ._artist import Artist from ._cover_art import CoverArt @@ -14,6 +15,12 @@ class RecordLabel(Model): + """Object that holds all the info about a record label. + + Attributes: + name (str): The name of the record label. + """ + def __init__(self, subsonic: "Subsonic", name: str) -> None: super().__init__(subsonic) @@ -21,6 +28,13 @@ def __init__(self, subsonic: "Subsonic", name: str) -> None: class Disc(Model): + """Object that holds all the info about a disc. + + Attributes: + disc_number (int): The number of the disc. + title (str): The title of the disc. + """ + def __init__(self, subsonic: "Subsonic", disc: int, title: str) -> None: super().__init__(subsonic) @@ -29,6 +43,14 @@ def __init__(self, subsonic: "Subsonic", disc: int, title: str) -> None: class ReleaseDate(Model): + """Object that holds all the info about the release date of a media. + + Attributes: + year (int): The year when it was released. + month (int): The month when it was released. + day (int): The day when it was released. + """ + def __init__( self, subsonic: "Subsonic", @@ -44,7 +66,17 @@ def __init__( class AlbumInfo(Model): - """Representation of all the data related to an album info in Subsonic.""" + """Object that holds all the info about the extra info of an album. + + Attributes: + album_id (str): The ID of the album where the extra info is from. + notes (str): Notes of the album. + music_brainz_id (str | None): The music brainz ID of the album. + last_fm_url (str | None): The last.fm URL of the album + small_image_user (str | None): The URL of the small sized image of the album. + medium_image_user (str | None): The URL of the medium sized image of the album. + large_image_user (str | None): The URL of the large sized image of the album. + """ def __init__( self, @@ -57,23 +89,6 @@ def __init__( mediumImageUrl: str | None, largeImageUrl: str | None, ) -> None: - """Representation of all the data related to an album info in Subsonic. - :param subsonic: The subsonic object to make all the internal requests with it. - :type subsonic: Subsonic - :param album_id: The ID3 of the album associated with the info. - :type album_id: str - :param notes: A note for the album. - :type notes: str - :param musicBrainzId:The ID in music Brainz of the album. - :type musicBrainzId: str - :param smallImageUrl: An URL to the small size cover image of the album. - :type smallImageUrl: str - :param mediumImageUrl: An URL to the medium size cover image of the album. - :type mediumImageUrl: str - :param largeImageUrl: An URL to the large size cover image of the album. - :type largeImageUrl: str - """ - super().__init__(subsonic) self.album_id = album_id @@ -85,21 +100,69 @@ def __init__( self.large_image_url = largeImageUrl def generate(self) -> "AlbumInfo": - """Return a new album info with all the data updated from the API, + """Return a new album info object with all the data updated from the API, using the endpoint that return the most information possible. - Useful for making copies with updated data or updating the object itself - with immutability, e.g., foo = foo.generate(). + Useful for making copies with updated data or updating the object + itself with immutability, e.g., `foo = foo.generate()`. - :return: A new album info object with all the data updated. - :rtype: AlbumInfo + Returns: + A new object with all the updated info. """ return self._subsonic.browsing.get_album_info(self.album_id) class Album(Model): - """Representation of all the data related to an album in Subsonic.""" + """Object that holds all the info of an album. + + Attributes: + id (str): The ID of the album. + parent (str | None): The ID of the parent media of the album. + name (str | None): The name of the album. + album (str | None): The name of the album (Can differ the `name` and + `title` attributes, **not documented in the [OpenSubsonic Spec](ht + tps://opensubsonic.netlify.app/docs/responses/albumid3/)). + is_dir (bool | None): If the album is a directory. + title (str | None): The name of the album (Can differ the `name` and + `album` attributes, **not documented in the [OpenSubsonic Spec](ht + tps://opensubsonic.netlify.app/docs/responses/albumid3/)). + artist (Artist | None): The artist of the album. + cover_art (CoverArt): All the info about the cover art of the album. + song_count (int | None): The number of songs inside the album. + duration (int | None): The total duration of the album in seconds. + play_count (int | None): The times the album has been played. + created (datetime | None): The timestamp when the album was created. + starred (datetime | None): The timestamp when the album was starred if + it is. + year (int | None): The year when the album was released. + genre (str | None): The genre of the album. + played (datetime | None): The timestamp when the album was played. + user_rating (int | None): The rating from 0 to 5 (inclusive) that the + used has given to the album if it is rated. + songs (list[Song] | None): The list of songs that the album contains. + info (AlbumInfo | None): Extra info about the album. + record_labels (list[RecordLabel] | None): List of all the record labels + that have licensed the album. + music_brainz_id (str | None): The ID of the MusicBrainz database entry + of the album. + genres (list[ItemGenre] | None): List of all the genres that the album + has. + artists (list[Artist] | None): List of all the artists involved with + the album. + display_artist (str | None): String that condense all the artists + involved with the album. + release_types (list[str] | None): The types of album that the + album is. + moods (list[str] | None): List of all the moods that the album + has. + sort_name (str | None): The name of the album used for sorting. + original_release_date (ReleaseDate | None): The original release date + of the album. + release_date (ReleaseDate | None): The release date of the album. + is_compilation (bool | None): If the album is a compilation or not. + discs (list[Disc] | None): + """ def __init__( self, @@ -136,51 +199,6 @@ def __init__( isCompilation: bool | None = None, discTitles: list[dict[str, Any]] | None = None, ) -> None: - """Representation of all the data related to an album in Subsonic. - - :param subsonic:The subsonic object to make all the internal requests with it. - The subsonic object to make all the internal requests with it. - :type subsonic: Subsonic - :param id: The ID of the album. - :type id: str - :param parent: The ID of the parent directory. - :type parent: str - :param album: The name of the album (Same as title and name). - :type album: str - :param title: The name of the album (Same as album and name). - :type title: str - :param name: The name of the album (Same as album and title). - :type name: str - :param isDir: If the album is a directory. - :type isDir: bool - :param artist: The name of the artist author of the album. - :type artist: str - :param artistId: The ID of the artist author of the album. - :type artistId: str - :param coverArt: The ID of the cover art of the album. - :type coverArt: str - :param songCount: The number of songs inside the album. - :type songCount: int - :param duration: The total duration of the album. - :type duration: int - :param playCount: The times the album has been played. - :type playCount: int - :param created: The time when the album was created. - :type created: str - :param starred: The time when the album was starred. - :type starred: str - :param year: The year when the album was released. - :type year: int - :param genre: The genre of the album. - :type genre: str - :param played: The time the album was last played. - :type played: str - :param userRating: - :type userRating: int - :param song: A list with all the songs of the album. - :type song: list[dict[str, Any]] - """ - super().__init__(subsonic) self.id = id @@ -243,14 +261,14 @@ def __init__( ) def generate(self) -> "Album": - """Return a new album with all the data updated from the API, - using the endpoints that return the most information possible. + """Return a new album object with all the data updated from the API, + using the endpoint that return the most information possible. - Useful for making copies with updated data or updating the object itself - with immutability, e.g., foo = foo.generate(). + Useful for making copies with updated data or updating the object + itself with immutability, e.g., `foo = foo.generate()`. - :return: A new album info object with all the data updated. - :rtype: Album + Returns: + A new object with all the updated info. """ new_album = self._subsonic.browsing.get_album(self.id) @@ -259,11 +277,11 @@ def generate(self) -> "Album": return new_album def get_album_info(self) -> AlbumInfo: - """Returns the extra info given by the "getAlbumInfo2" endpoint, - also sets it in the info property of the model. + """Get all the extra info about the album, it's + set to the `info` attribute of the object. - :return: An AlbumInfo object with all the extra info given by the API. - :rtype: AlbumInfo + Returns: + The extra info returned by the server. """ self.info = self._subsonic.browsing.get_album_info(self.id) diff --git a/src/knuckles/models/_artist.py b/src/knuckles/models/_artist.py index aa60d96..f99d33b 100644 --- a/src/knuckles/models/_artist.py +++ b/src/knuckles/models/_artist.py @@ -13,7 +13,23 @@ class ArtistInfo(Model): - """Representation of all the data related to an artist info in Subsonic.""" + """Object that holds all the extra info of an artist. + + Attributes: + artist_id (str): The ID of the artist. + biography (str): The biography of an artist. + music_brainz_id (str | None): The ID of the MusicBrainz database + entry of the artist. + last_fm_url (str | None): The last.fm URL of the artist. + small_image_url (str | None): The URL of the small sized image of + the artist. + medium_image_url (str | None): The URL of the medium sized image + of the artist. + large_image_url (str | None): The URL of the large sized image + of the artist. + similar_artists (list[Artist] | None): A list that contains the + all the info about similar artists. + """ def __init__( self, @@ -27,25 +43,6 @@ def __init__( largeImageUrl: str | None, similarArtist: list[dict[str, Any]] | None = None, ) -> None: - """Representation of all the data related to an album info in Subsonic. - :param subsonic: The subsonic object to make all the internal requests with it. - :type subsonic: Subsonic - :param artist_id: The ID3 of the artist associated with the info. - :type artist_id: str - :param biography: A biography for the album. - :type biography: str - :param musicBrainzId:The ID in music Brainz of the album. - :type musicBrainzId: str - :param smallImageUrl: An URL to the small size cover image of the artist. - :type smallImageUrl: str - :param mediumImageUrl: An URL to the medium size cover image of the artist. - :type mediumImageUrl: str - :param largeImageUrl: An URL to the large size cover image of the artist. - :type largeImageUrl: str - :param similarArtist: A list with all the similar artists. - :type similarArtist: list[str, Any] - """ - super().__init__(subsonic) self.artist_id = artist_id @@ -68,15 +65,37 @@ def generate(self) -> "ArtistInfo": Useful for making copies with updated data or updating the object itself with immutability, e.g., foo = foo.generate(). - :return: A new album info object with all the data updated. - :rtype: ArtistInfo + Returns: + A new object with the updated model. """ return self._subsonic.browsing.get_artist_info(self.artist_id) class Artist(Model): - """Representation of all the data related to an artist in Subsonic.""" + """Object that holds all the info of an artist. + + Attributes: + id (str): The ID of the artist. + name (str | None): The name of the artist. + cover_art (CoverArt | None): The cover art associated with the artist. + artist_image_url (str | None): The URL of the image of the artist. + album_count (int | None): The number of albums created by the artist. + starred (datetime | None): The timestamp when the artist was starred if + it is. + user_rating (int | None): The rating from 0 to 5 (inclusive) that the + used has given to the artist if it is rated. + average_rating (float | None): The average rating given by all the + users. + albums (list[Album] | None): A list that holds all the info about + all the albums created by the artist. + info (ArtistInfo | None): All the extra info about the artist. + music_brainz_id (str | None): The ID of the MusicBrainz database entry + of the artist. + sort_name (str | None): The sort name of the artist. + roles (list[str] | None): List with all the roles the artist has been + in. + """ def __init__( self, @@ -94,30 +113,6 @@ def __init__( sortName: str | None = None, roles: list[str] | None = None, ) -> None: - """Representation of all the data related to an artist in Subsonic. - - :param subsonic: The subsonic object to make all the internal requests with it. - :type subsonic: Subsonic - :param id: The ID of the artist. - :type id: str - :param name: The name of the artist. - :type name: str - :param coverArt: The ID of the cover art of the artist. - :type coverArt: str - :param albumCount: The number of albums that the artist has. - :type albumCount: int - :param artistImageUrl: A URL to an image of the artist. - :type artistImageUrl: str - :param starred: The time when the artist was starred. - :type starred: str - :param userRating: The rating of the authenticated user. - :type userRating: int - :param averageRating: The average rating of all the users. - :type averageRating: float - :param album: A list with all the albums made by the artist. - :type album: list[dict[str, Any]] - """ - super().__init__(subsonic) self.id = id @@ -142,14 +137,14 @@ def __init__( self.roles = roles def generate(self) -> "Artist": - """Return a new artist with all the data updated from the API, + """Return a new artist info with all the data updated from the API, using the endpoint that return the most information possible. Useful for making copies with updated data or updating the object itself with immutability, e.g., foo = foo.generate(). - :return: A new album info object with all the data updated. - :rtype: Artist + Returns: + A new object with the updated model. """ new_artist = self._subsonic.browsing.get_artist(self.id) @@ -158,11 +153,11 @@ def generate(self) -> "Artist": return new_artist def get_artist_info(self) -> ArtistInfo: - """Returns the extra info given by the "getAlbumInfo2" endpoint, - also sets it in the info property of the model. + """Get all the extra info about the artist, it's + set to the `info` attribute of the object. - :return: An AlbumInfo object with all the extra info given by the API. - :rtype: AlbumInfo + Returns: + The extra info returned by the server. """ self.info = self._subsonic.browsing.get_artist_info(self.id) diff --git a/src/knuckles/models/_index.py b/src/knuckles/models/_artist_index.py similarity index 61% rename from src/knuckles/models/_index.py rename to src/knuckles/models/_artist_index.py index f75ecd6..8231629 100644 --- a/src/knuckles/models/_index.py +++ b/src/knuckles/models/_artist_index.py @@ -7,7 +7,17 @@ from .._subsonic import Subsonic -class Index(Model): +class ArtistIndex(Model): + """Object that holds all the info about an artist index. + + Attributes: + ignored_articles (list[str]): Ignored articles in the index. + index (dict[str, list[Artist]] | None): Dictionary that holds + the index, where the key is the index letter and the value + a list of objects that holds all the info related with the + artists in that are in the given index. + """ + def __init__( self, subsonic: "Subsonic", diff --git a/src/knuckles/models/_bookmark.py b/src/knuckles/models/_bookmark.py index 8ae5e7f..d92ffde 100644 --- a/src/knuckles/models/_bookmark.py +++ b/src/knuckles/models/_bookmark.py @@ -12,7 +12,20 @@ class Bookmark(Model): - """Representation of all the data related to a bookmark in Subsonic.""" + """Object that holds all the info about a bookmark. + + Attributes: + song (Song): All the info about the bookmarked song. + position (int): The position in seconds of the playback + of the song when it was bookmarked. + user (User | None): All the info about the user that + created the bookmark. + comment (str | None): A comment attached to the bookmark. + created (datetime | None): The timestamp when the bookmark + was created. + changed (datetime | None): The timestamp when the bookmark + was updated. + """ def __init__( self, @@ -36,14 +49,14 @@ def __init__( self.changed = parser.parse(changed) if changed else None def generate(self) -> "Bookmark": - """Return a new bookmark with all the data updated from the API, + """Return a new album object with all the data updated from the API, using the endpoint that return the most information possible. - Useful for making copies with updated data or updating the object itself - with immutability, e.g., foo = foo.generate(). + Useful for making copies with updated data or updating the object + itself with immutability, e.g., `foo = foo.generate()`. - :return: A new album info object with all the data updated. - :rtype: Bookmark + Returns: + A new object with all the updated info. """ get_bookmark = self._subsonic.bookmarks.get_bookmark(self.song.id) @@ -54,10 +67,12 @@ def generate(self) -> "Bookmark": return get_bookmark def create(self) -> Self: - """Calls the "createBookmark" endpoint of the API. + """Create a new bookmark for the authenticated user + with the same data of the object where this method is + called. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.bookmarks.create_bookmark( @@ -67,11 +82,11 @@ def create(self) -> Self: return self def update(self) -> Self: - """Calls the "createBookmark" endpoint of the API, as creating and updating - a bookmark uses the same endpoint. Useful for having more self-descriptive code. + """Update the info about the bookmark of this song using the + current data of the object. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.bookmarks.update_bookmark( @@ -81,10 +96,10 @@ def update(self) -> Self: return self def delete(self) -> Self: - """Calls the "deleteBookmark" endpoint of the API. + """Delete the bookmark entry from the server. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.bookmarks.delete_bookmark(self.song.id) diff --git a/src/knuckles/models/_chat_message.py b/src/knuckles/models/_chat_message.py index a812fc5..e570bef 100644 --- a/src/knuckles/models/_chat_message.py +++ b/src/knuckles/models/_chat_message.py @@ -5,28 +5,25 @@ from .._subsonic import Subsonic from knuckles.models._model import Model +from knuckles.models._user import User class ChatMessage(Model): - """Representation of all the data related to a chat message in Subsonic.""" + """Object that holds all the info about a chat message. + + Attributes: + user (User): The user author of the chat message. + message: The message send by the user. + time (datetime): The timestamp when the chat message was send. + """ def __init__( self, subsonic: "Subsonic", username: str, time: int, message: str ) -> None: - """Representation of all the data related to a chat message in Subsonic. - - :param username: The username of the creator of the message - :type username: str - :param time: Time when the message was created. - :type time: int - :param message: The message content. - :type message: str - """ - super().__init__(subsonic) - self.username: str = username - self.message: str = message + self.user = User(self._subsonic, username) + self.message = message # Divide by 1000 as the Subsonic API return in milliseconds instead of seconds self.time: datetime = datetime.fromtimestamp(time / 1000) diff --git a/src/knuckles/models/_contributor.py b/src/knuckles/models/_contributor.py index a473c77..fcc93f6 100644 --- a/src/knuckles/models/_contributor.py +++ b/src/knuckles/models/_contributor.py @@ -9,6 +9,15 @@ class Contributor(Model): + """Object that holds all the info about a contributor. + + Attributes: + role (str): The role of the contributor. + artist (Artist): All the artist info associated with the + contributor. + subrole (str | None): The subrole of the contributor. + """ + def __init__( self, subsonic: "Subsonic", @@ -19,5 +28,5 @@ def __init__( super().__init__(subsonic) self.role = role - self.subrole = subRole self.artist = artist + self.subrole = subRole diff --git a/src/knuckles/models/_cover_art.py b/src/knuckles/models/_cover_art.py index f94b778..e9c66a5 100644 --- a/src/knuckles/models/_cover_art.py +++ b/src/knuckles/models/_cover_art.py @@ -7,15 +7,13 @@ class CoverArt(Model): - """Representation of all the data related to a cover art in Subsonic.""" + """Object that holds all the info of a cover art. - def __init__(self, subsonic: "Subsonic", id: str) -> None: - """Representation of all the data related to a cover art in Subsonic. - - :param id: The ID of the cover art. - :type id: str - """ + Attributes: + id: The ID of the cover art. + """ + def __init__(self, subsonic: "Subsonic", id: str) -> None: super().__init__(subsonic) self.id: str = id diff --git a/src/knuckles/models/_genre.py b/src/knuckles/models/_genre.py index d66e6a3..2c9c8e3 100644 --- a/src/knuckles/models/_genre.py +++ b/src/knuckles/models/_genre.py @@ -8,6 +8,12 @@ class ItemGenre(Model): + """Object that holds all the info about a item genre. + + Attributes: + name: The name of the genre. + """ + def __init__(self, subsonic: "Subsonic", name: str) -> None: super().__init__(subsonic) @@ -15,7 +21,15 @@ def __init__(self, subsonic: "Subsonic", name: str) -> None: class Genre(Model): - """Representation of all the data related to a genre in Subsonic.""" + """Object that holds all the info about a genre. + + Attributes: + value (str): The name of the genre. + song_count (int | None): Number of songs tagged with the + genre. + album_count (int | None): Number of albums tagged with + the genre. + """ def __init__( self, diff --git a/src/knuckles/models/_internet_radio_station.py b/src/knuckles/models/_internet_radio_station.py index 858c9d1..d2c6ee6 100644 --- a/src/knuckles/models/_internet_radio_station.py +++ b/src/knuckles/models/_internet_radio_station.py @@ -8,8 +8,13 @@ class InternetRadioStation(Model): - """Representation of all the data related to - an internet radio station in Subsonic. + """Object that holds all the info about a Internet radio station. + + Attributes: + id (str): The ID of the Internet radio station. + name (str): Then name of the Internet radio station. + stream_url (str): The URL of the stream of the Internet radio station. + homepage_url (str): The URl of the hompage of the Internet radio station. """ def __init__( @@ -20,21 +25,6 @@ def __init__( streamUrl: str, homepageUrl: str, ) -> None: - """Representation of all the data related to - an internet radio station in Subsonic. - - :param id: The id of the radio station. - :type streamUrl: str - :param name: The name of the radio station. - :type name: str - :param subsonic: The subsonic object to make all the internal requests with it. - :type subsonic: Subsonic - :param streamUrl: The stream url of the radio station. - :type streamUrl: str - :param homepageUrl: The url of the homepage of the radio station. - :type homepageUrl: str - """ - super().__init__(subsonic) self.id = id @@ -43,14 +33,14 @@ def __init__( self.homepage_url = homepageUrl def generate(self) -> "InternetRadioStation | None": - """Return a new internet radio station with all the data updated from the API, + """Return a new album object with all the data updated from the API, using the endpoint that return the most information possible. - Useful for making copies with updated data or updating the object itself - with immutability, e.g., foo = foo.generate(). + Useful for making copies with updated data or updating the object + itself with immutability, e.g., `foo = foo.generate()`. - :return: A new internet radio station object with all the data updated. - :rtype: InternetRadioStation + Returns: + A new object with all the updated info. """ get_station = self._subsonic.internet_radio.get_internet_radio_station(self.id) @@ -66,10 +56,12 @@ def generate(self) -> "InternetRadioStation | None": return get_station def create(self) -> Self: - """Calls the "createInternetRadioStation" endpoint of the API. + """Create a new Internet radio station for the authenticated user + with the same data of the object where this method is + called. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.internet_radio.create_internet_radio_station( @@ -79,10 +71,11 @@ def create(self) -> Self: return self def update(self) -> Self: - """Calls the "updateInternetRadioStation" endpoint of the API. + """Update the info about the Internet radio station using the + current data of the object. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.internet_radio.update_internet_radio_station( @@ -92,10 +85,10 @@ def update(self) -> Self: return self def delete(self) -> Self: - """Calls the "deleteInternetRadioStation" endpoint of the API. + """Delete the Internet radio station entry from the server. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.internet_radio.delete_internet_radio_station(self.id) diff --git a/src/knuckles/models/_jukebox.py b/src/knuckles/models/_jukebox.py index fa815f9..1291ea1 100644 --- a/src/knuckles/models/_jukebox.py +++ b/src/knuckles/models/_jukebox.py @@ -8,7 +8,18 @@ class Jukebox(Model): - """Representation of all the data related to the jukebox in Subsonic.""" + """Object that holds all the info about a jukebox. + + Attributes: + current_index (int): The index in the playlist of the + current playing song in the jukebox. + playing (bool): If the jukebox is playing a song + or not. + gain (float): The gain of the playback of the jukebox. + position (int): How many seconds the song has been already player. + playlist (list[Song] | None): A list that holds all the info about + all the songs that are in the playlist of the jukebox. + """ def __init__( self, @@ -19,22 +30,6 @@ def __init__( position: int, entry: list[dict[str, Any]] | None = None, ) -> None: - """Representation of all the data related to the jukebox in Subsonic. - - :param subsonic: The subsonic object to make all the internal requests with it. - :type subsonic: Subsonic - :param currentIndex: The current index of the jukebox. - :type currentIndex: int - :param playing: If the jukebox is playing a song. - :type playing: bool - :param gain: The gain of the jukebox. - :type gain: float - :param position: The position of the jukebox. - :type position: int - :param entry: A list with all the songs inside the jukebox, defaults to None. - :type entry: list[dict[str, Any]] | None, optional - """ - super().__init__(subsonic) self.current_index: int = currentIndex @@ -52,23 +47,23 @@ def __init__( self.playlist.append(Song(subsonic=self._subsonic, **song)) def generate(self) -> "Jukebox": - """Return a new jukebox with all the data updated from the API, + """Return a new jukebox object with all the data updated from the API, using the endpoint that return the most information possible. - Useful for making copies with updated data or updating the object itself - with immutability, e.g., foo = foo.generate(). + Useful for making copies with updated data or updating the object + itself with immutability, e.g., `foo = foo.generate()`. - :return: A new jukebox object with all the data updated. - :rtype: Jukebox + Returns: + A new object with all the updated info. """ return self._subsonic.jukebox.get() def start(self) -> Self: - """Calls the "jukeboxControl" endpoint of the API with the action "start". + """Start the playback of the next song in the playlist. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.jukebox.start() @@ -76,25 +71,25 @@ def start(self) -> Self: return self def stop(self) -> Self: - """Calls the "jukeboxControl" endpoint of the API with the action "stop". + """Stop the playback of the jukebox. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ - self._subsonic.jukebox.stop() return self def skip(self, index: int, offset: float = 0) -> Self: - """_summary_ + """Skips the current playing song of the jukebox to another one. - :param index: The index in the jukebox playlist to skip to. - :type index: int - :param offset: Start playing this many seconds into the track, defaults to 0. - :type offset: float, optional - :return: The object itself to allow method chaining. - :rtype: Self + Args: + index: The index of the song to skip to. + offset: An offset in seconds where the playback of the song + should start at. + + Returns: + The object itself. """ self._subsonic.jukebox.skip(index, offset) @@ -102,10 +97,10 @@ def skip(self, index: int, offset: float = 0) -> Self: return self def shuffle(self) -> Self: - """Calls the "jukeboxControl" endpoint of the API with the action "shuffle". + """Shuffle the playlist of the jukebox. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.jukebox.shuffle() @@ -117,12 +112,13 @@ def shuffle(self) -> Self: return self def set_gain(self, gain: float) -> Self: - """Calls the "jukeboxControl" endpoint of the API with the action "setGain" + """Set the gain of the jukebox. - :param gain: A number between 0 and 1 (inclusive) to set the gain. - :type gain: float - :return: The object itself to allow method chaining. - :rtype: Self + Args: + gain: The new gain of the jukebox. + + Returns: + The object itself. """ self._subsonic.jukebox.set_gain(gain) @@ -142,54 +138,57 @@ def clear(self) -> Self: return self - def set(self, id: str) -> Self: - """Calls the "jukeboxControl" endpoint of the API with the action "set". + def set(self, songs_ids: list[str]) -> Self: + """Set the songs of the playlist of the jukebox. - :param id: The ID of a song to set it in the jukebox. - :type id: str - :raises ValueError: Raised if the gain argument isn't between the valid range. - :return: The object itself to allow method chaining. - :rtype: Self + Args: + songs_ids: The IDs of the songs to be set the playlist to. + + Returns: + The object itself. """ - song_to_set: Song = Song(self._subsonic, id) + self._subsonic.jukebox.set(songs_ids) - self._subsonic.jukebox.set(song_to_set.id) - self.playlist = [song_to_set] + self.playlist = [ + Song(subsonic=self._subsonic, id=song_id) for song_id in songs_ids + ] return self - def add(self, id: str) -> Self: - """Calls the "jukeboxControl" endpoint of the API with the action "add". + def add(self, songs_ids: list[str]) -> Self: + """Add songs to the playlist of the jukebox. - :param id: The ID of a song to add it in the jukebox. - :type id: str - :raises TypeError: Raised if the passed value to song isn't a Song object - or an ID. - :return: The object itself to allow method chaining. - :rtype: Self - """ + Args: + songs_ids: The IDs of the songs to add. - song_to_add: Song = Song(self._subsonic, id) + Returns: + The object itself. + """ - self._subsonic.jukebox.add(song_to_add.id) + self._subsonic.jukebox.add(songs_ids) + songs_to_add = [ + Song(subsonic=self._subsonic, id=song_id) for song_id in songs_ids + ] if self.playlist is not None: - self.playlist.append(song_to_add) + self.playlist += songs_to_add return self - # If the playlist is None the real value of it is unknown, - # so a call the API is necessary to get a correct representation of the jukebox + # If the playlist is None then the real value of it is unknown, + # so a call the API is necessary to get a correct representation + # of the jukebox self.playlist = self.generate().playlist return self def remove(self, index: int) -> Self: - """Calls the "jukeboxControl" endpoint of the API with the action "remove". + """Remove a song from the playlist of the jukebox. - :param index: The index in the jukebox playlist for the song to remove. - :type index: int - :return: The object itself to allow method chaining. - :rtype: Self + Args: + index: The index of the song in the playlist to remove. + + Returns: + The object itself. """ self._subsonic.jukebox.remove(index) diff --git a/src/knuckles/models/_lyrics.py b/src/knuckles/models/_lyrics.py index 86fa436..27b4803 100644 --- a/src/knuckles/models/_lyrics.py +++ b/src/knuckles/models/_lyrics.py @@ -7,6 +7,14 @@ class Lyrics(Model): + """Object that holds all the info about the lyrics of a song. + + Attributes: + artist_name (str): The name of the artist of the song. + song_title (str): The title of the song. + lyrics (str): The lyrics text of the song. + """ + def __init__( self, subsonic: "Subsonic", artist: str, title: str, value: str ) -> None: diff --git a/src/knuckles/models/_model.py b/src/knuckles/models/_model.py index feb7867..b5029ff 100644 --- a/src/knuckles/models/_model.py +++ b/src/knuckles/models/_model.py @@ -5,5 +5,10 @@ class Model: + """Generic parent class for all the models. + Have an internal attribute to hold a Subsonic object to + access the OpenSubsonic REST API. + """ + def __init__(self, subsonic: "Subsonic") -> None: self._subsonic = subsonic diff --git a/src/knuckles/models/_music_directory.py b/src/knuckles/models/_music_directory.py index a8a06ea..9678ad3 100644 --- a/src/knuckles/models/_music_directory.py +++ b/src/knuckles/models/_music_directory.py @@ -10,6 +10,24 @@ class MusicDirectory(Model): + """Object that holds all the info about a music directory. + + Attributes: + id (str): The ID of the music directory. + name (str): + parent (str | None): + starred (datetime | None): The timestamp when the music directory + was starred by the authenticated user if it was. + user_rating (int): The rating given by the authenticated user + if they rated it. + average_rating (float | None): The average rating given to the music + directory. + play_count (int | None): The number of times songs have been played + that are in the music directory. + songs (list[Song] | None): List that holds all the info about all + the songs that are part of the music directory. + """ + def __init__( self, subsonic: "Subsonic", diff --git a/src/knuckles/models/_music_folder.py b/src/knuckles/models/_music_folder.py index 56400e7..2a4233f 100644 --- a/src/knuckles/models/_music_folder.py +++ b/src/knuckles/models/_music_folder.py @@ -7,33 +7,28 @@ class MusicFolder(Model): - """Representation of all the data related to a music folder in Subsonic.""" + """Object that holds all the info about a music folder - def __init__(self, subsonic: "Subsonic", id: str, name: str | None = None) -> None: - """Representation of all the data related to a music folder in Subsonic. - - :param subsonic: The subsonic object to make all the internal requests with it. - :type subsonic: Subsonic - :param id: The ID of the music folder. - :type id: str - :param name: The name of the music folder, defaults to None. - :type name: str | None, optional - """ + Attributes: + id: The ID of the music folder. + name: The name of the music folder. + """ + def __init__(self, subsonic: "Subsonic", id: str, name: str | None = None) -> None: super().__init__(subsonic) self.id = id self.name = name def generate(self) -> "MusicFolder": - """Return a new music folder with all the data updated from the API, - using the endpoint that return the most information possible. + """Return a new music folder object with all the data updated from the + API, using the endpoint that return the most information possible. - Useful for making copies with updated data or updating the object itself - with immutability, e.g., foo = foo.generate(). + Useful for making copies with updated data or updating the object + itself with immutability, e.g., `foo = foo.generate()`. - :return: A new album info object with all the data updated. - :rtype: MusicFolder + Returns: + A new object with all the updated info. """ music_folders = self._subsonic.browsing.get_music_folders() diff --git a/src/knuckles/models/_now_playing_entry.py b/src/knuckles/models/_now_playing_entry.py index 492a0d6..62789e5 100644 --- a/src/knuckles/models/_now_playing_entry.py +++ b/src/knuckles/models/_now_playing_entry.py @@ -9,6 +9,18 @@ class NowPlayingEntry(Model): + """Object that holds all the info about a now playing entry. + + Attributes: + user: The user that is currently playing a song. + song (Song): All the info about the song that is now playing. + minutes_ago (int | None): How many minutes ago the songs started + its playback. + player_id (int | None): The ID of the played where the song is playing. + player_name (song | None): The name of the player where the song is + playing. + """ + def __init__( self, subsonic: "Subsonic", diff --git a/src/knuckles/models/_play_queue.py b/src/knuckles/models/_play_queue.py index 6722392..48943aa 100644 --- a/src/knuckles/models/_play_queue.py +++ b/src/knuckles/models/_play_queue.py @@ -11,7 +11,20 @@ class PlayQueue(Model): - """Representation of all the data related to a play queue in Subsonic.""" + """Object that holds al the info about a play queue. + + Attributes: + songs (list[Song]): All the info about all the songs in + the play queue. + current (Song | None): The current playing song in the play queue. + position (int | None): The index of the current playing song in + the play queue. + user (User | None): The user owner of the play queue. + changed (timedate | None): The timestamp when the play queue + received any change. + changed_by (str | None): The name of the client that made the last + modification to the play queue. + """ def __init__( self, @@ -33,14 +46,14 @@ def __init__( self.songs = [Song(self._subsonic, **song) for song in entry] if entry else None def generate(self) -> "PlayQueue": - """Return a new play queue with all the data updated from the API, + """Return a new play queue object with all the data updated from the API, using the endpoint that return the most information possible. - Useful for making copies with updated data or updating the object itself - with immutability, e.g., foo = foo.generate(). + Useful for making copies with updated data or updating the object + itself with immutability, e.g., `foo = foo.generate()`. - :return: A new share object with all the data updated. - :rtype: PlayQueue + Returns: + A new object with all the updated info. """ get_play_queue = self._subsonic.bookmarks.get_play_queue() diff --git a/src/knuckles/models/_playlist.py b/src/knuckles/models/_playlist.py index e794db5..2b93328 100644 --- a/src/knuckles/models/_playlist.py +++ b/src/knuckles/models/_playlist.py @@ -12,7 +12,28 @@ class Playlist(Model): - """Representation of all the data related to a playlist in Subsonic.""" + """Object that holds all the info about a playlist. + + Attributes: + id (str): The ID of the playlist. + name (str | None): The name of the playlist. + song_count (int | None): The number of songs in the playlist. + duration (int | None): The total durations of all the songs in the + playlist. + created (datetime | None): The timestamp when the playlist was created. + changed (datetime | None): The timestamp when the playlist was last + edited. + comment (str | None): A comment attach with the playlist. + owner (User | None): All the info related with the user creator of + the playlist. + public (bool | None): If the playlist is public or not. + cover_art (CoverArt | None): All the info related with the cover art + of the playlist. + allowed_users (list[User] | None): List that holds all the info + related with all the users allowed to see the playlist. + songs (list[Song] | None): List that holds all the info about + all the songs in the playlist. + """ def __init__( self, @@ -30,37 +51,6 @@ def __init__( allowedUser: list[str] | None = None, entry: list[dict[str, Any]] | None = None, ) -> None: - """Representation of all the data related to a user in Subsonic. - - :param subsonic: The subsonic object to make all the internal requests with it. - :type subsonic: Subsonic - :param id: The ID of the playlist. - :type id: str - :param name: The name of the playlist, defaults to None. - :type name: str | None, optional - :param songCount: The numbers of songs inside the playlist, defaults to None. - :type songCount: int | None, optional - :param duration: The total duration of the playlist, defaults to None. - :type duration: int | None, optional - :param created: The time when the playlist was created, defaults to None. - :type created: str | None, optional - :param changed: The last time the playlist was changed, defaults to None. - :type changed: str | None, optional - :param comment: The comment of the playlist, defaults to None. - :type comment: str | None, optional - :param owner: The owner of the playlist, defaults to None. - :type owner: str | None, optional - :param public: If the playlist is public, defaults to None. - :type public: bool | None, optional - :param coverArt: The ID of the cover art of the playlist, defaults to None. - :type coverArt: str | None, optional - :param allowedUser: The list of users allowed to reproduce the playlist, - defaults to None. - :type allowedUser: list[str] | None, optional - :param entry: A list with all the songs inside the playlist, defaults to None. - :type entry: list[dict[str, Any]] | None, optional - """ - super().__init__(subsonic) self.id = id @@ -81,24 +71,25 @@ def __init__( self.songs = [Song(self._subsonic, **song) for song in entry] if entry else None def generate(self) -> "Playlist": - """Return a new playlist with all the data updated from the API, + """Return a new playlist object with all the data updated from the API, using the endpoint that return the most information possible. - :return: A new playlist object with all the data updated. - :rtype: Playlist + Useful for making copies with updated data or updating the object + itself with immutability, e.g., `foo = foo.generate()`. + + Returns: + A new object with all the updated info. """ return self._subsonic.playlists.get_playlist(self.id) def create(self) -> "Playlist": - """Calls the "createPlaylist" endpoint of the API. + """Create a playlist with the same info of the object. - Creates a new playlist with the same data of the object - where the method is called. - - :return: The new created playlist. - :rtype: Playlist + Returns: + The new created playlist. """ + # Create a list of Song IDs if songs is not None songs_ids = [song.id for song in self.songs] if self.songs else None @@ -114,17 +105,16 @@ def create(self) -> "Playlist": return new_playlist def update(self) -> Self: - """Calls the "updatePlaylist" endpoint of the API. + """Updates changed info between the model and the server. - Updates the name, comment and public state of the playlist with the ones - in the parameters of the object. + Warning: + It doesn't change the list of songs in the playlist. For do + it use the `add_songs` and `remove_songs` methods. - NOT the playlist list, please use TODO (add_songs) - and TODO (remove_songs) to reflect it in the model. - - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ + self._subsonic.playlists.update_playlist( self.id, self.name, self.comment, self.public ) @@ -132,25 +122,24 @@ def update(self) -> Self: return self def delete(self) -> Self: - """Calls the "deletePlaylist" endpoint of the API. + """Delete the playlist from the server. - Delete the playlist with the same ID as the id parameter in the object. - - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ + self._subsonic.playlists.delete_playlist(self.id) return self def add_songs(self, song_ids: list[str]) -> Self: - """Add any number of new songs to the playlist - It's reflected in the songs list in the model. + """Add songs to the playlist. - :param song_ids: A list with the IDs of the songs to add. + Args: + song_ids: The ID of songs to add. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.playlists.update_playlist(self.id, song_ids_to_add=song_ids) @@ -169,13 +158,13 @@ def add_songs(self, song_ids: list[str]) -> Self: return self def remove_songs(self, songs_indexes: list[int]) -> Self: - """Remove any number of new songs to the playlist - It's reflected in the songs list in the model. + """Remove songs from the playlist. - :param song_indexes: A list with the indexes of the songs to remove. + Args: + songs_indexes: The indexes of the songs to remove. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.playlists.update_playlist( diff --git a/src/knuckles/models/_podcast.py b/src/knuckles/models/_podcast.py index 0b92eef..e9d4f5a 100644 --- a/src/knuckles/models/_podcast.py +++ b/src/knuckles/models/_podcast.py @@ -11,7 +11,34 @@ class Episode(Model): - """Representation of all the data related to a podcast episode in Subsonic.""" + """Object that holds all the info about a episode + + Attributes: + id: (str) The ID of the episode + stream_id (str | None): The ID of the stream of the + episode. + channel (Channel | None): The channel where the episode is + from. + title (str | None): The title of the episode. + description (str | None): The description of the episode. + publish_date (datetime | None): The timestamp when the episode + was publised. + status (str | None): The status of the episode. + parent (str | None): The ID of the parent of the episode. + is_dir (bool | None): If the episode is a directory. + year (int | None): The year when the episode was released. + genre (str | None): The name of the genre of the episode. + cover_art (CoverArt | None): All the info related with the + cover art of the episode. + size (int | None): The size of the episode. + content_type (str | None): The HTTP Content-Type of the file + of the episode. + suffix (str | None): The suffix of the filename of the file + of the episode. + duration (int | None): The duration in seconds of the episode. + bit_rate (int | None): The bit rate of the episode. + path (str | None): The path of the episode. + """ def __init__( self, @@ -35,49 +62,6 @@ def __init__( bitRate: int | None = None, path: str | None = None, ) -> None: - """Representation of all the data related to a podcast episode in Subsonic. - - :param subsonic: The subsonic object to make all the internal requests with it. - :type subsonic: Subsonic - :param id: The ID of the episode. - :type id: str - :param streamId: The ID to stream the episode, defaults to None. - :type streamId: str | None, optional - :param channelId: The ID of the channel where the episode comes from, - defaults to None. - :type channelId: str | None, optional - :param title: The title of the episode, defaults to None. - :type title: str | None, optional - :param description: The description of the episode, defaults to None. - :type description: str | None, optional - :param publishDate: The date of publish of the episode, defaults to None. - :type publishDate: str | None, optional - :param status: The status of the episode, defaults to None. - :type status: str | None, optional - :param parent: The ID of the parent folder of the episode, defaults to None. - :type parent: str | None, optional - :param isDir: If the episode is a dir, defaults to None. - :type isDir: bool | None, optional - :param year: The year of release of the episode, defaults to None. - :type year: int | None, optional - :param genre: The genre of the episode, defaults to None. - :type genre: str | None, optional - :param coverArt: The cover art ID of the episode, defaults to None. - :type coverArt: str | None, optional - :param size: The file size of the episode, defaults to None. - :type size: int | None, optional - :param contentType: The content type of the episode file, defaults to None. - :type contentType: str | None, optional - :param suffix: The suffix of the episode file, defaults to None. - :type suffix: str | None, optional - :param duration: The duration in seconds of the episode, defaults to None. - :type duration: int | None, optional - :param bitRate: The bit rate of the episode, defaults to None. - :type bitRate: int | None, optional - :param path: The path of the episode, defaults to None. - :type path: str | None, optional - """ - super().__init__(subsonic) self.id = id @@ -100,17 +84,17 @@ def __init__( self.path = path def generate(self) -> "Episode": - """Return a new episode with all the data updated from the API, + """Return a new episode object with all the data updated from the API, using the endpoint that return the most information possible. - Useful for making copies with updated data or updating the object itself - with immutability, e.g., foo = foo.generate(). + Useful for making copies with updated data or updating the object + itself with immutability, e.g., `foo = foo.generate()`. - :return: A new episode object with all the data updated. - :rtype: Episode + Returns: + A new object with all the updated info. """ - get_episode = self._subsonic.podcast.get_episode(self.id) + get_episode = self._subsonic.podcast.get_podcast_episode(self.id) if get_episode is None: raise ResourceNotFound( @@ -120,10 +104,10 @@ def generate(self) -> "Episode": return get_episode def download(self) -> Self: - """Calls the "downloadPodcastEpisode" endpoint of the API. + """Request the server to download the episode. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.podcast.download_podcast_episode(self.id) @@ -131,10 +115,10 @@ def download(self) -> Self: return self def delete(self) -> Self: - """Calls the "deletePodcastEpisode" endpoint of the API. + """Delete the episode from the server. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.podcast.delete_podcast_episode(self.id) @@ -143,7 +127,21 @@ def delete(self) -> Self: class Channel(Model): - """Representation of all the data related to a podcast channel in Subsonic.""" + """Object that holds all the info about a channel. + + Attributes: + id (str): The ID of the channel. + url (str | None): The URL of the channel. + title (str | None): The title of the channel. + description (str | None): The description of the channel. + cover_art (CoverArt | None): All the info related with the + cover art of the channel. + original_image_url (str | None): The URL of the original image + of the channel. + status (str | None): The status of the channel. + episodes (list[Episode] | None): List that holds all the info about + all the episodes of the channel. + """ def __init__( self, @@ -157,29 +155,6 @@ def __init__( status: str | None = None, episode: list[dict[str, Any]] | None = None, ) -> None: - """Representation of all the data related to a podcast channel in Subsonic. - - :param subsonic: The subsonic object to make all the internal requests with it. - :type subsonic: Subsonic - :param id: The ID of the channel. - :type id: str - :param url: The url to get the episodes from, defaults to None. - :type url: str | None, optional - :param title: The title of the channel, defaults to None. - :type title: str | None, optional - :param description: The description of the channel, defaults to None. - :type description: str | None, optional - :param coverArt: The cover art ID of the channel, defaults to None. - :type coverArt: str | None, optional - :param originalImageUrl: The url of the original image of the channel, - defaults to None. - :type originalImageUrl: str | None, optional - :param status: The status of the channel, defaults to None. - :type status: str | None, optional - :param episode: A list will all the episodes of the podcast, defaults to None. - :type episode: list[dict[str, Any]] | None, optional - """ - super().__init__(subsonic) self.id = id @@ -196,23 +171,23 @@ def __init__( ) def generate(self) -> "Channel": - """Return a new channel with all the data updated from the API, + """Return a new channel object with all the data updated from the API, using the endpoint that return the most information possible. - Useful for making copies with updated data or updating the object itself - with immutability, e.g., foo = foo.generate(). + Useful for making copies with updated data or updating the object + itself with immutability, e.g., `foo = foo.generate()`. - :return: A new channel object with all the data updated. - :rtype: Channel + Returns: + A new object with all the updated info. """ - return self._subsonic.podcast.get_podcast(self.id) + return self._subsonic.podcast.get_podcast_channel(self.id) def create(self) -> Self: - """Calls the "createPodcastChannel" endpoint of the API. + """Create a new podcast with the info of the current one. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ # Ignore the None type error as the server @@ -224,10 +199,10 @@ def create(self) -> Self: return self def delete(self) -> Self: - """Calls the "deletePodcastChannel" endpoint of the API. + """Delete the podcast from the server. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.podcast.delete_podcast_channel(self.id) diff --git a/src/knuckles/models/_replay_gain.py b/src/knuckles/models/_replay_gain.py index c0a65c7..2767ac9 100644 --- a/src/knuckles/models/_replay_gain.py +++ b/src/knuckles/models/_replay_gain.py @@ -8,6 +8,19 @@ class ReplayGain(Model): + """Object that holds all the info about the gain of the playback + of a media. + + Attributes: + track_gain (str | None): The track replay gain in dB. + album_gain (str | None): The album replay gain in dB. + track_peak (int | None): The track peak value. + album_peak (int | None): The album peak value. + base_gain (int | None): The base replay gain in dB. + fallback_gain (int | None): Fallback gain in dB used when + the desired one is missing. + """ + def __init__( self, subsonic: "Subsonic", @@ -16,6 +29,7 @@ def __init__( trackPeak: str | None = None, albumPeak: str | None = None, baseGain: str | None = None, + fallbackGain: int | None = None, ) -> None: super().__init__(subsonic) @@ -24,3 +38,4 @@ def __init__( self.track_peak = trackPeak self.album_peak = albumPeak self.base_gain = baseGain + self.fallback_gain = fallbackGain diff --git a/src/knuckles/models/_scan_status.py b/src/knuckles/models/_scan_status.py index 7829974..9a1c452 100644 --- a/src/knuckles/models/_scan_status.py +++ b/src/knuckles/models/_scan_status.py @@ -7,20 +7,14 @@ class ScanStatus(Model): - """Representation of all the data related to the status - of a library scan in Subsonic. + """Object that holds all the info about a scan status. + + Attributes: + scanning (bool): If the server is scanning media or not. + count (int): The number of media already scanned. """ def __init__(self, subsonic: "Subsonic", scanning: bool, count: int) -> None: - """Representation of all the data related to the status - of a library scan in Subsonic. - - :param scanning: The status of the scan. - :type scanning: bool - :param count: Scanned item count. - :type count: int - """ - super().__init__(subsonic) self.scanning: bool = scanning diff --git a/src/knuckles/models/_search_result.py b/src/knuckles/models/_search_result.py index 332b294..76a88ed 100644 --- a/src/knuckles/models/_search_result.py +++ b/src/knuckles/models/_search_result.py @@ -11,6 +11,17 @@ class SearchResult(Model): + """Object that holds all the info about a search result. + + Attributes: + songs (list[Song] | None): List that holds all the info about + all the songs returned in the search result.- + albums (list[Album] | None): List that holds all the info about + all the albums returned in the search result. + artists (list[Artist] | None): List that holds all the info about + all the artists returned in the search result. + """ + def __init__( self, subsonic: "Subsonic", diff --git a/src/knuckles/models/_share.py b/src/knuckles/models/_share.py index 95cde4e..00056d0 100644 --- a/src/knuckles/models/_share.py +++ b/src/knuckles/models/_share.py @@ -12,7 +12,25 @@ class Share(Model): - """Representation of all the data related to a share in Subsonic.""" + """Object that holds all the info about a share. + + Attributes: + id (str): The ID of the share. + url (str | None): The URL to access the shared media. + description (str | None): The description of the share. + user (User | None): All the info related with the user creator + of the share. + created (datetime | None): The timestamp when the share + was created. + expires (datetime | None): The timestamp when the share + will expire. + last_visited (datetime | None): The timestamp when the + share was last visited. + visit_count (int | None): Number of times the share has + been visited. + songs (list[Song] | None): List that holds all the info about + all the songs available to access with the share. + """ def __init__( self, @@ -27,32 +45,6 @@ def __init__( visitCount: int | None = None, entry: list[dict[str, Any]] | None = None, ) -> None: - """Representation of all the data related to a share in Subsonic. - - :param subsonic: The subsonic object to make all the internal requests with it. - :type subsonic: Subsonic - :param id: The id of the share. - :type id: str - :param url: The url of the share, defaults to None. - :type url: str | None, optional - :param description: The description of the share, defaults to None. - :type description: str | None, optional - :param username: The username of the creator of the share, defaults to None. - :type username: str | None, optional - :param created: The time when the share was created, defaults to None. - :type created: str | None, optional - :param expires: The time when the share expires, defaults to None. - :type expires: str | None, optional - :param lastVisited: The last tim the share was used, defaults to None. - :type lastVisited: str | None, optional - :param visitCount: The number of times the share has been used, - defaults to None. - :type visitCount: int | None, optional - :param entry: A list with all the songs that the share gives access, - defaults to None. - :type entry: list[dict[str, Any]] | None, optional - """ - super().__init__(subsonic) self.id = id @@ -66,14 +58,14 @@ def __init__( self.songs = [Song(self._subsonic, **song) for song in entry] if entry else None def generate(self) -> "Share | None": - """Return a new share with all the data updated from the API, + """Return a new share object with all the data updated from the API, using the endpoint that return the most information possible. - Useful for making copies with updated data or updating the object itself - with immutability, e.g., foo = foo.generate(). + Useful for making copies with updated data or updating the object + itself with immutability, e.g., `foo = foo.generate()`. - :return: A new share object with all the data updated. - :rtype: Share + Returns: + A new object with all the updated info. """ get_share = self._subsonic.sharing.get_share(self.id) @@ -84,15 +76,14 @@ def generate(self) -> "Share | None": return get_share def create(self) -> "Share": - """Calls the "createShare" endpoint of the API. + """Create a new share with the same info of the current one. - Creates a new playlist with the same data of the object - where the method is called. + Raises: + ShareInvalidSongList: Raised if the song list contained in + the share is empty. - :raises ShareInvalidSongList: Raised if the list of songs - in the share is empty of None. - :return: The new created share. - :rtype: Share + Returns: + The new created share. """ if self.songs is None or self.songs == []: @@ -112,13 +103,10 @@ def create(self) -> "Share": return new_share def update(self) -> Self: - """Calls the "updateShare" endpoint of the API. + """Update the info of the share with the one in the model. - Updates the description and expire date of the share with the ones - in the parameters of the object. - - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.sharing.update_share(self.id, self.description, self.expires) @@ -126,12 +114,10 @@ def update(self) -> Self: return self def delete(self) -> Self: - """Calls the "deleteShare" endpoint of the API. - - Delete the share with the same ID as the id parameter in the object. + """Delete the share from the server. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.sharing.delete_share(self.id) diff --git a/src/knuckles/models/_song.py b/src/knuckles/models/_song.py index c803a90..3bc5e5e 100644 --- a/src/knuckles/models/_song.py +++ b/src/knuckles/models/_song.py @@ -19,7 +19,74 @@ class Song(Model): - """Representation of all the data related to a song in Subsonic.""" + """Object that holds all the info about a song. + + Attributes: + id (str): The ID of the song. + title (str | None): The title of the song. + parent (str | None): The ID of the parent of the song. + track (int | None): The track + year (int | None): The year when the song was released. + genre (Genre | None): All the info related with the genre + of the song. + size (int | None): The size of the file of the song. + content_type (str | None): The HTTP ContentType of the + file of the song. + suffix (str | None): The suffix of the filename of the + file of the song. + transcoded_content_type (str | None): The HTTP ContentType + of the transcoded file of the song. + transcoded_suffix (str | None): The suffix of the filename + of the transcoded file of the song. + duration (int | None): The duration in seconds of the song. + bit_rate (int | None): The bit rate of the song. + path (str | None): The path of the song. + user_rating (int | None): The rating given to the song by + the user. + average_rating (float | None): The average rating of all the + user for the song. + play_count (int | None): The number of the times the song + has been played. + disc_number (int | None): The disc number of the song. + type (str | None): The type of media. + bookmark_position (int | None): The position in seconds + where the song is bookmarked for the authenticated user. + album (Album | None): All the info related with the album + of the song. + artist (Artist | None): All the info related with the main + artist of the song. + cover_art (CoverArt | None): All the info related + with the cover art of the song. + created (datetime | None): The timestamp when the song + was created. + starred (datetime | None): The timestamp when the song + was starred by the authenticated user if they have. + played (datetime | None): The timestamp when the song + was last played. + bpm (int | None): The bpm of the song. + comment (str | None): The comment of the song. + sort_name (str | None): The sort name of the song. + music_brainz_id (str | None): The ID of the MusicBrainz entry + of the song. + genres (list[ItemGenre | None): List that holds all the info + about all the genres of the song. + artists (list[Artist] | None): List that holds all the info + about all the artists that made the song. + display_artist (str | None): The display name of the artist + of the song. + album_artists (list[Artist] | None): List that holds all the info + about all the artists that made the album where the song + is from. + display_album_artist (str | None): THe display name of the artist + of the album of the song. + contributors (list[Contributor] | None): List that holds all the + info about all the contributors of the song. + display_composer (str | None): The display name of the composer + of the song. + moods (list[str] | None): List off all the moods of the song. + replay_gain (ReplayGain | None): All the info about the replay + gain of the song. + """ def __init__( self, @@ -70,85 +137,6 @@ def __init__( moods: list[str] | None = None, replayGain: dict[str, Any] | None = None, ) -> None: - """Representation of all the data related to song in Subsonic. - - :param subsonic: The subsonic object to make all the internal requests with it. - :type subsonic: Subsonic - :param id: The id of the media. - :type id: str - :param title: The song name, defaults to None. - :type title: str | None, optional - :param isDir: If the media is a dir (should always be False), defaults to False. - :type isDir: bool, optional - :param parent: The ID of the parent folder, defaults to None. - :type parent: str | None, optional - :param album: The album name, defaults to None. - :type album: str | None, optional - :param artist: The artist name, defaults to None. - :type artist: str | None, optional - :param track: The track number, defaults to None. - :type track: int | None, optional - :param year: The media year, defaults to None. - :type year: int | None, optional - :param genre: The media genre, defaults to None. - :type genre: str | None, optional - :param coverArt: A covertArt id, defaults to None. - :type coverArt: str | None, optional - :param size: A file size of the media, defaults to None. - :type size: int | None, optional - :param contentType: The mimeType of the media, defaults to None. - :type contentType: str | None, optional - :param suffix: The file suffix of the media, defaults to None. - :type suffix: str | None, optional - :param transcodedContentType: The transcoded mediaType - if transcoding should happen, defaults to None. - :type transcodedContentType: str | None, optional - :param transcodedSuffix: The file suffix of the transcoded media, - defaults to None. - :type transcodedSuffix: str | None, optional - :param duration: The duration of the media in seconds, defaults to None. - :type duration: int | None, optional - :param bitRate: The bitrate of the media, defaults to None. - :type bitRate: int | None, optional - :param path: The full path of the media, defaults to None. - :type path: str | None, optional - :param isVideo: If the media is a video (should always be false), - defaults to False. - :type isVideo: bool, optional - :param userRating: The user rating of the media (between 1 and 5, inclusive), - defaults to None. - :type userRating: int | None, optional - :param averageRating: The average rating of the media - (between 1.0 and 5.0 inclusive), defaults to None. - :type averageRating: float | None, optional - :param playCount: The play count, defaults to None. - :type playCount: int | None, optional - :param discNumber: The disc number, defaults to None. - :type discNumber: int | None, optional - :param created: Date the media was created, defaults to None. - :type created: str | None, optional - :param starred: Date the media was starred, defaults to None. - :type starred: str | None, optional - :param albumId: The corresponding album id, defaults to None. - :type albumId: str | None, optional - :param artistId: The corresponding artist id, defaults to None. - :type artistId: str | None, optional - :param type: The media type, defaults to None. - :type type: str | None, optional - :param bookmarkPosition: The bookmark position in seconds, defaults to None. - :type bookmarkPosition: int | None, optional - :param originalWidth: The video original Width, defaults to None. - :type originalWidth: None, optional - :param originalHeight: The video original Height, defaults to None. - :type originalHeight: None, optional - :param played: Date the album was last played (OpenSubsonic), defaults to None. - :type played: str | None, optional - :raises VideoArgumentsInSong: Raised if arguments only valid - for videos are passed in. - :raises AlbumOrArtistArgumentsInSong: Raised if arguments only valid - for albums or artists are passed in. - """ - super().__init__(subsonic) self.id: str = id @@ -212,23 +200,23 @@ def __init__( ) def generate(self) -> "Song": - """Return a new song with all the data updated from the API, + """Return a new song object with all the data updated from the API, using the endpoint that return the most information possible. - Useful for making copies with updated data or updating the object itself - with immutability, e.g., foo = foo.generate(). + Useful for making copies with updated data or updating the object + itself with immutability, e.g., `foo = foo.generate()`. - :return: A new song object with all the data updated. - :rtype: Song + Returns: + A new object with all the updated info. """ return self._subsonic.browsing.get_song(self.id) def star(self) -> Self: - """Calls the "star" endpoint of the API. + """Star the song for the authenticated user. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.media_annotation.star_song(self.id) @@ -236,10 +224,10 @@ def star(self) -> Self: return self def unstar(self) -> Self: - """Calls the "unstar" endpoint of the API. + """Unstar the song for the authenticated user. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.media_annotation.unstar_song(self.id) @@ -247,12 +235,13 @@ def unstar(self) -> Self: return self def set_rating(self, rating: int) -> Self: - """Calls the "setRating" endpoint of the API. + """Set the rating of the song. - :param rating: The rating between 1 and 5 (inclusive). - :type rating: int - :return: The object itself to allow method chaining. - :rtype: Self + Args: + rating: The new rating for the song. + + Returns: + The object itself. """ self._subsonic.media_annotation.set_rating(self.id, rating) @@ -260,10 +249,10 @@ def set_rating(self, rating: int) -> Self: return self def remove_rating(self) -> Self: - """Calls the "setRating" endpoint of the API with a rating of 0. + """Remove the rating for the song. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.media_annotation.remove_rating(self.id) @@ -271,10 +260,15 @@ def remove_rating(self) -> Self: return self def scrobble(self, time: datetime, submission: bool = True) -> Self: - """Calls the "scrobble" endpoint of the API. + """Scrobble the song. + + Args: + time: The timestamp when the song was scrobble: + submission: If the scrobble is a request to a submission or + it is a now playing entry. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.media_annotation.scrobble([self.id], [time], submission) diff --git a/src/knuckles/models/_starred_content.py b/src/knuckles/models/_starred_content.py index e76ead2..444a78a 100644 --- a/src/knuckles/models/_starred_content.py +++ b/src/knuckles/models/_starred_content.py @@ -11,6 +11,17 @@ class StarredContent(Model): + """Object that holds all the info about starred content. + + Attributes: + songs (list[Song] | None): List that holds all the info + about all the starred songs. + albums (list[Album] | None): List that holds all the info + about all the starred albums. + artists (list[Artist] | None): List that holds all the info + about all the starred artists. + """ + def __init__( self, subsonic: "Subsonic", diff --git a/src/knuckles/models/_system.py b/src/knuckles/models/_system.py index 1ff5b84..8019b97 100644 --- a/src/knuckles/models/_system.py +++ b/src/knuckles/models/_system.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING from dateutil import parser + from knuckles.models._model import Model if TYPE_CHECKING: @@ -9,8 +10,16 @@ class SubsonicResponse(Model): - """Representation of the generic successful response data - in a request to the API. + """Object that holds all the generic info about a + response in a OpenSubsonic REST API call. + + Attributes: + status (str): The status of the response, can be "ok" or "failed". + version (str): The server supported version of the OpenSubonic REST API. + type (str | None): The name of the server reported by itself. + server_version (str | None): The server actual version. + open_subsonic (bool | None): If the server supports OpenSubsonic REST API + extensions. """ def __init__( @@ -20,37 +29,28 @@ def __init__( version: str, type: str | None = None, serverVersion: str | None = None, - openSubsonic: bool = False, + openSubsonic: bool | None = None, ) -> None: - """Representation of the generic successful response data - in a request to the API. - - Transform all the data in camelCase to snake_case. - - :param status: The command result. It can be "ok" or "failed". - :type status: str - :param version: The server supported Subsonic API version. - :type version: str - :param type: The server actual name (OpenSubsonic), defaults to None - :type type: str | None, optional - :param serverVersion: The server actual version (OpenSubsonic), defaults to None - :type serverVersion: str | None, optional - :param openSubsonic: The support of the OpenSubsonic v1 specifications, - defaults to False - :type openSubsonic: bool, optional - """ - super().__init__(subsonic) - self.status: str = status - self.version: str = version - self.type: str | None = type - self.server_version: str | None = serverVersion - self.open_subsonic: bool = openSubsonic + self.status = status + self.version = version + self.type = type + self.server_version = serverVersion + self.open_subsonic = openSubsonic class License(Model): - """Representation of the license related data in Subsonic.""" + """Object that holds all the info about the license status of the server. + + Attributes: + valid (bool): If the license of the server is valid. + email (str | None): The email of the authenticated user. + license_expires (datetime | None): The timestamp when the + license expires. + trial_expires (datetime | None): The timestamp when the + trial expires if it has not already. + """ def __init__( self, @@ -60,18 +60,6 @@ def __init__( licenseExpires: str | None = None, trialExpires: str | None = None, ) -> None: - """Representation of the license related data in Subsonic. - - :param valid: The status of the license - :type valid: bool - :param email: The email of the user which the request was made, defaults to None - :type email: str | None, optional - :param licenseExpires: End of license date, defaults to None - :type licenseExpires: str | None, optional - :param trialExpires: End of trial date., defaults to None - :type trialExpires: str | None, optional - """ - super().__init__(subsonic) self.valid: bool = valid diff --git a/src/knuckles/models/_user.py b/src/knuckles/models/_user.py index d89d7bc..7818859 100644 --- a/src/knuckles/models/_user.py +++ b/src/knuckles/models/_user.py @@ -9,7 +9,36 @@ class User(Model): - """Representation of all the data related to a user in Subsonic.""" + """Object that holds all the info about a user. + + Attributes: + username (str): The username of the user. + password (str | None): The password of the user. + email (str | None): The email of the user. + ldap_authenticated (bool | None): If the user is has been + authenticated using LDAP. + admin_role (bool | None): If the user has access to admin functionalities. + settings_role (bool | None): If the user has access to change the settings + of the server. + stream_role (bool | None): If the user has access to stream media. + jukebox_role (bool | None): If the user has access to control the jukebox. + download_role (bool | None): If the user has access to download media. + upload_role (bool | None): If the user has access to upload media. + playlist_role (bool | None): If the user has access to create, edit and + delete playlists. + cover_art_role (bool | None): If the user has access to manipulate + cover arts of media. + comment_role (bool | None): If the user has access to manipulate + comments. + podcast_role (bool | None): If the user has access to manipulate podcasts. + share_role (bool | None): If the user has access to create, modify and + delete shares. + video_conversion_role (bool | None): If the user is able to trigger + video conversions. + music_folder_id (list[str] | None): The IDs of the music folders + where the user is able to access content from. + max_bit_rate (int | None): The max bit rate the user can stream. + """ def __init__( self, @@ -55,25 +84,26 @@ def __init__( self.max_bit_rate = max_bit_rate def generate(self) -> "User": - """Returns the function to the same user with the maximum possible - information from the Subsonic API. + """Return a new user object with all the data updated from the API, + using the endpoint that return the most information possible. - Useful for making copies with updated data or updating the object itself - with immutability, e.g., foo = foo.generate(). + Useful for making copies with updated data or updating the object + itself with immutability, e.g., `foo = foo.generate()`. - :raises NoApiAccess: Raised if the subsonic property is None. - :return: A new user object with all the data updated. - :rtype: User + Returns: + A new object with all the updated info. """ return self._subsonic.user_management.get_user(self.username) def create(self) -> Self: - """Calls the "createUser" endpoint of the API. + """Create a new user with the attributes of the model. - :raises NoApiAccess: Raised if the subsonic property is None. - :return: The object itself to allow method chaining. - :rtype: Self + Raises: + MissingRequiredProperty: Raised if a required property to create + the user is missing. + Returns: + The object itself. """ if not self.email: @@ -110,14 +140,11 @@ def create(self) -> Self: return self def update(self) -> Self: - """Calls the "updateUser" endpoint of the API. + """Updates the info about the user in the server with + the one in the model. - The user will be updated with - the data stored in the properties of the object itself. - - :raises NoApiAccess: Raised if the subsonic property is None. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.user_management.update_user( @@ -144,11 +171,10 @@ def update(self) -> Self: return self def delete(self) -> Self: - """Calls the "deleteUser" endpoint of the API. + """Delete the user from the server. - :raises NoApiAccess: Raised if the subsonic property is None. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ self._subsonic.user_management.delete_user(self.username) @@ -156,17 +182,15 @@ def delete(self) -> Self: return self def change_password(self, new_password: str) -> Self: - """Calls the "changePassword" endpoint of the API. + """Change the password of the user. - The password is changed with the user corresponding - to the username property of the object. + Args: + new_password: The new password for the user - :param new_password: The new password for the user. - :type new_password: str - :raises NoApiAccess: Raised if the subsonic property is None. - :return: The object itself to allow method chaining. - :rtype: Self + Returns: + The object itself. """ + self._subsonic.user_management.change_password(self.username, new_password) return self diff --git a/src/knuckles/models/_video.py b/src/knuckles/models/_video.py index 0a4bff6..4431e12 100644 --- a/src/knuckles/models/_video.py +++ b/src/knuckles/models/_video.py @@ -19,6 +19,15 @@ class AudioTrack(Model): + """Object that holds all the info about an audio track. + + Attributes: + id (str): The ID of the audio track. + name (str | None): The name of the audio track. + language_code (str | None): The code of the language in which the + audio track is in. + """ + def __init__( self, subsonic: "Subsonic", @@ -34,6 +43,13 @@ def __init__( class Captions(Model): + """Object that holds all the info about captions: + + Attributes: + id (str): The ID of the captions. + name (str | None): The ID of the captions. + """ + def __init__(self, subsonic: "Subsonic", id: str, name: str | None = None) -> None: super().__init__(subsonic) @@ -42,6 +58,20 @@ def __init__(self, subsonic: "Subsonic", id: str, name: str | None = None) -> No class VideoInfo(Model): + """Object that holds all the info about extra video info. + + Attributes: + video_id (str): The ID of the video where the extra info are from. + id (str): The ID of the extra info. + captions (Captions | None): All the info about the captions of + the video. + conversion (Video | None): All the info about the converted video + of this one. + audio_tracks (dict[str, AudioTrack] | None): A dict that holds all the info + about the audio tracks of the video, with the key being the language code + of the audio track and the value the info about the track itself. + """ + def __init__( self, subsonic: "Subsonic", @@ -81,6 +111,75 @@ def generate(self) -> "VideoInfo": class Video(Model): + """Object that holds all the info about a video. + + Attributes: + id (str): The ID of the song. + title (str | None): The title of the song. + parent (str | None): The ID of the parent of the song. + track (int | None): The track + year (int | None): The year when the song was released. + genre (Genre | None): All the info related with the genre + of the song. + size (int | None): The size of the file of the song. + content_type (str | None): The HTTP ContentType of the + file of the song. + suffix (str | None): The suffix of the filename of the + file of the song. + transcoded_content_type (str | None): The HTTP ContentType + of the transcoded file of the song. + transcoded_suffix (str | None): The suffix of the filename + of the transcoded file of the song. + duration (int | None): The duration in seconds of the song. + bit_rate (int | None): The bit rate of the song. + path (str | None): The path of the song. + user_rating (int | None): The rating given to the song by + the user. + average_rating (float | None): The average rating of all the + user for the song. + play_count (int | None): The number of the times the song + has been played. + disc_number (int | None): The disc number of the song. + type (str | None): The type of media. + bookmark_position (int | None): The position in seconds + where the song is bookmarked for the authenticated user. + album (Album | None): All the info related with the album + of the song. + artist (Artist | None): All the info related with the main + artist of the song. + cover_art (CoverArt | None): All the info related + with the cover art of the song. + created (datetime | None): The timestamp when the song + was created. + starred (datetime | None): The timestamp when the song + was starred by the authenticated user if they have. + played (datetime | None): The timestamp when the song + was last played. + bpm (int | None): The bpm of the song. + comment (str | None): The comment of the song. + sort_name (str | None): The sort name of the song. + music_brainz_id (str | None): The ID of the MusicBrainz entry + of the song. + genres (list[ItemGenre | None): List that holds all the info + about all the genres of the song. + artists (list[Artist] | None): List that holds all the info + about all the artists that made the song. + display_artist (str | None): The display name of the artist + of the song. + album_artists (list[Artist] | None): List that holds all the info + about all the artists that made the album where the song + is from. + display_album_artist (str | None): THe display name of the artist + of the album of the song. + contributors (list[Contributor] | None): List that holds all the + info about all the contributors of the song. + display_composer (str | None): The display name of the composer + of the song. + moods (list[str] | None): List off all the moods of the song. + replay_gain (ReplayGain | None): All the info about the replay + gain of the song. + """ + def __init__( self, subsonic: "Subsonic", @@ -193,14 +292,14 @@ def __init__( self.info: VideoInfo | None = None def generate(self) -> "Video": - """Return a new song with all the data updated from the API, + """Return a new video object with all the data updated from the API, using the endpoint that return the most information possible. - Useful for making copies with updated data or updating the object itself - with immutability, e.g., foo = foo.generate(). + Useful for making copies with updated data or updating the object + itself with immutability, e.g., `foo = foo.generate()`. - :return: A new song object with all the data updated. - :rtype: Song + Returns: + A new object with all the updated info. """ video = self._subsonic.browsing.get_video(self.id) @@ -211,6 +310,13 @@ def generate(self) -> "Video": return video def get_video_info(self) -> VideoInfo: + """Get all the extra info about the video, it's + set to the `info` attribute of the object. + + Returns: + The extra info returned by the server. + """ + self.info = self._subsonic.browsing.get_video_info(self.id) return self.info diff --git a/tests/api/test_browsing.py b/tests/api/test_browsing.py index 99b3e55..2657773 100644 --- a/tests/api/test_browsing.py +++ b/tests/api/test_browsing.py @@ -24,7 +24,7 @@ def test_get_music_folders( @responses.activate -def test_get_indexes( +def test_get_artist_indexes( add_responses: AddResponses, subsonic: Subsonic, mock_get_indexes: list[Response], @@ -34,7 +34,9 @@ def test_get_indexes( ) -> None: add_responses(mock_get_indexes) - response = subsonic.browsing.get_indexes(music_folders[0]["id"], modified_date) + response = subsonic.browsing.get_artists_indexed( + music_folders[0]["id"], modified_date + ) print(response.index) diff --git a/tests/api/test_chat.py b/tests/api/test_chat.py index 14163ee..afdee85 100644 --- a/tests/api/test_chat.py +++ b/tests/api/test_chat.py @@ -33,7 +33,7 @@ def test_get_chat_messages( response: list[ChatMessage] = subsonic.chat.get_chat_messages() - assert response[0].username == message["username"] + assert response[0].user.username == message["username"] # Divide by 1000 because messages are saved in milliseconds instead of seconds assert response[0].time == datetime.fromtimestamp(message["time"] / 1000) diff --git a/tests/api/test_code_errors.py b/tests/api/test_error_codes.py similarity index 77% rename from tests/api/test_code_errors.py rename to tests/api/test_error_codes.py index 3b6cc2a..1aa0f20 100644 --- a/tests/api/test_code_errors.py +++ b/tests/api/test_error_codes.py @@ -8,28 +8,28 @@ from tests.conftest import AddResponses, MockGenerator code_errors = [ - (0, "A generic error.", knuckles.exceptions.CodeError0), - (10, "Required parameter is missing.", knuckles.exceptions.CodeError10), + (0, "A generic error.", knuckles.exceptions.ErrorCode0), + (10, "Required parameter is missing.", knuckles.exceptions.ErrorCode10), ( 20, "Incompatible Subsonic REST protocol version. Client must upgrade.", - knuckles.exceptions.CodeError20, + knuckles.exceptions.ErrorCode20, ), ( 30, "Incompatible Subsonic REST protocol version. Server must upgrade.", - knuckles.exceptions.CodeError30, + knuckles.exceptions.ErrorCode30, ), - (40, "Wrong username or password.", knuckles.exceptions.CodeError40), + (40, "Wrong username or password.", knuckles.exceptions.ErrorCode40), ( 41, "Token authentication not supported for LDAP users.", - knuckles.exceptions.CodeError41, + knuckles.exceptions.ErrorCode41, ), ( 50, "User is not authorized for the given operation.", - knuckles.exceptions.CodeError50, + knuckles.exceptions.ErrorCode50, ), ( 60, @@ -37,9 +37,9 @@ "The trial period for the Subsonic server is over. " + "Please upgrade to Subsonic Premium. Visit subsonic.org for details." ), - knuckles.exceptions.CodeError60, + knuckles.exceptions.ErrorCode60, ), - (70, "The requested data was not found.", knuckles.exceptions.CodeError70), + (70, "The requested data was not found.", knuckles.exceptions.ErrorCode70), (80, "The cake is a lie!", knuckles.exceptions.UnknownErrorCode), ] diff --git a/tests/api/test_jukebox_control.py b/tests/api/test_jukebox_control.py index 583b983..40b1034 100644 --- a/tests/api/test_jukebox_control.py +++ b/tests/api/test_jukebox_control.py @@ -55,7 +55,7 @@ def test_jukebox_set( ) -> None: add_responses(mock_jukebox_control_set) - response = subsonic.jukebox.set(song["id"]) + response = subsonic.jukebox.set([song["id"]]) assert response.current_index == jukebox_status["currentIndex"] assert response.playing == jukebox_status["playing"] @@ -148,7 +148,7 @@ def test_jukebox_add( ) -> None: add_responses(mock_jukebox_control_add) - response = subsonic.jukebox.add(song["id"]) + response = subsonic.jukebox.add([song["id"]]) assert response.current_index == jukebox_status["currentIndex"] assert response.playing == jukebox_status["playing"] diff --git a/tests/api/test_podcast.py b/tests/api/test_podcast.py index ea0afef..a821067 100644 --- a/tests/api/test_podcast.py +++ b/tests/api/test_podcast.py @@ -18,7 +18,7 @@ def test_get_podcasts_default( ) -> None: add_responses(mock_get_podcasts_with_episodes) - response = subsonic.podcast.get_podcasts() + response = subsonic.podcast.get_podcast_channels() assert response[0].id == channel["id"] assert isinstance(response[0].episodes, list) @@ -35,7 +35,7 @@ def test_get_podcasts_with_episodes( ) -> None: add_responses(mock_get_podcasts_with_episodes) - response = subsonic.podcast.get_podcasts(True) + response = subsonic.podcast.get_podcast_channels(True) assert response[0].id == channel["id"] assert isinstance(response[0].episodes, list) @@ -51,7 +51,7 @@ def test_get_podcasts_without_episodes( ) -> None: add_responses(mock_get_podcasts_without_episodes) - response = subsonic.podcast.get_podcasts(False) + response = subsonic.podcast.get_podcast_channels(False) assert response[0].id == channel["id"] assert response[0].episodes is None @@ -67,7 +67,7 @@ def test_get_podcast_default( ) -> None: add_responses(mock_get_podcast_default) - response = subsonic.podcast.get_podcast(channel["id"]) + response = subsonic.podcast.get_podcast_channel(channel["id"]) assert response.id == channel["id"] assert response.url == channel["url"] @@ -88,7 +88,7 @@ def test_get_podcast_with_episodes( ) -> None: add_responses(mock_get_podcast_with_episodes) - response = subsonic.podcast.get_podcast(channel["id"], True) + response = subsonic.podcast.get_podcast_channel(channel["id"], True) assert response.id == channel["id"] assert isinstance(response.episodes, list) @@ -120,7 +120,7 @@ def test_get_podcast_without_episodes( ) -> None: add_responses(mock_get_podcast_without_episodes) - response = subsonic.podcast.get_podcast(channel["id"], False) + response = subsonic.podcast.get_podcast_channel(channel["id"], False) assert response.id == channel["id"] assert response.episodes is None @@ -136,7 +136,7 @@ def test_get_newest_podcasts( ) -> None: add_responses(mock_get_newest_podcasts) - response = subsonic.podcast.get_newest_podcasts(number_of_new_episodes) + response = subsonic.podcast.get_newest_podcast_episodes(number_of_new_episodes) assert response[0].id == episode["id"] @@ -150,7 +150,7 @@ def test_get_episode( ) -> None: add_responses(mock_get_podcasts_with_episodes) - response = subsonic.podcast.get_episode(episode["id"]) + response = subsonic.podcast.get_podcast_episode(episode["id"]) assert response.id == episode["id"] diff --git a/tests/models/test_channel.py b/tests/models/test_channel.py index c08ecd8..6ad7d53 100644 --- a/tests/models/test_channel.py +++ b/tests/models/test_channel.py @@ -16,7 +16,7 @@ def test_generate( ) -> None: add_responses(mock_get_podcast_default) - response = subsonic.podcast.get_podcast(channel["id"]) + response = subsonic.podcast.get_podcast_channel(channel["id"]) response.title = "Foo" response = response.generate() @@ -34,7 +34,7 @@ def test_create( add_responses(mock_get_podcast_default) add_responses(mock_create_podcast_channel) - response = subsonic.podcast.get_podcast(channel["id"]) + response = subsonic.podcast.get_podcast_channel(channel["id"]) response = response.create() assert type(response) is Channel @@ -51,7 +51,7 @@ def test_delete( add_responses(mock_get_podcast_default) add_responses(mock_delete_podcast_channel) - response = subsonic.podcast.get_podcast(channel["id"]) + response = subsonic.podcast.get_podcast_channel(channel["id"]) response = response.delete() assert type(response) is Channel diff --git a/tests/models/test_episode.py b/tests/models/test_episode.py index e6be04e..20c2c31 100644 --- a/tests/models/test_episode.py +++ b/tests/models/test_episode.py @@ -18,7 +18,7 @@ def test_generate( ) -> None: add_responses(mock_get_podcasts_with_episodes) - response = subsonic.podcast.get_episode(episode["id"]) + response = subsonic.podcast.get_podcast_episode(episode["id"]) response.title = "Foo" response = response.generate() @@ -53,7 +53,7 @@ def test_download( add_responses(mock_get_podcasts_with_episodes) add_responses(mock_download_podcast_episode) - response = subsonic.podcast.get_episode(episode["id"]) + response = subsonic.podcast.get_podcast_episode(episode["id"]) response = response.download() assert type(response) is Episode @@ -70,7 +70,7 @@ def test_delete( add_responses(mock_get_podcasts_with_episodes) add_responses(mock_delete_podcast_episode) - requested_episode = subsonic.podcast.get_episode(episode["id"]) + requested_episode = subsonic.podcast.get_podcast_episode(episode["id"]) requested_episode = requested_episode.delete() assert type(requested_episode) is Episode diff --git a/tests/models/test_jukebox.py b/tests/models/test_jukebox.py index 03dfb62..9bd1c3f 100644 --- a/tests/models/test_jukebox.py +++ b/tests/models/test_jukebox.py @@ -159,7 +159,7 @@ def test_jukebox_set( add_responses(mock_jukebox_control_set) response: Jukebox = subsonic.jukebox.status() - response = response.set(song["id"]) + response = response.set([song["id"]]) assert isinstance(response, Jukebox) assert isinstance(response.playlist, list) @@ -196,7 +196,7 @@ def test_jukebox_add_with_a_populated_playlist( add_responses(mock_jukebox_control_add) response: Jukebox = subsonic.jukebox.get() - response = response.add(song["id"]) + response = response.add([song["id"]]) assert isinstance(response, Jukebox) assert isinstance(response.playlist, list) @@ -217,7 +217,7 @@ def test_jukebox_add_without_a_populated_playlist( add_responses(mock_jukebox_control_add) response: Jukebox = subsonic.jukebox.status() - response = response.add(song["id"]) + response = response.add([song["id"]]) assert isinstance(response, Jukebox) # Ignore the error as in normal conditions it should exist