Player refactor works with MPV player
This commit is contained in:
@@ -651,6 +651,7 @@ class SubsonicAdapter(Adapter):
|
|||||||
self._get(
|
self._get(
|
||||||
self._make_url("savePlayQueue"),
|
self._make_url("savePlayQueue"),
|
||||||
id=song_ids,
|
id=song_ids,
|
||||||
|
timeout=2,
|
||||||
current=song_ids[current_song_index]
|
current=song_ids[current_song_index]
|
||||||
if current_song_index is not None
|
if current_song_index is not None
|
||||||
else None,
|
else None,
|
||||||
|
@@ -265,7 +265,39 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def player_device_change_callback(event: PlayerDeviceEvent):
|
def player_device_change_callback(event: PlayerDeviceEvent):
|
||||||
pass
|
assert self.player_manager
|
||||||
|
state_device = self.app_config.state.current_device
|
||||||
|
|
||||||
|
if event.delta == PlayerDeviceEvent.Delta.ADD:
|
||||||
|
# If the device added is the one that's supposed to be active, activate
|
||||||
|
# it and set the volume.
|
||||||
|
if event.id == state_device:
|
||||||
|
self.player_manager.set_current_device_id(
|
||||||
|
self.app_config.state.current_device
|
||||||
|
)
|
||||||
|
self.player_manager.set_volume(self.app_config.state.volume)
|
||||||
|
self.app_config.state.connecting_to_device = False
|
||||||
|
# TODO actually add it
|
||||||
|
print("ADDED", event)
|
||||||
|
|
||||||
|
elif event.delta == PlayerDeviceEvent.Delta.REMOVE:
|
||||||
|
print("REMOVED", event)
|
||||||
|
|
||||||
|
self.update_window()
|
||||||
|
|
||||||
|
self.app_config.state.connecting_to_device = True
|
||||||
|
|
||||||
|
def check_if_connected():
|
||||||
|
if not self.app_config.state.connecting_to_device:
|
||||||
|
return
|
||||||
|
print(
|
||||||
|
f"Couldn't find device {self.app_config.state.current_device} in time."
|
||||||
|
)
|
||||||
|
self.app_config.state.current_device = "this device"
|
||||||
|
self.player_manager.set_current_device_id(
|
||||||
|
self.app_config.state.current_device
|
||||||
|
)
|
||||||
|
self.update_window()
|
||||||
|
|
||||||
self.player_manager = PlayerManager(
|
self.player_manager = PlayerManager(
|
||||||
on_timepos_change,
|
on_timepos_change,
|
||||||
@@ -274,15 +306,8 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
player_device_change_callback,
|
player_device_change_callback,
|
||||||
self.app_config.player_config,
|
self.app_config.player_config,
|
||||||
)
|
)
|
||||||
|
self.player_manager.init_players()
|
||||||
if self.app_config.state.current_device != "this device":
|
GLib.timeout_add(10000, check_if_connected)
|
||||||
# TODO (#120) attempt to connect to the previously connected device
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.app_config.state.current_device = "this device"
|
|
||||||
|
|
||||||
# Need to do this after we set the current device.
|
|
||||||
self.player.volume = self.app_config.state.volume
|
|
||||||
|
|
||||||
# Update after Adapter Initial Sync
|
# Update after Adapter Initial Sync
|
||||||
inital_sync_result = AdapterManager.initial_sync()
|
inital_sync_result = AdapterManager.initial_sync()
|
||||||
@@ -311,7 +336,7 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
connection,
|
connection,
|
||||||
self.on_dbus_method_call,
|
self.on_dbus_method_call,
|
||||||
self.on_dbus_set_property,
|
self.on_dbus_set_property,
|
||||||
lambda: (self.app_config, self.player),
|
lambda: (self.app_config, self.player_manager),
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -604,8 +629,8 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
|
|
||||||
self.app_config.state.playing = not self.app_config.state.playing
|
self.app_config.state.playing = not self.app_config.state.playing
|
||||||
|
|
||||||
if self.player.song_loaded:
|
if self.player_manager.song_loaded:
|
||||||
self.player.toggle_play()
|
self.player_manager.toggle_play()
|
||||||
self.save_play_queue()
|
self.save_play_queue()
|
||||||
else:
|
else:
|
||||||
# This is from a restart, start playing the file.
|
# This is from a restart, start playing the file.
|
||||||
@@ -743,7 +768,7 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
if self.app_config.state.playing:
|
if self.app_config.state.playing:
|
||||||
self.on_play_pause()
|
self.on_play_pause()
|
||||||
self.loading_state = True
|
self.loading_state = True
|
||||||
self.player.reset()
|
self.player_manager.reset()
|
||||||
AdapterManager.reset(self.app_config, self.on_song_download_progress)
|
AdapterManager.reset(self.app_config, self.on_song_download_progress)
|
||||||
self.loading_state = False
|
self.loading_state = False
|
||||||
|
|
||||||
@@ -838,20 +863,21 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# If already playing, then make the player itself seek.
|
# If already playing, then make the player itself seek.
|
||||||
if self.player and self.player.song_loaded:
|
if self.player_manager and self.player_manager.song_loaded:
|
||||||
self.player.seek(new_time)
|
self.player_manager.seek(new_time)
|
||||||
|
|
||||||
self.save_play_queue()
|
self.save_play_queue()
|
||||||
|
|
||||||
def on_device_update(self, win: Any, device_uuid: str):
|
def on_device_update(self, win: Any, device_uuid: str):
|
||||||
assert self.player
|
# TODO
|
||||||
|
assert self.player_manager
|
||||||
if device_uuid == self.app_config.state.current_device:
|
if device_uuid == self.app_config.state.current_device:
|
||||||
return
|
return
|
||||||
self.app_config.state.current_device = device_uuid
|
self.app_config.state.current_device = device_uuid
|
||||||
|
|
||||||
was_playing = self.app_config.state.playing
|
was_playing = self.app_config.state.playing
|
||||||
self.player.pause()
|
self.player_manager.pause()
|
||||||
self.player._song_loaded = False
|
self.player_manager._song_loaded = False
|
||||||
self.app_config.state.playing = False
|
self.app_config.state.playing = False
|
||||||
|
|
||||||
if self.dbus_manager:
|
if self.dbus_manager:
|
||||||
@@ -873,14 +899,14 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
@dbus_propagate()
|
@dbus_propagate()
|
||||||
def on_mute_toggle(self, *args):
|
def on_mute_toggle(self, *args):
|
||||||
self.app_config.state.is_muted = not self.app_config.state.is_muted
|
self.app_config.state.is_muted = not self.app_config.state.is_muted
|
||||||
self.player.is_muted = self.app_config.state.is_muted
|
self.player_manager.set_muted(self.app_config.state.is_muted)
|
||||||
self.update_window()
|
self.update_window()
|
||||||
|
|
||||||
@dbus_propagate()
|
@dbus_propagate()
|
||||||
def on_volume_change(self, _, value: float):
|
def on_volume_change(self, _, value: float):
|
||||||
assert self.player
|
assert self.player_manager
|
||||||
self.app_config.state.volume = value
|
self.app_config.state.volume = value
|
||||||
self.player.volume = self.app_config.state.volume
|
self.player_manager.set_volume(self.app_config.state.volume)
|
||||||
self.update_window()
|
self.update_window()
|
||||||
|
|
||||||
def on_window_key_press(self, window: Gtk.Window, event: Gdk.EventKey) -> bool:
|
def on_window_key_press(self, window: Gtk.Window, event: Gdk.EventKey) -> bool:
|
||||||
@@ -926,13 +952,11 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
if self.app_config.provider is None:
|
if self.app_config.provider is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.player:
|
if self.player_manager:
|
||||||
self.player.pause()
|
self.player_manager.pause()
|
||||||
self.chromecast_player.shutdown()
|
self.player_manager.shutdown()
|
||||||
self.mpv_player.shutdown()
|
|
||||||
|
|
||||||
self.app_config.save()
|
self.app_config.save()
|
||||||
self.save_play_queue()
|
|
||||||
if self.dbus_manager:
|
if self.dbus_manager:
|
||||||
self.dbus_manager.shutdown()
|
self.dbus_manager.shutdown()
|
||||||
AdapterManager.shutdown()
|
AdapterManager.shutdown()
|
||||||
@@ -973,7 +997,7 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
# prompt_confirm is False.
|
# prompt_confirm is False.
|
||||||
if not prompt_confirm and self.app_config.state.playing:
|
if not prompt_confirm and self.app_config.state.playing:
|
||||||
assert self.player
|
assert self.player
|
||||||
self.player.pause()
|
self.player_manager.pause()
|
||||||
self.app_config.state.playing = False
|
self.app_config.state.playing = False
|
||||||
self.update_window()
|
self.update_window()
|
||||||
|
|
||||||
@@ -1025,7 +1049,7 @@ 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.player_manager.reset()
|
||||||
self.app_config.state.current_notification = None
|
self.app_config.state.current_notification = None
|
||||||
self.update_window()
|
self.update_window()
|
||||||
|
|
||||||
@@ -1053,7 +1077,7 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
playable_song_search_direction: int = 1,
|
playable_song_search_direction: int = 1,
|
||||||
):
|
):
|
||||||
def do_reset():
|
def do_reset():
|
||||||
self.player.reset()
|
self.player_manager.reset()
|
||||||
self.app_config.state.song_progress = timedelta(0)
|
self.app_config.state.song_progress = timedelta(0)
|
||||||
self.should_scrobble_song = True
|
self.should_scrobble_song = True
|
||||||
|
|
||||||
@@ -1061,7 +1085,7 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
# in the callback.
|
# in the callback.
|
||||||
@dbus_propagate(self)
|
@dbus_propagate(self)
|
||||||
def do_play_song(order_token: int, song: Song):
|
def do_play_song(order_token: int, song: Song):
|
||||||
assert self.player
|
assert self.player_manager
|
||||||
if order_token != self.song_playing_order_token:
|
if order_token != self.song_playing_order_token:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1076,7 +1100,7 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
if order_token != self.song_playing_order_token:
|
if order_token != self.song_playing_order_token:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.player.play_media(
|
self.player_manager.play_media(
|
||||||
uri,
|
uri,
|
||||||
timedelta(0) if reset else self.app_config.state.song_progress,
|
timedelta(0) if reset else self.app_config.state.song_progress,
|
||||||
song,
|
song,
|
||||||
@@ -1163,9 +1187,10 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
# Switch to the local media if the player can hotswap without lag.
|
# Switch to the local media if the player can hotswap without lag.
|
||||||
# For example, MPV can is barely noticable whereas there's quite a
|
# For example, MPV can is barely noticable whereas there's quite a
|
||||||
# delay with Chromecast.
|
# delay with Chromecast.
|
||||||
assert self.player
|
assert self.player_manager
|
||||||
if self.player.can_hotswap_source:
|
if self.player_manager.can_start_playing_with_no_latency:
|
||||||
self.player.play_media(
|
print(self.player_manager)
|
||||||
|
self.player_manager.play_media(
|
||||||
AdapterManager.get_song_filename_or_stream(song),
|
AdapterManager.get_song_filename_or_stream(song),
|
||||||
self.app_config.state.song_progress,
|
self.app_config.state.song_progress,
|
||||||
song,
|
song,
|
||||||
|
@@ -190,7 +190,7 @@ class DBusManager:
|
|||||||
|
|
||||||
def property_dict(self) -> Dict[str, Any]:
|
def property_dict(self) -> Dict[str, Any]:
|
||||||
config, player_manager = self.get_config_and_player_manager()
|
config, player_manager = self.get_config_and_player_manager()
|
||||||
if config is None or player is None:
|
if config is None or player_manager is None:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
state = config.state
|
state = config.state
|
||||||
@@ -227,7 +227,7 @@ class DBusManager:
|
|||||||
(False, True): "Stopped",
|
(False, True): "Stopped",
|
||||||
(True, False): "Paused",
|
(True, False): "Paused",
|
||||||
(True, True): "Playing",
|
(True, True): "Playing",
|
||||||
}[player is not None and player.song_loaded, state.playing],
|
}[player_manager.song_loaded, state.playing],
|
||||||
"LoopStatus": state.repeat_type.as_mpris_loop_status(),
|
"LoopStatus": state.repeat_type.as_mpris_loop_status(),
|
||||||
"Rate": 1.0,
|
"Rate": 1.0,
|
||||||
"Shuffle": state.shuffle_on,
|
"Shuffle": state.shuffle_on,
|
||||||
|
@@ -66,6 +66,18 @@ class PlayerEvent:
|
|||||||
device_id: Optional[str] = None
|
device_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlayerDeviceEvent:
|
||||||
|
class Delta(Enum):
|
||||||
|
ADD = 0
|
||||||
|
REMOVE = 1
|
||||||
|
|
||||||
|
delta: Delta
|
||||||
|
player_type: Type
|
||||||
|
id: str
|
||||||
|
name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class Player(abc.ABC):
|
class Player(abc.ABC):
|
||||||
@property
|
@property
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -107,6 +119,7 @@ class Player(abc.ABC):
|
|||||||
on_timepos_change: Callable[[Optional[float]], None],
|
on_timepos_change: Callable[[Optional[float]], None],
|
||||||
on_track_end: Callable[[], None],
|
on_track_end: Callable[[], None],
|
||||||
on_player_event: Callable[[PlayerEvent], None],
|
on_player_event: Callable[[PlayerEvent], None],
|
||||||
|
player_device_change_callback: Callable[[PlayerDeviceEvent], None],
|
||||||
config: Dict[str, Union[str, int, bool]],
|
config: Dict[str, Union[str, int, bool]],
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -126,12 +139,6 @@ class Player(abc.ABC):
|
|||||||
Do any cleanup of the player.
|
Do any cleanup of the player.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_available_player_devices(self) -> Iterator[Tuple[str, str]]:
|
|
||||||
"""
|
|
||||||
:returns: an iterator of tuples containing the device ID and device name.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def playing(self) -> bool:
|
def playing(self) -> bool:
|
||||||
|
@@ -23,7 +23,7 @@ from urllib.parse import urlparse
|
|||||||
from sublime.adapters import AdapterManager
|
from sublime.adapters import AdapterManager
|
||||||
from sublime.adapters.api_objects import Song
|
from sublime.adapters.api_objects import Song
|
||||||
|
|
||||||
from .base import Player, PlayerEvent
|
from .base import Player, PlayerDeviceEvent, PlayerEvent
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pychromecast
|
import pychromecast
|
||||||
@@ -68,6 +68,7 @@ class ChromecastPlayer(Player):
|
|||||||
on_timepos_change: Callable[[Optional[float]], None],
|
on_timepos_change: Callable[[Optional[float]], None],
|
||||||
on_track_end: Callable[[], None],
|
on_track_end: Callable[[], None],
|
||||||
on_player_event: Callable[[PlayerEvent], None],
|
on_player_event: Callable[[PlayerEvent], None],
|
||||||
|
player_device_change_callback: Callable[[PlayerDeviceEvent], None],
|
||||||
config: Dict[str, Union[str, int, bool]],
|
config: Dict[str, Union[str, int, bool]],
|
||||||
):
|
):
|
||||||
self.server_process = None
|
self.server_process = None
|
||||||
@@ -78,15 +79,33 @@ class ChromecastPlayer(Player):
|
|||||||
args=("0.0.0.0", self.config.get(LAN_PORT_KEY)),
|
args=("0.0.0.0", self.config.get(LAN_PORT_KEY)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._stop_retrieve_chromecasts = None
|
||||||
if chromecast_imported:
|
if chromecast_imported:
|
||||||
self._chromecasts: List[Any] = []
|
self._chromecasts: Dict[str, pychromecast.Chromecast] = {}
|
||||||
self._current_chromecast = pychromecast.Chromecast
|
self._current_chromecast = pychromecast.Chromecast
|
||||||
|
|
||||||
|
def discovered_callback(chromecast: pychromecast.Chromecast):
|
||||||
|
self._chromecasts[chromecast.device.uuid] = chromecast
|
||||||
|
player_device_change_callback(
|
||||||
|
PlayerDeviceEvent(
|
||||||
|
PlayerDeviceEvent.Delta.ADD,
|
||||||
|
type(self),
|
||||||
|
str(chromecast.device.uuid),
|
||||||
|
chromecast.device.friendly_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._stop_retrieve_chromecasts = pychromecast.get_chromecasts(
|
||||||
|
blocking=False, callback=discovered_callback
|
||||||
|
)
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
if self._current_chromecast:
|
if self._current_chromecast:
|
||||||
self._current_chromecast.disconnect()
|
self._current_chromecast.disconnect()
|
||||||
if self.server_process:
|
if self.server_process:
|
||||||
self.server_process.terminate()
|
self.server_process.terminate()
|
||||||
|
if self._stop_retrieve_chromecasts:
|
||||||
|
self._stop_retrieve_chromecasts()
|
||||||
|
|
||||||
_serving_song_id = multiprocessing.Array("c", 1024) # huge buffer, just in case
|
_serving_song_id = multiprocessing.Array("c", 1024) # huge buffer, just in case
|
||||||
_serving_token = multiprocessing.Array("c", 12)
|
_serving_token = multiprocessing.Array("c", 12)
|
||||||
@@ -121,14 +140,6 @@ class ChromecastPlayer(Player):
|
|||||||
|
|
||||||
bottle.run(app, host=host, port=port)
|
bottle.run(app, host=host, port=port)
|
||||||
|
|
||||||
def get_available_player_devices(self) -> Iterator[Tuple[str, str]]:
|
|
||||||
if not chromecast_imported:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._chromecasts = pychromecast.get_chromecasts()
|
|
||||||
for chromecast in self._chromecasts:
|
|
||||||
yield (str(chromecast.device.uuid), chromecast.device.friendly_name)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def playing(self) -> bool:
|
def playing(self) -> bool:
|
||||||
if (
|
if (
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from time import sleep
|
from time import sleep
|
||||||
@@ -17,27 +18,18 @@ from typing import (
|
|||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from sublime.adapters.api_objects import Song
|
||||||
|
|
||||||
from .base import PlayerEvent
|
from .base import PlayerDeviceEvent, PlayerEvent
|
||||||
from .chromecast import ChromecastPlayer # noqa: F401
|
from .chromecast import ChromecastPlayer # noqa: F401
|
||||||
from .mpv import MPVPlayer # noqa: F401
|
from .mpv import MPVPlayer # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PlayerDeviceEvent:
|
|
||||||
class Delta(Enum):
|
|
||||||
ADD = 0
|
|
||||||
REMOVE = 1
|
|
||||||
|
|
||||||
delta: Delta
|
|
||||||
player_type: Type
|
|
||||||
id: str
|
|
||||||
name: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class PlayerManager:
|
class PlayerManager:
|
||||||
# Available Players. Order matters for UI display.
|
# Available Players. Order matters for UI display.
|
||||||
available_player_types: List[Type] = [MPVPlayer, ChromecastPlayer]
|
available_player_types: List[Type] = [MPVPlayer, ChromecastPlayer]
|
||||||
|
# TODO
|
||||||
|
# player_device_retrieval_process: Optional[multiprocessing.Process] = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_configuration_options() -> Dict[
|
def get_configuration_options() -> Dict[
|
||||||
@@ -64,62 +56,105 @@ class PlayerManager:
|
|||||||
self.on_timepos_change = on_timepos_change
|
self.on_timepos_change = on_timepos_change
|
||||||
self.on_track_end = on_track_end
|
self.on_track_end = on_track_end
|
||||||
self.on_player_event = on_player_event
|
self.on_player_event = on_player_event
|
||||||
|
self.config = config
|
||||||
|
self.players: Dict[Type, Any] = {}
|
||||||
|
self.device_id_type_map: Dict[str, Type] = {}
|
||||||
|
self._current_device_id = None
|
||||||
|
|
||||||
self.players = [
|
def callback_wrapper(pde: PlayerDeviceEvent):
|
||||||
player_type(
|
self.device_id_type_map[pde.id] = pde.player_type
|
||||||
on_timepos_change,
|
player_device_change_callback(pde)
|
||||||
on_track_end,
|
|
||||||
on_player_event,
|
self.player_device_change_callback = callback_wrapper
|
||||||
config.get(player_type.name),
|
|
||||||
|
# We have to have both init and and init_players so that by the time that any of the
|
||||||
|
# players start calling the callback, the player manager exists on the app.
|
||||||
|
def init_players(self):
|
||||||
|
self.players = {
|
||||||
|
player_type: player_type(
|
||||||
|
self.on_timepos_change,
|
||||||
|
self.on_track_end,
|
||||||
|
self.on_player_event,
|
||||||
|
self.player_device_change_callback,
|
||||||
|
self.config.get(player_type.name),
|
||||||
)
|
)
|
||||||
for player_type in PlayerManager.available_player_types
|
for player_type in PlayerManager.available_player_types
|
||||||
]
|
}
|
||||||
|
|
||||||
self.device_id_type_map: Dict[str, Type] = {}
|
has_done_one_retrieval = multiprocessing.Value("b", False)
|
||||||
self.player_device_change_callback = player_device_change_callback
|
|
||||||
self.player_device_retrieval_process = multiprocessing.Process(
|
|
||||||
target=self._retrieve_available_player_devices
|
|
||||||
)
|
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
print("SHUTDOWN PLAYER MANAGER")
|
pass
|
||||||
self.player_device_retrieval_process.terminate()
|
|
||||||
|
|
||||||
def _retrieve_available_player_devices(self):
|
def _get_current_player_type(self) -> Any:
|
||||||
seen_ids = set()
|
if device_id := self._current_device_id:
|
||||||
while True:
|
return self.device_id_type_map.get(device_id)
|
||||||
new_ids = set()
|
|
||||||
for t in PlayerManager.available_player_types:
|
|
||||||
if not t.enabled:
|
|
||||||
continue
|
|
||||||
for device_id, device_name in t.get_available_player_devices():
|
|
||||||
self.player_device_change_callback(
|
|
||||||
PlayerDeviceEvent(
|
|
||||||
PlayerDeviceEvent.Delta.ADD, t, device_id, device_name,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
new_ids.add((t, device_id))
|
|
||||||
self.device_id_type_map[device_id] = t
|
|
||||||
|
|
||||||
for t, device_id in seen_ids.difference(new_ids):
|
def _get_current_player(self) -> Any:
|
||||||
self.player_device_change_callback(
|
if current_player_type := self._get_current_player_type():
|
||||||
PlayerDeviceEvent(PlayerDeviceEvent.Delta.REMOVE, t, device_id)
|
return self.players.get(current_player_type)
|
||||||
)
|
|
||||||
del self.device_id_type_map[device_id]
|
|
||||||
|
|
||||||
seen_ids = new_ids
|
@property
|
||||||
sleep(15)
|
def can_start_playing_with_no_latency(self) -> bool:
|
||||||
|
if self._current_device_id:
|
||||||
def can_start_playing_with_no_latency(self, device_id: str) -> bool:
|
return self._get_current_player_type().can_start_playing_with_no_latency
|
||||||
return self.device_id_type_map[device_id].can_start_playing_with_no_latency
|
else:
|
||||||
|
return False
|
||||||
_current_device_id = None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_device_id(self) -> Optional[str]:
|
def current_device_id(self) -> Optional[str]:
|
||||||
return self._current_device_id
|
return self._current_device_id
|
||||||
|
|
||||||
@current_device_id.setter
|
def set_current_device_id(self, device_id: str):
|
||||||
def current_device_id(self, value: str):
|
logging.info(f"Setting current device id to '{device_id}'")
|
||||||
print("SET CURRENT DEVICE")
|
self._current_device_id = device_id
|
||||||
self._current_device_id = value
|
|
||||||
|
def reset(self):
|
||||||
|
if current_player := self._get_current_player():
|
||||||
|
current_player.reset()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def song_loaded(self) -> bool:
|
||||||
|
if current_player := self._get_current_player():
|
||||||
|
return current_player.song_loaded
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def playing(self) -> bool:
|
||||||
|
if current_player := self._get_current_player():
|
||||||
|
return current_player.playing
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_volume(self) -> float:
|
||||||
|
if current_player := self._get_current_player():
|
||||||
|
return current_player.get_volume()
|
||||||
|
return 100
|
||||||
|
|
||||||
|
def set_volume(self, volume: float):
|
||||||
|
if current_player := self._get_current_player():
|
||||||
|
current_player.set_volume(volume)
|
||||||
|
|
||||||
|
def get_is_muted(self) -> bool:
|
||||||
|
if current_player := self._get_current_player():
|
||||||
|
return current_player.get_is_muted()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_muted(self, muted: bool):
|
||||||
|
if current_player := self._get_current_player():
|
||||||
|
current_player.set_muted(muted)
|
||||||
|
|
||||||
|
def play_media(self, uri: str, progress: timedelta, song: Song):
|
||||||
|
if current_player := self._get_current_player():
|
||||||
|
current_player.play_media(uri, progress, song)
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
if current_player := self._get_current_player():
|
||||||
|
current_player.pause()
|
||||||
|
|
||||||
|
def toggle_play(self):
|
||||||
|
if current_player := self._get_current_player():
|
||||||
|
current_player.toggle_play()
|
||||||
|
|
||||||
|
def seek(self, position: timedelta):
|
||||||
|
if current_player := self._get_current_player():
|
||||||
|
current_player.seek(position)
|
||||||
|
@@ -17,7 +17,7 @@ import mpv
|
|||||||
|
|
||||||
from sublime.adapters.api_objects import Song
|
from sublime.adapters.api_objects import Song
|
||||||
|
|
||||||
from .base import Player, PlayerEvent
|
from .base import Player, PlayerDeviceEvent, PlayerEvent
|
||||||
|
|
||||||
REPLAY_GAIN_KEY = "Replay Gain"
|
REPLAY_GAIN_KEY = "Replay Gain"
|
||||||
|
|
||||||
@@ -44,6 +44,7 @@ class MPVPlayer(Player):
|
|||||||
on_timepos_change: Callable[[Optional[float]], None],
|
on_timepos_change: Callable[[Optional[float]], None],
|
||||||
on_track_end: Callable[[], None],
|
on_track_end: Callable[[], None],
|
||||||
on_player_event: Callable[[PlayerEvent], None],
|
on_player_event: Callable[[PlayerEvent], None],
|
||||||
|
player_device_change_callback: Callable[[PlayerDeviceEvent], None],
|
||||||
config: Dict[str, Union[str, int, bool]],
|
config: Dict[str, Union[str, int, bool]],
|
||||||
):
|
):
|
||||||
self.mpv = mpv.MPV()
|
self.mpv = mpv.MPV()
|
||||||
@@ -75,6 +76,13 @@ class MPVPlayer(Player):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Indicate to the UI that we exist.
|
||||||
|
player_device_change_callback(
|
||||||
|
PlayerDeviceEvent(
|
||||||
|
PlayerDeviceEvent.Delta.ADD, type(self), "this device", "This Device"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -83,9 +91,6 @@ class MPVPlayer(Player):
|
|||||||
with self._progress_value_lock:
|
with self._progress_value_lock:
|
||||||
self._progress_value_count = 0
|
self._progress_value_count = 0
|
||||||
|
|
||||||
def get_available_player_devices(self) -> Iterator[Tuple[str, str]]:
|
|
||||||
yield ("this device", "This Device")
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def playing(self) -> bool:
|
def playing(self) -> bool:
|
||||||
return not self.mpv.pause
|
return not self.mpv.pause
|
||||||
|
@@ -196,13 +196,14 @@ class MainWindow(Gtk.ApplicationWindow):
|
|||||||
# Main Settings
|
# Main Settings
|
||||||
self.notification_switch.set_active(app_config.song_play_notification)
|
self.notification_switch.set_active(app_config.song_play_notification)
|
||||||
|
|
||||||
# MPV Settings
|
# TODO
|
||||||
self.replay_gain_options.set_active_id(app_config.replay_gain.as_string())
|
# # MPV Settings
|
||||||
|
# self.replay_gain_options.set_active_id(app_config.replay_gain.as_string())
|
||||||
|
|
||||||
# Chromecast Settings
|
# # Chromecast Settings
|
||||||
self.serve_over_lan_switch.set_active(app_config.serve_over_lan)
|
# self.serve_over_lan_switch.set_active(app_config.serve_over_lan)
|
||||||
self.port_number_entry.set_value(app_config.port_number)
|
# self.port_number_entry.set_value(app_config.port_number)
|
||||||
self.port_number_entry.set_sensitive(app_config.serve_over_lan)
|
# self.port_number_entry.set_sensitive(app_config.serve_over_lan)
|
||||||
|
|
||||||
# Download Settings
|
# Download Settings
|
||||||
allow_song_downloads = app_config.allow_song_downloads
|
allow_song_downloads = app_config.allow_song_downloads
|
||||||
|
@@ -63,6 +63,7 @@ class UIState:
|
|||||||
song_progress: timedelta = timedelta()
|
song_progress: timedelta = timedelta()
|
||||||
song_stream_cache_progress: Optional[timedelta] = timedelta()
|
song_stream_cache_progress: Optional[timedelta] = timedelta()
|
||||||
current_device: str = "this device"
|
current_device: str = "this device"
|
||||||
|
connecting_to_device: bool = False
|
||||||
|
|
||||||
# UI state
|
# UI state
|
||||||
current_tab: str = "albums"
|
current_tab: str = "albums"
|
||||||
|
@@ -7,6 +7,7 @@ def test_init():
|
|||||||
empty_fn,
|
empty_fn,
|
||||||
empty_fn,
|
empty_fn,
|
||||||
empty_fn,
|
empty_fn,
|
||||||
|
empty_fn,
|
||||||
{
|
{
|
||||||
"Serve Local Files to Chromecasts on the LAN": True,
|
"Serve Local Files to Chromecasts on the LAN": True,
|
||||||
"LAN Server Port Number": 6969,
|
"LAN Server Port Number": 6969,
|
||||||
|
@@ -7,7 +7,7 @@ from sublime.players.mpv import MPVPlayer
|
|||||||
|
|
||||||
def test_init():
|
def test_init():
|
||||||
empty_fn = lambda *a, **k: None
|
empty_fn = lambda *a, **k: None
|
||||||
MPVPlayer(empty_fn, empty_fn, empty_fn, {"Replay Gain": "Disabled"})
|
MPVPlayer(empty_fn, empty_fn, empty_fn, empty_fn, {"Replay Gain": "Disabled"})
|
||||||
|
|
||||||
|
|
||||||
def is_close(expected: float, value: float, delta: float = 0.5) -> bool:
|
def is_close(expected: float, value: float, delta: float = 0.5) -> bool:
|
||||||
@@ -16,7 +16,9 @@ def is_close(expected: float, value: float, delta: float = 0.5) -> bool:
|
|||||||
|
|
||||||
def test_play():
|
def test_play():
|
||||||
empty_fn = lambda *a, **k: None
|
empty_fn = lambda *a, **k: None
|
||||||
mpv_player = MPVPlayer(empty_fn, empty_fn, empty_fn, {"Replay Gain": "Disabled"})
|
mpv_player = MPVPlayer(
|
||||||
|
empty_fn, empty_fn, empty_fn, empty_fn, {"Replay Gain": "Disabled"}
|
||||||
|
)
|
||||||
|
|
||||||
song_path = Path(__file__).parent.joinpath("mock_data/test-song.mp3")
|
song_path = Path(__file__).parent.joinpath("mock_data/test-song.mp3")
|
||||||
mpv_player.play_media(str(song_path), timedelta(seconds=10), None)
|
mpv_player.play_media(str(song_path), timedelta(seconds=10), None)
|
||||||
|
Reference in New Issue
Block a user