Closes #174: adds fill level for streaming
This commit is contained in:
@@ -126,19 +126,16 @@ class SubsonicAdapter(Adapter):
|
||||
|
||||
def initial_sync(self):
|
||||
# Wait for the ping to happen.
|
||||
t = 0
|
||||
while not self._server_available.value:
|
||||
sleep(0.1)
|
||||
t += 0.1
|
||||
if t >= 10: # timeout of 10 seconds on initial synchronization.
|
||||
break
|
||||
tries = 5
|
||||
while not self._server_available.value and tries < 5:
|
||||
self._set_ping_status()
|
||||
tries += 1
|
||||
|
||||
def shutdown(self):
|
||||
self.ping_process.terminate()
|
||||
|
||||
# Availability Properties
|
||||
# ==================================================================================
|
||||
_first_ping_happened = multiprocessing.Value("b", False)
|
||||
_server_available = multiprocessing.Value("b", False)
|
||||
|
||||
def _check_ping_thread(self):
|
||||
@@ -147,12 +144,8 @@ class SubsonicAdapter(Adapter):
|
||||
# TODO: also use NM to detect when the connection changes and update
|
||||
# accordingly.
|
||||
|
||||
# Try 5 times to ping the server.
|
||||
tries = 0
|
||||
while not self._server_available.value and tries < 5:
|
||||
while True:
|
||||
self._set_ping_status()
|
||||
|
||||
self._first_ping_happened.value = True
|
||||
sleep(15)
|
||||
|
||||
# TODO maybe expose something like this on the API?
|
||||
|
@@ -174,6 +174,7 @@ class SublimeMusicApp(Gtk.Application):
|
||||
self.window.player_controls.update_scrubber,
|
||||
self.app_config.state.song_progress,
|
||||
self.app_config.state.current_song.duration,
|
||||
self.app_config.state.song_stream_cache_progress,
|
||||
)
|
||||
|
||||
if (self.last_play_queue_update + timedelta(15)).total_seconds() <= value:
|
||||
@@ -201,12 +202,34 @@ class SublimeMusicApp(Gtk.Application):
|
||||
|
||||
GLib.idle_add(self.on_next_track)
|
||||
|
||||
@dbus_propagate(self)
|
||||
def on_player_event(event: PlayerEvent):
|
||||
if event.name == "play_state_change":
|
||||
self.app_config.state.playing = event.value
|
||||
elif event.name == "volume_change":
|
||||
self.app_config.state.volume = event.value
|
||||
if event.type == PlayerEvent.Type.PLAY_STATE_CHANGE:
|
||||
assert event.playing
|
||||
self.app_config.state.playing = event.playing
|
||||
if self.dbus_manager:
|
||||
self.dbus_manager.property_diff()
|
||||
elif event.type == PlayerEvent.Type.VOLUME_CHANGE:
|
||||
assert event.volume
|
||||
self.app_config.state.volume = event.volume
|
||||
if self.dbus_manager:
|
||||
self.dbus_manager.property_diff()
|
||||
elif event.type == PlayerEvent.Type.STREAM_CACHE_PROGRESS_CHANGE:
|
||||
if (
|
||||
self.loading_state
|
||||
or not self.window
|
||||
or not self.app_config.state.current_song
|
||||
or not event.stream_cache_duration
|
||||
):
|
||||
return
|
||||
self.app_config.state.song_stream_cache_progress = timedelta(
|
||||
seconds=event.stream_cache_duration
|
||||
)
|
||||
GLib.idle_add(
|
||||
self.window.player_controls.update_scrubber,
|
||||
self.app_config.state.song_progress,
|
||||
self.app_config.state.current_song.duration,
|
||||
self.app_config.state.song_stream_cache_progress,
|
||||
)
|
||||
|
||||
self.update_window()
|
||||
|
||||
@@ -751,6 +774,7 @@ class SublimeMusicApp(Gtk.Application):
|
||||
self.window.player_controls.update_scrubber(
|
||||
self.app_config.state.song_progress,
|
||||
self.app_config.state.current_song.duration,
|
||||
self.app_config.state.song_stream_cache_progress,
|
||||
)
|
||||
|
||||
# If already playing, then make the player itself seek.
|
||||
@@ -919,12 +943,11 @@ class SublimeMusicApp(Gtk.Application):
|
||||
self.app_config.state.current_song_index = (
|
||||
play_queue.current_index or 0
|
||||
)
|
||||
self.player.reset()
|
||||
self.update_window()
|
||||
|
||||
self.player.reset()
|
||||
self.update_window()
|
||||
|
||||
if was_playing:
|
||||
self.on_play_pause()
|
||||
if was_playing:
|
||||
self.on_play_pause()
|
||||
|
||||
play_queue_future = AdapterManager.get_play_queue()
|
||||
play_queue_future.add_done_callback(lambda f: GLib.idle_add(do_update, f))
|
||||
|
@@ -7,7 +7,9 @@ import os
|
||||
import socket
|
||||
import threading
|
||||
from concurrent.futures import Future, ThreadPoolExecutor
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from time import sleep
|
||||
from typing import Any, Callable, List, Optional
|
||||
from urllib.parse import urlparse
|
||||
@@ -22,14 +24,17 @@ from sublime.adapters.api_objects import Song
|
||||
from sublime.config import AppConfiguration
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlayerEvent:
|
||||
# TODO standardize this
|
||||
name: str
|
||||
value: Any
|
||||
class Type(Enum):
|
||||
PLAY_STATE_CHANGE = 0
|
||||
VOLUME_CHANGE = 1
|
||||
STREAM_CACHE_PROGRESS_CHANGE = 2
|
||||
|
||||
def __init__(self, name: str, value: Any):
|
||||
self.name = name
|
||||
self.value = value
|
||||
type: Type
|
||||
playing: Optional[bool] = False
|
||||
volume: Optional[float] = 0.0
|
||||
stream_cache_duration: Optional[float] = 0.0
|
||||
|
||||
|
||||
class Player(abc.ABC):
|
||||
@@ -37,6 +42,7 @@ class Player(abc.ABC):
|
||||
# because it's kinda a bit strange tbh.
|
||||
_can_hotswap_source: bool
|
||||
|
||||
# TODO unify on_timepos_change and on_player_event?
|
||||
def __init__(
|
||||
self,
|
||||
on_timepos_change: Callable[[Optional[float]], None],
|
||||
@@ -153,7 +159,7 @@ class MPVPlayer(Player):
|
||||
self._can_hotswap_source = True
|
||||
|
||||
@self.mpv.property_observer("time-pos")
|
||||
def time_observer(_: Any, value: Optional[float]):
|
||||
def time_observer(_, value: Optional[float]):
|
||||
self.on_timepos_change(value)
|
||||
if value is None and self.progress_value_count > 1:
|
||||
self.on_track_end()
|
||||
@@ -164,6 +170,15 @@ class MPVPlayer(Player):
|
||||
with self.progress_value_lock:
|
||||
self.progress_value_count += 1
|
||||
|
||||
@self.mpv.property_observer("demuxer-cache-time")
|
||||
def cache_size_observer(_, value: Optional[float]):
|
||||
on_player_event(
|
||||
PlayerEvent(
|
||||
PlayerEvent.Type.STREAM_CACHE_PROGRESS_CHANGE,
|
||||
stream_cache_duration=value,
|
||||
)
|
||||
)
|
||||
|
||||
def _is_playing(self) -> bool:
|
||||
return not self.mpv.pause
|
||||
|
||||
@@ -182,7 +197,7 @@ class MPVPlayer(Player):
|
||||
"loadfile",
|
||||
file_or_url,
|
||||
"replace",
|
||||
f"start={progress.total_seconds()}" if progress else "",
|
||||
f"force-seekable=yes,start={progress.total_seconds()}" if progress else "",
|
||||
)
|
||||
self._song_loaded = True
|
||||
|
||||
@@ -360,15 +375,17 @@ class ChromecastPlayer(Player):
|
||||
):
|
||||
self.on_player_event(
|
||||
PlayerEvent(
|
||||
"volume_change",
|
||||
status.volume_level * 100 if not status.volume_muted else 0,
|
||||
PlayerEvent.Type.VOLUME_CHANGE,
|
||||
volume=(status.volume_level * 100 if not status.volume_muted else 0),
|
||||
)
|
||||
)
|
||||
|
||||
# This normally happens when "Stop Casting" is pressed in the Google
|
||||
# Home app.
|
||||
if status.session_id is None:
|
||||
self.on_player_event(PlayerEvent("play_state_change", False))
|
||||
self.on_player_event(
|
||||
PlayerEvent(PlayerEvent.Type.PLAY_STATE_CHANGE, playing=False)
|
||||
)
|
||||
self._song_loaded = False
|
||||
|
||||
def on_new_media_status(
|
||||
@@ -386,7 +403,8 @@ class ChromecastPlayer(Player):
|
||||
|
||||
self.on_player_event(
|
||||
PlayerEvent(
|
||||
"play_state_change", status.player_state in ("PLAYING", "BUFFERING"),
|
||||
PlayerEvent.Type.PLAY_STATE_CHANGE,
|
||||
playing=(status.player_state in ("PLAYING", "BUFFERING")),
|
||||
)
|
||||
)
|
||||
|
||||
|
@@ -71,7 +71,14 @@ class PlayerControls(Gtk.ActionBar):
|
||||
if app_config.state.current_song
|
||||
else None
|
||||
)
|
||||
self.update_scrubber(app_config.state.song_progress, duration)
|
||||
song_stream_cache_progress = (
|
||||
app_config.state.song_stream_cache_progress
|
||||
if app_config.state.current_song
|
||||
else None
|
||||
)
|
||||
self.update_scrubber(
|
||||
app_config.state.song_progress, duration, song_stream_cache_progress
|
||||
)
|
||||
|
||||
icon = "pause" if app_config.state.playing else "start"
|
||||
self.play_button.set_icon(f"media-playback-{icon}-symbolic")
|
||||
@@ -306,7 +313,10 @@ class PlayerControls(Gtk.ActionBar):
|
||||
self.album_art.set_loading(False)
|
||||
|
||||
def update_scrubber(
|
||||
self, current: Optional[timedelta], duration: Optional[timedelta],
|
||||
self,
|
||||
current: Optional[timedelta],
|
||||
duration: Optional[timedelta],
|
||||
song_stream_cache_progress: Optional[timedelta],
|
||||
):
|
||||
if current is None or duration is None:
|
||||
self.song_duration_label.set_text("-:--")
|
||||
@@ -314,10 +324,16 @@ class PlayerControls(Gtk.ActionBar):
|
||||
self.song_scrubber.set_value(0)
|
||||
return
|
||||
|
||||
percent_complete = current.total_seconds() / duration.total_seconds() * 100
|
||||
percent_complete = current / duration * 100
|
||||
|
||||
if not self.editing:
|
||||
self.song_scrubber.set_value(percent_complete)
|
||||
|
||||
self.song_scrubber.set_show_fill_level(song_stream_cache_progress is not None)
|
||||
if song_stream_cache_progress:
|
||||
percent_cached = song_stream_cache_progress / duration * 100
|
||||
self.song_scrubber.set_fill_level(percent_cached)
|
||||
|
||||
self.song_duration_label.set_text(util.format_song_duration(duration))
|
||||
self.song_progress_label.set_text(
|
||||
util.format_song_duration(math.floor(current.total_seconds()))
|
||||
@@ -517,6 +533,7 @@ class PlayerControls(Gtk.ActionBar):
|
||||
)
|
||||
self.song_scrubber.set_name("song-scrubber")
|
||||
self.song_scrubber.set_draw_value(False)
|
||||
self.song_scrubber.set_restrict_to_fill_level(False)
|
||||
self.song_scrubber.connect(
|
||||
"change-value", lambda s, t, v: self.emit("song-scrub", v)
|
||||
)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from typing import Dict, Optional, Tuple
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
from sublime.adapters import AlbumSearchQuery
|
||||
from sublime.adapters.api_objects import Genre, Song
|
||||
@@ -45,6 +45,7 @@ class UIState:
|
||||
repeat_type: RepeatType = RepeatType.NO_REPEAT
|
||||
shuffle_on: bool = False
|
||||
song_progress: timedelta = timedelta()
|
||||
song_stream_cache_progress: Optional[timedelta] = timedelta()
|
||||
current_device: str = "this device"
|
||||
current_tab: str = "albums"
|
||||
selected_album_id: Optional[str] = None
|
||||
@@ -55,6 +56,15 @@ class UIState:
|
||||
album_page_size: int = 30
|
||||
album_page: int = 0
|
||||
|
||||
def __getstate__(self):
|
||||
state = self.__dict__.copy()
|
||||
del state["song_stream_cache_progress"]
|
||||
return state
|
||||
|
||||
def __setstate__(self, state: Dict[str, Any]):
|
||||
self.__dict__.update(state)
|
||||
self.song_stream_cache_progress = None
|
||||
|
||||
class _DefaultGenre(Genre):
|
||||
def __init__(self):
|
||||
self.name = "Rock"
|
||||
|
Reference in New Issue
Block a user