Pipris is an extensible MPRIS scrobbler written with Deno.
at main 152 lines 3.9 kB view raw
1import dbus from "dbus-next"; 2 3const MPRIS_PREFIX = "org.mpris.MediaPlayer2."; 4const PLAYER_IFACE = "org.mpris.MediaPlayer2.Player"; 5const PROPS_IFACE = "org.freedesktop.DBus.Properties"; 6 7// Lowercase substrings matched against the bus name suffix. 8// Only players matching this whitelist will be used. 9const ALLOWED_PLAYERS = [ 10 // local players 11 "gapless", 12 "elisa", 13 "rhythmbox", 14 "g4music", 15 "lollypop", 16 "amberol", 17 "gnome-music", 18 "strawberry", 19 "clementine", 20 "audacious", 21 "quodlibet", 22 "deadbeef", 23 "cmus", 24 "musikcube", 25 "aimp", 26 "spotify", 27 "tidal", 28 "deezer", 29 "youtube-music", 30 "youtubemusic", 31 "nuclear", 32 "cider", 33 "feishin", 34 "sonixd", 35 "navidrome", 36 "plexamp", 37]; 38 39/** 40 * Return the session bus connection. 41 */ 42export function sessionBus() { 43 return dbus.sessionBus(); 44} 45 46/** 47 * List all MPRIS player bus names currently registered. 48 */ 49export async function listPlayers(bus) { 50 const dbusProxy = await bus.getProxyObject( 51 "org.freedesktop.DBus", 52 "/org/freedesktop/DBus", 53 ); 54 const iface = dbusProxy.getInterface("org.freedesktop.DBus"); 55 const names = await iface.ListNames(); 56 return names.filter((n) => n.startsWith(MPRIS_PREFIX)); 57} 58 59/** 60 * Pick the best player from a list of bus names based on the list mode. 61 * "off" – return the first player found (ignore the list) 62 * "priority" – prefer listed players, fall back to any player 63 * "on" – only allow listed players (strict whitelist) 64 */ 65export function selectPlayer(players, mode = "on") { 66 if (mode === "off") { 67 return players[0] ?? null; 68 } 69 70 // Find the first player that matches the list 71 for (const name of players) { 72 const suffix = name.slice(MPRIS_PREFIX.length).toLowerCase(); 73 if (ALLOWED_PLAYERS.some((p) => suffix.includes(p))) { 74 return name; 75 } 76 } 77 78 // In priority mode, fall back to any player 79 if (mode === "priority") { 80 return players[0] ?? null; 81 } 82 83 return null; 84} 85 86/** 87 * Read Metadata, PlaybackStatus, and Position from a player. 88 */ 89export async function getPlayerData(bus, playerName) { 90 const proxy = await bus.getProxyObject(playerName, "/org/mpris/MediaPlayer2"); 91 const props = proxy.getInterface(PROPS_IFACE); 92 93 const [metadata, playbackStatus, position] = await Promise.all([ 94 props.Get(PLAYER_IFACE, "Metadata"), 95 props.Get(PLAYER_IFACE, "PlaybackStatus"), 96 props.Get(PLAYER_IFACE, "Position"), 97 ]); 98 99 return { 100 metadata: metadata.value, // dict of Variants 101 playbackStatus: playbackStatus.value, // string 102 positionUs: Number(position.value), // microseconds 103 }; 104} 105 106/** 107 * Subscribe to PropertiesChanged and Seeked signals on the player interface. 108 * `onChange` receives (changedProperties: object, invalidated: string[]). 109 * `onSeeked` receives (positionUs: number). 110 * Returns a cleanup function. 111 */ 112export async function watchPlayer(bus, playerName, onChange, onSeeked) { 113 const proxy = await bus.getProxyObject(playerName, "/org/mpris/MediaPlayer2"); 114 const props = proxy.getInterface(PROPS_IFACE); 115 const player = proxy.getInterface(PLAYER_IFACE); 116 117 const propsHandler = (ifaceName, changed, invalidated) => { 118 if (ifaceName === PLAYER_IFACE) { 119 onChange(changed, invalidated); 120 } 121 }; 122 123 const seekHandler = (positionUs) => { 124 onSeeked(Number(positionUs)); 125 }; 126 127 props.on("PropertiesChanged", propsHandler); 128 player.on("Seeked", seekHandler); 129 130 return () => { 131 props.off("PropertiesChanged", propsHandler); 132 player.off("Seeked", seekHandler); 133 }; 134} 135 136/** 137 * Watch for new or removed MPRIS players on the bus. 138 * `onPlayerChange` is called with no arguments whenever the set of players changes. 139 */ 140export async function watchBus(bus, onPlayerChange) { 141 const dbusProxy = await bus.getProxyObject( 142 "org.freedesktop.DBus", 143 "/org/freedesktop/DBus", 144 ); 145 const iface = dbusProxy.getInterface("org.freedesktop.DBus"); 146 147 iface.on("NameOwnerChanged", (name, _oldOwner, _newOwner) => { 148 if (name.startsWith(MPRIS_PREFIX)) { 149 onPlayerChange(); 150 } 151 }); 152}