diff --git a/resources/default-services/jukebox-spotify.service b/resources/default-services/jukebox-spotify.service deleted file mode 100644 index b2f0cc962..000000000 --- a/resources/default-services/jukebox-spotify.service +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=Junkebox-Spotify -Wants=network-online.target -After=network.target network-online.target -Requires=network-online.target - -[Service] -Type=simple -Restart=always -RestartSec=10 -PermissionsStartOnly=true -WorkingDirectory=HERE_DIR -ExecStartPre=/bin/sh -c 'until ping -c1 spotify.com; do sleep 5; done;' -ExecStart=/usr/bin/java -jar HERE_JAR_FILE - -[Install] -WantedBy=multi-user.target diff --git a/resources/default-settings/spotify.config.toml b/resources/default-settings/spotify.config.toml deleted file mode 100644 index daf154c35..000000000 --- a/resources/default-settings/spotify.config.toml +++ /dev/null @@ -1,84 +0,0 @@ -deviceId = "" ### Device ID (40 chars, leave empty for random) ### -deviceName = "Phoniebox" ### Device name ### -deviceType = "SPEAKER" ### Device type (COMPUTER, TABLET, SMARTPHONE, SPEAKER, TV, AVR, STB, AUDIO_DONGLE, GAME_CONSOLE, CAST_VIDEO, CAST_AUDIO, AUTOMOBILE, WEARABLE, UNKNOWN_SPOTIFY, CAR_THING, UNKNOWN) ### -preferredLocale = "de" ### Preferred locale ### -logLevel = "TRACE" ### Log level (OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE, ALL) ### - -[auth] ### Authentication ### -strategy = "USER_PASS" # Strategy (USER_PASS, ZEROCONF, BLOB, FACEBOOK, STORED) -username = "HERE_USERNAME" # Spotify username (BLOB, USER_PASS only) -password = "HERE_PASSWORD" # Spotify password (USER_PASS only) -blob = "" # Spotify authentication blob Base64-encoded (BLOB only) -storeCredentials = false # Whether to store reusable credentials on disk (not a plain password) -credentialsFile = "credentials.json" # Credentials file (JSON) - -[zeroconf] ### Zeroconf ### -listenPort = 12345 # Listen on this TCP port (`-1` for random) -listenAll = true # Listen on all interfaces (overrides `zeroconf.interfaces`) -interfaces = "" # Listen on these interfaces (comma separated list of names) - -[cache] ### Cache ### -enabled = false # Cache enabled -dir = "./cache/" -doCleanUp = true - -[network] ### Network ### -connectionTimeout = 10 # If ping isn't received within this amount of seconds, reconnect - -[preload] ### Preload ### -enabled = true # Preload enabled - -[time] ### Time correction ### -synchronizationMethod = "NTP" # Time synchronization method (NTP, PING, MELODY, MANUAL) -manualCorrection = 0 # Manual time correction in millis - -[player] ### Player ### -autoplayEnabled = false # Autoplay similar songs when your music ends -preferredAudioQuality = "NORMAL" # Preferred audio quality (NORMAL, HIGH, VERY_HIGH) -enableNormalisation = true # Whether to apply the Spotify loudness normalisation -normalisationPregain = +3.0 # Normalisation pregain in decibels (loud at +6, normal at +3, quiet at -5) -initialVolume = 65536 # Initial volume (0-65536) -volumeSteps = 64 # Number of volume notches -logAvailableMixers = true # Log available mixers -mixerSearchKeywords = "" # Mixer/backend search keywords (semicolon separated) -crossfadeDuration = 0 # Crossfade overlap time (in milliseconds) -output = "MIXER" # Audio output device (MIXER, PIPE, STDOUT, CUSTOM) -outputClass = "" # Audio output Java class name -releaseLineDelay = 20 # Release mixer line after set delay (in seconds) -pipe = "" # Output raw (signed) PCM to this file (`player.output` must be PIPE) -retryOnChunkError = true # Whether the player should retry fetching a chuck if it fails -metadataPipe = "" # Output metadata in Shairport Sync format (https://github.com/mikebrady/shairport-sync-metadata-reader) -bypassSinkVolume = false # Whether librespot-java should ignore volume events, sink volume is set to the max -localFilesPath = "" # Where librespot-java should search for local files - -[api] ### API ### -port = 24879 # API port (`api` module only) -host = "0.0.0.0" # API listen interface (`api` module only) - -[proxy] ### Proxy ### -enabled = false # Whether the proxy is enabled -type = "HTTP" # The proxy type (HTTP, SOCKS) -ssl = false # Connect to proxy using SSL (HTTP only) -address = "" # The proxy hostname -port = 0 # The proxy port -auth = false # Whether authentication is enabled on the server -username = "" # Basic auth username -password = "" # Basic auth password - -[shell] ### Shell ### -enabled = false # Shell events enabled -executeWithBash = false # Execute the command with `bash -c` -onContextChanged = "" -onTrackChanged = "" -onPlaybackEnded = "" -onPlaybackPaused = "" -onPlaybackResumed = "" -onTrackSeeked = "" -onMetadataAvailable = "" -onVolumeChanged = "" -onInactiveSession = "" -onPanicState = "" -onConnectionDropped = "" -onConnectionEstablished = "" -onStartedLoading = "" -onFinishedLoading = "" diff --git a/resources/default-settings/spotify_collection.example.yaml b/resources/default-settings/spotify_collection.example.yaml deleted file mode 100644 index a2acec798..000000000 --- a/resources/default-settings/spotify_collection.example.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# You can add your spotify uri's in this yaml file -# please stick to the syntax: -# - name: -# Example: -# - name: Gute Nacht! -# uri: spotify:playlist:37i9dQZF1DWSVYS2LMyMFg diff --git a/src/jukebox/components/playern/backends/mpd/interfacing_mpd.py b/src/jukebox/components/player/backends/mpd/interfacing_mpd.py similarity index 100% rename from src/jukebox/components/playern/backends/mpd/interfacing_mpd.py rename to src/jukebox/components/player/backends/mpd/interfacing_mpd.py diff --git a/src/jukebox/components/playern/core/__init__.py b/src/jukebox/components/player/core/__init__.py similarity index 100% rename from src/jukebox/components/playern/core/__init__.py rename to src/jukebox/components/player/core/__init__.py diff --git a/src/jukebox/components/playern/core/player_content.py b/src/jukebox/components/player/core/player_content.py similarity index 100% rename from src/jukebox/components/playern/core/player_content.py rename to src/jukebox/components/player/core/player_content.py diff --git a/src/jukebox/components/playern/core/player_status.py b/src/jukebox/components/player/core/player_status.py similarity index 100% rename from src/jukebox/components/playern/core/player_status.py rename to src/jukebox/components/player/core/player_status.py diff --git a/src/jukebox/components/playern/plugin/__init__.py b/src/jukebox/components/player/plugin/__init__.py similarity index 90% rename from src/jukebox/components/playern/plugin/__init__.py rename to src/jukebox/components/player/plugin/__init__.py index 1e24d0901..ed5c3c7d6 100644 --- a/src/jukebox/components/playern/plugin/__init__.py +++ b/src/jukebox/components/player/plugin/__init__.py @@ -8,10 +8,10 @@ import jukebox.plugs as plugin import jukebox.cfghandler -from components.playern.backends.mpd.interfacing_mpd import MPDBackend -from components.playern.core import PlayerCtrl -from components.playern.core.player_content import PlayerData -from components.playern.core.player_status import PlayerStatus +from components.player.backends.mpd.interfacing_mpd import MPDBackend +from components.player.core import PlayerCtrl +from components.player.core.player_content import PlayerData +from components.player.core.player_status import PlayerStatus logger = logging.getLogger('jb.player') cfg = jukebox.cfghandler.get_handler('jukebox') diff --git a/src/jukebox/components/playermpd/__init__.py b/src/jukebox/components/playermpd/__init__.py deleted file mode 100644 index ecac65ab8..000000000 --- a/src/jukebox/components/playermpd/__init__.py +++ /dev/null @@ -1,657 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Package for interfacing with the MPD Music Player Daemon - -Status information in three topics -1) Player Status: published only on change - This is a subset of the MPD status (and not the full MPD status) ?? - - folder - - song - - volume (volume is published only via player status, and not separatly to avoid too many Threads) - - ... -2) Elapsed time: published every 250 ms, unless constant - - elapsed -3) Folder Config: published only on change - This belongs to the folder being played - Publish: - - random, resume, single, loop - On save store this information: - Contains the information for resume functionality of each folder - - random, resume, single, loop - - if resume: - - current song, elapsed - - what is PLAYSTATUS for? - When to save - - on stop - Angstsave: - - on pause (only if box get turned off without proper shutdown - else stop gets implicitly called) - - on status change of random, resume, single, loop (for resume omit current status if currently playing- this has now meaning) - Load checks: - - if resume, but no song, elapsed -> log error and start from the beginning - -Status storing: - - Folder config for each folder (see above) - - Information to restart last folder playback, which is: - - last_folder -> folder_on_close - - song, elapsed - - random, resume, single, loop - - if resume is enabled, after start we need to set last_played_folder, such that card swipe is detected as second swipe?! - on the other hand: if resume is enabled, this is also saved to folder.config -> and that is checked by play card - -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 -https://mpd.readthedocs.io/en/latest/protocol.html - -sudo -u mpd speaker-test -t wav -c 2 -""" # noqa: E501 -# Warum ist "Second Swipe" im Player und nicht im RFID Reader? -# Second swipe ist abhängig vom Player State - nicht vom RFID state. -# Beispiel: RFID triggered Folder1, Webapp triggered Folder2, RFID Folder1: Dann muss das 2. Mal Folder1 auch als "first swipe" -# gewertet werden. Wenn der RFID das basierend auf IDs macht, kann der nicht unterscheiden und glaubt es ist 2. Swipe. -# Beispiel 2: Jemand hat RFID Reader (oder 1x RFID und 1x Barcode Scanner oder so) angeschlossen. Liest zuerst Karte mit -# Reader 1 und dann mit Reader 2: Reader 2 weiß nicht, was bei Reader 1 passiert ist und denkt es ist 1. swipe. -# Beispiel 3: RFID trigered Folder1, Playlist läuft durch und hat schon gestoppt, dann wird die Karte wieder vorgehalten. -# Dann muss das als 1. Swipe gewertet werden -# Beispiel 4: RFID triggered "Folder1", dann wird Karte "Volume Up" aufgelegt, dann wieder Karte "Folder1": Auch das ist -# aus Sicht ders Playbacks 2nd Swipe -# 2nd Swipe ist keine im Reader festgelegte Funktion extra fur den Player. -# -# In der aktuellen Implementierung weiß der Player (der second "swipe" dekodiert) überhaupt nichts vom RFID. -# Im Prinzip gibt es zwei "Play" Funktionen: (1) play always from start und (2) play with toggle action. -# Die Webapp ruft immer (1) auf und die RFID immer (2). Jetzt kann man sogar für einige Karten sagen -# immer (1) - also kein Second Swipe und für andere (2). -# Sollte der Reader das Swcond swipe dekodieren, muss aber der Reader den Status des Player kennen. -# Das ist allerdings ein Problem. In Version 2 ist das nicht aufgefallen, -# weil alles uber File I/Os lief - Thread safe ist das nicht! -# -# Beispiel: Second swipe bei anderen Funktionen, hier: WiFi on/off. -# Was die Karte Action tut ist ein Toggle. Der Toggle hängt vom Wifi State ab, den der RFID Kartenleser nicht kennt. -# Den kann der Leser auch nicht tracken. Der State kann ja auch über die WebApp oder Kommandozeile geändert werden. -# Toggle (und 2nd Swipe generell) ist immer vom Status des Zielsystems abhängig und kann damit nur vom Zielsystem geändert -# werden. Bei Wifi also braucht man 3 Funktionen: on / off / toggle. Toggle ist dann first swipe / second swipe - -import mpd -import threading -import logging -import time -import functools -import components.player -import jukebox.cfghandler -import jukebox.utils as utils -import jukebox.plugs as plugs -import jukebox.multitimer as multitimer -import jukebox.publishing as publishing -import jukebox.playlistgenerator as playlistgenerator -import misc - -from jukebox.NvManager import nv_manager -from .playcontentcallback import PlayContentCallbacks, PlayCardState - -logger = logging.getLogger('jb.PlayerMPD') -cfg = jukebox.cfghandler.get_handler('jukebox') - - -class MpdLock: - def __init__(self, client: mpd.MPDClient, host: str, port: int): - self._lock = threading.RLock() - self.client = client - self.host = host - self.port = port - - def _try_connect(self): - try: - self.client.connect(self.host, self.port) - except mpd.base.ConnectionError: - pass - - def __enter__(self): - self._lock.acquire() - self._try_connect() - return self - - def __exit__(self, exc_type, exc_value, traceback): - self._lock.release() - - def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: - locked = self._lock.acquire(blocking, timeout) - if locked: - self._try_connect() - return locked - - def release(self): - self._lock.release() - - def locked(self): - return self._lock.locked() - - -class PlayerMPD: - """Interface to MPD Music Player Daemon""" - - def __init__(self): - self.nvm = nv_manager() - self.mpd_host = cfg.getn('playermpd', 'host') - self.music_player_status = self.nvm.load(cfg.getn('playermpd', 'status_file')) - - self.second_swipe_action_dict = {'toggle': self.toggle, - 'play': self.play, - 'skip': self.next, - 'rewind': self.rewind, - 'replay': self.replay, - 'replay_if_stopped': self.replay_if_stopped} - self.second_swipe_action = None - self.decode_2nd_swipe_option() - - self.mpd_client = mpd.MPDClient() - # The timeout refer to the low-level socket time-out - # If these are too short and the response is not fast enough (due to the PI being busy), - # the current MPC command times out. Leave these at blocking calls, since we do not react on a timed out socket - # in any relevant matter anyway - self.mpd_client.timeout = None # network timeout in seconds (floats allowed), default: None - self.mpd_client.idletimeout = None # timeout for fetching the result of the idle command - self.connect() - logger.info(f"Connected to MPD Version: {self.mpd_client.mpd_version}") - - self.current_folder_status = {} - if not self.music_player_status: - self.music_player_status['player_status'] = {} - 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}") - - # 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.old_song = None - self.mpd_status = {} - self.mpd_status_poll_interval = 0.25 - self.mpd_lock = MpdLock(self.mpd_client, self.mpd_host, 6600) - self.status_is_closing = False - # self.status_thread = threading.Timer(self.mpd_status_poll_interval, self._mpd_status_poll).start() - - self.status_thread = multitimer.GenericEndlessTimerClass('mpd.timer_status', - self.mpd_status_poll_interval, self._mpd_status_poll) - self.status_thread.start() - - def exit(self): - logger.debug("Exit routine of playermpd started") - self.status_is_closing = True - self.status_thread.cancel() - self.mpd_client.disconnect() - self.nvm.save_all() - return self.status_thread.timer_thread - - def connect(self): - self.mpd_client.connect(self.mpd_host, 6600) - - def decode_2nd_swipe_option(self): - cfg_2nd_swipe_action = cfg.setndefault('playermpd', 'second_swipe_action', 'alias', value='none').lower() - if cfg_2nd_swipe_action not in [*self.second_swipe_action_dict.keys(), 'none', 'custom']: - logger.error(f"Config mpd.second_swipe_action must be one of " - f"{[*self.second_swipe_action_dict.keys(), 'none', 'custom']}. Ignore setting.") - if cfg_2nd_swipe_action in self.second_swipe_action_dict.keys(): - self.second_swipe_action = self.second_swipe_action_dict[cfg_2nd_swipe_action] - if cfg_2nd_swipe_action == 'custom': - custom_action = utils.decode_rpc_call(cfg.getn('playermpd', 'second_swipe_action', default=None)) - self.second_swipe_action = functools.partial(plugs.call_ignore_errors, - custom_action['package'], - custom_action['plugin'], - custom_action['method'], - custom_action['args'], - custom_action['kwargs']) - - def mpd_retry_with_mutex(self, mpd_cmd, *args): - """ - This method adds thread saftey for acceses to mpd via a mutex lock, - it shall be used for each access to mpd to ensure thread safety - In case of a communication error the connection will be reestablished and the pending command will be repeated 2 times - - I think this should be refactored to a decorator - """ - with self.mpd_lock: - try: - value = mpd_cmd(*args) - except Exception as e: - logger.error(f"{e.__class__.__qualname__}: {e}") - value = None - return value - - def _mpd_status_poll(self): - """ - this method polls the status from mpd and stores the important inforamtion in the music_player_status, - it will repeat itself in the intervall specified by self.mpd_status_poll_interval - """ - self.mpd_status.update(self.mpd_retry_with_mutex(self.mpd_client.status)) - self.mpd_status.update(self.mpd_retry_with_mutex(self.mpd_client.currentsong)) - - if self.mpd_status.get('elapsed') is not None: - self.current_folder_status["ELAPSED"] = self.mpd_status['elapsed'] - self.music_player_status['player_status']["CURRENTSONGPOS"] = self.mpd_status['song'] - self.music_player_status['player_status']["CURRENTFILENAME"] = self.mpd_status['file'] - - if self.mpd_status.get('file') is not None: - self.current_folder_status["CURRENTFILENAME"] = self.mpd_status['file'] - self.current_folder_status["CURRENTSONGPOS"] = self.mpd_status['song'] - self.current_folder_status["ELAPSED"] = self.mpd_status.get('elapsed', '0.0') - self.current_folder_status["PLAYSTATUS"] = self.mpd_status['state'] - self.current_folder_status["RESUME"] = "OFF" - self.current_folder_status["SHUFFLE"] = "OFF" - self.current_folder_status["LOOP"] = "OFF" - self.current_folder_status["SINGLE"] = "OFF" - - # Delete the volume key to avoid confusion - # Volume is published via the 'volume' component! - try: - del self.mpd_status['volume'] - except KeyError: - pass - publishing.get_publisher().send('playerstatus', self.mpd_status) - - @plugs.tag - def get_player_type_and_version(self): - with self.mpd_lock: - value = self.mpd_client.mpd_version() - return value - - @plugs.tag - def update(self): - with self.mpd_lock: - state = self.mpd_client.update() - return state - - @plugs.tag - def update_wait(self): - state = self.update() - self._db_wait_for_update(state) - return state - - @plugs.tag - def play(self): - with self.mpd_lock: - self.mpd_client.play() - - @plugs.tag - def stop(self): - with self.mpd_lock: - self.mpd_client.stop() - - @plugs.tag - def pause(self, state: int = 1): - """Enforce pause to state (1: pause, 0: resume) - - This is what you want as card removal action: pause the playback, so it can be resumed when card is placed - on the reader again. What happens on re-placement depends on configured second swipe option - """ - with self.mpd_lock: - self.mpd_client.pause(state) - - @plugs.tag - def prev(self): - logger.debug("Prev") - with self.mpd_lock: - self.mpd_client.previous() - - @plugs.tag - def next(self): - """Play next track in current playlist""" - logger.debug("Next") - with self.mpd_lock: - self.mpd_client.next() - - @plugs.tag - def seek(self, new_time): - with self.mpd_lock: - self.mpd_client.seekcur(new_time) - - @plugs.tag - def shuffle(self, random): - # As long as we don't work with waiting lists (aka playlist), this implementation is ok! - self.mpd_retry_with_mutex(self.mpd_client.random, 1 if random else 0) - - @plugs.tag - def rewind(self): - """ - Re-start current playlist from first track - - Note: Will not re-read folder config, but leave settings untouched""" - logger.debug("Rewind") - with self.mpd_lock: - self.mpd_client.play(1) - - @plugs.tag - def replay(self): - """ - Re-start playing the last-played folder - - 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']) - - @plugs.tag - def toggle(self): - """Toggle pause state, i.e. do a pause / resume depending on current state""" - logger.debug("Toggle") - 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']) - - @plugs.tag - def repeatmode(self, mode): - if mode == 'repeat': - repeat = 1 - single = 0 - elif mode == 'single': - repeat = 1 - single = 1 - else: - repeat = 0 - single = 0 - - with self.mpd_lock: - self.mpd_client.repeat(repeat) - self.mpd_client.single(single) - - @plugs.tag - def get_current_song(self, param): - return self.mpd_status - - @plugs.tag - def map_filename_to_playlist_pos(self, filename): - # self.mpd_client.playlistfind() - raise NotImplementedError - - @plugs.tag - def remove(self): - raise NotImplementedError - - @plugs.tag - def move(self): - # song_id = param.get("song_id") - # step = param.get("step") - # MPDClient.playlistmove(name, from, to) - # 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: - songpos = self.current_folder_status["CURRENTSONGPOS"] - elapsed = self.current_folder_status["ELAPSED"] - 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. - - :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']}") - with self.mpd_lock: - is_second_swipe = self.music_player_status['player_status']['last_played_folder'] == folder - 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) - - self.second_swipe_action() - else: - logger.debug('Calling first swipe action') - - # run callbacks before play_folder is invoked - play_card_callbacks.run_callbacks(folder, PlayCardState.firstSwipe) - - self.play_folder(folder, recursive) - - @plugs.tag - def get_folder_content(self, folder: str): - """ - Get the folder content as content list with meta-information. Depth is always 1. - - Call repeatedly to descend in hierarchy - - :param folder: Folder path relative to music library path - """ - plc = playlistgenerator.PlaylistCollector(components.player.get_music_library_path()) - 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 - # Clear the queue - # Check / Create the playlist - # - not needed if same folder is played again? Buf what if files have been added a mpc update has been run? - # - and this a re-trigger to start the new playlist - # If we must update the playlists everytime anyway why write them to file and not just keep them in the queue? - # Load the playlist - # Get folder config and apply settings - pass - - @plugs.tag - def playerstatus(self): - return self.mpd_status - - @plugs.tag - def playlistinfo(self): - with self.mpd_lock: - value = self.mpd_client.playlistinfo() - return value - - # Attention: MPD.listal will consume a lot of memory with large libs.. should be refactored at some point - @plugs.tag - def list_all_dirs(self): - with self.mpd_lock: - result = self.mpd_client.listall() - # list = [entry for entry in list if 'directory' in entry] - return result - - @plugs.tag - def list_albums(self): - with self.mpd_lock: - albums = self.mpd_retry_with_mutex(self.mpd_client.list, 'album', 'group', 'albumartist') - - return albums - - @plugs.tag - def list_song_by_artist_and_album(self, albumartist, album): - with self.mpd_lock: - albums = self.mpd_retry_with_mutex(self.mpd_client.find, 'albumartist', albumartist, 'album', album) - - return albums - - @plugs.tag - def get_song_by_url(self, song_url): - with self.mpd_lock: - song = self.mpd_retry_with_mutex(self.mpd_client.find, 'file', song_url) - - return song - - def get_volume(self): - """ - Get the current volume - - 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""" - with self.mpd_lock: - volume = self.mpd_client.status().get('volume') - return int(volume) - - def set_volume(self, volume): - """ - Set the volume - - 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""" - with self.mpd_lock: - self.mpd_client.setvol(volume) - return self.get_volume() - - def _db_wait_for_update(self, update_id: int): - logger.debug("Waiting for update to finish") - while self._db_is_updating(update_id): - # a little throttling - time.sleep(0.1) - - def _db_is_updating(self, update_id: int): - with self.mpd_lock: - _status = self.mpd_client.status() - _cur_update_id = _status.get('updating_db') - if _cur_update_id is not None and int(_cur_update_id) <= int(update_id): - return True - else: - return False - - -# --------------------------------------------------------------------------- -# Plugin Initializer / Finalizer -# --------------------------------------------------------------------------- - -player_ctrl: PlayerMPD -#: Callback handler instance for play_card events. -#: - is executed when play_card function is called -#: States: -#: - See :class:`PlayCardState` -#: See :class:`PlayContentCallbacks` -play_card_callbacks: PlayContentCallbacks[PlayCardState] - - -@plugs.initialize -def initialize(): - global player_ctrl - player_ctrl = PlayerMPD() - plugs.register(player_ctrl, name='ctrl') - - global play_card_callbacks - play_card_callbacks = PlayContentCallbacks[PlayCardState]('play_card_callbacks', logger, context=player_ctrl.mpd_lock) - - # Update mpc library - library_update = cfg.setndefault('playermpd', 'library', 'update_on_startup', value=True) - if library_update: - player_ctrl.update() - - # Check user rights on music library - library_check_user_rights = cfg.setndefault('playermpd', 'library', 'check_user_rights', value=True) - if library_check_user_rights is True: - music_library_path = components.player.get_music_library_path() - if music_library_path is not None: - logger.info(f"Change user rights for {music_library_path}") - misc.recursive_chmod(music_library_path, mode_files=0o666, mode_dirs=0o777) - - -@plugs.atexit -def atexit(**ignored_kwargs): - global player_ctrl - return player_ctrl.exit() diff --git a/src/jukebox/components/playermpd/playcontentcallback.py b/src/jukebox/components/playermpd/playcontentcallback.py deleted file mode 100644 index a60452a23..000000000 --- a/src/jukebox/components/playermpd/playcontentcallback.py +++ /dev/null @@ -1,37 +0,0 @@ - -from enum import Enum -from typing import Callable, Generic, TypeVar - -from jukebox.callingback import CallbackHandler - - -class PlayCardState(Enum): - firstSwipe = 0, - secondSwipe = 1 - - -STATE = TypeVar('STATE', bound=Enum) - - -class PlayContentCallbacks(Generic[STATE], CallbackHandler): - """ - Callbacks are executed in various play functions - """ - - def register(self, func: Callable[[str, STATE], None]): - """ - Add a new callback function :attr:`func`. - - Callback signature is - - .. py:function:: func(folder: str, state: STATE) - :noindex: - - :param folder: relativ path to folder to play - :param state: indicator of the state inside the calling - """ - super().register(func) - - def run_callbacks(self, folder: str, state: STATE): - """:meta private:""" - super().run_callbacks(folder, state) diff --git a/src/webapp/src/commands/index.js b/src/webapp/src/commands/index.js index dd87ddf48..bd8fd782e 100644 --- a/src/webapp/src/commands/index.js +++ b/src/webapp/src/commands/index.js @@ -9,16 +9,15 @@ const commands = { plugin: 'ctrl', method: 'list_all_dirs', }, - 'mpd.get_albums': { + albumList: { _package: 'player', - plugin: 'mpd', - method: 'get_albums', + plugin: 'ctrl', + method: 'list_albums', }, - 'mpd.get_album_tracks': { + songList: { _package: 'player', - plugin: 'mpd', - method: 'get_album_tracks', - argKeys: ['album_artist', 'album'] + plugin: 'ctrl', + method: 'list_song_by_artist_and_album', }, getSongByUrl: { _package: 'player', @@ -26,11 +25,10 @@ const commands = { method: 'get_song_by_url', argKeys: ['song_url'] }, - 'mpd.get_files': { + folderList: { _package: 'player', - plugin: 'mpd', - method: 'get_files', - argKeys: ['path'] + plugin: 'ctrl', + method: 'get_folder_content', }, cardsList: { _package: 'cards', @@ -56,6 +54,24 @@ const commands = { plugin: 'ctrl', method: 'play', }, + play_single: { + _package: 'player', + plugin: 'ctrl', + method: 'play_single', + argKeys: ['song_url'] + }, + play_folder: { + _package: 'player', + plugin: 'ctrl', + method: 'play_folder', + argKeys: ['folder'] + }, + play_album: { + _package: 'player', + plugin: 'ctrl', + method: 'play_album', + argKeys: ['albumartist', 'album'] + }, pause: { _package: 'player', plugin: 'ctrl', @@ -79,17 +95,12 @@ const commands = { repeat: { _package: 'player', plugin: 'ctrl', - method: 'repeat', + method: 'repeatmode', }, seek: { - _package: 'players', - plugin: 'seek', - }, - 'mpd.play_uri': { _package: 'player', - plugin: 'mpd', - method: 'play_uri', - argKeys: ['uri'] + plugin: 'ctrl', + method: 'seek', }, // Volume diff --git a/src/webapp/src/components/Library/lists/albums/index.js b/src/webapp/src/components/Library/lists/albums/index.js index 92b1caedd..3e915a9fa 100644 --- a/src/webapp/src/components/Library/lists/albums/index.js +++ b/src/webapp/src/components/Library/lists/albums/index.js @@ -30,7 +30,7 @@ const Albums = ({ musicFilter }) => { useEffect(() => { const fetchAlbumList = async () => { setIsLoading(true); - const { result, error } = await request('mpd.get_albums'); + const { result, error } = await request('albumList'); setIsLoading(false); if(result) setAlbums(result.reduce(flatByAlbum, [])); diff --git a/src/webapp/src/components/Library/lists/albums/song-list/index.js b/src/webapp/src/components/Library/lists/albums/song-list/index.js index 707964e2b..006ab791b 100644 --- a/src/webapp/src/components/Library/lists/albums/song-list/index.js +++ b/src/webapp/src/components/Library/lists/albums/song-list/index.js @@ -30,10 +30,10 @@ const SongList = ({ const getSongList = async () => { setIsLoading(true); const { result, error } = await request( - 'mpd.get_album_tracks', + 'songList', { - album_artist: decodeURIComponent(artist), album: decodeURIComponent(album), + albumartist: decodeURIComponent(artist), } ); setIsLoading(false); @@ -70,23 +70,21 @@ const SongList = ({ marginTop: '0' }} > - {isLoading && } - {!isLoading && !error && - - {songs.map(song => - - )} - + {isLoading + ? + : + {songs.map(song => + + )} + } {error && - - {`${t('library.albums.no-songs-in-album')} 🤔`} - + {`${t('library.albums.no-songs-in-album')} 🤔`} } diff --git a/src/webapp/src/components/Library/lists/albums/song-list/song-list-controls.js b/src/webapp/src/components/Library/lists/albums/song-list/song-list-controls.js index 4423c5896..b2391819e 100644 --- a/src/webapp/src/components/Library/lists/albums/song-list/song-list-controls.js +++ b/src/webapp/src/components/Library/lists/albums/song-list/song-list-controls.js @@ -18,20 +18,14 @@ const SongListControls = ({ isSelecting }) => { const { t } = useTranslation(); - - const command = 'mpd.play_uri'; - const uri = [ - 'mpd', - 'album', encodeURI(album), - 'albumartist', encodeURI(albumartist) - ].join(':'); + const command = 'play_album'; const playAlbum = () => ( - request(command, { uri }) + request(command, { albumartist, album }) ); const registerAlbumToCard = () => ( - registerMusicToCard(command, { uri }) + registerMusicToCard(command, { albumartist, album }) ); return ( diff --git a/src/webapp/src/components/Library/lists/albums/song-list/song-list-headline.js b/src/webapp/src/components/Library/lists/albums/song-list/song-list-headline.js index 7cf33153d..55f1ec9b7 100644 --- a/src/webapp/src/components/Library/lists/albums/song-list/song-list-headline.js +++ b/src/webapp/src/components/Library/lists/albums/song-list/song-list-headline.js @@ -6,7 +6,7 @@ import { } from '@mui/material'; const SongListHeadline = ({ artist, album }) => ( - + {album} diff --git a/src/webapp/src/components/Library/lists/albums/song-list/song-list-item.js b/src/webapp/src/components/Library/lists/albums/song-list/song-list-item.js index a367aa3c4..0f22d2df3 100644 --- a/src/webapp/src/components/Library/lists/albums/song-list/song-list-item.js +++ b/src/webapp/src/components/Library/lists/albums/song-list/song-list-item.js @@ -17,7 +17,7 @@ const SongListItem = ({ }) => { const { t } = useTranslation(); - const command = 'mpd.play_uri'; + const command = 'play_single'; const { artist, duration, @@ -25,14 +25,12 @@ const SongListItem = ({ title, } = song; - const uri = `mpd:file:${file}`; - - const playSingle = () => ( - request(command, { uri }) - ); + const playSingle = () => { + request(command, { song_url: file }) + } const registerSongToCard = () => ( - registerMusicToCard(command, { uri }) + registerMusicToCard(command, { song_url: file }) ); return ( diff --git a/src/webapp/src/components/Library/lists/folders/folder-link.js b/src/webapp/src/components/Library/lists/folders/folder-link.js index 95d05a33f..1f55415c3 100644 --- a/src/webapp/src/components/Library/lists/folders/folder-link.js +++ b/src/webapp/src/components/Library/lists/folders/folder-link.js @@ -7,10 +7,10 @@ import { const FolderLink = forwardRef((props, ref) => { const { search: urlSearch } = useLocation(); const { data } = props; - const path = encodeURIComponent(data?.path); + const dir = encodeURIComponent(data?.dir); // TODO: Introduce fallback incase artist or album are undefined - const location = `/library/folders/${path}${urlSearch}`; + const location = `/library/folders/${dir}${urlSearch}`; return }); diff --git a/src/webapp/src/components/Library/lists/folders/folder-list-item-back.js b/src/webapp/src/components/Library/lists/folders/folder-list-item-back.js index d406359fa..2ff14667b 100644 --- a/src/webapp/src/components/Library/lists/folders/folder-list-item-back.js +++ b/src/webapp/src/components/Library/lists/folders/folder-list-item-back.js @@ -11,14 +11,14 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import FolderLink from './folder-link'; -const FolderListItemBack = ({ path }) => { +const FolderListItemBack = ({ dir }) => { const { t } = useTranslation(); return ( diff --git a/src/webapp/src/components/Library/lists/folders/folder-list-item.js b/src/webapp/src/components/Library/lists/folders/folder-list-item.js index c6b37f58f..755feef15 100644 --- a/src/webapp/src/components/Library/lists/folders/folder-list-item.js +++ b/src/webapp/src/components/Library/lists/folders/folder-list-item.js @@ -21,15 +21,12 @@ const FolderListItem = ({ registerMusicToCard, }) => { const { t } = useTranslation(); - const { directory, file } = folder; - const type = directory ? 'directory' : 'file'; - const path = directory || file; - const name = directory || folder.title; + const { type, name, path } = folder; const playItem = () => { switch(type) { - case 'directory': return request('mpd.play_uri', { uri: `mpd:folder:${path}` }); - case 'file': return request('mpd.play_uri', { uri: `mpd:file:${path}` }); + case 'directory': return request('play_folder', { folder: path, recursive: true }); + case 'file': return request('play_single', { song_url: path.replace(`${DEFAULT_AUDIO_DIR}/`, '') }); // TODO: Add missing Podcast // TODO: Add missing Stream default: return; @@ -38,8 +35,8 @@ const FolderListItem = ({ const registerItemToCard = () => { switch(type) { - case 'directory': return registerMusicToCard('mpd.play_uri', { uri: `mpd:folder:${path}` }); - case 'file': return registerMusicToCard('mpd.play_uri', { uri: `mpd:file:${path}` }); + case 'directory': return registerMusicToCard('play_folder', { folder: path, recursive: true }); + case 'file': return registerMusicToCard('play_single', { song_url: path.replace(`${DEFAULT_AUDIO_DIR}/`, '') }); // TODO: Add missing Podcast // TODO: Add missing Stream default: return; @@ -53,7 +50,7 @@ const FolderListItem = ({ type === 'directory' ? diff --git a/src/webapp/src/components/Library/lists/folders/folder-list.js b/src/webapp/src/components/Library/lists/folders/folder-list.js index 7a8fcfd72..36a9bcebd 100644 --- a/src/webapp/src/components/Library/lists/folders/folder-list.js +++ b/src/webapp/src/components/Library/lists/folders/folder-list.js @@ -9,27 +9,28 @@ import FolderListItemBack from './folder-list-item-back'; import { ROOT_DIRS } from '../../../../config'; const FolderList = ({ - path, + dir, folders, isSelecting, registerMusicToCard, }) => { - const getParent = (path) => { - const decodedPath = decodeURIComponent(path); - + const getParentDir = (dir) => { // TODO: ROOT_DIRS should be removed after paths are relative - if (ROOT_DIRS.includes(decodedPath)) return undefined; + const decodedDir = decodeURIComponent(dir); + + if (ROOT_DIRS.includes(decodedDir)) return undefined; - return dropLast(1, decodedPath.split('/')).join('/') || './'; + const parentDir = dropLast(1, decodedDir.split('/')).join('/'); + return parentDir; } - const parent = getParent(path); + const parentDir = getParentDir(dir); return ( - {parent && + {parentDir && } {folders.map((folder, key) => diff --git a/src/webapp/src/components/Library/lists/folders/index.js b/src/webapp/src/components/Library/lists/folders/index.js index 9f075d4c7..32bf47c33 100644 --- a/src/webapp/src/components/Library/lists/folders/index.js +++ b/src/webapp/src/components/Library/lists/folders/index.js @@ -16,7 +16,7 @@ const Folders = ({ registerMusicToCard, }) => { const { t } = useTranslation(); - const { path = './' } = useParams(); + const { dir = './' } = useParams(); const [folders, setFolders] = useState([]); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -33,8 +33,8 @@ const Folders = ({ const fetchFolderList = async () => { setIsLoading(true); const { result, error } = await request( - 'mpd.get_files', - { path: decodeURIComponent(path) } + 'folderList', + { folder: decodeURIComponent(dir) } ); setIsLoading(false); @@ -43,7 +43,7 @@ const Folders = ({ } fetchFolderList(); - }, [path]); + }, [dir]); const filteredFolders = folders.filter(search); @@ -56,7 +56,7 @@ const Folders = ({ return ( { return ( {isSelecting && } - + { element={} /> { const { t } = useTranslation(); - const { state: { player_status } } = useContext(PubSubContext); + const { + state, + setState, + } = useContext(PlayerContext); const { - playing, - shuffle, - repeat, - trackid, - } = player_status; + isPlaying, + playerstatus, + isShuffle, + isRepeat, + isSingle, + songIsScheduled + } = state; const toggleShuffle = () => { - request('shuffle', { val: !shuffle }); + request('shuffle', { random: !isShuffle }); } const toggleRepeat = () => { - let mode = repeat + 1; - if (mode > 2) mode = -1; - request('repeat', { val: mode }); + let mode = null; + if (!isRepeat && !isSingle) mode = 'repeat'; + if (isRepeat && !isSingle) mode = 'single'; + + request('repeat', { mode }); } + useEffect(() => { + setState({ + ...state, + isPlaying: playerstatus?.state === 'play' ? true : false, + songIsScheduled: playerstatus?.songid ? true : false, + isShuffle: playerstatus?.random === '1' ? true : false, + isRepeat: playerstatus?.repeat === '1' ? true : false, + isSingle: playerstatus?.single === '1' ? true : false, + }); + }, [playerstatus]); + const iconStyles = { padding: '7px' }; const labelShuffle = () => ( - shuffle + isShuffle ? t('player.controls.shuffle.deactivate') : t('player.controls.shuffle.activate') ); - // Toggle or set repeat (-1 toggle, 0 no repeat, 1 context, 2 single) const labelRepeat = () => { - const labels = [ - t('player.controls.repeat.activate'), - t('player.controls.repeat.activate-single'), - t('player.controls.repeat.deactivate'), - ]; - - return labels[repeat]; + if (!isRepeat) return t('player.controls.repeat.activate'); + if (isRepeat && !isSingle) return t('player.controls.repeat.activate-single'); + if (isRepeat && isSingle) return t('player.controls.repeat.deactivate'); }; return ( @@ -67,7 +80,7 @@ const Controls = () => { {/* Shuffle */} { {/* Skip previous track */} request('previous')} + disabled={!songIsScheduled} + onClick={e => request('previous')} size="large" sx={iconStyles} title={t('player.controls.skip')} @@ -89,36 +102,36 @@ const Controls = () => { {/* Play */} - {/* {!playing && */} + {!isPlaying && request('play')} - // disabled={!trackid} + onClick={e => request('play')} + disabled={!songIsScheduled} size="large" sx={iconStyles} title={t('player.controls.play')} > - {/* } */} + } {/* Pause */} - {/* {playing && */} + {isPlaying && request('pause')} + onClick={e => request('pause')} size="large" sx={iconStyles} title={t('player.controls.pause')} > - {/* } */} + } {/* Skip next track */} request('next')} + disabled={!songIsScheduled} + onClick={e => request('next')} size="large" sx={iconStyles} title={t('player.controls.next')} @@ -129,18 +142,18 @@ const Controls = () => { {/* Repeat */} 0 ? 'primary' : undefined} + color={isRepeat ? 'primary' : undefined} onClick={toggleRepeat} size="large" sx={iconStyles} title={labelRepeat()} > { - repeat < 2 && + !isSingle && } { - repeat === 2 && + isSingle && } diff --git a/src/webapp/src/components/Player/cover.js b/src/webapp/src/components/Player/cover.js index d20133acb..e44e38e6b 100644 --- a/src/webapp/src/components/Player/cover.js +++ b/src/webapp/src/components/Player/cover.js @@ -32,7 +32,7 @@ const Cover = ({ coverImage }) => { {coverImage && {t('player.cover.title')}} {!coverImage && diff --git a/src/webapp/src/components/Player/display.js b/src/webapp/src/components/Player/display.js index 6c1acff16..446eba349 100644 --- a/src/webapp/src/components/Player/display.js +++ b/src/webapp/src/components/Player/display.js @@ -1,14 +1,14 @@ import React, { useContext } from 'react'; import { useTranslation } from 'react-i18next'; -import PubSubContext from '../../context/pubsub/context'; +import PlayerContext from '../../context/player/context'; import Grid from '@mui/material/Grid'; import Typography from '@mui/material/Typography'; const Display = () => { const { t } = useTranslation(); - const { state: { player_status } } = useContext(PubSubContext); + const { state: { playerstatus } } = useContext(PlayerContext); const dontBreak = { whiteSpace: 'nowrap', @@ -20,15 +20,15 @@ const Display = () => { return ( - {player_status.trackid - ? (player_status.title || t('player.display.unknown-title')) + {playerstatus?.songid + ? (playerstatus?.title || t('player.display.unknown-title')) : t('player.display.no-song-in-queue') } - {player_status.trackid && (player_status.artist || t('player.display.unknown-artist')) } - - {player_status.trackid && (player_status.album || player_status.file) } + {playerstatus?.songid && (playerstatus?.artist || t('player.display.unknown-artist')) } + + {playerstatus?.songid && (playerstatus?.album || playerstatus?.file) } ); diff --git a/src/webapp/src/components/Player/index.js b/src/webapp/src/components/Player/index.js index 2a3a0f3c8..5efa5e10a 100644 --- a/src/webapp/src/components/Player/index.js +++ b/src/webapp/src/components/Player/index.js @@ -8,24 +8,35 @@ import Display from './display'; import SeekBar from './seekbar'; import Volume from './volume'; +import PlayerContext from '../../context/player/context'; import PubSubContext from '../../context/pubsub/context'; +import request from '../../utils/request'; +import { pluginIsLoaded } from '../../utils/utils'; const Player = () => { - const { state: { player_status } } = useContext(PubSubContext); + const { state: { playerstatus } } = useContext(PlayerContext); + const { state: { 'core.plugins.loaded': plugins } } = useContext(PubSubContext); + const { file } = playerstatus || {}; const [coverImage, setCoverImage] = useState(undefined); const [backgroundImage, setBackgroundImage] = useState('none'); useEffect(() => { - if (player_status?.coverArt) { - const coverImageSrc = `https://i.scdn.co/image/${player_status.coverArt}`; - setCoverImage(coverImageSrc); - setBackgroundImage([ - 'linear-gradient(to bottom, rgba(18, 18, 18, 0.7), rgba(18, 18, 18, 1))', - `url(${coverImageSrc})` - ].join(',')); + const getMusicCover = async () => { + const { result } = await request('musicCoverByFilenameAsBase64', { audio_src: file }); + if (result) { + setCoverImage(result); + setBackgroundImage([ + 'linear-gradient(to bottom, rgba(18, 18, 18, 0.7), rgba(18, 18, 18, 1))', + `url(data:image/jpeg;base64,${result})` + ].join(',')); + }; } - }, [player_status]); + + if (pluginIsLoaded(plugins, 'music_cover_art') && file) { + getMusicCover(); + } + }, [file, plugins]); return ( { const { t } = useTranslation(); - const { state: { player_status } } = useContext(PubSubContext); + const { state } = useContext(PlayerContext); + const { playerstatus } = state; - const [isRunning, setIsRunning] = useState(player_status?.playing); const [isSeeking, setIsSeeking] = useState(false); const [progress, setProgress] = useState(0); - const [timeElapsed, setTimeElapsed] = useState(parseFloat(player_status?.elapsed) || 0); - const timeTotal = parseFloat(player_status?.duration) || 0; + const [timeElapsed, setTimeElapsed] = useState(parseFloat(playerstatus?.elapsed) || 0); + const timeTotal = parseFloat(playerstatus?.duration) || 0; const updateTimeAndProgress = (newTime) => { setTimeElapsed(newTime); @@ -35,7 +35,7 @@ const SeekBar = () => { updateTimeAndProgress(progressToTime(timeTotal, newPosition)); }; - // Only send command to backend when user committed to new position + // Only send commend to backend when user committed to new position // We don't send it while seeking (too many useless requests) const playFromNewTime = () => { request('seek', { new_time: timeElapsed.toFixed(3) }); @@ -46,16 +46,16 @@ const SeekBar = () => { // Avoid updating time and progress when user is seeking to new // song position if (!isSeeking) { - updateTimeAndProgress(player_status?.elapsed); + updateTimeAndProgress(playerstatus?.elapsed); } - }, [player_status]); + }, [playerstatus]); return <> { > - + {toHHMMSS(parseInt(timeElapsed))} diff --git a/src/webapp/src/components/Settings/timers/timer.js b/src/webapp/src/components/Settings/timers/timer.js index c6323fd4b..20b990edc 100644 --- a/src/webapp/src/components/Settings/timers/timer.js +++ b/src/webapp/src/components/Settings/timers/timer.js @@ -11,7 +11,7 @@ import { useTheme } from '@mui/material/styles'; import request from '../../../utils/request'; import { - Counter, + Countdown, SliderTimer } from '../../general'; @@ -86,7 +86,7 @@ const Timer = ({ type }) => { marginLeft: '0', }}> {status?.enabled && - setEnabled(false)} stringEnded={t('settings.timers.ended')} diff --git a/src/webapp/src/components/general/Countdown.js b/src/webapp/src/components/general/Countdown.js new file mode 100644 index 000000000..84e91cb88 --- /dev/null +++ b/src/webapp/src/components/general/Countdown.js @@ -0,0 +1,31 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { toHHMMSS } from '../../utils/utils'; + +const Countdown = ({ onEnd, seconds, stringEnded = undefined }) => { + // This is required to avoid async updates on unmounted compomemts + // https://github.com/facebook/react/issues/14227 + const isMounted = useRef(null); + const [time, setTime] = useState(seconds); + + const onEndCallback = useCallback(() => onEnd(), [onEnd]); + + useEffect(() => { + isMounted.current = true; + + if (time === 0) return onEndCallback(); + setTimeout(() => { + if (isMounted.current) setTime(time - 1) + }, 1000); + + return () => { + isMounted.current = false; + } + }, [onEndCallback, time]); + + if (time) return toHHMMSS(time); + if (stringEnded) return stringEnded; + return toHHMMSS(0); +} + +export default Countdown; diff --git a/src/webapp/src/components/general/Counter.js b/src/webapp/src/components/general/Counter.js deleted file mode 100644 index 26c5884c7..000000000 --- a/src/webapp/src/components/general/Counter.js +++ /dev/null @@ -1,48 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; - -import { toHHMMSS } from '../../utils/utils'; - -const Counter = ({ - seconds, - direction = 'down', - end = 0, - paused = false, - onEnd = () => {}, - stringEnded = undefined -}) => { - // This is required to avoid async updates on unmounted components - // https://github.com/facebook/react/issues/14227 - const isMounted = useRef(null); - const [time, setTime] = useState(parseInt(seconds)); - - const onEndCallback = useCallback(() => onEnd(), [onEnd]); - - useEffect(() => { - isMounted.current = true; - - const summand = direction === 'down' ? -1 : 1; - - if (!paused) { - if (time >= end) return onEndCallback(); - setTimeout(() => { - if (isMounted.current) setTime(time + summand) - }, 1000); - } - - return () => { - isMounted.current = false; - } - }, [ - direction, - end, - onEndCallback, - paused, - time, - ]); - - if (time) return toHHMMSS(time); - if (stringEnded) return stringEnded; - return toHHMMSS(0); -} - -export default Counter; diff --git a/src/webapp/src/components/general/index.js b/src/webapp/src/components/general/index.js index e4aedf07e..a97998977 100644 --- a/src/webapp/src/components/general/index.js +++ b/src/webapp/src/components/general/index.js @@ -1,9 +1,9 @@ -import Counter from "./Counter" +import Countdown from "./Countdown" import SliderTimer from "./SliderTimer" import SwitchWithLoader from "./SwitchWithLoader" export { - Counter, + Countdown, SliderTimer, SwitchWithLoader, }; diff --git a/src/webapp/src/config.js b/src/webapp/src/config.js index a241add74..4383a7897 100644 --- a/src/webapp/src/config.js +++ b/src/webapp/src/config.js @@ -13,7 +13,6 @@ const SUBSCRIPTIONS = [ 'host.timer.cputemp', 'host.temperature.cpu', 'playerstatus', - 'player_status', 'rfid.card_id', 'volume.level', ];