New Playlist functionality; better failure handling
This commit is contained in:
@@ -200,14 +200,20 @@ class LibremsonicApp(Gtk.Application):
|
||||
lambda *a, **k: CacheManager.get_song_details(*a, **k),
|
||||
)
|
||||
def play_song(self, song: Child):
|
||||
self.update_window()
|
||||
self.play_song(song)
|
||||
|
||||
song_filename_future = CacheManager.get_song_filename(song)
|
||||
# Do this the old fashioned way so that we can have access to ``song``
|
||||
# in the callback.
|
||||
song_filename_future = CacheManager.get_song_filename(
|
||||
song,
|
||||
before_download=lambda: None,
|
||||
)
|
||||
|
||||
def filename_future_done(song_file):
|
||||
self.state.current_song = song
|
||||
self.state.playing = True
|
||||
self.update_window()
|
||||
|
||||
self.player.loadfile(song_file, 'replace')
|
||||
self.player.pause = False
|
||||
|
||||
|
@@ -136,11 +136,13 @@ class CacheManager(metaclass=Singleton):
|
||||
self,
|
||||
relative_path: Union[Path, str],
|
||||
download_fn: Callable[[], bytes],
|
||||
before_download: Callable[[], None],
|
||||
force: bool = False,
|
||||
):
|
||||
abs_path = self.calculate_abs_path(relative_path)
|
||||
download_path = self.calculate_download_path(relative_path)
|
||||
if not abs_path.exists() or force:
|
||||
before_download()
|
||||
print(abs_path, 'not found. Downloading...')
|
||||
self.save_file(download_path, download_fn())
|
||||
|
||||
@@ -150,9 +152,14 @@ class CacheManager(metaclass=Singleton):
|
||||
|
||||
return str(abs_path)
|
||||
|
||||
def get_playlists(self, force: bool = False) -> Future:
|
||||
def get_playlists(
|
||||
self,
|
||||
before_download: Callable[[], None],
|
||||
force: bool = False,
|
||||
) -> Future:
|
||||
def do_get_playlists() -> List[Playlist]:
|
||||
if not self.playlists or force:
|
||||
before_download()
|
||||
self.playlists = self.server.get_playlists().playlist
|
||||
self.save_cache_info()
|
||||
return self.playlists
|
||||
@@ -162,10 +169,12 @@ class CacheManager(metaclass=Singleton):
|
||||
def get_playlist(
|
||||
self,
|
||||
playlist_id: int,
|
||||
before_download: Callable[[], None],
|
||||
force: bool = False,
|
||||
) -> Future:
|
||||
def do_get_playlist() -> PlaylistWithSongs:
|
||||
if not self.playlist_details.get(playlist_id) or force:
|
||||
before_download()
|
||||
playlist = self.server.get_playlist(playlist_id)
|
||||
self.playlist_details[playlist_id] = playlist
|
||||
|
||||
@@ -182,6 +191,7 @@ class CacheManager(metaclass=Singleton):
|
||||
def get_cover_art_filename(
|
||||
self,
|
||||
id: str,
|
||||
before_download: Callable[[], None],
|
||||
size: Union[str, int] = 200,
|
||||
force: bool = False,
|
||||
) -> Future:
|
||||
@@ -189,15 +199,21 @@ class CacheManager(metaclass=Singleton):
|
||||
return self.return_cache_or_download(
|
||||
f'cover_art/{id}_{size}',
|
||||
lambda: self.server.get_cover_art(id, str(size)),
|
||||
before_download=before_download,
|
||||
force=force,
|
||||
)
|
||||
|
||||
return CacheManager.executor.submit(do_get_cover_art_filename)
|
||||
|
||||
def get_song_details(self, song_id: int,
|
||||
force: bool = False) -> Future:
|
||||
def get_song_details(
|
||||
self,
|
||||
song_id: int,
|
||||
before_download: Callable[[], None],
|
||||
force: bool = False,
|
||||
) -> Future:
|
||||
def do_get_song_details() -> Child:
|
||||
if not self.song_details.get(song_id) or force:
|
||||
before_download()
|
||||
self.song_details[song_id] = self.server.get_song(song_id)
|
||||
self.save_cache_info()
|
||||
|
||||
@@ -208,12 +224,14 @@ class CacheManager(metaclass=Singleton):
|
||||
def get_song_filename(
|
||||
self,
|
||||
song: Child,
|
||||
before_download: Callable[[], None],
|
||||
force: bool = False,
|
||||
) -> Future:
|
||||
def do_get_song_filename() -> str:
|
||||
return self.return_cache_or_download(
|
||||
song.path,
|
||||
lambda: self.server.download(song.id),
|
||||
before_download=before_download,
|
||||
force=force,
|
||||
)
|
||||
|
||||
|
@@ -9,6 +9,16 @@
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
#playlist-list-new-playlist-entry {
|
||||
margin: 10px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#playlist-list-new-playlist-confirm {
|
||||
margin: 10px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#playlist-artwork-spinner {
|
||||
min-height: 35px;
|
||||
min-width: 35px;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import gi
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from typing import List, OrderedDict
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gio, Gtk, Pango, GObject, GLib
|
||||
@@ -21,7 +21,7 @@ class PlaylistsPanel(Gtk.Paned):
|
||||
),
|
||||
}
|
||||
|
||||
playlist_ids: List[str] = []
|
||||
playlist_map: OrderedDict[int, PlaylistWithSongs] = {}
|
||||
song_ids: List[int] = []
|
||||
|
||||
def __init__(self):
|
||||
@@ -43,6 +43,7 @@ class PlaylistsPanel(Gtk.Paned):
|
||||
box.add(image)
|
||||
box.add(Gtk.Label('New Playlist', margin=5))
|
||||
self.new_playlist.add(box)
|
||||
self.new_playlist.connect('clicked', self.on_new_playlist_clicked)
|
||||
playlist_list_actions.pack_start(self.new_playlist)
|
||||
|
||||
refresh_button = util.button_with_icon('view-refresh')
|
||||
@@ -58,6 +59,28 @@ class PlaylistsPanel(Gtk.Paned):
|
||||
active=True)
|
||||
self.playlist_list.add(self.playlist_list_loading)
|
||||
|
||||
self.new_playlist_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
|
||||
self.playlist_list_new_entry = Gtk.Entry(
|
||||
name='playlist-list-new-playlist-entry')
|
||||
self.playlist_list_new_entry.connect(
|
||||
'activate', self.on_playlist_list_new_entry_activate)
|
||||
self.playlist_list_new_entry.connect(
|
||||
'focus-out-event', self.on_playlist_list_new_entry_focus_loose)
|
||||
self.new_playlist_box.pack_start(self.playlist_list_new_entry, True,
|
||||
True, 0)
|
||||
|
||||
self.playlist_list_new_confirm_button = Gtk.Button.new_from_icon_name(
|
||||
'object-select-symbolic', Gtk.IconSize.BUTTON)
|
||||
self.playlist_list_new_confirm_button.set_name(
|
||||
'playlist-list-new-playlist-confirm')
|
||||
self.playlist_list_new_confirm_button.connect(
|
||||
'clicked', self.on_playlist_list_new_confirm_button_clicked)
|
||||
self.new_playlist_box.pack_end(self.playlist_list_new_confirm_button,
|
||||
False, True, 0)
|
||||
|
||||
self.playlist_list.add(self.new_playlist_box)
|
||||
|
||||
self.playlist_list.connect('row-activated', self.on_playlist_selected)
|
||||
list_scroll_window.add(self.playlist_list)
|
||||
playlist_list_vbox.pack_start(list_scroll_window, True, True, 0)
|
||||
@@ -173,14 +196,23 @@ class PlaylistsPanel(Gtk.Paned):
|
||||
|
||||
# Event Handlers
|
||||
# =========================================================================
|
||||
def on_new_playlist_clicked(self, new_playlist_button):
|
||||
self.playlist_list_new_entry.set_text('Untitled Playlist')
|
||||
self.playlist_list_new_entry.grab_focus()
|
||||
self.new_playlist_box.show()
|
||||
|
||||
def on_playlist_selected(self, playlist_list, row):
|
||||
row_idx = row.get_index()
|
||||
if row_idx == 0 or row_idx == len(self.playlist_map) + 1:
|
||||
# Clicked on the loading indicator or the new playlist entry.
|
||||
# TODO: make these just not activatable
|
||||
return
|
||||
|
||||
# TODO don't update if selecting the same playlist.
|
||||
# Use row index - 1 due to the loading indicator.
|
||||
playlist_id = self.playlist_ids[row.get_index() - 1]
|
||||
self.update_playlist_view(playlist_id)
|
||||
self.update_playlist_view(self.playlist_map[row_idx].id)
|
||||
|
||||
def on_list_refresh_click(self, button):
|
||||
# TODO: this should reselect the selected playlist. If the playlist no
|
||||
# longer exists, it should also update the playlist view on the right.
|
||||
self.update_playlist_list(force=True)
|
||||
|
||||
def on_song_double_click(self, treeview, idx, column):
|
||||
@@ -189,6 +221,23 @@ class PlaylistsPanel(Gtk.Paned):
|
||||
self.emit('song-clicked', song_id,
|
||||
[m[-1] for m in self.playlist_song_model])
|
||||
|
||||
def on_playlist_list_new_entry_activate(self, entry):
|
||||
try:
|
||||
CacheManager.create_playlist(name=entry.get_text())
|
||||
except ConnectionError:
|
||||
# TODO show message box
|
||||
return
|
||||
self.update_playlist_list(force=True)
|
||||
|
||||
def on_playlist_list_new_entry_focus_loose(self, entry, event):
|
||||
# TODO don't do this, have an X button
|
||||
print(entry, event)
|
||||
print('ohea')
|
||||
|
||||
def on_playlist_list_new_confirm_button_clicked(self, button):
|
||||
print('check')
|
||||
# TODO figure out hwo to make the button styled nicer so that it looks integrated
|
||||
|
||||
# Helper Methods
|
||||
# =========================================================================
|
||||
def make_label(self, text=None, name=None, **params):
|
||||
@@ -200,7 +249,7 @@ class PlaylistsPanel(Gtk.Paned):
|
||||
self.update_playlist_list()
|
||||
selected = self.playlist_list.get_selected_row()
|
||||
if selected:
|
||||
playlist_id = self.playlist_ids[selected.get_index() - 1]
|
||||
playlist_id = self.playlist_map[selected.get_index()].id
|
||||
self.update_playlist_view(playlist_id)
|
||||
|
||||
def set_playlist_list_loading(self, loading_status):
|
||||
@@ -224,27 +273,40 @@ class PlaylistsPanel(Gtk.Paned):
|
||||
|
||||
@util.async_callback(
|
||||
lambda *a, **k: CacheManager.get_playlists(*a, **k),
|
||||
before_fn=lambda self: self.set_playlist_list_loading(True),
|
||||
before_download=lambda self: self.set_playlist_list_loading(True),
|
||||
on_failure=lambda self, e: self.set_playlist_list_loading(False),
|
||||
)
|
||||
def update_playlist_list(self, playlists: List[PlaylistWithSongs]):
|
||||
not_seen = set(self.playlist_ids)
|
||||
selected_row = self.playlist_list.get_selected_row()
|
||||
selected_playlist = None
|
||||
if selected_row:
|
||||
selected_playlist = self.playlist_map.get(selected_row.get_index())
|
||||
|
||||
for playlist in playlists:
|
||||
if playlist.id not in self.playlist_ids:
|
||||
self.playlist_ids.append(playlist.id)
|
||||
self.playlist_list.add(self.create_playlist_label(playlist))
|
||||
else:
|
||||
not_seen.remove(playlist.id)
|
||||
for row in self.playlist_list.get_children()[1:-1]:
|
||||
self.playlist_list.remove(row)
|
||||
|
||||
for playlist_id in not_seen:
|
||||
print(playlist_id)
|
||||
self.playlist_map = {}
|
||||
selected_idx = None
|
||||
for i, playlist in enumerate(playlists):
|
||||
# Use i+1 due to loading indicator
|
||||
if playlist == selected_playlist:
|
||||
selected_idx = i + 1
|
||||
self.playlist_map[i + 1] = playlist
|
||||
self.playlist_list.insert(self.create_playlist_label(playlist),
|
||||
i + 1)
|
||||
if selected_idx:
|
||||
row = self.playlist_list.get_row_at_index(selected_idx)
|
||||
self.playlist_list.select_row(row)
|
||||
|
||||
self.playlist_list.show_all()
|
||||
self.set_playlist_list_loading(False)
|
||||
self.new_playlist_box.hide()
|
||||
|
||||
@util.async_callback(
|
||||
lambda *a, **k: CacheManager.get_playlist(*a, **k),
|
||||
before_fn=lambda self: self.set_playlist_view_loading(True),
|
||||
before_download=lambda self: self.set_playlist_view_loading(True),
|
||||
on_failure=lambda self, e: (self.set_playlist_view_loading(False) or
|
||||
self.set_playlist_art_loading(False)),
|
||||
)
|
||||
def update_playlist_view(self, playlist):
|
||||
# Update the Playlist Info panel
|
||||
@@ -279,7 +341,8 @@ class PlaylistsPanel(Gtk.Paned):
|
||||
|
||||
@util.async_callback(
|
||||
lambda *a, **k: CacheManager.get_cover_art_filename(*a, **k),
|
||||
before_fn=lambda self: self.set_playlist_art_loading(True),
|
||||
before_download=lambda self: self.set_playlist_art_loading(True),
|
||||
on_failure=lambda self, e: self.set_playlist_art_loading(False),
|
||||
)
|
||||
def update_playlist_artwork(self, cover_art_filename):
|
||||
self.playlist_artwork.set_from_file(cover_art_filename)
|
||||
|
@@ -27,7 +27,7 @@ def format_song_duration(duration_secs) -> str:
|
||||
return f'{duration_secs // 60}:{duration_secs % 60:02}'
|
||||
|
||||
|
||||
def async_callback(future_fn, before_fn=None):
|
||||
def async_callback(future_fn, before_download=None, on_failure=None):
|
||||
"""
|
||||
Defines the ``async_callback`` decorator.
|
||||
|
||||
@@ -43,12 +43,28 @@ def async_callback(future_fn, before_fn=None):
|
||||
def decorator(callback_fn):
|
||||
@functools.wraps(callback_fn)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if before_fn:
|
||||
before_fn(self)
|
||||
if before_download:
|
||||
on_before_download = (
|
||||
lambda: GLib.idle_add(before_download, self))
|
||||
else:
|
||||
on_before_download = (lambda: None)
|
||||
|
||||
future: Future = future_fn(*args, **kwargs)
|
||||
future.add_done_callback(
|
||||
lambda f: GLib.idle_add(callback_fn, self, f.result()), )
|
||||
def future_callback(f):
|
||||
try:
|
||||
result = f.result()
|
||||
except Exception as e:
|
||||
if on_failure:
|
||||
on_failure(self, e)
|
||||
return
|
||||
|
||||
return GLib.idle_add(callback_fn, self, result)
|
||||
|
||||
future: Future = future_fn(
|
||||
*args,
|
||||
before_download=on_before_download,
|
||||
**kwargs,
|
||||
)
|
||||
future.add_done_callback(future_callback)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
Reference in New Issue
Block a user