skip songs that aren't cached in offline mode and show errors if no more songs can be played
This commit is contained in:
123
sublime/app.py
123
sublime/app.py
@@ -32,7 +32,7 @@ except Exception:
|
||||
)
|
||||
glib_notify_exists = False
|
||||
|
||||
from .adapters import AdapterManager, AlbumSearchQuery, Result
|
||||
from .adapters import AdapterManager, AlbumSearchQuery, Result, SongCacheStatus
|
||||
from .adapters.api_objects import Playlist, PlayQueue, Song
|
||||
from .config import AppConfiguration
|
||||
from .dbus import dbus_propagate, DBusManager
|
||||
@@ -86,12 +86,7 @@ class SublimeMusicApp(Gtk.Application):
|
||||
add_action("browse-to", self.browse_to, parameter_type="s")
|
||||
add_action("go-to-playlist", self.on_go_to_playlist, parameter_type="s")
|
||||
|
||||
add_action(
|
||||
"go-online",
|
||||
lambda *a: self.on_refresh_window(
|
||||
None, {"__settings__": {"offline_mode": False}}
|
||||
),
|
||||
)
|
||||
add_action("go-online", self.on_go_online)
|
||||
add_action(
|
||||
"refresh-window", lambda *a: self.on_refresh_window(None, {}, True),
|
||||
)
|
||||
@@ -587,7 +582,12 @@ class SublimeMusicApp(Gtk.Application):
|
||||
# Go back to the beginning of the song.
|
||||
song_index_to_play = self.app_config.state.current_song_index
|
||||
|
||||
self.play_song(song_index_to_play, reset=True)
|
||||
self.play_song(
|
||||
song_index_to_play,
|
||||
reset=True,
|
||||
# search backwards for a song to play if offline
|
||||
playable_song_search_direction=-1,
|
||||
)
|
||||
|
||||
@dbus_propagate()
|
||||
def on_repeat_press(self, *args):
|
||||
@@ -670,6 +670,9 @@ class SublimeMusicApp(Gtk.Application):
|
||||
self.app_config.state.selected_playlist_id = playlist_id.get_string()
|
||||
self.update_window()
|
||||
|
||||
def on_go_online(self, *args):
|
||||
self.on_refresh_window(None, {"__settings__": {"offline_mode": False}})
|
||||
|
||||
def on_server_list_changed(self, action: Any, servers: GLib.Variant):
|
||||
self.app_config.servers = servers
|
||||
self.app_config.save()
|
||||
@@ -971,7 +974,13 @@ class SublimeMusicApp(Gtk.Application):
|
||||
reset: bool = False,
|
||||
old_play_queue: Tuple[str, ...] = None,
|
||||
play_queue: Tuple[str, ...] = None,
|
||||
playable_song_search_direction: int = 1,
|
||||
):
|
||||
def do_reset():
|
||||
self.player.reset()
|
||||
self.app_config.state.song_progress = timedelta(0)
|
||||
self.should_scrobble_song = True
|
||||
|
||||
# Do this the old fashioned way so that we can have access to ``reset``
|
||||
# in the callback.
|
||||
@dbus_propagate(self)
|
||||
@@ -984,9 +993,7 @@ class SublimeMusicApp(Gtk.Application):
|
||||
# Prevent it from doing the thing where it continually loads
|
||||
# songs when it has to download.
|
||||
if reset:
|
||||
self.player.reset()
|
||||
self.app_config.state.song_progress = timedelta(0)
|
||||
self.should_scrobble_song = True
|
||||
do_reset()
|
||||
|
||||
# Start playing the song.
|
||||
if order_token != self.song_playing_order_token:
|
||||
@@ -1151,14 +1158,98 @@ class SublimeMusicApp(Gtk.Application):
|
||||
),
|
||||
)
|
||||
|
||||
# If in offline mode, go to the first song in the play queue after the given
|
||||
# song that is actually playable.
|
||||
if self.app_config.offline_mode:
|
||||
statuses = AdapterManager.get_cached_statuses(
|
||||
self.app_config.state.play_queue
|
||||
)
|
||||
playable_statuses = (
|
||||
SongCacheStatus.CACHED,
|
||||
SongCacheStatus.PERMANENTLY_CACHED,
|
||||
)
|
||||
can_play = False
|
||||
current_song_index = self.app_config.state.current_song_index
|
||||
|
||||
if statuses[current_song_index] in playable_statuses:
|
||||
can_play = True
|
||||
elif self.app_config.state.repeat_type != RepeatType.REPEAT_SONG:
|
||||
# See if any other songs in the queue are playable.
|
||||
# TODO: deal with search backwards
|
||||
play_queue_len = len(self.app_config.state.play_queue)
|
||||
cursor = (
|
||||
current_song_index + playable_song_search_direction
|
||||
) % play_queue_len
|
||||
for _ in range(play_queue_len): # Don't infinite loop.
|
||||
if self.app_config.state.repeat_type == RepeatType.NO_REPEAT:
|
||||
if (
|
||||
playable_song_search_direction == 1
|
||||
and cursor < current_song_index
|
||||
) or (
|
||||
playable_song_search_direction == -1
|
||||
and cursor > current_song_index
|
||||
):
|
||||
# We wrapped around to the end of the play queue without
|
||||
# finding a song that can be played, and we aren't allowed
|
||||
# to loop back.
|
||||
break
|
||||
|
||||
# If we find a playable song, stop and play it.
|
||||
if statuses[cursor] in playable_statuses:
|
||||
self.play_song(cursor, reset)
|
||||
return
|
||||
|
||||
cursor = (cursor + playable_song_search_direction) % play_queue_len
|
||||
|
||||
if not can_play:
|
||||
# There are no songs that can be played. Show a notification that you
|
||||
# have to go online to play anything and then don't go further.
|
||||
was_playing = False
|
||||
if self.app_config.state.playing:
|
||||
was_playing = True
|
||||
self.on_play_pause()
|
||||
|
||||
def go_online_clicked():
|
||||
self.app_config.state.current_notification = None
|
||||
self.on_go_online()
|
||||
if was_playing:
|
||||
self.on_play_pause()
|
||||
|
||||
if all(s == SongCacheStatus.NOT_CACHED for s in statuses):
|
||||
markup = (
|
||||
"<b>None of the songs in your play queue are cached for "
|
||||
"offline playback.</b>\nGo online to start playing your queue."
|
||||
)
|
||||
else:
|
||||
markup = (
|
||||
"<b>None of the remaining songs in your play queue are cached "
|
||||
"for offline playback.</b>\nGo online to contiue playing your "
|
||||
"queue."
|
||||
)
|
||||
|
||||
self.app_config.state.current_notification = UIState.UINotification(
|
||||
icon="cloud-offline-symbolic",
|
||||
markup=markup,
|
||||
actions=(("Go Online", go_online_clicked),),
|
||||
)
|
||||
if reset:
|
||||
do_reset()
|
||||
self.update_window()
|
||||
return
|
||||
|
||||
song_details_future = AdapterManager.get_song_details(
|
||||
self.app_config.state.play_queue[self.app_config.state.current_song_index]
|
||||
)
|
||||
song_details_future.add_done_callback(
|
||||
lambda f: GLib.idle_add(
|
||||
partial(do_play_song, self.song_playing_order_token), f.result()
|
||||
),
|
||||
)
|
||||
if song_details_future.data_is_available:
|
||||
song_details_future.add_done_callback(
|
||||
lambda f: do_play_song(self.song_playing_order_token, f.result())
|
||||
)
|
||||
else:
|
||||
song_details_future.add_done_callback(
|
||||
lambda f: GLib.idle_add(
|
||||
partial(do_play_song, self.song_playing_order_token), f.result()
|
||||
),
|
||||
)
|
||||
|
||||
def save_play_queue(self, song_playing_order_token: int = None):
|
||||
if (
|
||||
|
@@ -1,10 +1,10 @@
|
||||
from datetime import timedelta
|
||||
from random import randint
|
||||
from typing import List, Sequence
|
||||
from typing import cast, List, Sequence
|
||||
|
||||
from gi.repository import Gio, GLib, GObject, Gtk, Pango
|
||||
|
||||
from sublime.adapters import AdapterManager, api_objects as API
|
||||
from sublime.adapters import AdapterManager, api_objects as API, CacheMissError
|
||||
from sublime.config import AppConfiguration
|
||||
from sublime.ui import util
|
||||
from sublime.ui.common import AlbumWithSongs, IconButton, LoadError, SpinnerImage
|
||||
@@ -518,11 +518,24 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
)
|
||||
|
||||
def get_artist_song_ids(self) -> List[str]:
|
||||
try:
|
||||
artist = AdapterManager.get_artist(self.artist_id).result()
|
||||
except CacheMissError as c:
|
||||
artist = cast(API.Artist, c.partial_data)
|
||||
|
||||
if not artist:
|
||||
return []
|
||||
|
||||
songs = []
|
||||
for album in AdapterManager.get_artist(self.artist_id).result().albums or []:
|
||||
for album in artist.albums or []:
|
||||
assert album.id
|
||||
album_songs = AdapterManager.get_album(album.id).result()
|
||||
for song in album_songs.songs or []:
|
||||
try:
|
||||
album_with_songs = AdapterManager.get_album(album.id).result()
|
||||
except CacheMissError as c:
|
||||
album_with_songs = cast(API.Album, c.partial_data)
|
||||
if not album_with_songs:
|
||||
continue
|
||||
for song in album_with_songs.songs or []:
|
||||
songs.append(song.id)
|
||||
|
||||
return songs
|
||||
|
@@ -62,8 +62,11 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
notification_box = Gtk.Box(can_focus=False, valign="start", spacing=10)
|
||||
notification_box.get_style_context().add_class("app-notification")
|
||||
|
||||
self.notification_icon = Gtk.Image()
|
||||
notification_box.pack_start(self.notification_icon, True, False, 5)
|
||||
|
||||
self.notification_text = Gtk.Label(use_markup=True)
|
||||
notification_box.pack_start(self.notification_text, True, False, 0)
|
||||
notification_box.pack_start(self.notification_text, True, False, 5)
|
||||
|
||||
self.notification_actions = Gtk.Box()
|
||||
notification_box.pack_start(self.notification_actions, True, False, 0)
|
||||
@@ -99,6 +102,14 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
notification = app_config.state.current_notification
|
||||
if notification and (h := hash(notification)) != self.current_notification_hash:
|
||||
self.current_notification_hash = h
|
||||
|
||||
if notification.icon:
|
||||
self.notification_icon.set_from_icon_name(
|
||||
notification.icon, Gtk.IconSize.DND
|
||||
)
|
||||
else:
|
||||
self.notification_icon.set_from_icon_name(None, Gtk.IconSize.DND)
|
||||
|
||||
self.notification_text.set_markup(notification.markup)
|
||||
|
||||
for c in self.notification_actions.get_children():
|
||||
|
@@ -41,6 +41,7 @@ class UIState:
|
||||
actions: Tuple[Tuple[str, Callable[[], None]], ...] = field(
|
||||
default_factory=tuple
|
||||
)
|
||||
icon: Optional[str] = None
|
||||
|
||||
version: int = 1
|
||||
|
||||
|
Reference in New Issue
Block a user