Play queue enabled/disabled depending on cache state

This commit is contained in:
Sumner Evans
2020-05-24 21:28:27 -06:00
parent 654b0902e7
commit 209491204f
7 changed files with 63 additions and 27 deletions

6
pyproject.toml Normal file
View File

@@ -0,0 +1,6 @@
[tool.black]
exclude = '''
(
/flatpak/
)
'''

View File

@@ -181,6 +181,10 @@
min-width: 50px; min-width: 50px;
} }
#play-queue-image-disabled {
opacity: 0.5;
}
/* ********** General ********** */ /* ********** General ********** */
.menu-button { .menu-button {
padding: 5px; padding: 5px;
@@ -258,15 +262,18 @@
margin: 15px; margin: 15px;
} }
@define-color box_shadow_color rgba(0, 0, 0, 0.2);
#artist-info-panel { #artist-info-panel {
box-shadow: 0 5px 5px @box_shadow_color;
margin-bottom: 10px; margin-bottom: 10px;
padding-bottom: 10px;
} }
@define-color detail_color rgba(0, 0, 0, 0.2);
#artist-detail-box { #artist-detail-box {
padding-top: 10px; padding-top: 10px;
padding-bottom: 10px; padding-bottom: 10px;
box-shadow: inset 0 5px 5px @detail_color, box-shadow: inset 0 5px 5px @box_shadow_color,
inset 0 -5px 5px @detail_color; inset 0 -5px 5px @box_shadow_color;
background-color: @detail_color; background-color: @box_shadow_color;
} }

View File

@@ -234,13 +234,13 @@ class ArtistDetailPanel(Gtk.Box):
# TODO: make these disabled if there are no songs that can be played. # TODO: make these disabled if there are no songs that can be played.
play_button = IconButton( play_button = IconButton(
"media-playback-start-symbolic", label="Play All", relief=True, "media-playback-start-symbolic", label="Play All", relief=True
) )
play_button.connect("clicked", self.on_play_all_clicked) play_button.connect("clicked", self.on_play_all_clicked)
self.play_shuffle_buttons.pack_start(play_button, False, False, 0) self.play_shuffle_buttons.pack_start(play_button, False, False, 0)
shuffle_button = IconButton( shuffle_button = IconButton(
"media-playlist-shuffle-symbolic", label="Shuffle All", relief=True, "media-playlist-shuffle-symbolic", label="Shuffle All", relief=True
) )
shuffle_button.connect("clicked", self.on_shuffle_all_button) shuffle_button.connect("clicked", self.on_shuffle_all_button)
self.play_shuffle_buttons.pack_start(shuffle_button, False, False, 5) self.play_shuffle_buttons.pack_start(shuffle_button, False, False, 5)

View File

@@ -7,7 +7,7 @@ from typing import Any, Callable, List, Optional, Tuple
from gi.repository import Gdk, GdkPixbuf, GLib, GObject, Gtk, Pango from gi.repository import Gdk, GdkPixbuf, GLib, GObject, Gtk, Pango
from pychromecast import Chromecast from pychromecast import Chromecast
from sublime.adapters import AdapterManager, Result from sublime.adapters import AdapterManager, Result, SongCacheStatus
from sublime.adapters.api_objects import Song from sublime.adapters.api_objects import Song
from sublime.config import AppConfiguration from sublime.config import AppConfiguration
from sublime.players import ChromecastPlayer from sublime.players import ChromecastPlayer
@@ -177,6 +177,7 @@ class PlayerControls(Gtk.ActionBar):
self.update_device_list() self.update_device_list()
# Short circuit if no changes to the play queue # Short circuit if no changes to the play queue
force |= self.offline_mode != app_config.offline_mode
self.offline_mode = app_config.offline_mode self.offline_mode = app_config.offline_mode
self.load_play_queue_button.set_sensitive(not self.offline_mode) self.load_play_queue_button.set_sensitive(not self.offline_mode)
@@ -224,7 +225,7 @@ class PlayerControls(Gtk.ActionBar):
if order_token != self.play_queue_update_order_token: if order_token != self.play_queue_update_order_token:
return return
self.play_queue_store[idx][0] = cover_art_filename self.play_queue_store[idx][1] = cover_art_filename
def get_cover_art_filename_or_create_future( def get_cover_art_filename_or_create_future(
cover_art_id: Optional[str], idx: int, order_token: int cover_art_id: Optional[str], idx: int, order_token: int
@@ -247,21 +248,26 @@ class PlayerControls(Gtk.ActionBar):
if order_token != self.play_queue_update_order_token: if order_token != self.play_queue_update_order_token:
return return
self.play_queue_store[idx][1] = calculate_label(song_details) self.play_queue_store[idx][2] = calculate_label(song_details)
# Cover Art # Cover Art
filename = get_cover_art_filename_or_create_future( filename = get_cover_art_filename_or_create_future(
song_details.cover_art, idx, order_token song_details.cover_art, idx, order_token
) )
if filename: if filename:
self.play_queue_store[idx][0] = filename self.play_queue_store[idx][1] = filename
current_play_queue = [x[-1] for x in self.play_queue_store] current_play_queue = [x[-1] for x in self.play_queue_store]
if app_config.state.play_queue != current_play_queue: if app_config.state.play_queue != current_play_queue:
self.play_queue_update_order_token += 1 self.play_queue_update_order_token += 1
song_details_results = [] song_details_results = []
for i, song_id in enumerate(app_config.state.play_queue): for i, (song_id, cached_status) in enumerate(
zip(
app_config.state.play_queue,
AdapterManager.get_cached_statuses(app_config.state.play_queue),
)
):
song_details_result = AdapterManager.get_song_details(song_id) song_details_result = AdapterManager.get_song_details(song_id)
cover_art_filename = "" cover_art_filename = ""
@@ -282,6 +288,11 @@ class PlayerControls(Gtk.ActionBar):
new_store.append( new_store.append(
[ [
(
not self.offline_mode
or cached_status
in (SongCacheStatus.CACHED, SongCacheStatus.PERMANENTLY_CACHED)
),
cover_art_filename, cover_art_filename,
label, label,
i == app_config.state.current_song_index, i == app_config.state.current_song_index,
@@ -361,6 +372,8 @@ class PlayerControls(Gtk.ActionBar):
self.play_queue_popover.show_all() self.play_queue_popover.show_all()
def on_song_activated(self, t: Any, idx: Gtk.TreePath, c: Any): def on_song_activated(self, t: Any, idx: Gtk.TreePath, c: Any):
if not self.play_queue_store[idx[0]][0]:
return
# The song ID is in the last column of the model. # The song ID is in the last column of the model.
self.emit( self.emit(
"song-clicked", "song-clicked",
@@ -481,7 +494,7 @@ class PlayerControls(Gtk.ActionBar):
# reordering_play_queue_song_list flag. # reordering_play_queue_song_list flag.
if self.reordering_play_queue_song_list: if self.reordering_play_queue_song_list:
currently_playing_index = [ currently_playing_index = [
i for i, s in enumerate(self.play_queue_store) if s[2] i for i, s in enumerate(self.play_queue_store) if s[3] # playing
][0] ][0]
self.emit( self.emit(
"refresh-window", "refresh-window",
@@ -706,6 +719,7 @@ class PlayerControls(Gtk.ActionBar):
) )
self.play_queue_store = Gtk.ListStore( self.play_queue_store = Gtk.ListStore(
bool, # playable
str, # image filename str, # image filename
str, # title, album, artist str, # title, album, artist
bool, # playing bool, # playing
@@ -714,30 +728,35 @@ class PlayerControls(Gtk.ActionBar):
self.play_queue_list = Gtk.TreeView( self.play_queue_list = Gtk.TreeView(
model=self.play_queue_store, reorderable=True, headers_visible=False, model=self.play_queue_store, reorderable=True, headers_visible=False,
) )
self.play_queue_list.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) selection = self.play_queue_list.get_selection()
selection.set_mode(Gtk.SelectionMode.MULTIPLE)
selection.set_select_function(lambda _, model, path, current: model[path[0]][0])
# Album Art column. # Album Art column. This function defines what image to use for the play queue
# song icon.
def filename_to_pixbuf( def filename_to_pixbuf(
column: Any, column: Any,
cell: Gtk.CellRendererPixbuf, cell: Gtk.CellRendererPixbuf,
model: Gtk.ListStore, model: Gtk.ListStore,
iter: Gtk.TreeIter, tree_iter: Gtk.TreeIter,
flags: Any, flags: Any,
): ):
filename = model.get_value(iter, 0) cell.set_property("sensitive", model.get_value(tree_iter, 0))
filename = model.get_value(tree_iter, 1)
if not filename: if not filename:
cell.set_property("icon_name", "") cell.set_property("icon_name", "")
return return
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(filename, 50, 50, True) pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(filename, 50, 50, True)
# If this is the playing song, then overlay the play icon. # If this is the playing song, then overlay the play icon.
if model.get_value(iter, 2): if model.get_value(tree_iter, 3):
play_overlay_pixbuf = GdkPixbuf.Pixbuf.new_from_file( play_overlay_pixbuf = GdkPixbuf.Pixbuf.new_from_file(
str(Path(__file__).parent.joinpath("images/play-queue-play.png")) str(Path(__file__).parent.joinpath("images/play-queue-play.png"))
) )
play_overlay_pixbuf.composite( play_overlay_pixbuf.composite(
pixbuf, 0, 0, 50, 50, 0, 0, 1, 1, GdkPixbuf.InterpType.NEAREST, 255 pixbuf, 0, 0, 50, 50, 0, 0, 1, 1, GdkPixbuf.InterpType.NEAREST, 200
) )
cell.set_property("pixbuf", pixbuf) cell.set_property("pixbuf", pixbuf)
@@ -749,8 +768,8 @@ class PlayerControls(Gtk.ActionBar):
column.set_resizable(True) column.set_resizable(True)
self.play_queue_list.append_column(column) self.play_queue_list.append_column(column)
renderer = Gtk.CellRendererText(markup=True, ellipsize=Pango.EllipsizeMode.END,) renderer = Gtk.CellRendererText(markup=True, ellipsize=Pango.EllipsizeMode.END)
column = Gtk.TreeViewColumn("", renderer, markup=1) column = Gtk.TreeViewColumn("", renderer, markup=2, sensitive=0)
self.play_queue_list.append_column(column) self.play_queue_list.append_column(column)
self.play_queue_list.connect("row-activated", self.on_song_activated) self.play_queue_list.connect("row-activated", self.on_song_activated)

View File

@@ -713,6 +713,8 @@ class PlaylistDetailPanel(Gtk.Overlay):
) )
def on_song_activated(self, _, idx: Gtk.TreePath, col: Any): def on_song_activated(self, _, idx: Gtk.TreePath, col: Any):
if not self.playlist_song_store[idx[0]][0]:
return
# The song ID is in the last column of the model. # The song ID is in the last column of the model.
self.emit( self.emit(
"song-clicked", "song-clicked",

View File

@@ -6,6 +6,7 @@ from pathlib import Path
from typing import Any, Generator, List, Tuple from typing import Any, Generator, List, Tuple
import pytest import pytest
from dateutil.tz import tzutc
from sublime.adapters.subsonic import ( from sublime.adapters.subsonic import (
api_objects as SubsonicAPI, api_objects as SubsonicAPI,
@@ -111,8 +112,8 @@ def test_get_playlists(adapter: SubsonicAdapter):
name="Test", name="Test",
song_count=132, song_count=132,
duration=timedelta(seconds=33072), duration=timedelta(seconds=33072),
created=datetime(2020, 3, 27, 5, 38, 45, 0, tzinfo=timezone.utc), created=datetime(2020, 3, 27, 5, 38, 45, 0, tzinfo=tzutc()),
changed=datetime(2020, 4, 9, 16, 3, 26, 0, tzinfo=timezone.utc), changed=datetime(2020, 4, 9, 16, 3, 26, 0, tzinfo=tzutc()),
comment="Foo", comment="Foo",
owner="foo", owner="foo",
public=True, public=True,
@@ -123,8 +124,8 @@ def test_get_playlists(adapter: SubsonicAdapter):
name="Bar", name="Bar",
song_count=23, song_count=23,
duration=timedelta(seconds=847), duration=timedelta(seconds=847),
created=datetime(2020, 3, 27, 5, 39, 4, 0, tzinfo=timezone.utc), created=datetime(2020, 3, 27, 5, 39, 4, 0, tzinfo=tzutc()),
changed=datetime(2020, 3, 27, 5, 45, 23, 0, tzinfo=timezone.utc), changed=datetime(2020, 3, 27, 5, 45, 23, 0, tzinfo=tzutc()),
comment="", comment="",
owner="foo", owner="foo",
public=False, public=False,
@@ -136,7 +137,7 @@ def test_get_playlists(adapter: SubsonicAdapter):
logging.info(filename) logging.info(filename)
logging.debug(data) logging.debug(data)
adapter._set_mock_data(data) adapter._set_mock_data(data)
assert adapter.get_playlists() == expected assert adapter.get_playlists() == sorted(expected, key=lambda e: e.name)
# When playlists is null, expect an empty list. # When playlists is null, expect an empty list.
adapter._set_mock_data(mock_json()) adapter._set_mock_data(mock_json())

View File

@@ -46,14 +46,15 @@ def test_yaml_load_unload():
def test_config_migrate(): def test_config_migrate():
config = AppConfiguration() config = AppConfiguration(always_stream=True)
server = ServerConfiguration( server = ServerConfiguration(
name="Test", server_address="https://test.host", username="test" name="Test", server_address="https://test.host", username="test"
) )
config.servers.append(server) config.servers.append(server)
config.migrate() config.migrate()
assert config.version == 3 assert config.version == 4
assert config.allow_song_downloads is False
for server in config.servers: for server in config.servers:
server.version == 0 server.version == 0