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