Files
subpy/Navidrome.py
0x01FEandJackson Hodge 2295af7b56 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
2026-03-04 11:30:52 -06:00

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']