updated songs screen
This commit is contained in:
1 parent
1e4616d498
commit
48c9aca87e
3 files changed
+177
-16
No files matched your search
@@ -0,0 +1,60 @@
|
|||||||
|
import requests
|
||||||
|
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 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)
|
||||||
|
print(len(response.content))
|
||||||
|
|
||||||
|
return [SubSonic.Song(song_d) for song_d in response.json()]
|
||||||
|
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@ BITRATE = int(BITRATE) if BITRATE else BITRATE
|
|||||||
IMAGE_MODE = config.getboolean('GENERAL', 'GUI')
|
IMAGE_MODE = config.getboolean('GENERAL', 'GUI')
|
||||||
|
|
||||||
import SubSonic
|
import SubSonic
|
||||||
|
import Navidrome
|
||||||
|
|
||||||
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
|
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
|
||||||
import pygame
|
import pygame
|
||||||
@@ -34,7 +35,7 @@ from textual import on, events, work
|
|||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
from textual.reactive import reactive
|
from textual.reactive import reactive
|
||||||
from textual.app import App, ComposeResult, RenderResult
|
from textual.app import App, ComposeResult, RenderResult
|
||||||
from textual.widgets import Footer, Header, OptionList, DataTable, ProgressBar, Label, Input, Placeholder, Static, Rule, Button
|
from textual.widgets import Footer, Header, OptionList, DataTable, ProgressBar, Label, Input, Placeholder, Static, Rule, Button, ListView, ListItem
|
||||||
from textual.widget import Widget
|
from textual.widget import Widget
|
||||||
from textual.containers import Horizontal, Vertical, Container, Center
|
from textual.containers import Horizontal, Vertical, Container, Center
|
||||||
from textual.screen import Screen, ModalScreen
|
from textual.screen import Screen, ModalScreen
|
||||||
@@ -46,9 +47,12 @@ from PIL import Image, ImageFile
|
|||||||
|
|
||||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
|
|
||||||
u, p = [s.strip() for s in open('auth', 'r').read().split(',')]
|
user, passw = [s.strip() for s in open('auth', 'r').read().split(',')]
|
||||||
|
|
||||||
|
MUSIC_SERVER_URL = 'https://music.0x01fe.net'
|
||||||
|
|
||||||
|
conn = SubSonic.SubpyConn(user, passw, MUSIC_SERVER_URL)
|
||||||
|
|
||||||
conn = SubSonic.SubpyConn(u, p, "https://music.0x01fe.net")
|
|
||||||
|
|
||||||
pygame.mixer.init()
|
pygame.mixer.init()
|
||||||
|
|
||||||
@@ -197,7 +201,7 @@ class ArtistAlbumsGUI(Screen):
|
|||||||
yield Label(Text("F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb")
|
yield Label(Text("F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb")
|
||||||
yield Label(Text("* F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb")
|
yield Label(Text("* F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb")
|
||||||
yield Label(Text("F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb")
|
yield Label(Text("F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb")
|
||||||
yield Label(Text("F4 ", style="bold orange").append_text(Text("Favourites", style="white")), classes="headerb")
|
yield Label(Text("F4 ", style="bold orange").append_text(Text("Songs", style="white")), classes="headerb")
|
||||||
yield Label(Text("F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb")
|
yield Label(Text("F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb")
|
||||||
yield Label(classes='artistNameHeader')
|
yield Label(classes='artistNameHeader')
|
||||||
yield Rule()
|
yield Rule()
|
||||||
@@ -297,7 +301,7 @@ class Artists(Screen):
|
|||||||
yield Label(Text("F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb")
|
yield Label(Text("F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb")
|
||||||
yield Label(Text("* F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb")
|
yield Label(Text("* F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb")
|
||||||
yield Label(Text("F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb")
|
yield Label(Text("F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb")
|
||||||
yield Label(Text("F4 ", style="bold orange").append_text(Text("Favourites", style="white")), classes="headerb")
|
yield Label(Text("F4 ", style="bold orange").append_text(Text("Songs", style="white")), classes="headerb")
|
||||||
yield Label(Text("F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb")
|
yield Label(Text("F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb")
|
||||||
with Vertical(id="artistContainer"):
|
with Vertical(id="artistContainer"):
|
||||||
yield Input(placeholder="Search")
|
yield Input(placeholder="Search")
|
||||||
@@ -438,7 +442,7 @@ class ArtistsGUI(Screen):
|
|||||||
yield Label(Text("F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb")
|
yield Label(Text("F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb")
|
||||||
yield Label(Text("* F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb")
|
yield Label(Text("* F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb")
|
||||||
yield Label(Text("F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb")
|
yield Label(Text("F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb")
|
||||||
yield Label(Text("F4 ", style="bold orange").append_text(Text("Favourites", style="white")), classes="headerb")
|
yield Label(Text("F4 ", style="bold orange").append_text(Text("Songs", style="white")), classes="headerb")
|
||||||
yield Label(Text("F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb")
|
yield Label(Text("F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb")
|
||||||
yield Input(placeholder="Search", id="artistSearchBar")
|
yield Input(placeholder="Search", id="artistSearchBar")
|
||||||
with Horizontal():
|
with Horizontal():
|
||||||
@@ -565,7 +569,7 @@ class Albums(Screen):
|
|||||||
yield Label(Text("F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb")
|
yield Label(Text("F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb")
|
||||||
yield Label(Text("F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb")
|
yield Label(Text("F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb")
|
||||||
yield Label(Text("* F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb")
|
yield Label(Text("* F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb")
|
||||||
yield Label(Text("F4 ", style="bold orange").append_text(Text("Favourites", style="white")), classes="headerb")
|
yield Label(Text("F4 ", style="bold orange").append_text(Text("Songs", style="white")), classes="headerb")
|
||||||
yield Label(Text("F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb")
|
yield Label(Text("F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb")
|
||||||
with Vertical(id="albumContainer"):
|
with Vertical(id="albumContainer"):
|
||||||
yield Input(placeholder="Search", id="albumSearchBar")
|
yield Input(placeholder="Search", id="albumSearchBar")
|
||||||
@@ -610,7 +614,7 @@ class Playlists(Screen):
|
|||||||
yield Label(Text("F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb")
|
yield Label(Text("F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb")
|
||||||
yield Label(Text("F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb")
|
yield Label(Text("F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb")
|
||||||
yield Label(Text("F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb")
|
yield Label(Text("F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb")
|
||||||
yield Label(Text("F4 ", style="bold orange").append_text(Text("Favourites", style="white")), classes="headerb")
|
yield Label(Text("F4 ", style="bold orange").append_text(Text("Songs", style="white")), classes="headerb")
|
||||||
yield Label(Text("* F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb")
|
yield Label(Text("* F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb")
|
||||||
yield DataTable()
|
yield DataTable()
|
||||||
yield Footer()
|
yield Footer()
|
||||||
@@ -703,12 +707,24 @@ class PlaylistSongs(Screen):
|
|||||||
|
|
||||||
class SongsScreen(Screen):
|
class SongsScreen(Screen):
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
|
("p", "play_song()", "Play Song"),
|
||||||
|
("q", "queue_song()", "Queue Song"),
|
||||||
|
("m", "shuffle_mix()", 'Shuffle Play'),
|
||||||
|
('a', "page_backward()", "Prev. Page"),
|
||||||
|
('d', "page_forward()", "Next Page")
|
||||||
]
|
]
|
||||||
|
|
||||||
songs: list[SubSonic.Song]
|
songs: list[SubSonic.Song]
|
||||||
selected_song_id: str | None = None
|
selected_song_id: str | None = None
|
||||||
|
|
||||||
|
# 0 = All, 1 = Favourites
|
||||||
|
mode: int = 0
|
||||||
|
|
||||||
|
page: dict[int, int] = {
|
||||||
|
0: 0,
|
||||||
|
1: 0
|
||||||
|
}
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Header(icon='')
|
yield Header(icon='')
|
||||||
|
|
||||||
@@ -716,15 +732,87 @@ class SongsScreen(Screen):
|
|||||||
yield Label(Text("F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb")
|
yield Label(Text("F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb")
|
||||||
yield Label(Text("F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb")
|
yield Label(Text("F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb")
|
||||||
yield Label(Text("F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb")
|
yield Label(Text("F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb")
|
||||||
yield Label(Text("* F4 ", style="bold orange").append_text(Text("Favourites", style="white")), classes="headerb")
|
yield Label(Text("* F4 ", style="bold orange").append_text(Text("Songs", style="white")), classes="headerb")
|
||||||
yield Label(Text("F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb")
|
yield Label(Text("F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb")
|
||||||
|
|
||||||
with Horizontal(id='songsScreen'):
|
with Horizontal(id='songsScreen'):
|
||||||
yield Placeholder(id='songModeSelectorList')
|
yield ListView(
|
||||||
yield Placeholder(id='songDatatable')
|
ListItem(Label('All')),
|
||||||
|
ListItem(Label('Favourites')),
|
||||||
|
id='songModeSelectorList'
|
||||||
|
)
|
||||||
|
yield DataTable(id='songDatatable')
|
||||||
|
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
app.sub_title = "Songs"
|
||||||
|
|
||||||
|
self.update_datatable()
|
||||||
|
# TODO @0x01fe : find better way to do space
|
||||||
|
|
||||||
|
@on(ListView.Selected)
|
||||||
|
def mode_selected(self, e: ListView.Selected) -> None:
|
||||||
|
self.mode = e.index
|
||||||
|
self.update_datatable()
|
||||||
|
|
||||||
|
def _get_offset(self) -> int:
|
||||||
|
return (self.page[self.mode]) * 30
|
||||||
|
|
||||||
|
def action_shuffle_mix(self) -> None:
|
||||||
|
if self.mode == 0:
|
||||||
|
q = conn.getRandomSongs(size=200)
|
||||||
|
app.play_song(q.pop(0))
|
||||||
|
app.song_queue = q
|
||||||
|
elif self.mode == 1:
|
||||||
|
q = self.songs.copy()
|
||||||
|
random.shuffle(q)
|
||||||
|
app.play_song(q.pop(0))
|
||||||
|
app.song_queue = q
|
||||||
|
|
||||||
|
def action_page_forward(self) -> None:
|
||||||
|
self.page[self.mode] += 1
|
||||||
|
self.update_datatable()
|
||||||
|
|
||||||
|
def action_page_backward(self) -> None:
|
||||||
|
if self.page[self.mode] - 1 < 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.page[self.mode] -= 1
|
||||||
|
self.update_datatable()
|
||||||
|
|
||||||
|
def update_datatable(self) -> None:
|
||||||
|
datatable = self.query_one(DataTable)
|
||||||
|
|
||||||
|
if self.mode == 0:
|
||||||
|
self.update_all(datatable)
|
||||||
|
elif self.mode == 1:
|
||||||
|
self.update_favorites(datatable)
|
||||||
|
|
||||||
|
def update_favorites(self, dt: DataTable) -> None:
|
||||||
|
self.songs = conn.getStarredSongs()
|
||||||
|
|
||||||
|
space = 30
|
||||||
|
offset = self._get_offset()
|
||||||
|
|
||||||
|
dt.clear()
|
||||||
|
|
||||||
|
for song in self.songs[offset:offset + space]:
|
||||||
|
dt.add_row(song.title, song.album, song.artist, song.playCount, song.duration)
|
||||||
|
|
||||||
|
def update_all(self, dt: DataTable) -> None:
|
||||||
|
space = 30
|
||||||
|
|
||||||
|
offset = self._get_offset()
|
||||||
|
self.songs = app.navidrome.getAllSongs(offset + space, offset)
|
||||||
|
|
||||||
|
dt.cursor_type = "row"
|
||||||
|
dt.clear()
|
||||||
|
dt.add_columns("Title", "Album", "Artist", "Plays", "Duration")
|
||||||
|
|
||||||
|
for song in self.songs:
|
||||||
|
dt.add_row(song.title, song.album, song.artist, song.playCount, song.duration)
|
||||||
|
|
||||||
class Favourites(Screen):
|
class Favourites(Screen):
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
('p', 'play()', 'Play Song'),
|
('p', 'play()', 'Play Song'),
|
||||||
@@ -741,7 +829,7 @@ class Favourites(Screen):
|
|||||||
yield Label(Text("F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb")
|
yield Label(Text("F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb")
|
||||||
yield Label(Text("F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb")
|
yield Label(Text("F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb")
|
||||||
yield Label(Text("F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb")
|
yield Label(Text("F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb")
|
||||||
yield Label(Text("* F4 ", style="bold orange").append_text(Text("Favourites", style="white")), classes="headerb")
|
yield Label(Text("* F4 ", style="bold orange").append_text(Text("Songs", style="white")), classes="headerb")
|
||||||
yield Label(Text("F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb")
|
yield Label(Text("F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb")
|
||||||
yield DataTable()
|
yield DataTable()
|
||||||
yield Footer()
|
yield Footer()
|
||||||
@@ -970,7 +1058,7 @@ class Player(Screen):
|
|||||||
yield Label(Text("* F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb")
|
yield Label(Text("* F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb")
|
||||||
yield Label(Text("F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb")
|
yield Label(Text("F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb")
|
||||||
yield Label(Text("F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb")
|
yield Label(Text("F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb")
|
||||||
yield Label(Text("F4 ", style="bold orange").append_text(Text("Favourites", style="white")), classes="headerb")
|
yield Label(Text("F4 ", style="bold orange").append_text(Text("Songs", style="white")), classes="headerb")
|
||||||
yield Label(Text("F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb")
|
yield Label(Text("F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb")
|
||||||
|
|
||||||
with Center(id='playerTrackTitleBox'):
|
with Center(id='playerTrackTitleBox'):
|
||||||
@@ -1064,7 +1152,7 @@ class SubPy(App):
|
|||||||
Binding('f1', "switch_mode('main')", 'Overview', show=False),
|
Binding('f1', "switch_mode('main')", 'Overview', show=False),
|
||||||
Binding('f2', "switch_mode('artists')", "Artists", show=False),
|
Binding('f2', "switch_mode('artists')", "Artists", show=False),
|
||||||
Binding('f3', "switch_mode('albums')", "Albums", show=False),
|
Binding('f3', "switch_mode('albums')", "Albums", show=False),
|
||||||
Binding('f4', "switch_mode('favourites')", "Favourites", show=False),
|
Binding('f4', "switch_mode('songs')", "Songs", show=False),
|
||||||
Binding('f5', "switch_mode('playlists')", "Playlists", show=False)
|
Binding('f5', "switch_mode('playlists')", "Playlists", show=False)
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1072,7 +1160,7 @@ class SubPy(App):
|
|||||||
"main" : Player,
|
"main" : Player,
|
||||||
"artists" : ArtistsGUI,
|
"artists" : ArtistsGUI,
|
||||||
"albums" : Albums,
|
"albums" : Albums,
|
||||||
"favourites" : SongsScreen,
|
"songs" : SongsScreen,
|
||||||
"playlists" : Playlists
|
"playlists" : Playlists
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1084,6 +1172,8 @@ class SubPy(App):
|
|||||||
is_paused: bool = False
|
is_paused: bool = False
|
||||||
place: int = 0
|
place: int = 0
|
||||||
|
|
||||||
|
navidrome: Navidrome.NavidromeAPI
|
||||||
|
|
||||||
music_command_queue: multiprocessing.Queue
|
music_command_queue: multiprocessing.Queue
|
||||||
|
|
||||||
check_playing_timer: Timer | None = None
|
check_playing_timer: Timer | None = None
|
||||||
@@ -1095,6 +1185,8 @@ class SubPy(App):
|
|||||||
|
|
||||||
self.check_music_commands = self.set_interval(2/3, self._music_player_loop, pause=False)
|
self.check_music_commands = self.set_interval(2/3, self._music_player_loop, pause=False)
|
||||||
|
|
||||||
|
self.navidrome = Navidrome.NavidromeAPI(user, passw, MUSIC_SERVER_URL)
|
||||||
|
|
||||||
@work(exclusive=True)
|
@work(exclusive=True)
|
||||||
async def play_song(self, song: SubSonic.Song) -> None:
|
async def play_song(self, song: SubSonic.Song) -> None:
|
||||||
self.currently_playing = song
|
self.currently_playing = song
|
||||||
|
|||||||
Reference in new issue
Block a user