""" This module integrates Mopidy music playback with ntfy notifications. It listens for music control commands sent via ntfy and executes them on the Mopidy server. """ import json import logging import time from collections import namedtuple from enum import Enum from platypush import Config, hook, run # Listen for ntfy message notifications from platypush.message.event.ntfy import NotificationEvent # NOTE: The topic used to send music commands to Mopidy via ntfy. # It must match the one configured in your ntfy mobile app. music_commands_topic = "music-commands-" music_plugin = "music.mopidy" # Or "music.mpd" if using MPD logger = logging.getLogger("music_integrations") ClassCommand = namedtuple("ClassCommand", ["name", "action"]) class MusicCommands(Enum): """ Enum representing music control commands and their corresponding actions. """ PLAY = ClassCommand("play", f"{music_plugin}.play") PAUSE = ClassCommand("pause", f"{music_plugin}.pause") STOP = ClassCommand("stop", f"{music_plugin}.stop") NEXT = ClassCommand("next", f"{music_plugin}.next") PREVIOUS = ClassCommand("previous", f"{music_plugin}.previous") TOGGLE = ClassCommand("toggle", f"{music_plugin}.toggle") supported_commands = {cmd.value.name: cmd.value.action for cmd in MusicCommands} def update_mobile_music_notification(): """ Updates the mobile music notification with the current track info retrieved from Mopidy, sending it via ntfy. """ # This matches the device_id configured in config.yaml device_id = Config.get_device_id() track = run("music.mopidy.current_track") status = run("music.mopidy.status") state = status.get("state") image = None if not state: return # Retrieve cover image if available if track.get("x-albumuri"): start_t = time.time() image = get_cover_by_uri(track["x-albumuri"]) logger.info("Retrieved track image info in %.2f seconds", time.time() - start_t) # Send the music notification via ntfy run( "ntfy.send_message", topic=music_notifications_topic, message=json.dumps( { "file": track.get("file"), "artist": track.get("artist"), "title": track.get("title"), "album": track.get("album"), "date": track.get("date"), "duration": status.get("track", {}).get("time"), "elapsed": status.get("time"), "image": image, "device_id": device_id, "state": state, "priority": "min", } ), ) @hook(NotificationEvent, topic=music_commands_topic) def on_music_command(event: NotificationEvent): """ Handles incoming music commands from ntfy notifications. """ try: payload = json.loads(event.message) except Exception as e: logger.warning("Invalid music command payload: %s", e) return # Ignore the command if it's not for this device. # This is useful if multiple devices share the same ntfy topic. device_id = payload.get("device_id") if device_id and device_id != Config.get_device_id(): return command = next( (cmd for cmd in MusicCommands if cmd.value.name == payload.get("command")), None ) if not command: logger.warning( "Unsupported music command: %s. Supported commands: %s", payload.get("command"), list(supported_commands.keys()), ) return # Handle the TOGGLE case depending on current playback state if command == MusicCommands.TOGGLE: status = run("music.mopidy.status") if status.get("state") == "play": action = MusicCommands.PAUSE.value.action else: action = MusicCommands.PLAY.value.action else: action = command.value.action logger.info( "Executing music command on %s@%s: %s", music_plugin, device_id, command.value.name, ) run(action)