diff --git a/.attachments/player.png b/.attachments/player.png index 1af698f..3e6979a 100644 Binary files a/.attachments/player.png and b/.attachments/player.png differ diff --git a/Navidrome.py b/Navidrome.py index ea0a837..5ef2107 100644 --- a/Navidrome.py +++ b/Navidrome.py @@ -80,3 +80,18 @@ class NavidromeAPI: 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) + + return info[0]['starred'] diff --git a/SubSonic.py b/SubSonic.py index b298d5e..146ddc7 100644 --- a/SubSonic.py +++ b/SubSonic.py @@ -306,7 +306,7 @@ class SubpyConn: return starred # Returns a requests Response object, the data stream can be found in Response.content - def stream(self, song_id: str, maxBitRate: int | None = None) -> requests.Response: + def stream(self, song_id: str, maxBitRate: int | None = None, format: str | None = None) -> requests.Response: url = self.base_url + '/rest/stream?' payload = self._getBasePayload() @@ -314,6 +314,9 @@ class SubpyConn: if maxBitRate: payload['maxBitRate'] = maxBitRate + if format: + payload['format'] = format + payload['id'] = song_id return requests.get(url + self._encodePayload(payload), stream=True) diff --git a/app.py b/app.py index 1441aee..e829708 100644 --- a/app.py +++ b/app.py @@ -56,6 +56,12 @@ conn = SubSonic.SubpyConn(user, passw, MUSIC_SERVER_URL) pygame.mixer.init() +SONG_LIKED_CHAR = '♥' +SONG_NOT_LIKED_CHAR = '♡' +LOOP_CHAR = '⮔' +INFINITY_CHAR = '∞' +THREE_BARS_CHAR = '≡' + class AlbumSongs(Screen): BINDINGS = [ ('escape', 'die()', "Back"), @@ -1080,6 +1086,9 @@ class Player(Screen): def on_mount(self) -> None: app.sub_title = "Player" + pause_play: Button = self.query_one("#play_pause_track", Button) + pause_play.focus() + def update_song(self, song: SubSonic.Song) -> None: if song.album and song.title and song.artist: @@ -1098,6 +1107,11 @@ class Player(Screen): if cover_id: cover_art.get_cover_art(cover_id) + if song.id: + is_song_starred: bool = app.navidrome.isSongStarred(song.id) + like_song_button: Button = self.query_one('#like_track', Button) + like_song_button.label = SONG_LIKED_CHAR if is_song_starred else SONG_NOT_LIKED_CHAR + def compose(self) -> ComposeResult: yield Header(icon="") with Horizontal(id="headerbs"): @@ -1121,9 +1135,11 @@ class Player(Screen): yield TotalDuration(classes='playerProgressBarTotalTime') with Horizontal(id='playerButtons'): + yield Button(SONG_NOT_LIKED_CHAR, id='like_track', classes='playerButton') yield Button('|<', id='prev_track', classes='playerButton') yield Button('|>/||', id='play_pause_track', classes='playerButton') yield Button('>|', id='next_track', classes='playerButton') + yield Button(THREE_BARS_CHAR, id='loop_queue', classes='playerButton') yield Footer() @@ -1132,6 +1148,9 @@ class Player(Screen): app.sub_title = "Player" app.update_now_playing() + pause_play: Button = self.query_one("#play_pause_track", Button) + pause_play.focus() + @on(Button.Pressed, "#prev_track") def prev_track_pressed(self) -> None: app.action_rewind() @@ -1148,6 +1167,40 @@ class Player(Screen): # app.install_screen(QueueScreen, 'queue') app.push_screen(QueueScreen()) + @on(Button.Pressed, '#like_track') + def like_track(self) -> None: + song = app.currently_playing + if song: + + is_song_starred: bool = app.navidrome.isSongStarred(song.id) + like_song_button: Button = self.query_one('#like_track', Button) + + if not is_song_starred: + conn.star(song.id) + app.notify(f'Starred song "{song.title}"') + like_song_button.label = SONG_LIKED_CHAR + else: + conn.unstar(song.id) + app.notify(f'Unstarred song "{song.title}"') + like_song_button.label = SONG_NOT_LIKED_CHAR + + @on(Button.Pressed, '#loop_queue') + def loop_queue(self) -> None: + queue_button: Button = self.query_one('#loop_queue', Button) + + if app.queue_mode == 0: + app.queue_mode = 1 + queue_button.label = LOOP_CHAR + app.notify('Queue mode set to "Loop Queue"') + elif app.queue_mode == 1: + app.queue_mode = 2 + queue_button.label = INFINITY_CHAR + app.notify('Queue mode set to "Loop Song"') + elif app.queue_mode == 2: + app.queue_mode = 0 + queue_button.label = THREE_BARS_CHAR + app.notify('Queue mode set to "Queue"') + # Startup keyboard listener for media keys def listen_media_keys(queue: multiprocessing.Queue): @@ -1225,6 +1278,14 @@ class SubPy(App): check_playing_timer: Timer | None = None check_music_commands: Timer + """ + Modes: + 0: standard queue, is done when queue is done + 1: loop, loops queue after queue is done + 2: loop song, loops song + """ + queue_mode: int = 0 + def on_mount(self) -> None: self.title = "SubPy" self.switch_mode('main') @@ -1298,9 +1359,16 @@ class SubPy(App): def check_done_playing(self) -> None: if not pygame.mixer.music.get_busy() and not self.is_paused and self.currently_playing: + if self.queue_mode == 2: + self.play_song(app.currently_playing) + + if self.check_playing_timer: + self.check_playing_timer.stop() + return + self.play_history.append(self.currently_playing) - self.is_played = False + self.is_playing = False next_song: SubSonic.Song | None = None if self.user_queue: @@ -1308,6 +1376,13 @@ class SubPy(App): elif self.song_queue: next_song = self.song_queue.pop(0) + if not next_song and self.queue_mode == 1: + self.song_queue = self.play_history.copy() + if self.user_queue: + next_song = self.user_queue.pop(0) + elif self.song_queue: + next_song = self.song_queue.pop(0) + if next_song: self.play_song(next_song) @@ -1328,6 +1403,13 @@ class SubPy(App): elif self.song_queue: next_song = self.song_queue.pop(0) + if not next_song and self.queue_mode == 1: + self.song_queue = self.play_history.copy() + if self.user_queue: + next_song = self.user_queue.pop(0) + elif self.song_queue: + next_song = self.song_queue.pop(0) + if next_song: self.play_song(next_song)