Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce new PlayContentHandler to abstract Second Swipe #2452

Open
wants to merge 15 commits into
base: future3/develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions documentation/developers/docstring/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@
* [resolve](#misc.simplecolors.resolve)
* [print](#misc.simplecolors.print)
* [components](#components)
* [components.playermpd.playcontentcallback](#components.playermpd.playcontentcallback)
* [PlayContentCallbacks](#components.playermpd.playcontentcallback.PlayContentCallbacks)
* [register](#components.playermpd.playcontentcallback.PlayContentCallbacks.register)
* [run\_callbacks](#components.playermpd.playcontentcallback.PlayContentCallbacks.run_callbacks)
* [components.playermpd.play_content_callback](#components.playermpd.play_content_callback)
* [PlayContentCallbacks](#components.playermpd.play_content_callback.PlayContentCallbacks)
* [register](#components.playermpd.play_content_callback.PlayContentCallbacks.register)
* [run\_callbacks](#components.playermpd.play_content_callback.PlayContentCallbacks.run_callbacks)
* [components.playermpd](#components.playermpd)
* [PlayerMPD](#components.playermpd.PlayerMPD)
* [mpd\_retry\_with\_mutex](#components.playermpd.PlayerMPD.mpd_retry_with_mutex)
Expand Down Expand Up @@ -761,11 +761,11 @@ Use just as a regular print function, but with first parameter as color

# components

<a id="components.playermpd.playcontentcallback"></a>
<a id="components.playermpd.play_content_callback"></a>

# components.playermpd.playcontentcallback
# components.playermpd.play_content_callback

<a id="components.playermpd.playcontentcallback.PlayContentCallbacks"></a>
<a id="components.playermpd.play_content_callback.PlayContentCallbacks"></a>

## PlayContentCallbacks Objects

Expand All @@ -776,7 +776,7 @@ class PlayContentCallbacks(Generic[STATE], CallbackHandler)
Callbacks are executed in various play functions


<a id="components.playermpd.playcontentcallback.PlayContentCallbacks.register"></a>
<a id="components.playermpd.play_content_callback.PlayContentCallbacks.register"></a>

#### register

Expand All @@ -796,7 +796,7 @@ Callback signature is
- `folder`: relativ path to folder to play
- `state`: indicator of the state inside the calling

<a id="components.playermpd.playcontentcallback.PlayContentCallbacks.run_callbacks"></a>
<a id="components.playermpd.play_content_callback.PlayContentCallbacks.run_callbacks"></a>

#### run\_callbacks

Expand Down
288 changes: 175 additions & 113 deletions src/jukebox/components/playermpd/__init__.py

Large diffs are not rendered by default.

97 changes: 97 additions & 0 deletions src/jukebox/components/playermpd/play_content_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from enum import Enum, auto
from dataclasses import dataclass
from typing import Union, Optional, Callable, Protocol
import logging

from .play_content_callback import PlayCardState # Add this import

logger = logging.getLogger('jb.PlayerMPD')


class PlayContentType(Enum):
SINGLE = auto()
ALBUM = auto()
FOLDER = auto()


@dataclass
class PlayContent:
"""Represents playable content with its type and metadata"""
type: PlayContentType
content: Union[str, tuple[str, str]] # str for SINGLE/FOLDER, tuple(artist, album) for ALBUM
recursive: bool = False


class PlayerProtocol(Protocol):
"""Protocol defining required player methods"""
def _play_single_internal(self, song_url: str) -> None:
"""Play a single track"""

def _play_album_internal(self, artist: str, album: str) -> None:
"""Play an album"""

def _play_folder_internal(self, folder: str, recursive: bool) -> None:
"""Play a folder"""

@property
def play_card_callbacks(self) -> any:
"""Access to callbacks"""


class PlayContentHandler:
"""Handles different types of playback content with second swipe support"""

def __init__(self, player: PlayerProtocol):
self.player = player
self.last_played_content: Optional[PlayContent] = None
self._second_swipe_action = None

def set_second_swipe_action(self, action: Optional[Callable]) -> None:
"""Set the action to be performed on second swipe"""
self._second_swipe_action = action

def _play_content(self, content: PlayContent) -> None:
"""Internal method to play content based on its type"""
if content.type == PlayContentType.SINGLE:
logger.debug(f"Playing single track: {content.content}")
self.player._play_single_internal(content.content)
elif content.type == PlayContentType.ALBUM:
artist, album = content.content
logger.debug(f"Playing album: {album} by {artist}")
self.player._play_album_internal(artist, album)
elif content.type == PlayContentType.FOLDER:
logger.debug(f"Playing folder: {content.content} (recursive={content.recursive})")
self.player._play_folder_internal(content.content, content.recursive)

def play_content(self, content: PlayContent) -> None:
"""
Main entry point for playing content with second swipe support

Checks for second trigger of the same content and calls first/second swipe
action accordingly.
"""
is_second_swipe = False

if self.last_played_content is not None:
if (content.type == self.last_played_content.type
and content.content == self.last_played_content.content):
is_second_swipe = True

if self._second_swipe_action is not None and is_second_swipe:
logger.debug('Calling second swipe action')
# run callbacks before second_swipe_action is invoked
self.player.play_card_callbacks.run_callbacks(
str(content.content),
PlayCardState.secondSwipe # Use imported PlayCardState directly
)
self._second_swipe_action()
else:
logger.debug('Calling first swipe action')
# run callbacks before play_content is invoked
self.player.play_card_callbacks.run_callbacks(
str(content.content),
PlayCardState.firstSwipe # Use imported PlayCardState directly
)
self._play_content(content)

self.last_played_content = content
46 changes: 26 additions & 20 deletions src/jukebox/components/rfid/cards/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import logging
import time
from typing import (List, Dict, Optional)
from typing import List, Dict, Optional, Union
import jukebox.utils as utils
import jukebox.cfghandler
import jukebox.plugs as plugs
Expand Down Expand Up @@ -89,42 +89,48 @@ def delete_card(card_id: str, auto_save: bool = True):

@plugs.register
def register_card(card_id: str, cmd_alias: str,
args: Optional[List] = None, kwargs: Optional[Dict] = None,
ignore_card_removal_action: Optional[bool] = None, ignore_same_id_delay: Optional[bool] = None,
overwrite: bool = False,
auto_save: bool = True):
"""Register a new card based on quick-selection

If you are going to call this through the RPC it will get a little verbose

**Example:** Registering a new card with ID *0009* for increment volume with a custom argument to inc_volume
(*here: 15*) and custom *ignore_same_id_delay value*::

plugin.call_ignore_errors('cards', 'register_card',
args=['0009', 'inc_volume'],
kwargs={'args': [15], 'ignore_same_id_delay': True, 'overwrite': True})

"""
args: Optional[Union[List, Dict]] = None,
kwargs: Optional[Dict] = None,
ignore_card_removal_action: Optional[bool] = None,
ignore_same_id_delay: Optional[bool] = None,
overwrite: bool = False,
auto_save: bool = True):
"""Register a new card based on quick-selection"""
if cmd_alias not in cmd_alias_definitions.keys():
msg = f"Unknown RPC command alias: '{cmd_alias}'"
log.error(msg)
raise KeyError(msg)

with cfg_cards:
if not overwrite and card_id in cfg_cards.keys():
msg = f"Card already registered: '{card_id}'. Abort. (use overwrite=True to overrule)"
log.error(msg)
raise KeyError(msg)

cfg_cards[card_id] = {'alias': cmd_alias}
if args is not None:

# For play_from_reader, expect a single dict of args
if cmd_alias == 'play_from_reader':
# Use either kwargs or args if it's a dict
if kwargs is not None:
cfg_cards[card_id]['args'] = kwargs
elif isinstance(args, dict):
cfg_cards[card_id]['args'] = args
else:
log.error(f"play_from_reader requires dict arguments, got: {type(args)}")
raise ValueError("play_from_reader requires dict arguments")
# For other commands, maintain list args support
elif args is not None:
cfg_cards[card_id]['args'] = args
if kwargs is not None:
cfg_cards[card_id]['kwargs'] = args

if ignore_same_id_delay is not None:
cfg_cards[card_id]['ignore_same_id_delay'] = ignore_same_id_delay
if ignore_card_removal_action is not None:
cfg_cards[card_id]['ignore_card_removal_action'] = ignore_card_removal_action

if auto_save:
cfg_cards.save()

publishing.get_publisher().send(f'{plugs.loaded_as(__name__)}.database.has_changed', time.ctime())


Expand Down
6 changes: 3 additions & 3 deletions src/jukebox/components/rpc_command_alias.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
# --------------------------------------------------------------
cmd_alias_definitions = {
# Player
'play_card': {
'title': 'Play music folder triggered by card swipe',
'play_from_reader': {
'title': 'Play content triggered by card swipe, supports second swipe',
'note': "This function you'll want to use most often",
'package': 'player',
'plugin': 'ctrl',
'method': 'play_card'},
'method': 'play_from_reader'},
'play_album': {
'title': 'Play Album triggered by card swipe',
'note': "This function plays the content of a given album",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import shutil

from components.rfid.reader import RfidCardDetectState
from components.playermpd.playcontentcallback import PlayCardState
from components.playermpd.play_content_callback import PlayCardState


logger = logging.getLogger('jb.sync_rfidcards')
Expand Down
Loading
Loading