From 110c7f3425abae2ccadc5d9db1315940c274916a Mon Sep 17 00:00:00 2001 From: JISAUAY Date: Fri, 31 Jan 2025 13:18:26 -0600 Subject: [PATCH] made playing info a custom widget and updated the subsonic api library --- SubSonic.py | 233 ++++++++++++++++++++++++++---------------------- screens-test.py | 3 + textual.log | 9 +- 3 files changed, 139 insertions(+), 106 deletions(-) diff --git a/SubSonic.py b/SubSonic.py index 51c2084..5c9d6b9 100644 --- a/SubSonic.py +++ b/SubSonic.py @@ -5,9 +5,6 @@ import hashlib import logging import urllib.parse -import xml.etree -import xml.etree.ElementTree - import requests @@ -110,58 +107,17 @@ class Artist: self.coverArt = coverArt self.artistImageUrl = artistImageUrl + def from_dict(info: dict): + return Artist( + info['id'] if 'id' in info else None, + info['name'] if 'name' in info else None, + info['albumCount'] if 'albumCount' in info else None, + info['coverArt'] if 'coverArt' in info else None, + info['artistImageUrl'] if 'artistImageUrl' in info else None + ) -class ArtistIndex: - name: str - artists: list[Artist] - - def __init__(self, indexInfo: dict, artists: list[dict]): - self.name = indexInfo['name'] - - self.artists = [] - for artist in artists: - self.artists.append(Artist( - artist['id'], - artist['name'], - artist['albumCount'], - artist['coverArt'], - artist['artistImageUrl'] - )) - -class ArtistIndexes: - indexes: list[ArtistIndex] - - def __init__(self, xml_data: bytes): - self._parseXml(xml_data.decode('utf-8')) - - def _parseXml(self, xml_data: str): - root = xml.etree.ElementTree.fromstring(xml_data) - - self.indexes: list[ArtistIndex] = [] - - if root.get('status') != 'ok': - return None - - in_index = False - artists = [] - index_info = None - for item in root.iter(): - # print(item.tag, dict(item.items())) - - if item.tag == '{http://subsonic.org/restapi}index': - - if in_index: - in_index = not in_index - - self.indexes.append(ArtistIndex(index_info, artists)) - - artists = [] - else: - in_index = not in_index - index_info = dict(item.items()) - - elif item.tag == '{http://subsonic.org/restapi}artist': - artists.append(dict(item.items())) +class BadSubsonicResponse(Exception): + pass class SubpyConn: username: str @@ -192,17 +148,13 @@ class SubpyConn: "t" : self._hashPass(), "s" : self.salt, "v" : self.version, - "c" : self.client_name + "c" : self.client_name, + "f": "json" } def _encodePayload(self, payload: dict) -> str: return urllib.parse.urlencode(payload, quote_via=urllib.parse.quote_plus) - def getIndexes(self) -> ArtistIndexes: - url = self.base_url + '/rest/getIndexes?' - - return ArtistIndexes(requests.get(url + self._encodePayload(self._getBasePayload())).content) - def _getMusicDirectory(self, id: str): url = self.base_url + '/rest/getMusicDirectory?' @@ -212,48 +164,44 @@ class SubpyConn: return requests.get(url + self._encodePayload(payload)) def getArtistAlbums(self, artist_id: str) -> list[Album]: - r: requests.Response = self._getMusicDirectory(artist_id) - xml_data: str = r.content.decode('utf-8') + response: requests.Response = self._getMusicDirectory(artist_id).json()['subsonic-response'] + + if response['status'] != 'ok': + raise BadSubsonicResponse() - root: xml.etree.ElementTree.Element = xml.etree.ElementTree.fromstring(xml_data) albums: list[Album] = [] - - for item in root.iter(): - if item.tag == '{http://subsonic.org/restapi}child': - albums.append(Album(dict(item.items()))) + for item in response['directory']['child']: + albums.append(Album(item)) return albums def getAlbumSongs(self, album_id: str) -> list[Song]: - r: requests.Response = self._getMusicDirectory(album_id) - logger.debug(f"Requesting all songs for album {album_id} using url {self.base_url}") - logger.debug(f'Got response with status code {r.status_code}') + response: requests.Response = self._getMusicDirectory(album_id).json()['subsonic-response'] - xml_data: str = r.content.decode('utf-8') + if response['status'] != 'ok': + raise BadSubsonicResponse() - if r.status_code != 200: - return [] - - root: xml.etree.ElementTree.Element = xml.etree.ElementTree.fromstring(xml_data) songs: list[Song] = [] - - logger.debug(f"Request Status: {root.get('status')}") - if root.get('status') != 'ok': - return [] - - for item in root.iter(): - - if item.tag == '{http://subsonic.org/restapi}child': - songs.append(Song(dict(item.items()))) + for item in response['directory']['child']: + songs.append(Song(item)) return songs def getArtists(self) -> list[Artist]: - artistIndexes: ArtistIndexes = self.getIndexes() + url = self.base_url + '/rest/getArtist?' + + payload = self._getBasePayload() + + response = requests.get(url + self._encodePayload(payload)).json()['subsonic-response'] + + if response['status'] != 'ok': + raise BadSubsonicResponse() artists: list[Artist] = [] - for artistIndex in artistIndexes.indexes: - artists.extend(artistIndex.artists) + + for artist_index in response['artists']['index']: + for artist in artist_index['artist']: + artists.append(Artist.from_dict(artist)) return artists @@ -264,7 +212,6 @@ class SubpyConn: payload['type'] = 'alphabeticalByName' payload['size'] = size payload['offset'] = offset - payload['f'] = 'json' r = requests.get(url + self._encodePayload(payload)) json_r = r.json() @@ -293,7 +240,6 @@ class SubpyConn: url = self.base_url + "/rest/getPlaylists?" payload = self._getBasePayload() - payload['f'] = 'json' r = requests.get(url + self._encodePayload(payload)) @@ -309,7 +255,6 @@ class SubpyConn: url = self.base_url + "/rest/getPlaylist?" payload = self._getBasePayload() - payload['f'] = 'json' payload['id'] = playlist_id r = requests.get(url + self._encodePayload(payload)) @@ -326,7 +271,6 @@ class SubpyConn: url = self.base_url + "/rest/getStarred?" payload = self._getBasePayload() - payload['f'] = 'json' r = requests.get(url + self._encodePayload(payload)) json_r = r.json() @@ -336,13 +280,7 @@ class SubpyConn: starred.append(Album(album)) for artist in json_r['subsonic-response']['starred']['artist']: - starred.append(Artist( - artist['id'], - artist['name'], - artist['albumCount'], - artist['coverArt'], - artist['artistImageUrl'] - )) + starred.append(Artist.from_dict(artist)) for song in json_r['subsonic-response']['starred']['song']: starred.append(Song(song)) @@ -353,7 +291,6 @@ class SubpyConn: url = self.base_url + "/rest/getStarred?" payload = self._getBasePayload() - payload['f'] = 'json' r = requests.get(url + self._encodePayload(payload)) json_r = r.json() @@ -402,13 +339,99 @@ class SubpyConn: payload = self._getBasePayload() payload['id'] = song_id - r = requests.get(url + self._encodePayload(payload)) + response = requests.get(url + self._encodePayload(payload)).json()['subsonic-response'] - xml_data = r.content.decode('utf-8') + if response['status'] != 'ok': + raise BadSubsonicResponse() + + return Song(response['song']) + + def star(self, id: str) -> None: + url = self.base_url + '/rest/star?' + + payload = self._getBasePayload() + payload['id'] = id + + response = requests.get(url + self._encodePayload(payload)).json()['subsonic-response'] + + if response['status'] != 'ok': + raise BadSubsonicResponse() + + def unstar(self, id: str) -> None: + url = self.base_url + '/rest/unstar?' + + payload = self._getBasePayload() + payload['id'] = id + + response = requests.get(url + self._encodePayload(payload)).json()['subsonic-response'] + + if response['status'] != 'ok': + raise BadSubsonicResponse() + + """ + https://opensubsonic.netlify.app/docs/endpoints/scrobble/ + + id: str - unique identifier of the media to scrobble + OPTIONAL time: int - posix time of when the song was listened to + OPTIONAL submission: bool - Whether this is a "submission" or a "now playing" notification. Default: True + """ + def scrobble(self, id: str, time: int | None = None, submission: bool | None = True) -> None: + url = self.base_url + '/rest/scrobble?' + + payload = self._getBasePayload() + payload['id'] = id + + if time: + payload['time'] = time + + payload['submission'] = submission + + response = requests.get(url + self._encodePayload(payload)).json()['subsonic-response'] + + if response['status'] != 'ok': + raise BadSubsonicResponse() + + """ + https://opensubsonic.netlify.app/docs/endpoints/getrandomsongs/ + """ + def getRandomSongs(self, + size: int | None = 10, + genre: str | None = None, + fromYear: str | int | None = None, + toYear: str | int | None = None, + musicFolderId: str | int | None = None + ) -> list[Song]: + url = self.base_url + '/rest/getRandomSongs?' + + payload = self._getBasePayload() + + # The default being 10 is only to make the method match the internal docs + if size and size != 10: + payload['size'] = size + + if genre: + payload['genre'] = genre + + if fromYear: + payload['fromYear'] = fromYear + + if toYear: + payload['toYear'] = toYear + + if musicFolderId: + payload['musicFolderId'] = musicFolderId + + response = requests.get(url + self._encodePayload(payload)).json()['subsonic-response'] + + if response['status'] != 'ok': + raise BadSubsonicResponse() + + random_songs: list[Song] = [] + + for song in response['randomSongs']['song']: + random_songs.append(Song(song)) + + return random_songs - root = xml.etree.ElementTree.fromstring(xml_data) - for item in root.iter(): - if item.tag == '{http://subsonic.org/restapi}song': - return Song(dict(item.items())) diff --git a/screens-test.py b/screens-test.py index 9ae6a38..ecddd06 100644 --- a/screens-test.py +++ b/screens-test.py @@ -340,6 +340,7 @@ class PlayingInfo(Widget): table.add_column("") table.add_column("") + table.show_header = False if app.currently_playing: song = app.currently_playing @@ -351,6 +352,8 @@ class PlayingInfo(Widget): table.add_row("Track Number", str(song.trackNumber)) table.add_row("Bit Rate", str(song.bitRate)) + return table + class MainView(Screen): CSS_PATH = 'main.tcss' diff --git a/textual.log b/textual.log index 10f68ed..a587a9e 100644 --- a/textual.log +++ b/textual.log @@ -1 +1,8 @@ -[2025-01-29 17:02:16,865] DEBUG in selector_events: Using selector: EpollSelector +[2025-01-31 13:17:33,930] DEBUG in selector_events: Using selector: EpollSelector +[2025-01-31 13:17:40,998] DEBUG in connectionpool: Starting new HTTPS connection (1): music.0x01fe.net:443 +[2025-01-31 13:17:41,453] DEBUG in connectionpool: https://music.0x01fe.net:443 "GET /rest/getPlaylists?u=pseudo&t=26045eee6c481e32f1f1876d416249c8&s=CQ0WT5Y4&v=1.16.1&c=testing&f=json HTTP/1.1" 200 712 +[2025-01-31 13:17:42,466] DEBUG in connectionpool: Starting new HTTPS connection (1): music.0x01fe.net:443 +[2025-01-31 13:17:42,708] DEBUG in connectionpool: https://music.0x01fe.net:443 "GET /rest/getPlaylist?u=pseudo&t=830bf3dc3c1d2cab4c2ac57488e70f11&s=RK19ZT46&v=1.16.1&c=testing&f=json&id=e6e8ff27-2a60-4e33-8aa1-09586191166a HTTP/1.1" 200 None +[2025-01-31 13:17:50,183] DEBUG in screens-test: Playing song Gilgamesh by King Gizzard & The Lizard Wizard. +[2025-01-31 13:17:50,186] DEBUG in connectionpool: Starting new HTTPS connection (1): music.0x01fe.net:443 +[2025-01-31 13:17:50,602] DEBUG in connectionpool: https://music.0x01fe.net:443 "GET /rest/stream?u=pseudo&t=eb21ada2bb8e6f8bdc7cc92469db2605&s=D0INLK0D&v=1.16.1&c=testing&f=json&id=8033582249221c9c67cc57da9e0aba14 HTTP/1.1" 200 None