config.yaml
· 1.1 KiB · YAML
Raw
#!~/.config/platypush/config.yaml
# Platypush configuration file for Termux Mopidy Notifier
# A name for your device
device_id: phone
# Enable the Web server
backend.http:
# port: 8008
# Reduce the number of workers to avoid phantom process SIGKILL
# See https://ivonblog.com/en-us/posts/fix-termux-signal9-error/
num_workers: 1
ntfy:
# Uncomment and set your custom server URL if you're using a self-hosted ntfy server
# Otherwise, the default public server (https://ntfy.sh) will be used.
# server_url: https://your-ntfy-server.com
subscriptions:
# Subscribe to the topic where music commands will be sent.
# NOTE: It must match the topic configured in your ntfy mobile app
- music-commands-<your-unique-id>
music.mopidy:
host: localhost
# port: 6680
# Optional, if you also want to enable the MPD bindings
music.mpd:
host: localhost
# port: 6600
# Set poll_interval to null - music.mopidy is already enabled and it
# receives updates over WebSocket. Avoid polling the MPD interface to
# prevent battery drain.
poll_interval: null
| 1 | #!~/.config/platypush/config.yaml |
| 2 | |
| 3 | # Platypush configuration file for Termux Mopidy Notifier |
| 4 | |
| 5 | # A name for your device |
| 6 | device_id: phone |
| 7 | |
| 8 | # Enable the Web server |
| 9 | backend.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 | |
| 15 | ntfy: |
| 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 | |
| 24 | music.mopidy: |
| 25 | host: localhost |
| 26 | # port: 6680 |
| 27 | |
| 28 | # Optional, if you also want to enable the MPD bindings |
| 29 | music.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
· 4.0 KiB · Python
Raw
"""
This module integrates Mopidy music events with mobile notifications
using the ntfy service. It listens for music playback events and updates
the mobile notification with the current track information, including
cover art if available.
"""
import json
import logging
import os
import pathlib
import time
import requests
from platypush import Config, hook, run
# Music events to hook into
from platypush.message.event.music import (
MusicPlayEvent,
MusicPauseEvent,
MusicStopEvent,
NewPlayingTrackEvent,
)
# NOTE: The topic that will be used to send music notifications.
# It must match the one configured in your ntfy mobile app.
music_notifications_topic = "music-notifications-<your-unique-id>"
logger = logging.getLogger("music_integrations")
def get_cover_by_uri(uri):
"""
A utility method that retrieves the cover image URL for a given Mopidy URI,
using directly the Mopidy HTTP JSON-RPC API. It also caches the results
locally to avoid repeated requests.
"""
cache_path = os.path.join(
os.path.expanduser("~"), ".cache", "platypush", "mopidy", "covers.json"
)
cached_images = {}
pathlib.Path(cache_path).parent.mkdir(exist_ok=True, parents=True)
if os.path.isfile(cache_path):
try:
with open(cache_path, "r") as f:
cached_images = json.load(f)
except Exception as e:
logger.warning("Could not load cached cover images: %s", e)
# Return cached image URL if available
if cached_images.get(uri):
return cached_images[uri]
resp = None
try:
# Make a POST request to the Mopidy JSON-RPC API to get the image URL
resp = requests.post(
"http://localhost:6680/mopidy/rpc",
timeout=5.0,
json={
"jsonrpc": "2.0",
"id": 1,
"method": "core.library.get_images",
"params": {"uris": [uri]},
},
)
resp.raise_for_status()
except Exception as e:
logger.warning("Could not retrieve cover for %s: %s", uri, e)
return None
image_url = cached_images[uri] = (
resp.json().get("result", {}).get(uri, [{}])[0].get("uri")
)
# Update the cache file with the new image URL
with open(cache_path, "w") as f:
f.write(json.dumps(cached_images))
return image_url
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(MusicPlayEvent)
def on_music_play():
update_mobile_music_notification()
@hook(MusicPauseEvent)
def on_music_pause():
update_mobile_music_notification()
@hook(MusicStopEvent)
def on_music_stop():
update_mobile_music_notification()
@hook(NewPlayingTrackEvent)
def on_new_track():
update_mobile_music_notification()
| 1 | """ |
| 2 | This module integrates Mopidy music events with mobile notifications |
| 3 | using the ntfy service. It listens for music playback events and updates |
| 4 | the mobile notification with the current track information, including |
| 5 | cover art if available. |
| 6 | """ |
| 7 | |
| 8 | import json |
| 9 | import logging |
| 10 | import os |
| 11 | import pathlib |
| 12 | import time |
| 13 | |
| 14 | import requests |
| 15 | |
| 16 | from platypush import Config, hook, run |
| 17 | |
| 18 | # Music events to hook into |
| 19 | from 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. |
| 28 | music_notifications_topic = "music-notifications-<your-unique-id>" |
| 29 | |
| 30 | logger = logging.getLogger("music_integrations") |
| 31 | |
| 32 | |
| 33 | def 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 | |
| 87 | def 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": status.get("track", {}).get("time"), |
| 121 | "elapsed": status.get("time"), |
| 122 | "image": image, |
| 123 | "device_id": device_id, |
| 124 | "state": state, |
| 125 | "priority": "min", |
| 126 | } |
| 127 | ), |
| 128 | ) |
| 129 | |
| 130 | |
| 131 | @hook(MusicPlayEvent) |
| 132 | def on_music_play(): |
| 133 | update_mobile_music_notification() |
| 134 | |
| 135 | |
| 136 | @hook(MusicPauseEvent) |
| 137 | def on_music_pause(): |
| 138 | update_mobile_music_notification() |
| 139 | |
| 140 | |
| 141 | @hook(MusicStopEvent) |
| 142 | def on_music_stop(): |
| 143 | update_mobile_music_notification() |
| 144 | |
| 145 | |
| 146 | @hook(NewPlayingTrackEvent) |
| 147 | def on_new_track(): |
| 148 | update_mobile_music_notification() |