More cleanup; pulled out song list column to its own class
This commit is contained in:
111
sublime/app.py
111
sublime/app.py
@@ -2,6 +2,7 @@ import logging
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
@@ -11,7 +12,7 @@ from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk, Notify
|
||||
from .cache_manager import CacheManager
|
||||
from .dbus_manager import dbus_propagate, DBusManager
|
||||
from .players import ChromecastPlayer, MPVPlayer, PlayerEvent
|
||||
from .server.api_objects import Child, Directory
|
||||
from .server.api_objects import Child, Directory, Playlist
|
||||
from .state_manager import ApplicationState, RepeatType
|
||||
from .ui.configure_servers import ConfigureServersDialog
|
||||
from .ui.main import MainWindow
|
||||
@@ -32,7 +33,7 @@ class SublimeMusicApp(Gtk.Application):
|
||||
def do_startup(self):
|
||||
Gtk.Application.do_startup(self)
|
||||
|
||||
def add_action(name: str, fn, parameter_type=None):
|
||||
def add_action(name: str, fn: Callable, parameter_type: str = None):
|
||||
"""Registers an action with the application."""
|
||||
if type(parameter_type) == str:
|
||||
parameter_type = GLib.VariantType(parameter_type)
|
||||
@@ -124,8 +125,9 @@ class SublimeMusicApp(Gtk.Application):
|
||||
self.loading_state = False
|
||||
self.should_scrobble_song = False
|
||||
|
||||
def time_observer(value):
|
||||
if self.loading_state:
|
||||
def time_observer(value: Optional[float]):
|
||||
if (self.loading_state or not self.window
|
||||
or not self.state.current_song):
|
||||
return
|
||||
|
||||
if value is None:
|
||||
@@ -161,7 +163,6 @@ class SublimeMusicApp(Gtk.Application):
|
||||
if event.name == 'play_state_change':
|
||||
self.state.playing = event.value
|
||||
elif event.name == 'volume_change':
|
||||
self.state.old_volume = self.state.volume
|
||||
self.state.volume = event.value
|
||||
|
||||
GLib.idle_add(self.update_window)
|
||||
@@ -197,15 +198,13 @@ class SublimeMusicApp(Gtk.Application):
|
||||
self.dbus_manager.property_diff()
|
||||
|
||||
# ########## DBUS MANAGMENT ########## #
|
||||
def do_dbus_register(self, connection, path):
|
||||
def get_state_and_player():
|
||||
return (self.state, getattr(self, 'player', None))
|
||||
|
||||
def do_dbus_register(
|
||||
self, connection: Gio.DBusConnection, path: str) -> bool:
|
||||
self.dbus_manager = DBusManager(
|
||||
connection,
|
||||
self.on_dbus_method_call,
|
||||
self.on_dbus_set_property,
|
||||
get_state_and_player,
|
||||
lambda: (self.state, getattr(self, 'player', None)),
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -221,13 +220,15 @@ class SublimeMusicApp(Gtk.Application):
|
||||
):
|
||||
second_microsecond_conversion = 1000000
|
||||
|
||||
def seek_fn(offset):
|
||||
def seek_fn(offset: float):
|
||||
if not self.state.current_song:
|
||||
return
|
||||
offset_seconds = offset / second_microsecond_conversion
|
||||
new_seconds = self.state.song_progress + offset_seconds
|
||||
self.on_song_scrub(
|
||||
None, new_seconds / self.state.current_song.duration * 100)
|
||||
|
||||
def set_pos_fn(track_id, position=0):
|
||||
def set_pos_fn(track_id: str, position: float = 0):
|
||||
if self.state.playing:
|
||||
self.on_play_pause()
|
||||
pos_seconds = position / second_microsecond_conversion
|
||||
@@ -242,18 +243,29 @@ class SublimeMusicApp(Gtk.Application):
|
||||
|
||||
self.play_song(song_index)
|
||||
|
||||
def get_tracks_metadata(track_ids):
|
||||
def get_tracks_metadata(track_ids: List[str]) -> GLib.Variant:
|
||||
if len(track_ids):
|
||||
# We are lucky, just return an empty list.
|
||||
return GLib.Variant('(aa{sv})', ([], ))
|
||||
|
||||
# Have to calculate all of the metadatas so that we can deal with
|
||||
# repeat song IDs.
|
||||
metadatas = [
|
||||
metadatas: Iterable[Any] = [
|
||||
self.dbus_manager.get_mpris_metadata(i, self.state.play_queue)
|
||||
for i in range(len(self.state.play_queue))
|
||||
]
|
||||
|
||||
# Get rid of all of the tracks that were not requested.
|
||||
metadatas = filter(
|
||||
lambda m: m['mpris:trackid'] in track_ids, metadatas)
|
||||
|
||||
# Sort them so they get returned in the same order as they were
|
||||
# requested.
|
||||
metadatas = sorted(
|
||||
metadatas, key=lambda m: track_ids.index(m['mpris:trackid']))
|
||||
|
||||
# Turn them into dictionaries that can actually be serialized into
|
||||
# a GLib.Variant.
|
||||
metadatas = map(
|
||||
lambda m: {k: DBusManager.to_variant(v)
|
||||
for k, v in m.items()},
|
||||
@@ -262,7 +274,7 @@ class SublimeMusicApp(Gtk.Application):
|
||||
|
||||
return GLib.Variant('(aa{sv})', (list(metadatas), ))
|
||||
|
||||
def activate_playlist(playlist_id):
|
||||
def activate_playlist(playlist_id: str):
|
||||
playlist_id = playlist_id.split('/')[-1]
|
||||
playlist = CacheManager.get_playlist(playlist_id).result()
|
||||
|
||||
@@ -278,7 +290,12 @@ class SublimeMusicApp(Gtk.Application):
|
||||
{'active_playlist_id': playlist_id},
|
||||
)
|
||||
|
||||
def get_playlists(index, max_count, order, reverse_order):
|
||||
def get_playlists(
|
||||
index: int,
|
||||
max_count: int,
|
||||
order: str,
|
||||
reverse_order: bool,
|
||||
) -> GLib.Variant:
|
||||
playlists_result = CacheManager.get_playlists()
|
||||
if playlists_result.is_future:
|
||||
# We don't want to wait for the response in this case, so just
|
||||
@@ -297,20 +314,19 @@ class SublimeMusicApp(Gtk.Application):
|
||||
reverse=reverse_order,
|
||||
)
|
||||
|
||||
def make_playlist_tuple(p: Playlist):
|
||||
cover_art_filename = CacheManager.get_cover_art_filename(
|
||||
p.coverArt,
|
||||
allow_download=False,
|
||||
).result()
|
||||
return (f'/playlist/{p.id}', p.name, cover_art_filename or '')
|
||||
|
||||
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
|
||||
], ))
|
||||
list(
|
||||
map(
|
||||
make_playlist_tuple,
|
||||
playlists[index:(index + max_count)])), ))
|
||||
|
||||
method_call_map = {
|
||||
'org.mpris.MediaPlayer2': {
|
||||
@@ -381,15 +397,20 @@ class SublimeMusicApp(Gtk.Application):
|
||||
|
||||
# ########## ACTION HANDLERS ########## #
|
||||
@dbus_propagate()
|
||||
def on_refresh_window(self, _, state_updates, force=False):
|
||||
def on_refresh_window(
|
||||
self,
|
||||
_: Any,
|
||||
state_updates: Dict[str, Any],
|
||||
force: bool = False,
|
||||
):
|
||||
for k, v in state_updates.items():
|
||||
setattr(self.state, k, v)
|
||||
self.update_window(force=force)
|
||||
|
||||
def on_configure_servers(self, action, param):
|
||||
def on_configure_servers(self, *args):
|
||||
self.show_configure_servers_dialog()
|
||||
|
||||
def on_settings(self, action, param):
|
||||
def on_settings(self, *args):
|
||||
"""Show the Settings dialog."""
|
||||
dialog = SettingsDialog(self.window, self.state.config)
|
||||
result = dialog.run()
|
||||
@@ -412,7 +433,7 @@ class SublimeMusicApp(Gtk.Application):
|
||||
self.reset_state()
|
||||
dialog.destroy()
|
||||
|
||||
def on_window_go_to(self, win, action, value):
|
||||
def on_window_go_to(self, win: Any, action: str, value: str):
|
||||
{
|
||||
'album': self.on_go_to_album,
|
||||
'artist': self.on_go_to_artist,
|
||||
@@ -467,14 +488,14 @@ class SublimeMusicApp(Gtk.Application):
|
||||
self.play_song(song_index_to_play, reset=True)
|
||||
|
||||
@dbus_propagate()
|
||||
def on_repeat_press(self, action, params):
|
||||
def on_repeat_press(self, *args):
|
||||
# Cycle through the repeat types.
|
||||
new_repeat_type = RepeatType((self.state.repeat_type.value + 1) % 3)
|
||||
self.state.repeat_type = new_repeat_type
|
||||
self.update_window()
|
||||
|
||||
@dbus_propagate()
|
||||
def on_shuffle_press(self, action, params):
|
||||
def on_shuffle_press(self, *args):
|
||||
if self.state.shuffle_on:
|
||||
# Revert to the old play queue.
|
||||
self.state.current_song_index = self.state.old_play_queue.index(
|
||||
@@ -494,7 +515,7 @@ class SublimeMusicApp(Gtk.Application):
|
||||
self.update_window()
|
||||
|
||||
@dbus_propagate()
|
||||
def on_play_next(self, action, song_ids):
|
||||
def on_play_next(self, action: Any, song_ids: List[str]):
|
||||
if self.state.current_song is None:
|
||||
insert_at = 0
|
||||
else:
|
||||
@@ -507,12 +528,12 @@ class SublimeMusicApp(Gtk.Application):
|
||||
self.update_window()
|
||||
|
||||
@dbus_propagate()
|
||||
def on_add_to_queue(self, action, song_ids):
|
||||
def on_add_to_queue(self, action: Any, song_ids: GLib.Variant):
|
||||
self.state.play_queue.extend(song_ids)
|
||||
self.state.old_play_queue.extend(song_ids)
|
||||
self.update_window()
|
||||
|
||||
def on_go_to_album(self, action, album_id):
|
||||
def on_go_to_album(self, action: Any, album_id: GLib.Variant):
|
||||
# Switch to the By Year view (or genre, if year is not available) to
|
||||
# guarantee that the album is there.
|
||||
album = CacheManager.get_album(album_id.get_string()).result()
|
||||
@@ -545,26 +566,30 @@ class SublimeMusicApp(Gtk.Application):
|
||||
self.state.selected_album_id = album_id.get_string()
|
||||
self.update_window(force=True)
|
||||
|
||||
def on_go_to_artist(self, action, artist_id):
|
||||
def on_go_to_artist(self, action: Any, artist_id: GLib.Variant):
|
||||
self.state.current_tab = 'artists'
|
||||
self.state.selected_artist_id = artist_id.get_string()
|
||||
self.update_window()
|
||||
|
||||
def browse_to(self, action, item_id):
|
||||
def browse_to(self, action: Any, item_id: GLib.Variant):
|
||||
self.state.current_tab = 'browse'
|
||||
self.state.selected_browse_element_id = item_id.get_string()
|
||||
self.update_window()
|
||||
|
||||
def on_go_to_playlist(self, action, playlist_id):
|
||||
def on_go_to_playlist(self, action: Any, playlist_id: GLib.Variant):
|
||||
self.state.current_tab = 'playlists'
|
||||
self.state.selected_playlist_id = playlist_id.get_string()
|
||||
self.update_window()
|
||||
|
||||
def on_server_list_changed(self, action, servers):
|
||||
def on_server_list_changed(self, action: Any, servers: GLib.Variant):
|
||||
self.state.config.servers = servers
|
||||
self.state.save_config()
|
||||
|
||||
def on_connected_server_changed(self, action, current_server):
|
||||
def on_connected_server_changed(
|
||||
self,
|
||||
action: Any,
|
||||
current_server: GLib.Variant,
|
||||
):
|
||||
if self.state.config.server:
|
||||
self.state.save()
|
||||
self.state.config.current_server = current_server
|
||||
@@ -583,7 +608,7 @@ class SublimeMusicApp(Gtk.Application):
|
||||
# Update the window according to the new server configuration.
|
||||
self.update_window()
|
||||
|
||||
def on_stack_change(self, stack, child):
|
||||
def on_stack_change(self, stack: Gtk.Stack, _: Any):
|
||||
self.state.current_tab = stack.get_visible_child_name()
|
||||
self.update_window()
|
||||
|
||||
|
@@ -18,6 +18,7 @@ from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
DefaultDict,
|
||||
Dict,
|
||||
Generic,
|
||||
Iterable,
|
||||
List,
|
||||
@@ -245,7 +246,7 @@ class CacheManager(metaclass=Singleton):
|
||||
return self.future is not None
|
||||
|
||||
@staticmethod
|
||||
def ready():
|
||||
def ready() -> bool:
|
||||
return CacheManager._instance is not None
|
||||
|
||||
@staticmethod
|
||||
@@ -257,14 +258,15 @@ class CacheManager(metaclass=Singleton):
|
||||
logging.info('CacheManager shutdown complete')
|
||||
|
||||
@staticmethod
|
||||
def calculate_server_hash(server: Optional[ServerConfiguration]):
|
||||
def calculate_server_hash(
|
||||
server: Optional[ServerConfiguration]) -> Optional[str]:
|
||||
if server is None:
|
||||
return None
|
||||
server_info = (server.name + server.server_address + server.username)
|
||||
return hashlib.md5(server_info.encode('utf-8')).hexdigest()[:8]
|
||||
|
||||
class CacheEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
def default(self, obj: Any) -> Optional[Union[int, List, Dict]]:
|
||||
"""
|
||||
Encodes Python objects to JSON.
|
||||
|
||||
@@ -413,13 +415,13 @@ class CacheManager(metaclass=Singleton):
|
||||
with open(absolute_path, 'wb+') as f:
|
||||
f.write(data)
|
||||
|
||||
def calculate_abs_path(self, *relative_paths):
|
||||
def calculate_abs_path(self, *relative_paths) -> Path:
|
||||
return Path(self.app_config.cache_location).joinpath(
|
||||
CacheManager.calculate_server_hash(self.server_config),
|
||||
*relative_paths,
|
||||
)
|
||||
|
||||
def calculate_download_path(self, *relative_paths):
|
||||
def calculate_download_path(self, *relative_paths) -> Path:
|
||||
"""
|
||||
Determine where to temporarily put the file as it is downloading.
|
||||
"""
|
||||
@@ -473,7 +475,7 @@ class CacheManager(metaclass=Singleton):
|
||||
self.save_file(download_path, download_fn())
|
||||
except requests.exceptions.ConnectionError:
|
||||
with self.download_set_lock:
|
||||
self.current_downloads.discard(abs_path)
|
||||
self.current_downloads.discard(abs_path_str)
|
||||
|
||||
# Move the file to its cache download location.
|
||||
os.makedirs(abs_path.parent, exist_ok=True)
|
||||
@@ -494,7 +496,7 @@ class CacheManager(metaclass=Singleton):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_future(fn, *args):
|
||||
def create_future(fn: Callable, *args) -> Future:
|
||||
"""
|
||||
Creates a future on the CacheManager's executor.
|
||||
"""
|
||||
@@ -516,7 +518,7 @@ class CacheManager(metaclass=Singleton):
|
||||
if self.cache.get('playlists') and not force:
|
||||
return CacheManager.Result.from_data(self.cache['playlists'])
|
||||
|
||||
def after_download(playlists):
|
||||
def after_download(playlists: List[Playlist]):
|
||||
with self.cache_lock:
|
||||
self.cache['playlists'] = playlists
|
||||
self.save_cache_info()
|
||||
@@ -546,7 +548,7 @@ class CacheManager(metaclass=Singleton):
|
||||
return CacheManager.Result.from_data(
|
||||
playlist_details[playlist_id])
|
||||
|
||||
def after_download(playlist):
|
||||
def after_download(playlist: PlaylistWithSongs):
|
||||
with self.cache_lock:
|
||||
self.cache['playlist_details'][playlist_id] = playlist
|
||||
|
||||
@@ -597,7 +599,7 @@ class CacheManager(metaclass=Singleton):
|
||||
artists.extend(index.artist)
|
||||
return artists
|
||||
|
||||
def after_download(artists):
|
||||
def after_download(artists: List[ArtistID3]):
|
||||
with self.cache_lock:
|
||||
self.cache[cache_name] = artists
|
||||
self.save_cache_info()
|
||||
@@ -610,7 +612,7 @@ class CacheManager(metaclass=Singleton):
|
||||
|
||||
def get_artist(
|
||||
self,
|
||||
artist_id,
|
||||
artist_id: int,
|
||||
before_download: Callable[[], None] = lambda: None,
|
||||
force: bool = False,
|
||||
) -> 'CacheManager.Result[ArtistWithAlbumsID3]':
|
||||
@@ -620,7 +622,7 @@ class CacheManager(metaclass=Singleton):
|
||||
return CacheManager.Result.from_data(
|
||||
self.cache[cache_name][artist_id])
|
||||
|
||||
def after_download(artist):
|
||||
def after_download(artist: ArtistWithAlbumsID3):
|
||||
with self.cache_lock:
|
||||
self.cache[cache_name][artist_id] = artist
|
||||
self.save_cache_info()
|
||||
@@ -647,7 +649,7 @@ class CacheManager(metaclass=Singleton):
|
||||
artists.extend(index.artist)
|
||||
return artists
|
||||
|
||||
def after_download(artists):
|
||||
def after_download(artists: List[Artist]):
|
||||
with self.cache_lock:
|
||||
self.cache[cache_name] = artists
|
||||
self.save_cache_info()
|
||||
@@ -670,9 +672,9 @@ class CacheManager(metaclass=Singleton):
|
||||
return CacheManager.Result.from_data(
|
||||
self.cache[cache_name][id])
|
||||
|
||||
def after_download(album):
|
||||
def after_download(directory: Directory):
|
||||
with self.cache_lock:
|
||||
self.cache[cache_name][id] = album
|
||||
self.cache[cache_name][id] = directory
|
||||
self.save_cache_info()
|
||||
|
||||
return CacheManager.Result.from_server(
|
||||
@@ -683,7 +685,7 @@ class CacheManager(metaclass=Singleton):
|
||||
|
||||
def get_artist_info(
|
||||
self,
|
||||
artist_id,
|
||||
artist_id: int,
|
||||
before_download: Callable[[], None] = lambda: None,
|
||||
force: bool = False,
|
||||
) -> 'CacheManager.Result[ArtistInfo2]':
|
||||
@@ -693,7 +695,7 @@ class CacheManager(metaclass=Singleton):
|
||||
return CacheManager.Result.from_data(
|
||||
self.cache[cache_name][artist_id])
|
||||
|
||||
def after_download(artist_info: Optional[ArtistInfo2]):
|
||||
def after_download(artist_info: ArtistInfo2):
|
||||
if not artist_info:
|
||||
return
|
||||
|
||||
@@ -714,7 +716,9 @@ class CacheManager(metaclass=Singleton):
|
||||
before_download: Callable[[], None] = lambda: None,
|
||||
force: bool = False,
|
||||
) -> 'CacheManager.Result[Optional[str]]':
|
||||
def do_get_artist_artwork(artist_info):
|
||||
def do_get_artist_artwork(
|
||||
artist_info: ArtistInfo2
|
||||
) -> 'CacheManager.Result[Optional[str]]':
|
||||
lastfm_url = ''.join(artist_info.largeImageUrl or [])
|
||||
|
||||
# If it is the placeholder LastFM image, try and use the cover
|
||||
@@ -741,7 +745,9 @@ class CacheManager(metaclass=Singleton):
|
||||
force=force,
|
||||
)
|
||||
|
||||
def download_fn(artist_info):
|
||||
def download_fn(
|
||||
artist_info: CacheManager.Result[ArtistInfo2]
|
||||
) -> Optional[str]:
|
||||
# In this case, artist_info is a future, so we have to wait for
|
||||
# its result before calculating. Then, immediately unwrap the
|
||||
# result() because we are already within a future.
|
||||
@@ -763,7 +769,7 @@ class CacheManager(metaclass=Singleton):
|
||||
force: bool = False,
|
||||
# Look at documentation for get_album_list in server.py:
|
||||
**params,
|
||||
) -> 'CacheManager.Result[List[AlbumWithSongsID3]]':
|
||||
) -> 'CacheManager.Result[List[AlbumID3]]':
|
||||
cache_name = 'albums'
|
||||
|
||||
if (len(self.cache.get(cache_name, {}).get(type_, [])) > 0
|
||||
@@ -771,8 +777,11 @@ class CacheManager(metaclass=Singleton):
|
||||
return CacheManager.Result.from_data(
|
||||
self.cache[cache_name][type_])
|
||||
|
||||
def do_get_album_list() -> List[AlbumWithSongsID3]:
|
||||
def get_page(offset, page_size=500):
|
||||
def do_get_album_list() -> List[AlbumID3]:
|
||||
def get_page(
|
||||
offset: int,
|
||||
page_size: int = 500,
|
||||
) -> List[AlbumID3]:
|
||||
return self.server.get_album_list2(
|
||||
type_,
|
||||
size=page_size,
|
||||
@@ -794,7 +803,7 @@ class CacheManager(metaclass=Singleton):
|
||||
|
||||
return albums
|
||||
|
||||
def after_download(albums):
|
||||
def after_download(albums: List[AlbumID3]):
|
||||
with self.cache_lock:
|
||||
if not self.cache[cache_name].get(type_):
|
||||
self.cache[cache_name][type_] = []
|
||||
@@ -807,18 +816,9 @@ class CacheManager(metaclass=Singleton):
|
||||
after_download=after_download,
|
||||
)
|
||||
|
||||
def invalidate_album_list(self, type_):
|
||||
# TODO (#24): make this invalidate instead of delete
|
||||
cache_name = 'albums'
|
||||
if not self.cache.get(cache_name, {}).get(type_):
|
||||
return
|
||||
with self.cache_lock:
|
||||
self.cache[cache_name][type_] = []
|
||||
self.save_cache_info()
|
||||
|
||||
def get_album(
|
||||
self,
|
||||
album_id,
|
||||
album_id: int,
|
||||
before_download: Callable[[], None] = lambda: None,
|
||||
force: bool = False,
|
||||
) -> 'CacheManager.Result[AlbumWithSongsID3]':
|
||||
@@ -828,7 +828,7 @@ class CacheManager(metaclass=Singleton):
|
||||
return CacheManager.Result.from_data(
|
||||
self.cache[cache_name][album_id])
|
||||
|
||||
def after_download(album):
|
||||
def after_download(album: AlbumWithSongsID3):
|
||||
with self.cache_lock:
|
||||
self.cache[cache_name][album_id] = album
|
||||
|
||||
@@ -865,24 +865,28 @@ class CacheManager(metaclass=Singleton):
|
||||
before_download: Callable[[], None],
|
||||
on_song_download_complete: Callable[[int], None],
|
||||
) -> Future:
|
||||
def do_download_song(song_id):
|
||||
# If a song download is already in the queue and then the ap is
|
||||
# exited, this prevents the download.
|
||||
if CacheManager.should_exit:
|
||||
return
|
||||
def do_download_song(song_id: int):
|
||||
try:
|
||||
# If a song download is already in the queue and then the
|
||||
# app is exited, this prevents the download.
|
||||
if CacheManager.should_exit:
|
||||
return
|
||||
|
||||
# Do the actual download. Call .result() because we are already
|
||||
# inside of a future.
|
||||
song = CacheManager.get_song_details(song_id).result()
|
||||
self.return_cached_or_download(
|
||||
song.path,
|
||||
lambda: self.server.download(song.id),
|
||||
before_download=before_download,
|
||||
).result()
|
||||
|
||||
# Allow the next song in the queue to be downloaded.
|
||||
self.download_limiter_semaphore.release()
|
||||
on_song_download_complete(song_id)
|
||||
# Do the actual download. Call .result() because we are
|
||||
# already inside of a future.
|
||||
song = CacheManager.get_song_details(song_id).result()
|
||||
self.return_cached_or_download(
|
||||
song.path,
|
||||
lambda: self.server.download(song.id),
|
||||
before_download=before_download,
|
||||
).result()
|
||||
on_song_download_complete(song_id)
|
||||
finally:
|
||||
# Release the semaphore lock. This will allow the next song
|
||||
# in the queue to be downloaded. I'm doing this in the
|
||||
# finally block so that it always runs, regardless of
|
||||
# whether an exception is thrown or the function returns.
|
||||
self.download_limiter_semaphore.release()
|
||||
|
||||
def do_batch_download_songs():
|
||||
for song_id in song_ids:
|
||||
@@ -927,7 +931,7 @@ class CacheManager(metaclass=Singleton):
|
||||
return CacheManager.Result.from_data(
|
||||
self.cache[cache_name][song_id])
|
||||
|
||||
def after_download(song_details):
|
||||
def after_download(song_details: Child):
|
||||
with self.cache_lock:
|
||||
self.cache[cache_name][song_id] = song_details
|
||||
self.save_cache_info()
|
||||
@@ -959,7 +963,7 @@ class CacheManager(metaclass=Singleton):
|
||||
def get_song_filename_or_stream(
|
||||
self,
|
||||
song: Child,
|
||||
format=None,
|
||||
format: str = None,
|
||||
force_stream: bool = False,
|
||||
) -> Tuple[str, bool]:
|
||||
abs_path = self.calculate_abs_path(song.path)
|
||||
@@ -979,7 +983,7 @@ class CacheManager(metaclass=Singleton):
|
||||
if self.cache.get('genres') and not force:
|
||||
return CacheManager.Result.from_data(self.cache['genres'])
|
||||
|
||||
def after_download(genres):
|
||||
def after_download(genres: List[Genre]):
|
||||
with self.cache_lock:
|
||||
self.cache['genres'] = genres
|
||||
self.save_cache_info()
|
||||
@@ -992,7 +996,7 @@ class CacheManager(metaclass=Singleton):
|
||||
|
||||
def search(
|
||||
self,
|
||||
query,
|
||||
query: str,
|
||||
search_callback: Callable[[SearchResult, bool], None],
|
||||
before_download: Callable[[], None] = lambda: None,
|
||||
) -> 'CacheManager.Result':
|
||||
|
@@ -3,16 +3,17 @@ import os
|
||||
import re
|
||||
|
||||
from collections import defaultdict
|
||||
from typing import Dict
|
||||
from typing import Any, Callable, Dict, Tuple
|
||||
|
||||
from deepdiff import DeepDiff
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
from .cache_manager import CacheManager
|
||||
from .state_manager import RepeatType
|
||||
from .players import Player
|
||||
from .state_manager import ApplicationState, RepeatType
|
||||
|
||||
|
||||
def dbus_propagate(param_self=None):
|
||||
def dbus_propagate(param_self: Any = None) -> Callable:
|
||||
"""
|
||||
Wraps a function which causes changes to DBus properties.
|
||||
"""
|
||||
@@ -34,10 +35,10 @@ class DBusManager:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
connection,
|
||||
connection: Gio.DBusConnection,
|
||||
do_on_method_call,
|
||||
on_set_property,
|
||||
get_state_and_player,
|
||||
get_state_and_player: Callable[[], Tuple[ApplicationState, Player]],
|
||||
):
|
||||
self.get_state_and_player = get_state_and_player
|
||||
self.do_on_method_call = do_on_method_call
|
||||
@@ -84,22 +85,22 @@ class DBusManager:
|
||||
|
||||
def on_get_property(
|
||||
self,
|
||||
connection,
|
||||
connection: Gio.DBusConnection,
|
||||
sender,
|
||||
path,
|
||||
interface,
|
||||
property_name,
|
||||
interface: str,
|
||||
property_name: str,
|
||||
):
|
||||
value = self.property_dict().get(interface, {}).get(property_name)
|
||||
return DBusManager.to_variant(value)
|
||||
|
||||
def on_method_call(
|
||||
self,
|
||||
connection,
|
||||
connection: Gio.DBusConnection,
|
||||
sender,
|
||||
path,
|
||||
interface,
|
||||
method,
|
||||
interface: str,
|
||||
method: str,
|
||||
params,
|
||||
invocation,
|
||||
):
|
||||
@@ -132,7 +133,7 @@ class DBusManager:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def to_variant(value):
|
||||
def to_variant(value: Any) -> GLib.Variant:
|
||||
if callable(value):
|
||||
return DBusManager.to_variant(value())
|
||||
|
||||
@@ -160,7 +161,7 @@ class DBusManager:
|
||||
return value
|
||||
return GLib.Variant(variant_type, value)
|
||||
|
||||
def property_dict(self):
|
||||
def property_dict(self) -> Dict[str, Any]:
|
||||
state, player = self.get_state_and_player()
|
||||
has_current_song = state.current_song is not None
|
||||
has_next_song = False
|
||||
|
@@ -306,7 +306,7 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
artist: ArtistWithAlbumsID3,
|
||||
state: ApplicationState,
|
||||
force: bool = False,
|
||||
order_token: Optional[int] = None,
|
||||
order_token: int = None,
|
||||
):
|
||||
if order_token != self.update_order_token:
|
||||
return
|
||||
@@ -337,7 +337,7 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
artist_info: ArtistInfo2,
|
||||
state: ApplicationState,
|
||||
force: bool = False,
|
||||
order_token: Optional[int] = None,
|
||||
order_token: int = None,
|
||||
):
|
||||
if order_token != self.update_order_token:
|
||||
return
|
||||
@@ -372,7 +372,7 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
cover_art_filename: str,
|
||||
state: ApplicationState,
|
||||
force: bool = False,
|
||||
order_token: Optional[int] = None,
|
||||
order_token: int = None,
|
||||
):
|
||||
if order_token != self.update_order_token:
|
||||
return
|
||||
|
@@ -1,14 +1,14 @@
|
||||
from typing import List, Tuple, Union
|
||||
from typing import Any, List, Optional, Tuple, Type, Union
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gio, GLib, GObject, Gtk, Pango
|
||||
from gi.repository import Gdk, Gio, GLib, GObject, Gtk, Pango
|
||||
|
||||
from sublime.cache_manager import CacheManager
|
||||
from sublime.server.api_objects import Artist, Child
|
||||
from sublime.server.api_objects import Artist, Child, Directory
|
||||
from sublime.state_manager import ApplicationState
|
||||
from sublime.ui import util
|
||||
from sublime.ui.common import IconButton
|
||||
from sublime.ui.common import IconButton, SongListColumn
|
||||
|
||||
|
||||
class BrowsePanel(Gtk.Overlay):
|
||||
@@ -55,13 +55,13 @@ class BrowsePanel(Gtk.Overlay):
|
||||
)
|
||||
self.add_overlay(self.spinner)
|
||||
|
||||
def update(self, state: ApplicationState, force=False):
|
||||
def update(self, state: ApplicationState, force: bool = False):
|
||||
if not CacheManager.ready:
|
||||
return
|
||||
|
||||
self.update_order_token += 1
|
||||
|
||||
def do_update(id_stack, update_order_token):
|
||||
def do_update(id_stack: List[int], update_order_token: int):
|
||||
if self.update_order_token != update_order_token:
|
||||
return
|
||||
|
||||
@@ -72,7 +72,7 @@ class BrowsePanel(Gtk.Overlay):
|
||||
)
|
||||
self.spinner.hide()
|
||||
|
||||
def calculate_path(update_order_token) -> Tuple[List[str], int]:
|
||||
def calculate_path(update_order_token: int) -> Tuple[List[str], int]:
|
||||
if state.selected_browse_element_id is None:
|
||||
return [], update_order_token
|
||||
|
||||
@@ -113,7 +113,7 @@ class ListAndDrilldown(Gtk.Paned):
|
||||
|
||||
id_stack = None
|
||||
|
||||
def __init__(self, list_type):
|
||||
def __init__(self, list_type: Type):
|
||||
Gtk.Paned.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
|
||||
|
||||
self.list = list_type()
|
||||
@@ -132,10 +132,10 @@ class ListAndDrilldown(Gtk.Paned):
|
||||
|
||||
def update(
|
||||
self,
|
||||
id_stack,
|
||||
id_stack: List[int],
|
||||
state: ApplicationState,
|
||||
force=False,
|
||||
directory_id=None,
|
||||
force: bool = False,
|
||||
directory_id: int = None,
|
||||
):
|
||||
self.list.update(
|
||||
None if len(id_stack) == 0 else id_stack[-1],
|
||||
@@ -237,19 +237,6 @@ class DrilldownList(Gtk.Box):
|
||||
str, # song ID
|
||||
)
|
||||
|
||||
def create_column(header, text_idx, bold=False, align=0, width=None):
|
||||
renderer = Gtk.CellRendererText(
|
||||
xalign=align,
|
||||
weight=Pango.Weight.BOLD if bold else Pango.Weight.NORMAL,
|
||||
ellipsize=Pango.EllipsizeMode.END,
|
||||
)
|
||||
renderer.set_fixed_size(width or -1, 35)
|
||||
|
||||
column = Gtk.TreeViewColumn(header, renderer, text=text_idx)
|
||||
column.set_resizable(True)
|
||||
column.set_expand(not width)
|
||||
return column
|
||||
|
||||
self.directory_song_list = Gtk.TreeView(
|
||||
model=self.directory_song_store,
|
||||
name='album-songs-list',
|
||||
@@ -266,9 +253,9 @@ class DrilldownList(Gtk.Box):
|
||||
self.directory_song_list.append_column(column)
|
||||
|
||||
self.directory_song_list.append_column(
|
||||
create_column('TITLE', 1, bold=True))
|
||||
SongListColumn('TITLE', 1, bold=True))
|
||||
self.directory_song_list.append_column(
|
||||
create_column('DURATION', 2, align=1, width=40))
|
||||
SongListColumn('DURATION', 2, align=1, width=40))
|
||||
|
||||
self.directory_song_list.connect(
|
||||
'row-activated', self.on_song_activated)
|
||||
@@ -279,7 +266,7 @@ class DrilldownList(Gtk.Box):
|
||||
self.scroll_window.add(scrollbox)
|
||||
self.pack_start(self.scroll_window, True, True, 0)
|
||||
|
||||
def on_song_activated(self, treeview, idx, column):
|
||||
def on_song_activated(self, treeview: Any, idx: Gtk.TreePath, column: Any):
|
||||
# The song ID is in the last column of the model.
|
||||
self.emit(
|
||||
'song-clicked',
|
||||
@@ -288,7 +275,11 @@ class DrilldownList(Gtk.Box):
|
||||
{},
|
||||
)
|
||||
|
||||
def on_song_button_press(self, tree, event):
|
||||
def on_song_button_press(
|
||||
self,
|
||||
tree: Gtk.TreeView,
|
||||
event: Gdk.EventButton,
|
||||
) -> bool:
|
||||
if event.button == 3: # Right click
|
||||
clicked_path = tree.get_path_at_pos(event.x, event.y)
|
||||
if not clicked_path:
|
||||
@@ -323,7 +314,9 @@ class DrilldownList(Gtk.Box):
|
||||
if not allow_deselect:
|
||||
return True
|
||||
|
||||
def do_update_store(self, elements):
|
||||
return False
|
||||
|
||||
def do_update_store(self, elements: Optional[List[Any]]):
|
||||
new_directories_store = []
|
||||
new_songs_store = []
|
||||
selected_dir_idx = None
|
||||
@@ -368,7 +361,8 @@ class DrilldownList(Gtk.Box):
|
||||
|
||||
self.loading_indicator.hide()
|
||||
|
||||
def create_row(self, model: 'DrilldownList.DrilldownElement'):
|
||||
def create_row(
|
||||
self, model: 'DrilldownList.DrilldownElement') -> Gtk.ListBoxRow:
|
||||
row = Gtk.ListBoxRow(
|
||||
action_name='app.browse-to',
|
||||
action_target=GLib.Variant('s', model.id),
|
||||
@@ -396,9 +390,9 @@ class IndexList(DrilldownList):
|
||||
|
||||
def update(
|
||||
self,
|
||||
selected_id,
|
||||
selected_id: int,
|
||||
state: ApplicationState = None,
|
||||
force=False,
|
||||
force: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
self.update_order_token += 1
|
||||
@@ -409,7 +403,7 @@ class IndexList(DrilldownList):
|
||||
order_token=self.update_order_token,
|
||||
)
|
||||
|
||||
def on_refresh_clicked(self, _):
|
||||
def on_refresh_clicked(self, _: Any):
|
||||
self.update(self.selected_id, force=True)
|
||||
|
||||
@util.async_callback(
|
||||
@@ -419,17 +413,17 @@ class IndexList(DrilldownList):
|
||||
)
|
||||
def update_store(
|
||||
self,
|
||||
artists,
|
||||
artists: List[Artist],
|
||||
state: ApplicationState = None,
|
||||
force=False,
|
||||
order_token=None,
|
||||
force: bool = False,
|
||||
order_token: int = None,
|
||||
):
|
||||
if order_token != self.update_order_token:
|
||||
return
|
||||
|
||||
self.do_update_store(artists)
|
||||
|
||||
def on_download_state_change(self, song_id=None):
|
||||
def on_download_state_change(self, song_id: int = None):
|
||||
self.update(self.selected_id)
|
||||
|
||||
|
||||
@@ -438,10 +432,10 @@ class MusicDirectoryList(DrilldownList):
|
||||
|
||||
def update(
|
||||
self,
|
||||
selected_id,
|
||||
selected_id: int,
|
||||
state: ApplicationState = None,
|
||||
force=False,
|
||||
directory_id=None,
|
||||
force: bool = False,
|
||||
directory_id: int = None,
|
||||
):
|
||||
self.directory_id = directory_id
|
||||
self.selected_id = selected_id
|
||||
@@ -452,7 +446,7 @@ class MusicDirectoryList(DrilldownList):
|
||||
order_token=self.update_order_token,
|
||||
)
|
||||
|
||||
def on_refresh_clicked(self, _):
|
||||
def on_refresh_clicked(self, _: Any):
|
||||
self.update(
|
||||
self.selected_id, force=True, directory_id=self.directory_id)
|
||||
|
||||
@@ -463,15 +457,15 @@ class MusicDirectoryList(DrilldownList):
|
||||
)
|
||||
def update_store(
|
||||
self,
|
||||
directory,
|
||||
directory: Directory,
|
||||
state: ApplicationState = None,
|
||||
force=False,
|
||||
order_token=None,
|
||||
force: bool = False,
|
||||
order_token: int = None,
|
||||
):
|
||||
if order_token != self.update_order_token:
|
||||
return
|
||||
|
||||
self.do_update_store(directory.child)
|
||||
|
||||
def on_download_state_change(self, song_id=None):
|
||||
def on_download_state_change(self, song_id: int = None):
|
||||
self.update(self.selected_id, directory_id=self.directory_id)
|
||||
|
@@ -1,11 +1,13 @@
|
||||
from .album_with_songs import AlbumWithSongs
|
||||
from .edit_form_dialog import EditFormDialog
|
||||
from .icon_button import IconButton
|
||||
from .song_list_column import SongListColumn
|
||||
from .spinner_image import SpinnerImage
|
||||
|
||||
__all__ = (
|
||||
'AlbumWithSongs',
|
||||
'EditFormDialog',
|
||||
'IconButton',
|
||||
'SongListColumn',
|
||||
'SpinnerImage',
|
||||
)
|
||||
|
@@ -1,5 +1,5 @@
|
||||
from random import randint
|
||||
from typing import Any, Optional, Union
|
||||
from typing import Any, Union
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
@@ -10,6 +10,7 @@ from sublime.server.api_objects import AlbumWithSongsID3, Child, Directory
|
||||
from sublime.state_manager import ApplicationState
|
||||
from sublime.ui import util
|
||||
from sublime.ui.common.icon_button import IconButton
|
||||
from sublime.ui.common.song_list_column import SongListColumn
|
||||
from sublime.ui.common.spinner_image import SpinnerImage
|
||||
|
||||
|
||||
@@ -123,25 +124,6 @@ class AlbumWithSongs(Gtk.Box):
|
||||
str, # song ID
|
||||
)
|
||||
|
||||
def create_column(
|
||||
header: str,
|
||||
text_idx: int,
|
||||
bold: bool = False,
|
||||
align: int = 0,
|
||||
width: Optional[int] = None,
|
||||
) -> Gtk.TreeViewColumn:
|
||||
renderer = Gtk.CellRendererText(
|
||||
xalign=align,
|
||||
weight=Pango.Weight.BOLD if bold else Pango.Weight.NORMAL,
|
||||
ellipsize=Pango.EllipsizeMode.END,
|
||||
)
|
||||
renderer.set_fixed_size(width or -1, 35)
|
||||
|
||||
column = Gtk.TreeViewColumn(header, renderer, text=text_idx)
|
||||
column.set_resizable(True)
|
||||
column.set_expand(not width)
|
||||
return column
|
||||
|
||||
self.loading_indicator = Gtk.Spinner(
|
||||
name='album-list-song-list-spinner')
|
||||
album_details.add(self.loading_indicator)
|
||||
@@ -164,9 +146,9 @@ class AlbumWithSongs(Gtk.Box):
|
||||
column.set_resizable(True)
|
||||
self.album_songs.append_column(column)
|
||||
|
||||
self.album_songs.append_column(create_column('TITLE', 1, bold=True))
|
||||
self.album_songs.append_column(SongListColumn('TITLE', 1, bold=True))
|
||||
self.album_songs.append_column(
|
||||
create_column('DURATION', 2, align=1, width=40))
|
||||
SongListColumn('DURATION', 2, align=1, width=40))
|
||||
|
||||
self.album_songs.connect('row-activated', self.on_song_activated)
|
||||
self.album_songs.connect(
|
||||
@@ -286,7 +268,7 @@ class AlbumWithSongs(Gtk.Box):
|
||||
album: Union[AlbumWithSongsID3, Child, Directory],
|
||||
state: ApplicationState,
|
||||
force: bool = False,
|
||||
order_token: Optional[int] = None,
|
||||
order_token: int = None,
|
||||
):
|
||||
new_store = [
|
||||
[
|
||||
|
@@ -31,5 +31,5 @@ class IconButton(Gtk.Button):
|
||||
|
||||
self.add(box)
|
||||
|
||||
def set_icon(self, icon_name: str):
|
||||
def set_icon(self, icon_name: Optional[str]):
|
||||
self.image.set_from_icon_name(icon_name, self.icon_size)
|
||||
|
24
sublime/ui/common/song_list_column.py
Normal file
24
sublime/ui/common/song_list_column.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Pango
|
||||
|
||||
|
||||
class SongListColumn(Gtk.TreeViewColumn):
|
||||
def __init__(
|
||||
self,
|
||||
header: str,
|
||||
text_idx: int,
|
||||
bold: bool = False,
|
||||
align: int = 0,
|
||||
width: int = None,
|
||||
):
|
||||
renderer = Gtk.CellRendererText(
|
||||
xalign=align,
|
||||
weight=Pango.Weight.BOLD if bold else Pango.Weight.NORMAL,
|
||||
ellipsize=Pango.EllipsizeMode.END,
|
||||
)
|
||||
renderer.set_fixed_size(width or -1, 35)
|
||||
|
||||
super().__init__(header, renderer, text=text_idx)
|
||||
self.set_resizable(True)
|
||||
self.set_expand(not width)
|
@@ -2,15 +2,16 @@ import math
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
from typing import Any, Callable, List
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import GdkPixbuf, GLib, GObject, Gtk, Pango
|
||||
from gi.repository import Gdk, GdkPixbuf, GLib, GObject, Gtk, Pango
|
||||
from pychromecast import Chromecast
|
||||
|
||||
from sublime.cache_manager import CacheManager
|
||||
from sublime.players import ChromecastPlayer
|
||||
from sublime.server.api_objects import Child
|
||||
from sublime.state_manager import ApplicationState, RepeatType
|
||||
from sublime.ui import util
|
||||
from sublime.ui.common import IconButton, SpinnerImage
|
||||
@@ -169,42 +170,53 @@ class PlayerControls(Gtk.ActionBar):
|
||||
|
||||
new_store = []
|
||||
|
||||
def calculate_label(song_details) -> str:
|
||||
def calculate_label(song_details: Child) -> str:
|
||||
title = util.esc(song_details.title)
|
||||
album = util.esc(song_details.album)
|
||||
artist = util.esc(song_details.artist)
|
||||
return f'<b>{title}</b>\n{util.dot_join(album, artist)}'
|
||||
|
||||
def make_idle_index_capturing_function(idx, order_token, fn):
|
||||
return lambda f: GLib.idle_add(
|
||||
fn, idx, order_token, f.result())
|
||||
def make_idle_index_capturing_function(
|
||||
idx: int,
|
||||
order_tok: int,
|
||||
fn: Callable[[int, int, Any], None],
|
||||
) -> Callable[[CacheManager.Result], None]:
|
||||
return lambda f: GLib.idle_add(fn, idx, order_tok, f.result())
|
||||
|
||||
def on_cover_art_future_done(idx, order_token, cover_art_filename):
|
||||
def on_cover_art_future_done(
|
||||
idx: int,
|
||||
order_token: int,
|
||||
cover_art_filename: str,
|
||||
):
|
||||
if order_token != self.play_queue_update_order_token:
|
||||
return
|
||||
|
||||
self.play_queue_store[idx][0] = cover_art_filename
|
||||
|
||||
def on_song_details_future_done(idx, order_token, song_details):
|
||||
def on_song_details_future_done(
|
||||
idx: int,
|
||||
order_token: int,
|
||||
song_details: Child,
|
||||
):
|
||||
if order_token != self.play_queue_update_order_token:
|
||||
return
|
||||
|
||||
self.play_queue_store[idx][1] = calculate_label(song_details)
|
||||
|
||||
# Cover Art
|
||||
cover_art_future = CacheManager.get_cover_art_filename(
|
||||
cover_art_result = CacheManager.get_cover_art_filename(
|
||||
song_details.coverArt)
|
||||
if cover_art_result.is_future:
|
||||
# We don't have the cover art already cached.
|
||||
cover_art_result.add_done_callback(
|
||||
make_idle_index_capturing_function(
|
||||
i,
|
||||
idx,
|
||||
order_token,
|
||||
on_cover_art_future_done,
|
||||
))
|
||||
else:
|
||||
# We have the cover art already cached.
|
||||
self.play_queue_store[idx][0] = cover_art_future.result()
|
||||
self.play_queue_store[idx][0] = cover_art_result.result()
|
||||
|
||||
if state.play_queue != [x[-1] for x in self.play_queue_store]:
|
||||
self.play_queue_update_order_token += 1
|
||||
@@ -268,7 +280,7 @@ class PlayerControls(Gtk.ActionBar):
|
||||
cover_art_filename: str,
|
||||
state: ApplicationState,
|
||||
force: bool = False,
|
||||
order_token: Optional[int] = None,
|
||||
order_token: int = None,
|
||||
):
|
||||
if order_token != self.cover_art_update_order_token:
|
||||
return
|
||||
@@ -276,7 +288,7 @@ class PlayerControls(Gtk.ActionBar):
|
||||
self.album_art.set_from_file(cover_art_filename)
|
||||
self.album_art.set_loading(False)
|
||||
|
||||
def update_scrubber(self, current, duration):
|
||||
def update_scrubber(self, current: float, duration: int):
|
||||
if current is None or duration is None:
|
||||
self.song_duration_label.set_text('-:--')
|
||||
self.song_progress_label.set_text('-:--')
|
||||
@@ -292,14 +304,11 @@ class PlayerControls(Gtk.ActionBar):
|
||||
self.song_progress_label.set_text(
|
||||
util.format_song_duration(math.floor(current)))
|
||||
|
||||
def on_scrub_change_value(self, scale, scroll_type, value):
|
||||
self.emit('song-scrub', value)
|
||||
|
||||
def on_volume_change(self, scale):
|
||||
def on_volume_change(self, scale: Gtk.Scale):
|
||||
if not self.editing:
|
||||
self.emit('volume-change', scale.get_value())
|
||||
|
||||
def on_play_queue_click(self, button):
|
||||
def on_play_queue_click(self, _: Any):
|
||||
if self.play_queue_popover.is_visible():
|
||||
self.play_queue_popover.popdown()
|
||||
else:
|
||||
@@ -307,7 +316,7 @@ class PlayerControls(Gtk.ActionBar):
|
||||
self.play_queue_popover.popup()
|
||||
self.play_queue_popover.show_all()
|
||||
|
||||
def on_song_activated(self, treeview, idx, column):
|
||||
def on_song_activated(self, t: Any, idx: Gtk.TreePath, c: Any):
|
||||
# The song ID is in the last column of the model.
|
||||
self.emit(
|
||||
'song-clicked',
|
||||
@@ -348,17 +357,17 @@ class PlayerControls(Gtk.ActionBar):
|
||||
self.last_device_list_update = datetime.now()
|
||||
|
||||
update_diff = (
|
||||
None if not self.last_device_list_update else
|
||||
(datetime.now() - self.last_device_list_update).seconds)
|
||||
self.last_device_list_update
|
||||
and (datetime.now() - self.last_device_list_update).seconds > 60)
|
||||
if (force or len(self.chromecasts) == 0
|
||||
or (self.last_device_list_update and update_diff > 60)):
|
||||
or (update_diff and update_diff > 60)):
|
||||
future = ChromecastPlayer.get_chromecasts()
|
||||
future.add_done_callback(
|
||||
lambda f: GLib.idle_add(chromecast_callback, f.result()))
|
||||
else:
|
||||
chromecast_callback(self.chromecasts)
|
||||
|
||||
def on_device_click(self, button):
|
||||
def on_device_click(self, _: Any):
|
||||
self.devices_requested = True
|
||||
if self.device_popover.is_visible():
|
||||
self.device_popover.popdown()
|
||||
@@ -367,17 +376,17 @@ class PlayerControls(Gtk.ActionBar):
|
||||
self.device_popover.show_all()
|
||||
self.update_device_list()
|
||||
|
||||
def on_device_refresh_click(self, button):
|
||||
def on_device_refresh_click(self, _: Any):
|
||||
self.update_device_list(force=True)
|
||||
|
||||
def on_play_queue_button_press(self, tree, event):
|
||||
def on_play_queue_button_press(self, tree: Any, event: Gdk.EventButton):
|
||||
if event.button == 3: # Right click
|
||||
clicked_path = tree.get_path_at_pos(event.x, event.y)
|
||||
|
||||
store, paths = tree.get_selection().get_selected_rows()
|
||||
allow_deselect = False
|
||||
|
||||
def on_download_state_change(song_id=None):
|
||||
def on_download_state_change(song_id: int = None):
|
||||
# Refresh the entire window (no force) because the song could
|
||||
# be in a list anywhere in the window.
|
||||
self.emit('refresh-window', {}, False)
|
||||
@@ -394,7 +403,7 @@ class PlayerControls(Gtk.ActionBar):
|
||||
'Remove ' + util.pluralize('song', len(song_ids))
|
||||
+ ' from queue')
|
||||
|
||||
def on_remove_songs_click(_):
|
||||
def on_remove_songs_click(_: Any):
|
||||
self.emit('songs-removed', [p.get_indices()[0] for p in paths])
|
||||
|
||||
util.show_song_popover(
|
||||
@@ -436,7 +445,7 @@ class PlayerControls(Gtk.ActionBar):
|
||||
else:
|
||||
self.reordering_play_queue_song_list = True
|
||||
|
||||
def create_song_display(self):
|
||||
def create_song_display(self) -> Gtk.Box:
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
|
||||
self.album_art = SpinnerImage(
|
||||
@@ -448,7 +457,7 @@ class PlayerControls(Gtk.ActionBar):
|
||||
details_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
details_box.pack_start(Gtk.Box(), True, True, 0)
|
||||
|
||||
def make_label(name):
|
||||
def make_label(name: str) -> Gtk.Label:
|
||||
return Gtk.Label(
|
||||
name=name,
|
||||
halign=Gtk.Align.START,
|
||||
@@ -471,7 +480,7 @@ class PlayerControls(Gtk.ActionBar):
|
||||
|
||||
return box
|
||||
|
||||
def create_playback_controls(self):
|
||||
def create_playback_controls(self) -> Gtk.Box:
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
|
||||
# Scrubber and song progress/length labels
|
||||
@@ -484,7 +493,8 @@ class PlayerControls(Gtk.ActionBar):
|
||||
orientation=Gtk.Orientation.HORIZONTAL, min=0, max=100, step=5)
|
||||
self.song_scrubber.set_name('song-scrubber')
|
||||
self.song_scrubber.set_draw_value(False)
|
||||
self.song_scrubber.connect('change-value', self.on_scrub_change_value)
|
||||
self.song_scrubber.connect(
|
||||
'change-value', lambda s, t, v: self.emit('song-scrub', v))
|
||||
scrubber_box.pack_start(self.song_scrubber, True, True, 0)
|
||||
|
||||
self.song_duration_label = Gtk.Label(label='-:--')
|
||||
@@ -533,7 +543,7 @@ class PlayerControls(Gtk.ActionBar):
|
||||
|
||||
return box
|
||||
|
||||
def create_play_queue_volume(self):
|
||||
def create_play_queue_volume(self) -> Gtk.Box:
|
||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
vbox.pack_start(Gtk.Box(), True, True, 0)
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
@@ -645,7 +655,13 @@ class PlayerControls(Gtk.ActionBar):
|
||||
Gtk.SelectionMode.MULTIPLE)
|
||||
|
||||
# Album Art column.
|
||||
def filename_to_pixbuf(column, cell, model, iter, flags):
|
||||
def filename_to_pixbuf(
|
||||
column: Any,
|
||||
cell: Gtk.CellRendererPixbuf,
|
||||
model: Gtk.ListStore,
|
||||
iter: Gtk.TreeIter,
|
||||
flags: Any,
|
||||
):
|
||||
filename = model.get_value(iter, 0)
|
||||
if not filename:
|
||||
cell.set_property('icon_name', '')
|
||||
|
@@ -1,6 +1,6 @@
|
||||
from functools import lru_cache
|
||||
from random import randint
|
||||
from typing import Any, Iterable, List, Optional, Tuple
|
||||
from typing import Any, Iterable, List, Tuple
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
@@ -11,7 +11,12 @@ from sublime.cache_manager import CacheManager
|
||||
from sublime.server.api_objects import PlaylistWithSongs
|
||||
from sublime.state_manager import ApplicationState
|
||||
from sublime.ui import util
|
||||
from sublime.ui.common import EditFormDialog, IconButton, SpinnerImage
|
||||
from sublime.ui.common import (
|
||||
EditFormDialog,
|
||||
IconButton,
|
||||
SongListColumn,
|
||||
SpinnerImage,
|
||||
)
|
||||
|
||||
|
||||
class EditPlaylistDialog(EditFormDialog):
|
||||
@@ -181,7 +186,7 @@ class PlaylistList(Gtk.Box):
|
||||
playlists: List[PlaylistWithSongs],
|
||||
state: ApplicationState,
|
||||
force: bool = False,
|
||||
order_token: Optional[int] = None,
|
||||
order_token: int = None,
|
||||
):
|
||||
new_store = []
|
||||
selected_idx = None
|
||||
@@ -340,25 +345,6 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
# Playlist songs list
|
||||
playlist_view_scroll_window = Gtk.ScrolledWindow()
|
||||
|
||||
def create_column(
|
||||
header: str,
|
||||
text_idx: int,
|
||||
bold: bool = False,
|
||||
align: int = 0,
|
||||
width: int = None,
|
||||
):
|
||||
renderer = Gtk.CellRendererText(
|
||||
xalign=align,
|
||||
weight=Pango.Weight.BOLD if bold else Pango.Weight.NORMAL,
|
||||
ellipsize=Pango.EllipsizeMode.END,
|
||||
)
|
||||
renderer.set_fixed_size(width or -1, 35)
|
||||
|
||||
column = Gtk.TreeViewColumn(header, renderer, text=text_idx)
|
||||
column.set_resizable(True)
|
||||
column.set_expand(not width)
|
||||
return column
|
||||
|
||||
self.playlist_song_store = Gtk.ListStore(
|
||||
str, # cache status
|
||||
str, # title
|
||||
@@ -404,11 +390,12 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
column.set_resizable(True)
|
||||
self.playlist_songs.append_column(column)
|
||||
|
||||
self.playlist_songs.append_column(create_column('TITLE', 1, bold=True))
|
||||
self.playlist_songs.append_column(create_column('ALBUM', 2))
|
||||
self.playlist_songs.append_column(create_column('ARTIST', 3))
|
||||
self.playlist_songs.append_column(
|
||||
create_column('DURATION', 4, align=1, width=40))
|
||||
SongListColumn('TITLE', 1, bold=True))
|
||||
self.playlist_songs.append_column(SongListColumn('ALBUM', 2))
|
||||
self.playlist_songs.append_column(SongListColumn('ARTIST', 3))
|
||||
self.playlist_songs.append_column(
|
||||
SongListColumn('DURATION', 4, align=1, width=40))
|
||||
|
||||
self.playlist_songs.connect('row-activated', self.on_song_activated)
|
||||
self.playlist_songs.connect(
|
||||
@@ -470,7 +457,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
playlist: PlaylistWithSongs,
|
||||
state: ApplicationState = None,
|
||||
force: bool = False,
|
||||
order_token: Optional[int] = None,
|
||||
order_token: int = None,
|
||||
):
|
||||
if self.update_playlist_view_order_token != order_token:
|
||||
return
|
||||
@@ -532,7 +519,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
cover_art_filename: str,
|
||||
state: ApplicationState,
|
||||
force: bool = False,
|
||||
order_token: Optional[int] = None,
|
||||
order_token: int = None,
|
||||
):
|
||||
if self.update_playlist_view_order_token != order_token:
|
||||
return
|
||||
|
@@ -1,7 +1,17 @@
|
||||
import functools
|
||||
import re
|
||||
from concurrent.futures import Future
|
||||
from typing import Any, Callable, cast, List, Match, Optional, Tuple, Union
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
cast,
|
||||
Iterable,
|
||||
List,
|
||||
Match,
|
||||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
import gi
|
||||
from deepdiff import DeepDiff
|
||||
@@ -9,6 +19,7 @@ gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gdk, GLib, Gtk
|
||||
|
||||
from sublime.cache_manager import CacheManager, SongCacheStatus
|
||||
from sublime.server.api_objects import Playlist
|
||||
from sublime.state_manager import ApplicationState
|
||||
|
||||
|
||||
@@ -28,7 +39,7 @@ def format_song_duration(duration_secs: int) -> str:
|
||||
def pluralize(
|
||||
string: str,
|
||||
number: int,
|
||||
pluralized_form: Optional[str] = None,
|
||||
pluralized_form: str = None,
|
||||
) -> str:
|
||||
"""
|
||||
Pluralize the given string given the count as a number.
|
||||
@@ -102,12 +113,12 @@ def get_cached_status_icon(cache_status: SongCacheStatus) -> str:
|
||||
return cache_icon[cache_status]
|
||||
|
||||
|
||||
def _parse_diff_location(location: str):
|
||||
def _parse_diff_location(location: str) -> Tuple:
|
||||
match = re.match(r'root\[(\d*)\](?:\[(\d*)\]|\.(.*))?', location)
|
||||
return tuple(g for g in cast(Match, match).groups() if g is not None)
|
||||
|
||||
|
||||
def diff_song_store(store_to_edit, new_store):
|
||||
def diff_song_store(store_to_edit: Any, new_store: Iterable[Any]):
|
||||
"""
|
||||
Diffing song stores is nice, because we can easily make edits by modifying
|
||||
the underlying store.
|
||||
@@ -132,7 +143,7 @@ def diff_song_store(store_to_edit, new_store):
|
||||
del store_to_edit[remove_at]
|
||||
|
||||
|
||||
def diff_model_store(store_to_edit, new_store):
|
||||
def diff_model_store(store_to_edit: Any, new_store: Iterable[Any]):
|
||||
"""
|
||||
The diff here is that if there are any differences, then we refresh the
|
||||
entire list. This is because it is too hard to do editing.
|
||||
@@ -158,20 +169,20 @@ def show_song_popover(
|
||||
show_remove_from_playlist_button: bool = False,
|
||||
extra_menu_items: List[Tuple[Gtk.ModelButton, Any]] = [],
|
||||
):
|
||||
def on_download_songs_click(button):
|
||||
def on_download_songs_click(_: Any):
|
||||
CacheManager.batch_download_songs(
|
||||
song_ids,
|
||||
before_download=on_download_state_change,
|
||||
on_song_download_complete=on_download_state_change,
|
||||
)
|
||||
|
||||
def on_remove_downloads_click(button):
|
||||
def on_remove_downloads_click(_: Any):
|
||||
CacheManager.batch_delete_cached_songs(
|
||||
song_ids,
|
||||
on_song_delete=on_download_state_change,
|
||||
)
|
||||
|
||||
def on_add_to_playlist_click(button, playlist):
|
||||
def on_add_to_playlist_click(_: Any, playlist: Playlist):
|
||||
CacheManager.executor.submit(
|
||||
CacheManager.update_playlist,
|
||||
playlist_id=playlist.id,
|
||||
@@ -327,7 +338,7 @@ def async_callback(
|
||||
*args,
|
||||
state: ApplicationState = None,
|
||||
force: bool = False,
|
||||
order_token: Optional[int] = None,
|
||||
order_token: int = None,
|
||||
**kwargs,
|
||||
):
|
||||
if before_download:
|
||||
|
Reference in New Issue
Block a user