diff --git a/src/mpd_now_playable/cli.py b/src/mpd_now_playable/cli.py index 0e201e7..34153c2 100644 --- a/src/mpd_now_playable/cli.py +++ b/src/mpd_now_playable/cli.py @@ -10,15 +10,15 @@ from .mpd.listener import MpdStateListener from .song_receiver import ( Receiver, choose_loop_factory, - import_receiver, + construct_receiver, ) async def listen( - config: Config, listener: MpdStateListener, receiver_types: Iterable[type[Receiver]] + config: Config, listener: MpdStateListener, receivers: Iterable[Receiver] ) -> None: await listener.start(config.mpd) - receivers = (rec(listener, config) for rec in receiver_types) + await asyncio.gather(*(rec.start(listener) for rec in receivers)) await listener.loop(receivers) @@ -28,11 +28,11 @@ def main() -> None: print(config) listener = MpdStateListener(config.cache) - receiver_types = tuple(import_receiver(rec) for rec in config.receivers) + receivers = tuple(construct_receiver(rec_config) for rec_config in config.receivers) + factory = choose_loop_factory(receivers) - factory = choose_loop_factory(receiver_types) asyncio.run( - listen(config, listener, receiver_types), + listen(config, listener, receivers), loop_factory=factory.make_loop, debug=True, ) diff --git a/src/mpd_now_playable/config/model.py b/src/mpd_now_playable/config/model.py index 7e2e6af..a5b67f2 100644 --- a/src/mpd_now_playable/config/model.py +++ b/src/mpd_now_playable/config/model.py @@ -21,7 +21,7 @@ class BaseReceiverConfig(Protocol): @dataclass(slots=True) class CocoaReceiverConfig(BaseReceiverConfig): - kind: Literal["cocoa"] = "cocoa" + kind: Literal["cocoa"] = field(default="cocoa", repr=False) ReceiverConfig = Annotated[ diff --git a/src/mpd_now_playable/mpd/listener.py b/src/mpd_now_playable/mpd/listener.py index 50f256a..bceaaf3 100644 --- a/src/mpd_now_playable/mpd/listener.py +++ b/src/mpd_now_playable/mpd/listener.py @@ -83,8 +83,7 @@ class MpdStateListener(Player): if status["state"] == "stop": print("Nothing playing") - for r in self.receivers: - r.update(None) + await self.update(None) return art = await self.art_cache.get_cached_artwork(current) @@ -93,8 +92,10 @@ class MpdStateListener(Player): song = mpd_current_to_song(status, current, to_artwork(art)) rprint(song) - for r in self.receivers: - r.update(song) + await self.update(song) + + async def update(self, song: Song | None) -> None: + await asyncio.gather(*(r.update(song) for r in self.receivers)) async def get_art(self, file: str) -> bytes | None: picture = await self.readpicture(file) diff --git a/src/mpd_now_playable/receivers/cocoa/now_playing.py b/src/mpd_now_playable/receivers/cocoa/now_playing.py index 158c5a5..60c2b49 100644 --- a/src/mpd_now_playable/receivers/cocoa/now_playing.py +++ b/src/mpd_now_playable/receivers/cocoa/now_playing.py @@ -37,7 +37,7 @@ from MediaPlayer import ( MPRemoteCommandHandlerStatusSuccess, ) -from ...config.model import Config +from ...config.model import CocoaReceiverConfig from ...player import Player from ...song import PlaybackState, Song from ...song_receiver import LoopFactory, Receiver @@ -146,7 +146,10 @@ class CocoaNowPlayingReceiver(Receiver): def loop_factory(cls) -> LoopFactory[CoreFoundationEventLoop]: return CocoaLoopFactory() - def __init__(self, player: Player, config: Config): + def __init__(self, config: CocoaReceiverConfig): + pass + + async def start(self, player: Player) -> None: self.cmd_center = MPRemoteCommandCenter.sharedCommandCenter() self.info_center = MPNowPlayingInfoCenter.defaultCenter() @@ -184,7 +187,7 @@ class CocoaNowPlayingReceiver(Receiver): # unpause with remote commands. self.info_center.setPlaybackState_(MPMusicPlaybackStatePlaying) - def update(self, song: Song | None) -> None: + async def update(self, song: Song | None) -> None: if song: self.info_center.setNowPlayingInfo_(song_to_media_item(song)) self.info_center.setPlaybackState_(playback_state_to_cocoa(song.state)) diff --git a/src/mpd_now_playable/song_receiver.py b/src/mpd_now_playable/song_receiver.py index d6f5b8e..cf18326 100644 --- a/src/mpd_now_playable/song_receiver.py +++ b/src/mpd_now_playable/song_receiver.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from importlib import import_module from typing import Generic, Iterable, Literal, Protocol, TypeVar, cast -from .config.model import BaseReceiverConfig, Config +from .config.model import BaseReceiverConfig from .player import Player from .song import Song from .tools.types import not_none @@ -20,10 +20,12 @@ class LoopFactory(Generic[T], Protocol): class Receiver(Protocol): - def __init__(self, player: Player, config: Config) -> None: ... + def __init__(self, config: BaseReceiverConfig): ... @classmethod def loop_factory(cls) -> LoopFactory[AbstractEventLoop]: ... - def update(self, song: Song | None) -> None: ... + + async def start(self, player: Player) -> None: ... + async def update(self, song: Song | None) -> None: ... class ReceiverModule(Protocol): @@ -42,8 +44,8 @@ class DefaultLoopFactory(LoopFactory[AbstractEventLoop]): @dataclass class IncompatibleReceiverError(Exception): - a: type[Receiver] - b: type[Receiver] + a: Receiver + b: Receiver def import_receiver(config: BaseReceiverConfig) -> type[Receiver]: @@ -53,13 +55,18 @@ def import_receiver(config: BaseReceiverConfig) -> type[Receiver]: return mod.receiver +def construct_receiver(config: BaseReceiverConfig) -> Receiver: + cls = import_receiver(config) + return cls(config) + + def choose_loop_factory( - receivers: Iterable[type[Receiver]], + receivers: Iterable[Receiver], ) -> LoopFactory[AbstractEventLoop]: """Given the desired receivers, determine which asyncio event loop implementation will support all of them. Will raise an IncompatibleReceiverError if no such implementation exists.""" chosen_fac: LoopFactory[AbstractEventLoop] = DefaultLoopFactory() - chosen_rec: type[Receiver] | None = None + chosen_rec: Receiver | None = None for rec in receivers: fac = rec.loop_factory()