Pipris is an extensible MPRIS scrobbler written with Deno.

initial

clay.rip 1ba47c77

+1243
+2
.gitignore
··· 1 + config/*.json 2 + node_modules/
+12
README.md
··· 1 + # Pipris ![Version 0.0.1](https://img.shields.io/badge/version-0.0.1-blue) 2 + 3 + Pipris is an extensible MPRIS scrobbler written in Deno. It was originally created for 4 + [teal.fm](https://teal.fm), but can be extended to support a wide variety of 5 + services (e.g. Last.fm or Discord rich presence). 6 + 7 + The name is derived from [piper](https://tangled.org/teal.fm/piper) + 8 + [MPRIS](https://specifications.freedesktop.org/mpris/latest). 9 + 10 + ## Usage 11 + 12 + There is an example module in `src/modules/example.js.disabled`, and a teal.fm module in `src/modules/teal.js`.
+5
config/teal.json.example
··· 1 + { 2 + "service": "https://bsky.social", 3 + "handle": "your-handle.bsky.social", 4 + "password": "your-app-password" 5 + }
+12
deno.json
··· 1 + { 2 + "tasks": { 3 + "dev": "deno run --watch src/main.js" 4 + }, 5 + "imports": { 6 + "@atcute/client": "npm:@atcute/client@^4.2.1", 7 + "@atcute/password-session": "npm:@atcute/password-session@^0.1.0", 8 + "dbus-next": "npm:dbus-next@^0.10.2" 9 + }, 10 + "nodeModulesDir": "auto", 11 + "allowScripts": ["npm:usocket@0.3.0"] 12 + }
+688
deno.lock
··· 1 + { 2 + "version": "5", 3 + "specifiers": { 4 + "npm:@atcute/client@^4.2.1": "4.2.1", 5 + "npm:@atcute/password-session@0.1": "0.1.0", 6 + "npm:dbus-next@~0.10.2": "0.10.2" 7 + }, 8 + "npm": { 9 + "@atcute/client@4.2.1": { 10 + "integrity": "sha512-ZBFM2pW075JtgGFu5g7HHZBecrClhlcNH8GVP9Zz1aViWR+cjjBsTpeE63rJs+FCOHFYlirUyo5L8SGZ4kMINw==", 11 + "dependencies": [ 12 + "@atcute/identity", 13 + "@atcute/lexicons" 14 + ] 15 + }, 16 + "@atcute/identity@1.1.3": { 17 + "integrity": "sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng==", 18 + "dependencies": [ 19 + "@atcute/lexicons", 20 + "@badrap/valita" 21 + ] 22 + }, 23 + "@atcute/lexicons@1.2.9": { 24 + "integrity": "sha512-/RRHm2Cw9o8Mcsrq0eo8fjS9okKYLGfuFwrQ0YoP/6sdSDsXshaTLJsvLlcUcaDaSJ1YFOuHIo3zr2Om2F/16g==", 25 + "dependencies": [ 26 + "@atcute/uint8array", 27 + "@atcute/util-text", 28 + "@standard-schema/spec", 29 + "esm-env" 30 + ] 31 + }, 32 + "@atcute/password-session@0.1.0": { 33 + "integrity": "sha512-r4iUNT7aQ1J6XXGO+pu39037hFQd0GYEhOuw/aykoNI3HHFLX2t5YyrxWTu5uKMGECk3s7zEgc8B8ol9JsMjRA==", 34 + "dependencies": [ 35 + "@atcute/client", 36 + "@atcute/identity", 37 + "@atcute/lexicons" 38 + ] 39 + }, 40 + "@atcute/uint8array@1.1.1": { 41 + "integrity": "sha512-3LsC8XB8TKe9q/5hOA5sFuzGaIFdJZJNewC5OKa3o/eU6+K7JR6see9Zy2JbQERNVnRl11EzbNov1efgLMAs4g==" 42 + }, 43 + "@atcute/util-text@1.1.1": { 44 + "integrity": "sha512-JH0SxzUQJAmbOBTYyhxQbkkI6M33YpjlVLEcbP5GYt43xgFArzV0FJVmEpvIj0kjsmphHB45b6IitdvxPdec9w==", 45 + "dependencies": [ 46 + "unicode-segmenter" 47 + ] 48 + }, 49 + "@badrap/valita@0.4.6": { 50 + "integrity": "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==" 51 + }, 52 + "@nornagon/put@0.0.8": { 53 + "integrity": "sha512-ugvXJjwF5ldtUpa7D95kruNJ41yFQDEKyF5CW4TgKJnh+W/zmlBzXXeKTyqIgwMFrkePN2JqOBqcF0M0oOunow==" 54 + }, 55 + "@standard-schema/spec@1.1.0": { 56 + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==" 57 + }, 58 + "abbrev@1.1.1": { 59 + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" 60 + }, 61 + "ajv@6.12.6": { 62 + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 63 + "dependencies": [ 64 + "fast-deep-equal", 65 + "fast-json-stable-stringify", 66 + "json-schema-traverse", 67 + "uri-js" 68 + ] 69 + }, 70 + "ansi-regex@2.1.1": { 71 + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==" 72 + }, 73 + "aproba@1.2.0": { 74 + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" 75 + }, 76 + "are-we-there-yet@1.1.7": { 77 + "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", 78 + "dependencies": [ 79 + "delegates", 80 + "readable-stream" 81 + ], 82 + "deprecated": true 83 + }, 84 + "asn1@0.2.6": { 85 + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", 86 + "dependencies": [ 87 + "safer-buffer" 88 + ] 89 + }, 90 + "assert-plus@1.0.0": { 91 + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" 92 + }, 93 + "asynckit@0.4.0": { 94 + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 95 + }, 96 + "aws-sign2@0.7.0": { 97 + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" 98 + }, 99 + "aws4@1.13.2": { 100 + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" 101 + }, 102 + "balanced-match@1.0.2": { 103 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 104 + }, 105 + "bcrypt-pbkdf@1.0.2": { 106 + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", 107 + "dependencies": [ 108 + "tweetnacl" 109 + ] 110 + }, 111 + "bindings@1.5.0": { 112 + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", 113 + "dependencies": [ 114 + "file-uri-to-path" 115 + ] 116 + }, 117 + "brace-expansion@1.1.12": { 118 + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", 119 + "dependencies": [ 120 + "balanced-match", 121 + "concat-map" 122 + ] 123 + }, 124 + "caseless@0.12.0": { 125 + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" 126 + }, 127 + "chownr@2.0.0": { 128 + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" 129 + }, 130 + "code-point-at@1.1.0": { 131 + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==" 132 + }, 133 + "combined-stream@1.0.8": { 134 + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 135 + "dependencies": [ 136 + "delayed-stream" 137 + ] 138 + }, 139 + "concat-map@0.0.1": { 140 + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" 141 + }, 142 + "console-control-strings@1.1.0": { 143 + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" 144 + }, 145 + "core-util-is@1.0.2": { 146 + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" 147 + }, 148 + "dashdash@1.14.1": { 149 + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", 150 + "dependencies": [ 151 + "assert-plus" 152 + ] 153 + }, 154 + "dbus-next@0.10.2": { 155 + "integrity": "sha512-kLNQoadPstLgKKGIXKrnRsMgtAK/o+ix3ZmcfTfvBHzghiO9yHXpoKImGnB50EXwnfSFaSAullW/7UrSkAISSQ==", 156 + "dependencies": [ 157 + "@nornagon/put", 158 + "event-stream", 159 + "hexy", 160 + "jsbi", 161 + "long", 162 + "safe-buffer", 163 + "xml2js" 164 + ], 165 + "optionalDependencies": [ 166 + "usocket" 167 + ] 168 + }, 169 + "delayed-stream@1.0.0": { 170 + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" 171 + }, 172 + "delegates@1.0.0": { 173 + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" 174 + }, 175 + "duplexer@0.1.2": { 176 + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" 177 + }, 178 + "ecc-jsbn@0.1.2": { 179 + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", 180 + "dependencies": [ 181 + "jsbn", 182 + "safer-buffer" 183 + ] 184 + }, 185 + "env-paths@2.2.1": { 186 + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" 187 + }, 188 + "esm-env@1.2.2": { 189 + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" 190 + }, 191 + "event-stream@3.3.4": { 192 + "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", 193 + "dependencies": [ 194 + "duplexer", 195 + "from", 196 + "map-stream", 197 + "pause-stream", 198 + "split", 199 + "stream-combiner", 200 + "through" 201 + ] 202 + }, 203 + "extend@3.0.2": { 204 + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 205 + }, 206 + "extsprintf@1.3.0": { 207 + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" 208 + }, 209 + "fast-deep-equal@3.1.3": { 210 + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 211 + }, 212 + "fast-json-stable-stringify@2.1.0": { 213 + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 214 + }, 215 + "file-uri-to-path@1.0.0": { 216 + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" 217 + }, 218 + "forever-agent@0.6.1": { 219 + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" 220 + }, 221 + "form-data@2.3.3": { 222 + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 223 + "dependencies": [ 224 + "asynckit", 225 + "combined-stream", 226 + "mime-types" 227 + ] 228 + }, 229 + "from@0.1.7": { 230 + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==" 231 + }, 232 + "fs-minipass@2.1.0": { 233 + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", 234 + "dependencies": [ 235 + "minipass@3.3.6" 236 + ] 237 + }, 238 + "fs.realpath@1.0.0": { 239 + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" 240 + }, 241 + "gauge@2.7.4": { 242 + "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", 243 + "dependencies": [ 244 + "aproba", 245 + "console-control-strings", 246 + "has-unicode", 247 + "object-assign", 248 + "signal-exit", 249 + "string-width", 250 + "strip-ansi", 251 + "wide-align" 252 + ], 253 + "deprecated": true 254 + }, 255 + "getpass@0.1.7": { 256 + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", 257 + "dependencies": [ 258 + "assert-plus" 259 + ] 260 + }, 261 + "glob@7.2.3": { 262 + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 263 + "dependencies": [ 264 + "fs.realpath", 265 + "inflight", 266 + "inherits", 267 + "minimatch", 268 + "once", 269 + "path-is-absolute" 270 + ], 271 + "deprecated": true 272 + }, 273 + "graceful-fs@4.2.11": { 274 + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" 275 + }, 276 + "har-schema@2.0.0": { 277 + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==" 278 + }, 279 + "har-validator@5.1.5": { 280 + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", 281 + "dependencies": [ 282 + "ajv", 283 + "har-schema" 284 + ], 285 + "deprecated": true 286 + }, 287 + "has-unicode@2.0.1": { 288 + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" 289 + }, 290 + "hexy@0.2.11": { 291 + "integrity": "sha512-ciq6hFsSG/Bpt2DmrZJtv+56zpPdnq+NQ4ijEFrveKN0ZG1mhl/LdT1NQZ9se6ty1fACcI4d4vYqC9v8EYpH2A==", 292 + "bin": true 293 + }, 294 + "http-signature@1.2.0": { 295 + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", 296 + "dependencies": [ 297 + "assert-plus", 298 + "jsprim", 299 + "sshpk" 300 + ] 301 + }, 302 + "inflight@1.0.6": { 303 + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 304 + "dependencies": [ 305 + "once", 306 + "wrappy" 307 + ], 308 + "deprecated": true 309 + }, 310 + "inherits@2.0.4": { 311 + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 312 + }, 313 + "is-fullwidth-code-point@1.0.0": { 314 + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", 315 + "dependencies": [ 316 + "number-is-nan" 317 + ] 318 + }, 319 + "is-typedarray@1.0.0": { 320 + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" 321 + }, 322 + "isarray@1.0.0": { 323 + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" 324 + }, 325 + "isexe@2.0.0": { 326 + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" 327 + }, 328 + "isstream@0.1.2": { 329 + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" 330 + }, 331 + "jsbi@2.0.5": { 332 + "integrity": "sha512-TzO/62Hxeb26QMb4IGlI/5X+QLr9Uqp1FPkwp2+KOICW+Q+vSuFj61c8pkT6wAns4WcK56X7CmSHhJeDGWOqxQ==" 333 + }, 334 + "jsbn@0.1.1": { 335 + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" 336 + }, 337 + "json-schema-traverse@0.4.1": { 338 + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 339 + }, 340 + "json-schema@0.4.0": { 341 + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" 342 + }, 343 + "json-stringify-safe@5.0.1": { 344 + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" 345 + }, 346 + "jsprim@1.4.2": { 347 + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", 348 + "dependencies": [ 349 + "assert-plus", 350 + "extsprintf", 351 + "json-schema", 352 + "verror" 353 + ] 354 + }, 355 + "long@4.0.0": { 356 + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" 357 + }, 358 + "map-stream@0.1.0": { 359 + "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==" 360 + }, 361 + "mime-db@1.52.0": { 362 + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" 363 + }, 364 + "mime-types@2.1.35": { 365 + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 366 + "dependencies": [ 367 + "mime-db" 368 + ] 369 + }, 370 + "minimatch@3.1.2": { 371 + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 372 + "dependencies": [ 373 + "brace-expansion" 374 + ] 375 + }, 376 + "minipass@3.3.6": { 377 + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", 378 + "dependencies": [ 379 + "yallist" 380 + ] 381 + }, 382 + "minipass@5.0.0": { 383 + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" 384 + }, 385 + "minizlib@2.1.2": { 386 + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", 387 + "dependencies": [ 388 + "minipass@3.3.6", 389 + "yallist" 390 + ] 391 + }, 392 + "mkdirp@1.0.4": { 393 + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", 394 + "bin": true 395 + }, 396 + "nan@2.25.0": { 397 + "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==" 398 + }, 399 + "node-gyp@7.1.2": { 400 + "integrity": "sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ==", 401 + "dependencies": [ 402 + "env-paths", 403 + "glob", 404 + "graceful-fs", 405 + "nopt", 406 + "npmlog", 407 + "request", 408 + "rimraf", 409 + "semver", 410 + "tar", 411 + "which" 412 + ], 413 + "bin": true 414 + }, 415 + "nopt@5.0.0": { 416 + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", 417 + "dependencies": [ 418 + "abbrev" 419 + ], 420 + "bin": true 421 + }, 422 + "npmlog@4.1.2": { 423 + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", 424 + "dependencies": [ 425 + "are-we-there-yet", 426 + "console-control-strings", 427 + "gauge", 428 + "set-blocking" 429 + ], 430 + "deprecated": true 431 + }, 432 + "number-is-nan@1.0.1": { 433 + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==" 434 + }, 435 + "oauth-sign@0.9.0": { 436 + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 437 + }, 438 + "object-assign@4.1.1": { 439 + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" 440 + }, 441 + "once@1.4.0": { 442 + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 443 + "dependencies": [ 444 + "wrappy" 445 + ] 446 + }, 447 + "path-is-absolute@1.0.1": { 448 + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" 449 + }, 450 + "pause-stream@0.0.11": { 451 + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", 452 + "dependencies": [ 453 + "through" 454 + ] 455 + }, 456 + "performance-now@2.1.0": { 457 + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" 458 + }, 459 + "process-nextick-args@2.0.1": { 460 + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 461 + }, 462 + "psl@1.15.0": { 463 + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", 464 + "dependencies": [ 465 + "punycode" 466 + ] 467 + }, 468 + "punycode@2.3.1": { 469 + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" 470 + }, 471 + "qs@6.5.5": { 472 + "integrity": "sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==" 473 + }, 474 + "readable-stream@2.3.8": { 475 + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", 476 + "dependencies": [ 477 + "core-util-is", 478 + "inherits", 479 + "isarray", 480 + "process-nextick-args", 481 + "safe-buffer", 482 + "string_decoder", 483 + "util-deprecate" 484 + ] 485 + }, 486 + "request@2.88.2": { 487 + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", 488 + "dependencies": [ 489 + "aws-sign2", 490 + "aws4", 491 + "caseless", 492 + "combined-stream", 493 + "extend", 494 + "forever-agent", 495 + "form-data", 496 + "har-validator", 497 + "http-signature", 498 + "is-typedarray", 499 + "isstream", 500 + "json-stringify-safe", 501 + "mime-types", 502 + "oauth-sign", 503 + "performance-now", 504 + "qs", 505 + "safe-buffer", 506 + "tough-cookie", 507 + "tunnel-agent", 508 + "uuid" 509 + ], 510 + "deprecated": true 511 + }, 512 + "rimraf@3.0.2": { 513 + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 514 + "dependencies": [ 515 + "glob" 516 + ], 517 + "deprecated": true, 518 + "bin": true 519 + }, 520 + "safe-buffer@5.1.2": { 521 + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 522 + }, 523 + "safer-buffer@2.1.2": { 524 + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 525 + }, 526 + "sax@1.5.0": { 527 + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==" 528 + }, 529 + "semver@7.7.4": { 530 + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", 531 + "bin": true 532 + }, 533 + "set-blocking@2.0.0": { 534 + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" 535 + }, 536 + "signal-exit@3.0.7": { 537 + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" 538 + }, 539 + "split@0.3.3": { 540 + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", 541 + "dependencies": [ 542 + "through" 543 + ] 544 + }, 545 + "sshpk@1.18.0": { 546 + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", 547 + "dependencies": [ 548 + "asn1", 549 + "assert-plus", 550 + "bcrypt-pbkdf", 551 + "dashdash", 552 + "ecc-jsbn", 553 + "getpass", 554 + "jsbn", 555 + "safer-buffer", 556 + "tweetnacl" 557 + ], 558 + "bin": true 559 + }, 560 + "stream-combiner@0.0.4": { 561 + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", 562 + "dependencies": [ 563 + "duplexer" 564 + ] 565 + }, 566 + "string-width@1.0.2": { 567 + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", 568 + "dependencies": [ 569 + "code-point-at", 570 + "is-fullwidth-code-point", 571 + "strip-ansi" 572 + ] 573 + }, 574 + "string_decoder@1.1.1": { 575 + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 576 + "dependencies": [ 577 + "safe-buffer" 578 + ] 579 + }, 580 + "strip-ansi@3.0.1": { 581 + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", 582 + "dependencies": [ 583 + "ansi-regex" 584 + ] 585 + }, 586 + "tar@6.2.1": { 587 + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", 588 + "dependencies": [ 589 + "chownr", 590 + "fs-minipass", 591 + "minipass@5.0.0", 592 + "minizlib", 593 + "mkdirp", 594 + "yallist" 595 + ], 596 + "deprecated": true 597 + }, 598 + "through@2.3.8": { 599 + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" 600 + }, 601 + "tough-cookie@2.5.0": { 602 + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", 603 + "dependencies": [ 604 + "psl", 605 + "punycode" 606 + ] 607 + }, 608 + "tunnel-agent@0.6.0": { 609 + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", 610 + "dependencies": [ 611 + "safe-buffer" 612 + ] 613 + }, 614 + "tweetnacl@0.14.5": { 615 + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" 616 + }, 617 + "unicode-segmenter@0.14.5": { 618 + "integrity": "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==" 619 + }, 620 + "uri-js@4.4.1": { 621 + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 622 + "dependencies": [ 623 + "punycode" 624 + ] 625 + }, 626 + "usocket@0.3.0": { 627 + "integrity": "sha512-V/H02RNiaOCJZuPoKont/y12VJaImC6C5xW7OzPFjYu9qnig0yv9hyp9E7Wqjm6d8yZuZouH3NAfDATVMgh2SQ==", 628 + "dependencies": [ 629 + "bindings", 630 + "nan", 631 + "node-gyp" 632 + ], 633 + "scripts": true 634 + }, 635 + "util-deprecate@1.0.2": { 636 + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 637 + }, 638 + "uuid@3.4.0": { 639 + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", 640 + "deprecated": true, 641 + "bin": true 642 + }, 643 + "verror@1.10.0": { 644 + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", 645 + "dependencies": [ 646 + "assert-plus", 647 + "core-util-is", 648 + "extsprintf" 649 + ] 650 + }, 651 + "which@2.0.2": { 652 + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 653 + "dependencies": [ 654 + "isexe" 655 + ], 656 + "bin": true 657 + }, 658 + "wide-align@1.1.5": { 659 + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", 660 + "dependencies": [ 661 + "string-width" 662 + ] 663 + }, 664 + "wrappy@1.0.2": { 665 + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 666 + }, 667 + "xml2js@0.4.23": { 668 + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", 669 + "dependencies": [ 670 + "sax", 671 + "xmlbuilder" 672 + ] 673 + }, 674 + "xmlbuilder@11.0.1": { 675 + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" 676 + }, 677 + "yallist@4.0.0": { 678 + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 679 + } 680 + }, 681 + "workspace": { 682 + "dependencies": [ 683 + "npm:@atcute/client@^4.2.1", 684 + "npm:@atcute/password-session@0.1", 685 + "npm:dbus-next@~0.10.2" 686 + ] 687 + } 688 + }
+62
src/formatter.js
··· 1 + const TEN_MINUTES_MS = 10 * 60 * 1000; 2 + 3 + /** 4 + * Build the data object expected by the stub module. 5 + * 6 + * @param {object} metadata Raw MPRIS Metadata dict 7 + * @param {string} playbackStatus "Playing" | "Paused" | "Stopped" 8 + * @param {number} positionUs Current position in microseconds 9 + * @param {string} playedTime ISO timestamp of when the track was first seen 10 + * @returns {object|null} Formatted data, or null if metadata is missing 11 + */ 12 + export function formatTrackData(metadata, playbackStatus, positionUs, playedTime) { 13 + const title = metaString(metadata, "xesam:title"); 14 + if (!title) return null; 15 + 16 + const artistList = metaStringArray(metadata, "xesam:artist"); 17 + const album = metaString(metadata, "xesam:album") ?? ""; 18 + const lengthUs = metaInt(metadata, "mpris:length") ?? 0; 19 + 20 + const now = Date.now(); 21 + const positionMs = Math.floor(positionUs / 1000); 22 + const lengthMs = Math.floor(lengthUs / 1000); 23 + 24 + let expiry; 25 + if (playbackStatus === "Paused") { 26 + expiry = new Date(now + TEN_MINUTES_MS).toISOString(); 27 + } else { 28 + const remainingMs = Math.max(0, lengthMs - positionMs); 29 + expiry = new Date(now + remainingMs).toISOString(); 30 + } 31 + 32 + return { 33 + artists: artistList.map((name) => ({ artistName: name })), 34 + trackName: title, 35 + playedTime, 36 + releaseName: album, 37 + duration: Math.round(lengthMs / 1000), 38 + expiry, 39 + }; 40 + } 41 + 42 + // --- helpers for reading dbus-next Variant values ---- 43 + 44 + function metaString(metadata, key) { 45 + const v = metadata[key]; 46 + if (!v) return undefined; 47 + return typeof v.value === "string" ? v.value : String(v.value); 48 + } 49 + 50 + function metaStringArray(metadata, key) { 51 + const v = metadata[key]; 52 + if (!v) return []; 53 + const arr = v.value; 54 + return Array.isArray(arr) ? arr.map(String) : [String(arr)]; 55 + } 56 + 57 + function metaInt(metadata, key) { 58 + const v = metadata[key]; 59 + if (!v) return undefined; 60 + const n = Number(v.value); 61 + return Number.isFinite(n) ? n : undefined; 62 + }
+184
src/index.js
··· 1 + import { readdir, readFile } from "node:fs/promises"; 2 + import { pathToFileURL } from "node:url"; 3 + import path from "node:path"; 4 + import { 5 + sessionBus, 6 + listPlayers, 7 + selectPlayer, 8 + getPlayerData, 9 + watchPlayer, 10 + watchBus, 11 + } from "./mpris.js"; 12 + import { formatTrackData } from "./formatter.js"; 13 + 14 + const modulesDir = path.resolve(import.meta.dirname, "modules"); 15 + const configDir = path.resolve(import.meta.dirname, "../config"); 16 + let modules = []; 17 + 18 + async function loadConfig(moduleName) { 19 + const baseName = moduleName.replace(/\.js$/, ""); 20 + const configPath = path.join(configDir, `${baseName}.json`); 21 + try { 22 + return JSON.parse(await readFile(configPath, "utf-8")); 23 + } catch { 24 + return null; 25 + } 26 + } 27 + 28 + async function loadModules() { 29 + const files = await readdir(modulesDir); 30 + const jsFiles = files.filter((f) => f.endsWith(".js")); 31 + 32 + modules = []; 33 + for (const file of jsFiles) { 34 + const url = pathToFileURL(path.join(modulesDir, file)).href; 35 + try { 36 + const mod = await import(url); 37 + if (typeof mod.onData !== "function") { 38 + console.warn(`[tealMpris] Skipping ${file} (no onData export)`); 39 + continue; 40 + } 41 + 42 + const config = await loadConfig(file); 43 + 44 + if (typeof mod.init === "function") { 45 + await mod.init(config); 46 + } 47 + 48 + modules.push({ name: file, onData: mod.onData }); 49 + console.log(`[tealMpris] Loaded module: ${file}`); 50 + } catch (err) { 51 + console.error(`[tealMpris] Failed to load ${file}: ${err.message}`); 52 + } 53 + } 54 + } 55 + 56 + async function callModules(data) { 57 + for (const mod of modules) { 58 + try { 59 + await mod.onData(data); 60 + } catch (err) { 61 + console.error(`[tealMpris] Module ${mod.name} error: ${err.message}`); 62 + } 63 + } 64 + } 65 + 66 + let bus; 67 + let activePlayer = null; 68 + let cleanupWatch = null; 69 + let currentTrackId = null; 70 + let currentPlayedTime = null; 71 + 72 + async function emitCurrentState() { 73 + if (!activePlayer) return; 74 + 75 + try { 76 + const { metadata, playbackStatus, positionUs } = await getPlayerData( 77 + bus, 78 + activePlayer, 79 + ); 80 + 81 + const trackId = metaTrackId(metadata); 82 + 83 + if (trackId !== currentTrackId) { 84 + currentTrackId = trackId; 85 + currentPlayedTime = new Date().toISOString(); 86 + } 87 + 88 + const data = formatTrackData( 89 + metadata, 90 + playbackStatus, 91 + positionUs, 92 + currentPlayedTime, 93 + ); 94 + if (data) { 95 + callModules(data); 96 + } 97 + } catch (err) { 98 + console.error(`[tealMpris] Failed to read player data: ${err.message}`); 99 + } 100 + } 101 + 102 + function metaTrackId(metadata) { 103 + const id = metadata["mpris:trackid"]; 104 + if (id) return String(id.value); 105 + const title = metadata["xesam:title"]; 106 + const url = metadata["xesam:url"]; 107 + return `${title?.value ?? ""}|${url?.value ?? ""}`; 108 + } 109 + 110 + async function attachToPlayer(playerName) { 111 + if (cleanupWatch) { 112 + cleanupWatch(); 113 + cleanupWatch = null; 114 + } 115 + 116 + activePlayer = playerName; 117 + console.log(`[tealMpris] Attached to ${playerName}`); 118 + 119 + await emitCurrentState(); 120 + 121 + cleanupWatch = await watchPlayer( 122 + bus, 123 + playerName, 124 + async (changed) => { 125 + if ( 126 + changed.PlaybackStatus !== undefined || 127 + changed.Metadata !== undefined 128 + ) { 129 + await emitCurrentState(); 130 + } 131 + }, 132 + async (_positionUs) => { 133 + await emitCurrentState(); 134 + }, 135 + ); 136 + } 137 + 138 + async function scan() { 139 + const players = await listPlayers(bus); 140 + 141 + if (players.length === 0) { 142 + if (activePlayer) { 143 + console.log("[tealMpris] No MPRIS players found. Waiting..."); 144 + if (cleanupWatch) { 145 + cleanupWatch(); 146 + cleanupWatch = null; 147 + } 148 + activePlayer = null; 149 + } 150 + return; 151 + } 152 + 153 + const best = selectPlayer(players); 154 + 155 + // Only re-attach if the selected player changed 156 + if (best !== activePlayer) { 157 + await attachToPlayer(best); 158 + } 159 + } 160 + 161 + async function main() { 162 + await loadModules(); 163 + 164 + bus = sessionBus(); 165 + 166 + // Watch for players appearing / disappearing 167 + await watchBus(bus, () => { 168 + scan().catch((err) => 169 + console.error(`[tealMpris] Scan error: ${err.message}`), 170 + ); 171 + }); 172 + 173 + // Initial scan 174 + await scan(); 175 + 176 + if (!activePlayer) { 177 + console.log("[tealMpris] No MPRIS players found. Waiting for one to start..."); 178 + } 179 + } 180 + 181 + main().catch((err) => { 182 + console.error(`[tealMpris] Fatal: ${err.message}`); 183 + process.exit(1); 184 + });
+12
src/modules/example.js.disabled
··· 1 + /** 2 + * Example module - logs track data to the console. 3 + * @param {object} data 4 + * @param {Array<{artistName: string}>} data.artists 5 + * @param {string} data.trackName 6 + * @param {string} data.playedTime - ISO 8601 timestamp 7 + * @param {string} data.releaseName 8 + * @param {string} data.expiry - ISO 8601 timestamp 9 + */ 10 + export function onData(data) { 11 + console.log("[example]", JSON.stringify(data, null, 2)); 12 + }
+136
src/modules/teal.js
··· 1 + import { Client } from "@atcute/client"; 2 + import { PasswordSession } from "@atcute/password-session"; 3 + 4 + const AGENT_STRING = "pipris/0.0.1 (https://tangled.org/clay.rip/pipris)"; 5 + const PLAY_FINISH_TOLERANCE_MS = 5000; 6 + 7 + let rpc; 8 + let did; 9 + let prevTrack = null; 10 + 11 + // --- TID generation (AT Protocol timestamp-based ID) --- 12 + 13 + const B32_CHARS = "234567abcdefghijklmnopqrstuvwxyz"; 14 + let lastTimestamp = 0; 15 + let clockId = Math.floor(Math.random() * 1024); 16 + 17 + function generateTid() { 18 + let timestamp = Date.now() * 1000; // microseconds 19 + if (timestamp <= lastTimestamp) { 20 + timestamp = lastTimestamp + 1; 21 + } 22 + lastTimestamp = timestamp; 23 + 24 + const id = BigInt(timestamp) << 10n | BigInt(clockId); 25 + let encoded = ""; 26 + let val = id; 27 + for (let i = 0; i < 13; i++) { 28 + encoded = B32_CHARS[Number(val & 31n)] + encoded; 29 + val >>= 5n; 30 + } 31 + return encoded; 32 + } 33 + 34 + // --- Initialisation --- 35 + 36 + export async function init(config) { 37 + if (!config) { 38 + throw new Error("Missing config/teal.json"); 39 + } 40 + 41 + const session = await PasswordSession.login({ 42 + service: config.service, 43 + identifier: config.handle, 44 + password: config.password, 45 + }); 46 + 47 + did = session.did; 48 + rpc = new Client({ handler: session }); 49 + 50 + console.log("[teal] Logged in as", did); 51 + } 52 + 53 + // --- Status update --- 54 + 55 + async function updateStatus(data) { 56 + await rpc.post("com.atproto.repo.putRecord", { 57 + input: { 58 + repo: did, 59 + collection: "fm.teal.alpha.actor.status", 60 + rkey: "self", 61 + record: { 62 + $type: "fm.teal.alpha.actor.status", 63 + time: data.playedTime, 64 + expiry: data.expiry, 65 + item: { 66 + trackName: data.trackName, 67 + artists: data.artists, 68 + releaseName: data.releaseName || undefined, 69 + duration: data.duration || undefined, 70 + playedTime: data.playedTime, 71 + musicServiceBaseDomain: "local", 72 + submissionClientAgent: AGENT_STRING, 73 + }, 74 + }, 75 + }, 76 + }); 77 + } 78 + 79 + // --- Play submission --- 80 + 81 + async function submitPlay(track) { 82 + await rpc.post("com.atproto.repo.createRecord", { 83 + input: { 84 + repo: did, 85 + collection: "fm.teal.alpha.feed.play", 86 + rkey: generateTid(), 87 + record: { 88 + $type: "fm.teal.alpha.feed.play", 89 + trackName: track.trackName, 90 + artists: track.artists, 91 + releaseName: track.releaseName || undefined, 92 + duration: track.duration || undefined, 93 + playedTime: track.playedTime, 94 + musicServiceBaseDomain: "local", 95 + submissionClientAgent: AGENT_STRING, 96 + }, 97 + }, 98 + }); 99 + console.log(`[teal] Submitted play: ${track.trackName}`); 100 + } 101 + 102 + function shouldSubmitPlay(prev) { 103 + if (!prev) return false; 104 + const expiryMs = new Date(prev.expiry).getTime(); 105 + return Date.now() >= expiryMs - PLAY_FINISH_TOLERANCE_MS; 106 + } 107 + 108 + // --- Module entry point --- 109 + 110 + export async function onData(data) { 111 + if (!rpc) return; 112 + 113 + const trackKey = `${data.trackName}|${data.artists.map((a) => a.artistName).join(",")}`; 114 + const prevKey = prevTrack 115 + ? `${prevTrack.trackName}|${prevTrack.artists.map((a) => a.artistName).join(",")}` 116 + : null; 117 + 118 + if (trackKey !== prevKey) { 119 + if (shouldSubmitPlay(prevTrack)) { 120 + try { 121 + await submitPlay(prevTrack); 122 + } catch (err) { 123 + console.error(`[teal] Failed to submit play: ${err.message}`); 124 + } 125 + } 126 + prevTrack = { ...data }; 127 + } else { 128 + prevTrack = { ...data }; 129 + } 130 + 131 + try { 132 + await updateStatus(data); 133 + } catch (err) { 134 + console.error(`[teal] Failed to update status: ${err.message}`); 135 + } 136 + }
+130
src/mpris.js
··· 1 + import dbus from "dbus-next"; 2 + 3 + const MPRIS_PREFIX = "org.mpris.MediaPlayer2."; 4 + const PLAYER_IFACE = "org.mpris.MediaPlayer2.Player"; 5 + const PROPS_IFACE = "org.freedesktop.DBus.Properties"; 6 + 7 + // Lowercase substrings matched against the bus name suffix. 8 + // These are local/offline music players that should be preferred. 9 + const PREFERRED_PLAYERS = [ 10 + "gapless", 11 + "elisa", 12 + "rhythmbox", 13 + "g4music", 14 + "lollypop", 15 + "amberol", 16 + "gnome-music", 17 + "strawberry", 18 + "clementine", 19 + "audacious", 20 + "quodlibet", 21 + "deadbeef", 22 + "celluloid", 23 + "cmus", 24 + "musikcube", 25 + "aimp", 26 + ]; 27 + 28 + /** 29 + * Return the session bus connection. 30 + */ 31 + export function sessionBus() { 32 + return dbus.sessionBus(); 33 + } 34 + 35 + /** 36 + * List all MPRIS player bus names currently registered. 37 + */ 38 + export async function listPlayers(bus) { 39 + const dbusProxy = await bus.getProxyObject( 40 + "org.freedesktop.DBus", 41 + "/org/freedesktop/DBus", 42 + ); 43 + const iface = dbusProxy.getInterface("org.freedesktop.DBus"); 44 + const names = await iface.ListNames(); 45 + return names.filter((n) => n.startsWith(MPRIS_PREFIX)); 46 + } 47 + 48 + /** 49 + * Pick the best player from a list of bus names. 50 + * Prefers local music players; falls back to the first in the list. 51 + */ 52 + export function selectPlayer(players) { 53 + if (players.length === 0) return null; 54 + 55 + for (const name of players) { 56 + const suffix = name.slice(MPRIS_PREFIX.length).toLowerCase(); 57 + if (PREFERRED_PLAYERS.some((p) => suffix.includes(p))) { 58 + return name; 59 + } 60 + } 61 + return players[0]; 62 + } 63 + 64 + /** 65 + * Read Metadata, PlaybackStatus, and Position from a player. 66 + */ 67 + export async function getPlayerData(bus, playerName) { 68 + const proxy = await bus.getProxyObject(playerName, "/org/mpris/MediaPlayer2"); 69 + const props = proxy.getInterface(PROPS_IFACE); 70 + 71 + const [metadata, playbackStatus, position] = await Promise.all([ 72 + props.Get(PLAYER_IFACE, "Metadata"), 73 + props.Get(PLAYER_IFACE, "PlaybackStatus"), 74 + props.Get(PLAYER_IFACE, "Position"), 75 + ]); 76 + 77 + return { 78 + metadata: metadata.value, // dict of Variants 79 + playbackStatus: playbackStatus.value, // string 80 + positionUs: Number(position.value), // microseconds 81 + }; 82 + } 83 + 84 + /** 85 + * Subscribe to PropertiesChanged and Seeked signals on the player interface. 86 + * `onChange` receives (changedProperties: object, invalidated: string[]). 87 + * `onSeeked` receives (positionUs: number). 88 + * Returns a cleanup function. 89 + */ 90 + export async function watchPlayer(bus, playerName, onChange, onSeeked) { 91 + const proxy = await bus.getProxyObject(playerName, "/org/mpris/MediaPlayer2"); 92 + const props = proxy.getInterface(PROPS_IFACE); 93 + const player = proxy.getInterface(PLAYER_IFACE); 94 + 95 + const propsHandler = (ifaceName, changed, invalidated) => { 96 + if (ifaceName === PLAYER_IFACE) { 97 + onChange(changed, invalidated); 98 + } 99 + }; 100 + 101 + const seekHandler = (positionUs) => { 102 + onSeeked(Number(positionUs)); 103 + }; 104 + 105 + props.on("PropertiesChanged", propsHandler); 106 + player.on("Seeked", seekHandler); 107 + 108 + return () => { 109 + props.off("PropertiesChanged", propsHandler); 110 + player.off("Seeked", seekHandler); 111 + }; 112 + } 113 + 114 + /** 115 + * Watch for new or removed MPRIS players on the bus. 116 + * `onPlayerChange` is called with no arguments whenever the set of players changes. 117 + */ 118 + export async function watchBus(bus, onPlayerChange) { 119 + const dbusProxy = await bus.getProxyObject( 120 + "org.freedesktop.DBus", 121 + "/org/freedesktop/DBus", 122 + ); 123 + const iface = dbusProxy.getInterface("org.freedesktop.DBus"); 124 + 125 + iface.on("NameOwnerChanged", (name, _oldOwner, _newOwner) => { 126 + if (name.startsWith(MPRIS_PREFIX)) { 127 + onPlayerChange(); 128 + } 129 + }); 130 + }