Last active 1765754500

Platypush configuration for a Termux setup on Android with a local Mopidy music server and support for notifications and music commands over ntfy

Revision 4020862c808e005e39be4d7b2653eeef55b22f69

config.yaml Raw
1#!~/.config/platypush/config.yaml
2
3# Platypush configuration file for Termux Mopidy Notifier
4
5# A name for your device
6device_id: phone
7
8# Enable the Web server
9backend.http:
10 # port: 8008
11 # Reduce the number of workers to avoid phantom process SIGKILL
12 # See https://ivonblog.com/en-us/posts/fix-termux-signal9-error/
13 num_workers: 1
14
15ntfy:
16 # Uncomment and set your custom server URL if you're using a self-hosted ntfy server
17 # Otherwise, the default public server (https://ntfy.sh) will be used.
18 # server_url: https://your-ntfy-server.com
19 subscriptions:
20 # Subscribe to the topic where music commands will be sent.
21 # NOTE: It must match the topic configured in your ntfy mobile app
22 - music-commands-<your-unique-id>
23
24music.mopidy:
25 host: localhost
26 # port: 6680
27
28# Optional, if you also want to enable the MPD bindings
29music.mpd:
30 host: localhost
31 # port: 6600
32 # Set poll_interval to null - music.mopidy is already enabled and it
33 # receives updates over WebSocket. Avoid polling the MPD interface to
34 # prevent battery drain.
35 poll_interval: null
mopidy_notifications.py Raw
1"""
2This module integrates Mopidy music events with mobile notifications
3using the ntfy service. It listens for music playback events and updates
4the mobile notification with the current track information, including
5cover art if available.
6"""
7
8import json
9import logging
10import os
11import pathlib
12import time
13
14import requests
15
16from platypush import Config, hook, run
17
18# Music events to hook into
19from platypush.message.event.music import (
20 MusicPlayEvent,
21 MusicPauseEvent,
22 MusicStopEvent,
23 NewPlayingTrackEvent,
24)
25
26# NOTE: The topic that will be used to send music notifications.
27# It must match the one configured in your ntfy mobile app.
28music_notifications_topic = "music-notifications-<your-unique-id>"
29
30logger = logging.getLogger("music_integrations")
31
32
33def get_cover_by_uri(uri):
34 """
35 A utility method that retrieves the cover image URL for a given Mopidy URI,
36 using directly the Mopidy HTTP JSON-RPC API. It also caches the results
37 locally to avoid repeated requests.
38 """
39 cache_path = os.path.join(
40 os.path.expanduser("~"), ".cache", "platypush", "mopidy", "covers.json"
41 )
42
43 cached_images = {}
44 pathlib.Path(cache_path).parent.mkdir(exist_ok=True, parents=True)
45 if os.path.isfile(cache_path):
46 try:
47 with open(cache_path, "r") as f:
48 cached_images = json.load(f)
49 except Exception as e:
50 logger.warning("Could not load cached cover images: %s", e)
51
52 # Return cached image URL if available
53 if cached_images.get(uri):
54 return cached_images[uri]
55
56 resp = None
57
58 try:
59 # Make a POST request to the Mopidy JSON-RPC API to get the image URL
60 resp = requests.post(
61 "http://localhost:6680/mopidy/rpc",
62 timeout=5.0,
63 json={
64 "jsonrpc": "2.0",
65 "id": 1,
66 "method": "core.library.get_images",
67 "params": {"uris": [uri]},
68 },
69 )
70
71 resp.raise_for_status()
72 except Exception as e:
73 logger.warning("Could not retrieve cover for %s: %s", uri, e)
74 return None
75
76 image_url = cached_images[uri] = (
77 resp.json().get("result", {}).get(uri, [{}])[0].get("uri")
78 )
79
80 # Update the cache file with the new image URL
81 with open(cache_path, "w") as f:
82 f.write(json.dumps(cached_images))
83
84 return image_url
85
86
87def update_mobile_music_notification():
88 """
89 Updates the mobile music notification with the current track info
90 retrieved from Mopidy, sending it via ntfy.
91 """
92
93 # This matches the device_id configured in config.yaml
94 device_id = Config.get_device_id()
95 track = run("music.mopidy.current_track")
96 status = run("music.mopidy.status")
97 state = status.get("state")
98 image = None
99
100 if not state:
101 return
102
103 # Retrieve cover image if available
104 if track.get("x-albumuri"):
105 start_t = time.time()
106 image = get_cover_by_uri(track["x-albumuri"])
107 logger.info("Retrieved track image info in %.2f seconds", time.time() - start_t)
108
109 # Send the music notification via ntfy
110 run(
111 "ntfy.send_message",
112 topic=music_notifications_topic,
113 message=json.dumps(
114 {
115 "file": track.get("file"),
116 "artist": track.get("artist"),
117 "title": track.get("title"),
118 "album": track.get("album"),
119 "date": track.get("date"),
120 "duration": int(float(track["time"])) if track.get("time") else None,
121 "elapsed": (
122 int(float(status["elapsed"])) if status.get("elapsed") else None
123 ),
124 "image": image,
125 "device_id": device_id,
126 "state": state,
127 "priority": "min",
128 }
129 ),
130 )
131
132
133@hook(MusicPlayEvent)
134def on_music_play():
135 update_mobile_music_notification()
136
137
138@hook(MusicPauseEvent)
139def on_music_pause():
140 update_mobile_music_notification()
141
142
143@hook(MusicStopEvent)
144def on_music_stop():
145 update_mobile_music_notification()
146
147
148@hook(NewPlayingTrackEvent)
149def on_new_track():
150 update_mobile_music_notification()