New SongView, UV & More (#6)
Honestly, I don't remember everything I've done in this branch. Important things: - Started using UV for dependency / python version management - Fixed the stupid queue skipping bug (still can't believe it was a race condition) - New SongView widget that multiple screens share - New features on most views with songs! - Songs can be shared - Songs can be added to playlists - Songs can be rated - Upgraded my Navidrome server version & had to change some stuff for it Co-authored-by: Jackson Hodge <jackson.d.hodge@jbhunt.com> Reviewed-on: #6
This commit is contained in:
1 parent
d3de6ee914
commit
2295af7b56
10 files changed
+763
-201
No files matched your search
+121
-25
@@ -38,24 +38,24 @@ class Album:
|
||||
songCount: int
|
||||
playCount: int
|
||||
year: int | None
|
||||
duration: str
|
||||
duration_f: str
|
||||
|
||||
def __init__(self, album_info: dict):
|
||||
self.id = album_info['id']
|
||||
self.name = album_info['name']
|
||||
self.title = album_info['title'] if 'title' in album_info else self.name
|
||||
self.album = album_info['album'] if 'album' in album_info else self.name
|
||||
self.artist = album_info['artist']
|
||||
self.artist = album_info.get('artist')
|
||||
if not self.artist:
|
||||
self.artist = album_info.get('albumArtist')
|
||||
self.coverArtId = album_info['coverArt'] if 'coverArt' in album_info else None
|
||||
self.duration = int(album_info['duration'])
|
||||
self.artistId = album_info['artistId']
|
||||
self.artistId = album_info.get('albumArtistId')
|
||||
self.songCount = int(album_info['songCount'])
|
||||
self.playCount = int(album_info['playCount']) if 'playCount' in album_info else 0
|
||||
self.year = int(album_info['year']) if 'year' in album_info else None
|
||||
self.duration = self._secondToMinutes(int(album_info['duration']))
|
||||
self.duration_f = self.duration = time.strftime('%H:%M:%S', time.gmtime(int(album_info['duration'])))
|
||||
|
||||
def _secondToMinutes(self, s: int) -> str:
|
||||
return f'{s // 60}:{s % 60 if s % 60 > 9 else "0" + str(s % 60)}'
|
||||
|
||||
class Song:
|
||||
id: str
|
||||
@@ -76,16 +76,20 @@ class Song:
|
||||
samplingRate: int
|
||||
playCount: int | None
|
||||
coverArtId: str | None
|
||||
# Navidrome Fields
|
||||
rating: int | None
|
||||
|
||||
def __init__(self, info: dict | None = {}):
|
||||
def __init__(self, info: dict | None = None):
|
||||
if info is None:
|
||||
info = {}
|
||||
self.id = info['id'] if 'id' in info else ""
|
||||
self.title = info['title'] if 'title' in info else ""
|
||||
self.album = info['album'] if 'album' in info else ""
|
||||
self.artist = info['artist'] if 'artist' in info else ""
|
||||
self.trackNumber = int(info['track']) if 'track' in info else ""
|
||||
self.trackNumber = int(info['track']) if 'track' in info else 0
|
||||
self.contentType = info['contentType'] if 'contentType' in info else ""
|
||||
self.suffix = info['suffix'] if 'suffix' in info else ""
|
||||
self.duration = int(info['duration']) if 'duration' in info else ""
|
||||
self.duration = int(info['duration']) if 'duration' in info else 0
|
||||
self.bitRate = int(info['bitRate']) if 'bitRate' in info else ""
|
||||
self.path = info['path'] if 'path' in info else ""
|
||||
self.discNumber = int(info['discNumber']) if 'discNumber' in info else ""
|
||||
@@ -96,6 +100,7 @@ class Song:
|
||||
self.samplingRate = int(info['samplingRate']) if 'samplingRate' in info else ""
|
||||
self.playCount = int(info['playCount']) if 'playCount' in info else ""
|
||||
self.coverArtId = info['coverArt'] if 'coverArt' in info else None
|
||||
self.rating = info.get('rating')
|
||||
|
||||
class Artist:
|
||||
id: str
|
||||
@@ -130,34 +135,30 @@ class SubpyConn:
|
||||
salt: str
|
||||
salt_len: int = 8
|
||||
version: str = "1.16.1"
|
||||
client_name: str = "testing"
|
||||
client_name: str = "SubSonic Python API Lib"
|
||||
|
||||
def __init__(self, u: str, p: str, base_url: str, salt_len: int | None = 8):
|
||||
def __init__(self, u: str, p: str, base_url: str, salt_len: int | None = 8, client_name: str | None = "SubSonic Python API Lib"):
|
||||
self.username = u
|
||||
self.password = p
|
||||
self.base_url = base_url
|
||||
self.client_name = client_name
|
||||
|
||||
self.salt_len = salt_len
|
||||
|
||||
def _saltGenerator(self) -> None:
|
||||
self.salt = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(self.salt_len))
|
||||
|
||||
def _hashPass(self) -> str:
|
||||
self._saltGenerator()
|
||||
return hashlib.md5((self.password + self.salt).encode('utf-8')).hexdigest()
|
||||
|
||||
def _getBasePayload(self) -> dict:
|
||||
salt = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(self.salt_len))
|
||||
token = hashlib.md5((self.password + salt).encode('utf-8')).hexdigest()
|
||||
return {
|
||||
"u" : self.username,
|
||||
"t" : self._hashPass(),
|
||||
"s" : self.salt,
|
||||
"v" : self.version,
|
||||
"c" : self.client_name,
|
||||
"u": self.username,
|
||||
"t": token,
|
||||
"s": salt,
|
||||
"v": self.version,
|
||||
"c": self.client_name,
|
||||
"f": "json"
|
||||
}
|
||||
|
||||
def _encodePayload(self, payload: dict) -> str:
|
||||
return urllib.parse.urlencode(payload, quote_via=urllib.parse.quote_plus)
|
||||
return urllib.parse.urlencode(payload, quote_via=urllib.parse.quote_plus, doseq=True)
|
||||
|
||||
def _getMusicDirectory(self, id: str):
|
||||
url = self.base_url + '/rest/getMusicDirectory?'
|
||||
@@ -265,6 +266,9 @@ class SubpyConn:
|
||||
|
||||
json_r = r.json()
|
||||
|
||||
if 'entry' not in json_r['subsonic-response']['playlist']:
|
||||
return []
|
||||
|
||||
songs: list[Song] = []
|
||||
for song in json_r['subsonic-response']['playlist']['entry']:
|
||||
songs.append(Song(song))
|
||||
@@ -280,7 +284,7 @@ class SubpyConn:
|
||||
json_r = r.json()
|
||||
|
||||
starred: list[Album | Song | Artist] = []
|
||||
for album in json_r['subsonic-response']['starred']['artist']:
|
||||
for album in json_r['subsonic-response']['starred']['album']:
|
||||
starred.append(Album(album))
|
||||
|
||||
for artist in json_r['subsonic-response']['starred']['artist']:
|
||||
@@ -384,6 +388,21 @@ class SubpyConn:
|
||||
if response['status'] != 'ok':
|
||||
raise BadSubsonicResponse()
|
||||
|
||||
"""
|
||||
https://opensubsonic.netlify.app/docs/endpoints/setrating/
|
||||
|
||||
id A string which uniquely identifies the file (song) or folder (album/artist) to rate.
|
||||
rating The rating between 1 and 5 (inclusive), or 0 to remove the rating: only accepts ints
|
||||
"""
|
||||
def setRating(self, id: str, rating: int) -> None:
|
||||
url = self.base_url + '/rest/setRating?'
|
||||
|
||||
payload = self._getBasePayload()
|
||||
payload['id'] = id
|
||||
payload['rating'] = rating
|
||||
|
||||
requests.get(url + self._encodePayload(payload))
|
||||
|
||||
"""
|
||||
https://opensubsonic.netlify.app/docs/endpoints/scrobble/
|
||||
|
||||
@@ -450,4 +469,81 @@ class SubpyConn:
|
||||
random_songs.append(Song(song))
|
||||
|
||||
return random_songs
|
||||
|
||||
"""
|
||||
https://www.subsonic.org/pages/api.jsp#getLyrics
|
||||
"""
|
||||
def getLyrics(self, artist: str, title: str):
|
||||
url = self.base_url + '/rest/getLyrics?'
|
||||
|
||||
payload = self._getBasePayload()
|
||||
|
||||
payload['artist'] = artist
|
||||
payload['title'] = title
|
||||
|
||||
response = requests.get(url + self._encodePayload(payload)).json()
|
||||
|
||||
return response
|
||||
|
||||
"""
|
||||
https://www.subsonic.org/pages/api.jsp#createPlaylist
|
||||
songs (Optional): can either be a list of songId's as a string OR a list of SubPy Song Objects
|
||||
"""
|
||||
def createPlaylist(self, name: str, songs: list[str] | list[Song] | None = None) -> Playlist:
|
||||
url = self.base_url + '/rest/createPlaylist?'
|
||||
|
||||
payload = self._getBasePayload()
|
||||
|
||||
payload['name'] = name
|
||||
|
||||
response = requests.get(url + self._encodePayload(payload)).json()
|
||||
|
||||
return Playlist(response['subsonic-response']['playlist'])
|
||||
|
||||
"""
|
||||
https://www.subsonic.org/pages/api.jsp#updatePlaylist
|
||||
|
||||
returns True on success
|
||||
"""
|
||||
def updatePlaylist(self, playlistId: str, name: str | None = None, comment: str | None = None, public: bool | None = None, songIdToAdd: list[str] | None = None) -> bool:
|
||||
url = self.base_url + '/rest/updatePlaylist?'
|
||||
|
||||
payload = self._getBasePayload()
|
||||
|
||||
payload['playlistId'] = playlistId
|
||||
|
||||
if songIdToAdd:
|
||||
payload['songIdToAdd'] = songIdToAdd
|
||||
|
||||
response = requests.get(url + self._encodePayload(payload)).json()
|
||||
|
||||
return bool(response['subsonic-response'])
|
||||
|
||||
"""
|
||||
https://www.subsonic.org/pages/api.jsp#createShare
|
||||
|
||||
Params:
|
||||
m_id: ID of a song, album or video to share. Use one id parameter for each entry to share.
|
||||
description (OPTIONAL): A user-defined description that will be displayed to people visiting the shared media.
|
||||
expires (OPTIONAL): The time at which the share expires. Given as milliseconds since 1970. Default is a year.
|
||||
|
||||
Returns:
|
||||
List of share links?
|
||||
|
||||
TODO @0x01FE : Finish this method
|
||||
"""
|
||||
def createShare(self, m_id: str, description: str | None = None, expires: int | None = None) -> list[str]:
|
||||
url = self.base_url + '/rest/createShare?'
|
||||
|
||||
payload = self._getBasePayload()
|
||||
|
||||
payload['id'] = m_id
|
||||
|
||||
if expires:
|
||||
payload['expires'] = expires
|
||||
|
||||
response = requests.get(url + self._encodePayload(payload)).json()
|
||||
|
||||
return response['subsonic-response']['shares']['share'][0]['url']
|
||||
|
||||
|
||||
Reference in new issue
Block a user