New Playlist functionality; better failure handling

This commit is contained in:
Sumner Evans
2019-06-25 18:35:21 -06:00
parent d7a2c7b458
commit 2abbbd0bea
5 changed files with 143 additions and 30 deletions

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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;

View File

@@ -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)

View File

@@ -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