diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 8b9b534..0000000 --- a/TODO.md +++ /dev/null @@ -1,133 +0,0 @@ -# TODO -- [ ] Check and rewrite all docstrings taking care about raising exceptions. -- [ ] Spin up a `MkDocs` documentation. - - [ ] Add the URL in the GitHub page. -- [ ] Decide if use `Subsonic/OpenSubsonic`, `(Open)Subsonic` or `OpenSubsonic`. - -## Implementation status -The final objetive of Knuckles to be a fully compatible implementation wrapper around the [OpenSubsonic API Spec](https://opensubsonic.netlify.app/), a superset of the [Subsonic API Spec](https://subsonic.org/pages/api.jsp) that tries to improve and extend the API without breaking changes. - -### Requests verbs -- [x] `GET` verb -- [x] `POST` verb - -### Response format -- [x] `JSON` -- [ ] `XML` -- [ ] `JSONP` - -### Authentication -- [ ] Clear text _(Should be tested globally like the verbs and not in test by itself)_ -- [ ] Hex encoded -- [x] Authentication token - -### Error handling -- [x] Error codes - -### Endpoints -#### System -- [x] `ping` -- [x] `getLicense` -- [x] `getOpenSubsonicExtensions` - -#### Browsing -- [x] `getMusicFolders` -- [x] `getIndexes` -- [x] `getMusicDirectory` -- [x] `getGenres` -- [x] `getArtists` -- [x] `getArtist` -- [x] `getAlbum` -- [x] `getSong` -- [x] `getVideos` -- [x] `getVideoInfo` -- [x] `getArtistInfo` -- [x] `getArtistInfo2` -- [x] `getAlbumInfo` -- [x] `getAlbumInfo2` -- [x] `getSimilarSongs` -- [x] `getSimilarSongs2` -- [x] `getTopSongs` - -#### Album/song lists -- [x] `getAlbumList` -- [x] `getAlbumList2` -- [x] `getRandomSongs` -- [x] `getSongsByGenre` -- [x] `getNowPlaying` -- [x] `getStarred` -- [x] `getStarred2` - -#### Searching -- [x] `search` _(Will never be implemented, deprecated)_ -- [x] `search2` -- [x] `search3` - -#### Playlists -- [x] `getPlaylists` -- [x] `getPlaylist` -- [x] `createPlaylist` -- [x] `updatePlaylist` -- [x] `deletePlaylist` - -#### Media retrieval -- [x] `stream` _(Only URL handling)_ -- [x] `download` -- [x] `hls` _(Only URL handling)_ -- [x] `getCaptions` -- [x] `getCoverArt` -- [x] `getLyrics` -- [x] `getAvatar` - -#### Media annotation -- [x] `star` -- [x] `unstar` -- [x] `setRating` -- [x] `scrobble` - -#### Sharing -- [x] `getShares` -- [x] `createShare` -- [x] `updateShare` -- [x] `deleteShare` - -#### Podcast -- [x] `getPodcasts` -- [x] `getNewestPodcasts` -- [x] `refreshPodcasts` -- [x] `createPodcastChannel` -- [x] `deletePodcastChannel` -- [x] `deletePodcastEpisode` -- [x] `downloadPodcastEpisode` - -#### Jukebox -- [x] `jukeboxControl` - -#### Internet radio -- [x] `getInternetRadioStations` -- [x] `createInternetRadioStation` -- [x] `updateInternetRadioStation` -- [x] `deleteInternetRadioStation` - -#### Chat -- [x] `getChatMessages` -- [x] `addChatMessage` - -#### User management -- [x] `getUser` -- [x] `getUsers` -- [x] `createUser` -- [x] `updateUser` -- [x] `deleteUser` -- [x] `changePassword` - -#### Bookmarks -- [x] `getBookmarks` -- [x] `createBookmark` -- [x] `deleteBookmark` -- [x] `getPlayQueue` -- [x] `savePlayQueue` - -#### Media library scanning -- [x] `getScanStatus` -- [x] `startScan` diff --git a/pyproject.toml b/pyproject.toml index 8a22cf0..4b00f99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "knuckles" -requires-python = ">=3.11" +requires-python = ">=3.11.0" dependencies = [ "requests>=2.31.0", "python-dateutil>=2.8.2" diff --git a/python-version.txt b/python-version.txt new file mode 100644 index 0000000..afad818 --- /dev/null +++ b/python-version.txt @@ -0,0 +1 @@ +3.11.0 diff --git a/src/knuckles/_api.py b/src/knuckles/_api.py index 1b430dc..aa43f63 100644 --- a/src/knuckles/_api.py +++ b/src/knuckles/_api.py @@ -18,46 +18,37 @@ class RequestMethod(Enum): class Api: - """Class used to internally access and requests to the Subsonic API. - - Allow easy interactions with the Subsonic API - with the given authentication values. + """Class in charge of managing the access to the REST API + of the OpenSubsonic server. """ def __init__( self, url: str, - user: str, + username: str, password: str, client: str, - use_https: bool, - use_token: bool, - request_method: RequestMethod, + use_https: bool = True, + use_token: bool = True, + request_method: RequestMethod = RequestMethod.GET, ) -> None: - """Class used to internally access and interacts with the Subsonic API. - - Allow easy interactions with the Subsonic API - with the given authentication values. - - :param url: The url of the Subsonic server. - :type url: str - :param user: The user to authenticate with - :type user: str - :param password: The password to authenticate with - :type password: str - :param client: A unique string identifying the client application. - :type client: str - :param use_https: If the requests should be sent using HTTPS, - defaults to True - :type use_https: bool, optional - :param use_token: If the connection should send to the server the clean password - or encode it in a token with a random salt, defaults to True - :type use_token: bool, optional + """Class in charge of managing the access to the REST API of + the OpenSubsonic server. + + Args: + url: The base URL that points to the server, + **without** the `/rest/` path. + username: The name of the user to login as in the API. + password: The password of the user to login as in the API. + client: The name of the client to report to the API. + use_https: If HTTPS should be used. + use_token: If the modern token based authentication should be used. + request_method: If the requests should send the data as + GET parameters or POST form data. """ - pass - self.user = user + self.username = username self.password = password self.client = client self.use_token = use_token @@ -77,22 +68,16 @@ def __init__( def _generate_params( self, extra_params: dict[str, Any] | None = None ) -> dict[str, Any]: - """Generate the parameters for any request to the API. - - This allows the user to change any variable in any time without issues. - If it's enabled (True by default) it will generate a different token and salt - for each call. - - :param extra_params: Extra parameters to be attached to the generated ones, - defaults to {}. - :type extra_params: dict[str, Any], optional - :return: All the parameters needed by the Subsonic API to authenticate - with the extra ones attached to them. - :rtype: dict[str, Any] - """ + """Generates the parameters needed for a request to the API. + Args: + extra_params: Extra parameters to be added to the request. + + Returns: + All the parameters with a randomly generated salt. + """ params: dict[str, Any] = { - "u": self.user, + "u": self.username, "v": "1.16.1", "c": self.client, "f": "json", @@ -115,21 +100,26 @@ def _generate_params( return {**params, "t": token, "s": salt} - def generate_url(self, endpoint: str, params: dict[str, Any]) -> str: - """Using the PreparedRequest object of the Requests request package generates a - valid URL for any endpoint with a valid authentication parameter. + def generate_url(self, endpoint: str, extra_params: dict[str, Any]) -> str: + """Using the PreparedRequest object of the Requests request package + generates a valid URL for any endpoint with + a valid authentication parameter. - :param endpoint: The endpoint of the API to be used. - :type endpoint: str - :param params: Extra parameters to be added to the URL. - :type params: dict[str, Any] - :return: The generated url. - :rtype: str + + Args: + endpoint: The endpoint to be appended in the URL, **without** the + leading `/rest/`. + extra_params: The extra parameters to be added to the URL. + + Returns: + A valid URL pointing to the desired endpoint and with the + requested parameters, including the ones needed + for authentication. """ prepared_request = PreparedRequest() prepared_request.prepare_url( - f"{self.url}/rest/{endpoint}", {**self._generate_params(params)} + f"{self.url}/rest/{endpoint}", {**self._generate_params(extra_params)} ) # Ignore the type error caused by the url parameter of prepared_request @@ -139,20 +129,18 @@ def generate_url(self, endpoint: str, params: dict[str, Any]) -> str: def raw_request( self, endpoint: str, extra_params: dict[str, Any] | None = None ) -> Response: - """Make a request to the Subsonic API. - - :param endpoint: The endpoint where the request should be made, - only specifies the route name, without slashes. - E.g. "ping", "getLicense", etc. - :type endpoint: str - :param extra_params: The extra parameters required by the endpoint, - defaults to {}. - :raises code_error: Raises an exception with the format CodeErrorXX or - UnknownCodeError if the request fails. - :return: The Response object returned by the request. - :rtype: dict[str, Any] - """ + """Makes a request to the OpenSubsonic server REST API. + Args: + endpoint: The endpoint to be appended in the URL, **without** the + leading `/rest/`. + extra_params: Extra parameters to the added to the request. + + Returns: + The + [`requests`](https://docs.python-requests.org/en/latest/index.html) + `response` object of the executed request. + """ match self.request_method: case RequestMethod.POST: return requests.post( @@ -169,20 +157,24 @@ def raw_request( def json_request( self, endpoint: str, extra_params: dict[str, Any] | None = None ) -> dict[str, Any]: - """Make a request to the Subsonic API and returns a JSON response. - Don't use with binary data endpoints. - - :param endpoint: The endpoint where the request should be made, - only specifies the route name, without slashes. - E.g. "ping", "getLicense", etc. - :type endpoint: str - :param extra_params: The extra parameters required by the endpoint, - defaults to {}. - :type extra_params: dict[str, Any], optional - :raises code_error: Raises an exception with the format CodeErrorXX or - UnknownCodeError if the request fails. - :return: The JSON data inside the "subsonic-response" object. - :rtype: dict[str, Any] + """Makes a request to the OpenSubsonic server REST API and returns the + data from the `subsonic_response` property. Should **never** be used + with non-json compatible endpoints. + + Args: + endpoint: The endpoint to be appended in the URL, **without** the + leading `/rest/`. + extra_params: Extra parameters to the added to the request. + + Raises: + code_error: Raise an error if the server reports and issue with the + request in the form of a code error, the raised follows + the form `CodeErrorXX` where `XX` is the raised code error. + `UnknownCodeError` is raised if the error code + is not part of the standard. + + Returns: + The data contained in the `subsonic_response` property. """ response = self.raw_request(endpoint, extra_params)