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
119 lines
3.5 KiB
Python
119 lines
3.5 KiB
Python
import requests
|
|
import logging
|
|
import urllib.parse
|
|
|
|
import SubSonic
|
|
|
|
|
|
"""
|
|
Navidrome has some API endpoints that are not included in the SubSonic or OpenSubSonic standards.
|
|
Nor are they documented anywhere I can find, the NavidromeAPI class is a collection of methods to interact with this hidden API.
|
|
The endpoints are reverse engineered from observing requests made by the Navidrome server webUI.
|
|
"""
|
|
class NavidromeAPI:
|
|
username: str
|
|
password: str
|
|
base_url: str
|
|
|
|
auth_token: str
|
|
auth_header: dict = {}
|
|
|
|
def __init__(self, u: str, p: str, base_url: str):
|
|
self.username = u
|
|
self.password = p
|
|
self.base_url = base_url
|
|
|
|
self.auth_token = self._login()
|
|
self.auth_header['X-Nd-Authorization'] = f"Bearer {self.auth_token}"
|
|
|
|
def _encodePayload(self, payload: dict) -> str:
|
|
return urllib.parse.urlencode(payload, quote_via=urllib.parse.quote_plus)
|
|
|
|
def _login(self) -> str:
|
|
url = self.base_url + '/auth/login'
|
|
|
|
payload = {
|
|
'username': self.username,
|
|
'password': self.password
|
|
}
|
|
|
|
response = requests.post(url, json=payload)
|
|
|
|
j = response.json()
|
|
|
|
return j['token']
|
|
|
|
def getAllSongs(self, end: int, start: int, order: str = 'ASC', sort: str = 'title') -> list[SubSonic.Song]:
|
|
url = self.base_url + '/api/song?'
|
|
|
|
payload = {
|
|
"_end": end,
|
|
"_order": order,
|
|
"_sort": sort,
|
|
"_start": start
|
|
}
|
|
|
|
response = requests.get(url + self._encodePayload(payload), headers=self.auth_header)
|
|
|
|
return [SubSonic.Song(song_d) for song_d in response.json()]
|
|
|
|
def getAlbums(self, end: int, start: int, order: str = 'ACS', sort: str = 'name', name: str | None = None, seed: float | None = None) -> list[SubSonic.Album]:
|
|
url = self.base_url + '/api/album?'
|
|
|
|
payload = {
|
|
"_end": end,
|
|
"_order": order,
|
|
"_sort" : sort,
|
|
"_start": start,
|
|
"seed": 0.0
|
|
}
|
|
|
|
if seed:
|
|
payload['seed'] = seed
|
|
|
|
if name:
|
|
payload['name'] = name
|
|
|
|
response = requests.get(url + self._encodePayload(payload), headers=self.auth_header)
|
|
|
|
print(response.content)
|
|
|
|
return [SubSonic.Album(album_d) for album_d in response.json()]
|
|
|
|
def getSong(self, id: str):
|
|
url = self.base_url + '/api/song?'
|
|
|
|
payload = {
|
|
"id" : id
|
|
}
|
|
|
|
response = requests.get(url + self._encodePayload(payload), headers=self.auth_header)
|
|
|
|
return response.json()
|
|
|
|
def isSongStarred(self, id: str) -> bool:
|
|
info = self.getSong(id)
|
|
|
|
logging.info(info)
|
|
|
|
return info[0].get('starred') if info[0].get('starred') else False
|
|
|
|
"""
|
|
A strange one. Used for reordering tracks on a playlist, since it's not a native SubSonic thing.
|
|
playlist_id (str): Id of the playlist you're updating
|
|
song_index (int): Track you want to move
|
|
insert_before_index (int): For example, if you want to move a track to the start of a playlist, this is 1.
|
|
|
|
RETURNS: index of the editting song (song_index)
|
|
"""
|
|
def playlistInsertTrackBefore(self, playlist_id: str, song_index: int, insert_before_index: int) -> int:
|
|
url = self.base_url + f'/api/playlist/{playlist_id}/tracks/{song_index}'
|
|
|
|
payload = {
|
|
"insert_before" : str(insert_before_index)
|
|
}
|
|
|
|
response = requests.put(url, headers=self.auth_header, json=payload)
|
|
|
|
return response['id']
|