diff --git a/documentation/builders/card-database.md b/documentation/builders/card-database.md index 340933750..1c2f92d56 100644 --- a/documentation/builders/card-database.md +++ b/documentation/builders/card-database.md @@ -21,15 +21,17 @@ using the alias option: '0002': # A RPC command using the alias definition with one arguments # Here: Trigger music playback through the card interface - alias: play_card - args: [path/to/folder] + alias: play_folder + args: + - /path/to/folder '0003': # A RPC command using keyword arguments. Args and kwargs can also be mixed. # Args and Kwargs translate directly into the function python call # Some as in '0002' but using kwargs - alias: play_card + alias: play_album kwargs: - folder: path/to/folder + albumartist: Some Artist Name + recursive: A song Name ``` > [!NOTE] diff --git a/documentation/builders/rpc-commands.md b/documentation/builders/rpc-commands.md index a45e0b643..09579bceb 100644 --- a/documentation/builders/rpc-commands.md +++ b/documentation/builders/rpc-commands.md @@ -37,21 +37,21 @@ The keyword ``method`` is optional. If needs to be used depends on the function ## Aliases Not so complicated, right? It will get even easier. For common commands we have defined aliases. An alias simply maps -to a pre-defined RPC command, e.g. ``play_card`` maps to ``player.ctrl.play_card``. +to a pre-defined RPC command, e.g. ``play_folder`` maps to ``player.ctrl.play_folder``. Instead of ```yml package: player plugin: ctrl -method: play_card +method: play_folder args: [path/to/folder] ``` you can simply specify instead: ```yml -alias: play_card +alias: play_folder args: [path/to/folder] ``` @@ -60,10 +60,10 @@ Using in alias is optional. But if the keyword is present in the configuration i ## Arguments Arguments can be specified in similar fashion to Python function arguments: as positional arguments and / or -keyword arguments. Let's check out play_card, which is defined as: +keyword arguments. Let's check out play_folder, which is defined as: ```python -play_card(...) -> player.ctrl.play_card(folder: str, recursive: bool = False) +play_folder(...) -> player.ctrl.play_folder(folder: str, recursive: bool = False) :noindex: :param folder: Folder path relative to music library path @@ -79,19 +79,19 @@ In the following examples, we will always use the alias for smaller configuratio do exactly the same, but use different ways of specifying the command. ```yml -alias: play_card +alias: play_folder args: [path/to/folder, True] ``` ```yml -alias: play_card +alias: play_folder args: [path/to/folder] kwargs: recursive: True ``` ```yml -alias: play_card +alias: play_folder kwargs: folder: path/to/folder recursive: True diff --git a/documentation/developers/docstring/README.md b/documentation/developers/docstring/README.md index c48c34a41..b46ae1542 100644 --- a/documentation/developers/docstring/README.md +++ b/documentation/developers/docstring/README.md @@ -40,14 +40,12 @@ * [replay](#components.playermpd.PlayerMPD.replay) * [toggle](#components.playermpd.PlayerMPD.toggle) * [replay\_if\_stopped](#components.playermpd.PlayerMPD.replay_if_stopped) - * [play\_card](#components.playermpd.PlayerMPD.play_card) * [get\_single\_coverart](#components.playermpd.PlayerMPD.get_single_coverart) * [get\_folder\_content](#components.playermpd.PlayerMPD.get_folder_content) * [play\_folder](#components.playermpd.PlayerMPD.play_folder) * [play\_album](#components.playermpd.PlayerMPD.play_album) * [get\_volume](#components.playermpd.PlayerMPD.get_volume) * [set\_volume](#components.playermpd.PlayerMPD.set_volume) - * [play\_card\_callbacks](#components.playermpd.play_card_callbacks) * [components.playermpd.coverart\_cache\_manager](#components.playermpd.coverart_cache_manager) * [components.rpc\_command\_alias](#components.rpc_command_alias) * [components.synchronisation.rfidcards](#components.synchronisation.rfidcards) @@ -852,11 +850,6 @@ Internal status - last played folder: Needed to detect second swipe -Saving {'player_status': {'last_played_folder': 'TraumfaengerStarkeLieder', 'CURRENTSONGPOS': '0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3'}, -'audio_folder_status': -{'TraumfaengerStarkeLieder': {'ELAPSED': '1.0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3', 'CURRENTSONGPOS': '0', 'PLAYSTATUS': 'stop', 'RESUME': 'OFF', 'SHUFFLE': 'OFF', 'LOOP': 'OFF', 'SINGLE': 'OFF'}, -'Giraffenaffen': {'ELAPSED': '1.0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3', 'CURRENTSONGPOS': '0', 'PLAYSTATUS': 'play', 'RESUME': 'OFF', 'SHUFFLE': 'OFF', 'LOOP': 'OFF', 'SINGLE': 'OFF'}}} - References: https://github.com/Mic92/python-mpd2 https://python-mpd2.readthedocs.io/en/latest/topics/commands.html @@ -975,25 +968,6 @@ Re-start playing the last-played folder unless playlist is still playing > but we keep it as it is specifically implemented in box 2.X - - -#### play\_card - -```python -@plugs.tag -def play_card(folder: str, recursive: bool = False) -``` - -Main entry point for trigger music playing from RFID reader. Decodes second swipe options before playing folder content - -Checks for second (or multiple) trigger of the same folder and calls first swipe / second swipe action -accordingly. - -**Arguments**: - -- `folder`: Folder path relative to music library path -- `recursive`: Add folder recursively - #### get\_single\_coverart @@ -1089,22 +1063,6 @@ For volume control do not use directly, but use through the plugin 'volume', as the user may have configured a volume control manager other than MPD - - -#### play\_card\_callbacks - -Callback handler instance for play_card events. - -- is executed when play_card function is called -States: -- See :class:`PlayCardState` -See :class:`PlayContentCallbacks` - - - - -# components.playermpd.coverart\_cache\_manager - # components.rpc\_command\_alias diff --git a/resources/default-settings/cards.example.yaml b/resources/default-settings/cards.example.yaml index e01a45ba6..762a10887 100644 --- a/resources/default-settings/cards.example.yaml +++ b/resources/default-settings/cards.example.yaml @@ -53,10 +53,10 @@ 'T': package: player plugin: ctrl - method: play_card + method: play_folder args: [path/to/music/folder] 'G': - alias: play_card + alias: play_folder args: path/to/music/folder 'V+': alias: change_volume diff --git a/src/jukebox/components/playermpd/__init__.py b/src/jukebox/components/playermpd/__init__.py index 772b8c654..c647b5ff2 100644 --- a/src/jukebox/components/playermpd/__init__.py +++ b/src/jukebox/components/playermpd/__init__.py @@ -41,12 +41,6 @@ Internal status - last played folder: Needed to detect second swipe - -Saving {'player_status': {'last_played_folder': 'TraumfaengerStarkeLieder', 'CURRENTSONGPOS': '0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3'}, -'audio_folder_status': -{'TraumfaengerStarkeLieder': {'ELAPSED': '1.0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3', 'CURRENTSONGPOS': '0', 'PLAYSTATUS': 'stop', 'RESUME': 'OFF', 'SHUFFLE': 'OFF', 'LOOP': 'OFF', 'SINGLE': 'OFF'}, -'Giraffenaffen': {'ELAPSED': '1.0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3', 'CURRENTSONGPOS': '0', 'PLAYSTATUS': 'play', 'RESUME': 'OFF', 'SHUFFLE': 'OFF', 'LOOP': 'OFF', 'SINGLE': 'OFF'}}} - References: https://github.com/Mic92/python-mpd2 https://python-mpd2.readthedocs.io/en/latest/topics/commands.html @@ -87,6 +81,7 @@ import logging import time import functools +from typing import Callable from pathlib import Path import components.player import jukebox.cfghandler @@ -151,8 +146,7 @@ def __init__(self): 'play': self.play, 'skip': self.next, 'rewind': self.rewind, - 'replay': self.replay, - 'replay_if_stopped': self.replay_if_stopped} + 'replay': self.replay} self.second_swipe_action = None self.decode_2nd_swipe_option() @@ -196,24 +190,24 @@ def __init__(self): self.music_player_status['audio_folder_status'] = {} self.music_player_status.save_to_json() self.current_folder_status = {} - self.music_player_status['player_status']['last_played_folder'] = '' - else: - last_played_folder = self.music_player_status['player_status'].get('last_played_folder') - if last_played_folder: - # current_folder_status is a dict, but last_played_folder a str - self.current_folder_status = self.music_player_status['audio_folder_status'][last_played_folder] - # Restore the playlist status in mpd - # But what about playback position? - self.mpd_client.clear() - # This could fail and cause load fail of entire package: - # self.mpd_client.add(last_played_folder) - logger.info(f"Last Played Folder: {last_played_folder}") + # self.music_player_status['player_status']['last_played_folder'] = '' + # else: + # last_played_folder = self.music_player_status['player_status'].get('last_played_folder') + # if last_played_folder: + # # current_folder_status is a dict, but last_played_folder a str + # self.current_folder_status = self.music_player_status['audio_folder_status'][last_played_folder] + # # Restore the playlist status in mpd + # # But what about playback position? + # self.mpd_client.clear() + # # This could fail and cause load fail of entire package: + # # self.mpd_client.add(last_played_folder) + # logger.info(f"Last Played Folder: {last_played_folder}") # Clear last folder played, as we actually did not play any folder yet # Needed for second swipe detection # TODO: This will loose the last_played_folder information is the box is started and closed with playing anything... # Change this to last_played_folder and shutdown_state (for restoring) - self.music_player_status['player_status']['last_played_folder'] = '' + # self.music_player_status['player_status']['last_played_folder'] = '' self.old_song = None self.mpd_status = {} @@ -326,6 +320,10 @@ def update_wait(self): self._db_wait_for_update(state) return state + # --------------- + # Player + # --------------- + @plugs.tag def play(self): with self.mpd_lock: @@ -415,7 +413,8 @@ def replay(self): Will reset settings to folder config""" logger.debug("Replay") with self.mpd_lock: - self.play_folder(self.music_player_status['player_status']['last_played_folder']) + # TODO: Restore from PlayPositionTracker and play + return @plugs.tag def toggle(self): @@ -423,18 +422,6 @@ def toggle(self): with self.mpd_lock: self.mpd_client.pause() - @plugs.tag - def replay_if_stopped(self): - """ - Re-start playing the last-played folder unless playlist is still playing - - > [!NOTE] - > To me this seems much like the behaviour of play, - > but we keep it as it is specifically implemented in box 2.X""" - with self.mpd_lock: - if self.mpd_status['state'] == 'stop': - self.play_folder(self.music_player_status['player_status']['last_played_folder']) - # Shuffle def _shuffle(self, random): # As long as we don't work with waiting lists (aka playlist), this implementation is ok! @@ -519,13 +506,6 @@ def move(self): # MPDClient.swapid(song1, song2) raise NotImplementedError - @plugs.tag - def play_single(self, song_url): - with self.mpd_lock: - self.mpd_client.clear() - self.mpd_client.addid(song_url) - self.mpd_client.play() - @plugs.tag def resume(self): with self.mpd_lock: @@ -534,45 +514,76 @@ def resume(self): self.mpd_client.seek(songpos, elapsed) self.mpd_client.play() - @plugs.tag - def play_card(self, folder: str, recursive: bool = False): - """ - Main entry point for trigger music playing from RFID reader. Decodes second swipe options before playing folder content - - Checks for second (or multiple) trigger of the same folder and calls first swipe / second swipe action - accordingly. + # --------------- + # Play songs + # --------------- - :param folder: Folder path relative to music library path - :param recursive: Add folder recursively - """ - # Developers notes: - # - # * 2nd swipe trigger may also happen, if playlist has already stopped playing - # --> Generally, treat as first swipe - # * 2nd swipe of same Card ID may also happen if a different song has been played in between from WebUI - # --> Treat as first swipe - # * With place-not-swipe: Card is placed on reader until playlist expieres. Music stop. Card is removed and - # placed again on the reader: Should be like first swipe - # * TODO: last_played_folder is restored after box start, so first swipe of last played card may look like - # second swipe - # - logger.debug(f"last_played_folder = {self.music_player_status['player_status']['last_played_folder']}") + def call_with_second_swipe(self, identifier: str, action: Callable, **kwargs): + logger.debug(f"last_played_identifier = {self.music_player_status['player_status']['last_played_identifier']}") with self.mpd_lock: - is_second_swipe = self.music_player_status['player_status']['last_played_folder'] == folder + is_second_swipe = self.music_player_status['player_status']['last_played_identifier'] == identifier + 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 - play_card_callbacks.run_callbacks(folder, PlayCardState.secondSwipe) - + if 'folder' in kwargs: # TODO: Legacy code from previous implementation. should be adapted + play_card_callbacks.run_callbacks(kwargs.get('folder'), PlayCardState.secondSwipe) self.second_swipe_action() else: logger.debug('Calling first swipe action') + if 'folder' in kwargs: # TODO: Legacy code from previous implementation. should be adapted + play_card_callbacks.run_callbacks(kwargs.get('folder'), PlayCardState.firstSwipe) + action(**kwargs) + self.music_player_status['player_status']['last_played_identifier'] = identifier + + @plugs.tag + def play_card(self, identifier: str, **kwargs): + actions = { + 'play_single': self.play_single, + 'play_album': self.play_album, + 'play_folder': self.play_folder + } + + action = actions.get(identifier) + if action: + self.call_with_second_swipe(identifier, action, **kwargs) + else: + logger.error(f"Function '{identifier}' does not exist.") + + @plugs.tag + def play_single(self, song_url: str): + with self.mpd_lock: + self.mpd_client.clear() + self.mpd_client.addid(song_url) + self.mpd_client.play() + + @plugs.tag + def play_album(self, albumartist: str, album: str): + with self.mpd_lock: + self.mpd_client.clear() + self.mpd_retry_with_mutex(self.mpd_client.findadd, 'albumartist', albumartist, 'album', album) + self.mpd_client.play() + + @plugs.tag + def play_folder(self, folder: str, recursive: bool = False): + with self.mpd_lock: + self.mpd_client.clear() + + plc = playlistgenerator.PlaylistCollector(components.player.get_music_library_path()) + plc.parse(folder, recursive) + uri = '--unset--' + try: + for uri in plc: + self.mpd_client.addid(uri) + except mpd.base.CommandError as e: + logger.error(f"{e.__class__.__qualname__}: {e} at uri {uri}") + except Exception as e: + logger.error(f"{e.__class__.__qualname__}: {e} at uri {uri}") - # run callbacks before play_folder is invoked - play_card_callbacks.run_callbacks(folder, PlayCardState.firstSwipe) + self.mpd_client.play() - self.play_folder(folder, recursive) + # --------------- + # Cover Art + # --------------- @plugs.tag def get_single_coverart(self, song_url): @@ -595,6 +606,10 @@ def flush_coverart_cache(self): return self.coverart_cache_manager.flush_cache() + # --------------- + # Playlists + # --------------- + @plugs.tag def get_folder_content(self, folder: str): """ @@ -608,58 +623,6 @@ def get_folder_content(self, folder: str): plc.get_directory_content(folder) return plc.playlist - @plugs.tag - def play_folder(self, folder: str, recursive: bool = False) -> None: - """ - Playback a music folder. - - Folder content is added to the playlist as described by :mod:`jukebox.playlistgenerator`. - The playlist is cleared first. - - :param folder: Folder path relative to music library path - :param recursive: Add folder recursively - """ - # TODO: This changes the current state -> Need to save last state - with self.mpd_lock: - logger.info(f"Play folder: '{folder}'") - self.mpd_client.clear() - - plc = playlistgenerator.PlaylistCollector(components.player.get_music_library_path()) - plc.parse(folder, recursive) - uri = '--unset--' - try: - for uri in plc: - self.mpd_client.addid(uri) - except mpd.base.CommandError as e: - logger.error(f"{e.__class__.__qualname__}: {e} at uri {uri}") - except Exception as e: - logger.error(f"{e.__class__.__qualname__}: {e} at uri {uri}") - - self.music_player_status['player_status']['last_played_folder'] = folder - - self.current_folder_status = self.music_player_status['audio_folder_status'].get(folder) - if self.current_folder_status is None: - self.current_folder_status = self.music_player_status['audio_folder_status'][folder] = {} - - self.mpd_client.play() - - @plugs.tag - def play_album(self, albumartist: str, album: str): - """ - Playback a album found in MPD database. - - All album songs are added to the playlist - The playlist is cleared first. - - :param albumartist: Artist of the Album provided by MPD database - :param album: Album name provided by MPD database - """ - with self.mpd_lock: - logger.info(f"Play album: '{album}' by '{albumartist}") - self.mpd_client.clear() - self.mpd_retry_with_mutex(self.mpd_client.findadd, 'albumartist', albumartist, 'album', album) - self.mpd_client.play() - @plugs.tag def queue_load(self, folder): # There was something playing before -> stop and save state @@ -713,6 +676,10 @@ def get_song_by_url(self, song_url): return song + # --------------- + # Volume + # --------------- + def get_volume(self): """ Get the current volume diff --git a/src/jukebox/components/rpc_command_alias.py b/src/jukebox/components/rpc_command_alias.py index 5a7820733..c880e7d32 100644 --- a/src/jukebox/components/rpc_command_alias.py +++ b/src/jukebox/components/rpc_command_alias.py @@ -12,12 +12,6 @@ # -------------------------------------------------------------- cmd_alias_definitions = { # Player - 'play_card': { - 'title': 'Play music folder triggered by card swipe', - 'note': "This function you'll want to use most often", - 'package': 'player', - 'plugin': 'ctrl', - 'method': 'play_card'}, 'play_album': { 'title': 'Play Album triggered by card swipe', 'note': "This function plays the content of a given album", diff --git a/src/jukebox/jukebox/utils.py b/src/jukebox/jukebox/utils.py index 4cc0270ae..1fd222c7a 100644 --- a/src/jukebox/jukebox/utils.py +++ b/src/jukebox/jukebox/utils.py @@ -210,9 +210,9 @@ def generate_cmd_alias_rst(stream): print(".. |--| unicode:: U+2014", file=stream) print(".. |->| unicode:: U+21d2\n", file=stream) - # 'play_card': - # Executes : player.ctrl.play_card() - # Signature : player.ctrl.play_card(folder=None) + # 'play_folder': + # Executes : player.ctrl.play_folder() + # Signature : player.ctrl.play_folder(folder=None) # Description: Main entry point for trigger music playing from RFID reader for cmd, action in cmd_alias_definitions.items(): try: