From d66da573b459e19c5e7d9960fd3305500150f24b Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Sat, 12 Oct 2019 23:32:01 -0600 Subject: [PATCH] Implement the Playlists interface --- libremsonic/app.py | 52 ++++++++++++++++++++++++++++++++++++ libremsonic/cache_manager.py | 6 +++++ libremsonic/dbus_manager.py | 24 +++++++++++++++-- libremsonic/state_manager.py | 3 +++ libremsonic/ui/playlists.py | 19 ++++++++++--- 5 files changed, 98 insertions(+), 6 deletions(-) diff --git a/libremsonic/app.py b/libremsonic/app.py index 69d81cc..2e4aad5 100644 --- a/libremsonic/app.py +++ b/libremsonic/app.py @@ -266,6 +266,7 @@ class LibremsonicApp(Gtk.Application): ): second_microsecond_conversion = 1000000 track_id_re = re.compile(r'/song/(.*)') + playlist_id_re = re.compile(r'/playlist/(.*)') def seek_fn(offset): offset_seconds = offset / second_microsecond_conversion @@ -297,6 +298,50 @@ class LibremsonicApp(Gtk.Application): return GLib.Variant('(aa{sv})', (metadatas, )) + def activate_playlist(playlist_id): + playlist_id = playlist_id_re.match(playlist_id).group(1) + playlist = CacheManager.get_playlist(playlist_id).result() + + # Calculate the song id to play. + song_id = playlist.entry[0].id + if self.state.shuffle_on: + rand_idx = random.randint(0, len(playlist.entry) - 1) + song_id = playlist.entry[rand_idx].id + + self.on_song_clicked( + None, + song_id, + [s.id for s in playlist.entry], + {'active_playlist_id': playlist_id}, + ) + + def get_playlists(index, max_count, order, reverse_order): + playlists = CacheManager.get_playlists().result() + sorters = { + 'Alphabetical': lambda p: p.name, + 'Created': lambda p: p.created, + 'Modified': lambda p: p.changed, + } + playlists.sort( + key=sorters.get(order, lambda p: p), + reverse=reverse_order, + ) + + return GLib.Variant( + '(a(oss))', ( + [ + ( + '/playlist/' + p.id, + p.name, + CacheManager.get_cover_art_filename( + p.coverArt, + allow_download=False, + ).result() or '', + ) + for p in playlists[index:(index + max_count)] + if p.songCount > 0 + ], )) + method_call_map = { 'org.mpris.MediaPlayer2': { 'Raise': self.window.present, @@ -316,6 +361,10 @@ class LibremsonicApp(Gtk.Application): 'GoTo': set_pos_fn, 'GetTracksMetadata': get_track_metadata, }, + 'org.mpris.MediaPlayer2.Playlists': { + 'ActivatePlaylist': activate_playlist, + 'GetPlaylists': get_playlists, + }, } method = method_call_map.get(interface, {}).get(method) if method is None: @@ -533,6 +582,9 @@ class LibremsonicApp(Gtk.Application): if metadata.get('force_shuffle_state') is not None: self.state.shuffle_on = metadata['force_shuffle_state'] + if metadata.get('active_playlist_id') is not None: + self.state.active_playlist_id = metadata.get('active_playlist_id') + # If shuffle is enabled, then shuffle the playlist. if self.state.shuffle_on: song_queue.remove(song_id) diff --git a/libremsonic/cache_manager.py b/libremsonic/cache_manager.py index 3d352ca..2c05d33 100644 --- a/libremsonic/cache_manager.py +++ b/libremsonic/cache_manager.py @@ -210,10 +210,14 @@ class CacheManager(metaclass=Singleton): download_fn: Callable[[], bytes], before_download: Callable[[], None] = lambda: None, force: bool = False, + allow_download: bool = True, ): abs_path = self.calculate_abs_path(relative_path) download_path = self.calculate_download_path(relative_path) if not abs_path.exists() or force: + if not allow_download: + return None + before_download() resource_downloading = False with self.download_set_lock: @@ -556,6 +560,7 @@ class CacheManager(metaclass=Singleton): before_download: Callable[[], None] = lambda: None, size: Union[str, int] = 200, force: bool = False, + allow_download: bool = True, ) -> Future: def do_get_cover_art_filename() -> str: tag = 'tag_' if self.browse_by_tags else '' @@ -564,6 +569,7 @@ class CacheManager(metaclass=Singleton): lambda: self.server.get_cover_art(id, str(size)), before_download=before_download, force=force, + allow_download=allow_download, ) return CacheManager.executor.submit(do_get_cover_art_filename) diff --git a/libremsonic/dbus_manager.py b/libremsonic/dbus_manager.py index e95abb5..dfadb88 100644 --- a/libremsonic/dbus_manager.py +++ b/libremsonic/dbus_manager.py @@ -157,6 +157,26 @@ class DBusManager: current = state.play_queue.index(state.current_song.id) has_next_song = current < len(state.play_queue) - 1 + if state.active_playlist_id is None: + active_playlist = (False, GLib.Variant('(oss)', ('/', '', ''))) + else: + playlist = CacheManager.get_playlist( + state.active_playlist_id).result() + active_playlist = ( + True, + GLib.Variant( + '(oss)', + ( + '/playlist/' + playlist.id, + playlist.name, + CacheManager.get_cover_art_filename( + playlist.coverArt, + allow_download=False, + ).result() or '', + ), + ), + ) + return { 'org.mpris.MediaPlayer2': { 'CanQuit': True, @@ -217,8 +237,8 @@ class DBusManager: # TODO this may do a network request. This really is a case for # doing the whole thing with caching some data beforehand. 'PlaylistCount': len(CacheManager.get_playlists().result()), - 'Orderings': ['Alphabetical'], - 'ActivePlaylist': None, + 'Orderings': ['Alphabetical', 'Created', 'Modified'], + 'ActivePlaylist': ('(b(oss))', active_playlist), }, } diff --git a/libremsonic/state_manager.py b/libremsonic/state_manager.py index 2313639..bc6947d 100644 --- a/libremsonic/state_manager.py +++ b/libremsonic/state_manager.py @@ -66,6 +66,7 @@ class ApplicationState: selected_artist_id: str = None selected_playlist_id: str = None current_album_sort: str = 'random' + active_playlist_id: str = None def to_json(self): current_song = ( @@ -89,6 +90,7 @@ class ApplicationState: 'selected_playlist_id': getattr(self, 'selected_playlist_id', None), 'current_album_sort': getattr(self, 'current_album_sort', None), + 'active_playlist_id': getattr(self, 'active_playlist_id', None), } def load_from_json(self, json_object): @@ -115,6 +117,7 @@ class ApplicationState: 'selected_playlist_id', None) self.current_album_sort = json_object.get( 'current_album_sort', 'random') + self.active_playlist_id = json_object.get('active_playlist_id', None) def load(self): self.config = self.get_config(self.config_file) diff --git a/libremsonic/ui/playlists.py b/libremsonic/ui/playlists.py index 1cdbdab..41107e6 100644 --- a/libremsonic/ui/playlists.py +++ b/libremsonic/ui/playlists.py @@ -568,8 +568,14 @@ class PlaylistDetailPanel(Gtk.Overlay): def on_play_all_clicked(self, btn): song_id = self.playlist_song_store[0][-1] self.emit( - 'song-clicked', song_id, [m[-1] for m in self.playlist_song_store], - {'force_shuffle_state': False}) + 'song-clicked', + song_id, + [m[-1] for m in self.playlist_song_store], + { + 'force_shuffle_state': False, + 'active_playlist_id': self.playlist_id, + }, + ) def on_shuffle_all_button(self, btn): rand_idx = randint(0, len(self.playlist_song_store) - 1) @@ -577,7 +583,10 @@ class PlaylistDetailPanel(Gtk.Overlay): 'song-clicked', self.playlist_song_store[rand_idx][-1], [m[-1] for m in self.playlist_song_store], - {'force_shuffle_state': True}, + { + 'force_shuffle_state': True, + 'active_playlist_id': self.playlist_id, + }, ) def on_song_activated(self, treeview, idx, column): @@ -586,7 +595,9 @@ class PlaylistDetailPanel(Gtk.Overlay): 'song-clicked', self.playlist_song_store[idx][-1], [m[-1] for m in self.playlist_song_store], - {}, + { + 'active_playlist_id': self.playlist_id, + }, ) def on_song_button_press(self, tree, event):