[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: Implement MPRIS Source

FoxxMD (Mar 2, 2023, 12:57 PM EST) 5ae79dec 285f593a

+654 -6
+1
README.md
··· 14 14 * [Youtube Music](/docs/configuration.md#youtube-music) 15 15 * [Last.fm](/docs/configuration.md#lastfm-source) 16 16 * [Deezer](/docs/configuration.md#deezer) 17 + * [MPRIS (Linux Desktop)](/docs/configuration.md#mpris) 17 18 * Supports scrobbling to many clients 18 19 * [Maloja](/docs/configuration.md#maloja) 19 20 * [Last.fm](/docs/configuration.md#lastfm)
assets/mpris.jpg

This is a binary file and will not be displayed.

+9
config/mpris.json.example
··· 1 + [ 2 + { 3 + "name": "ubuntu", 4 + "data": { 5 + "whitelist": ["vlc", "mpd"], 6 + "blacklist": ["spotify"] 7 + } 8 + } 9 + ]
+25
docs/configuration.md
··· 12 12 * [Last.fm (Source)](#lastfm--source-) 13 13 * [Deezer](#deezer) 14 14 * [Youtube Music](#youtube-music) 15 + * [MPRIS (Linux Desktop)](#mpris) 15 16 * [Client Configurations](#client-configurations) 16 17 * [Maloja](#maloja) 17 18 * [Last.fm](#lastfm) ··· 321 322 ### File-Based 322 323 323 324 See [`ytmusic.json.example`](/config/ytmusic.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FYTMusicSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json) 325 + 326 + ## [MPRIS](https://specifications.freedesktop.org/mpris-spec/latest/) 327 + 328 + MPRIS is a standard interface for communicating with Music Players on **linux operating systems.** 329 + 330 + If you run Linux and have a notification tray that shows what media you are listening to, you likely have access to MPRIS. 331 + 332 + ![Notification Tray](/assets/mpris.jpg) 333 + 334 + multi-scrobbler can listen to this interface and scrobble tracks played by **any media player** that communicates to the operating system with MPRIS. 335 + 336 + **NOTE:** multi-scrobbler needs to be running as a [**Local Installation**](/docs/installation.md#local) in order to use MPRIS. This cannot be used from docker. 337 + 338 + ### ENV-Based 339 + 340 + | Environmental Variable | Required? | Default | Description | 341 + |------------------------|-----------|---------|----------------------------------------------------------------------------------| 342 + | MPRIS_ENABLE | No | | Use MPRIS as a Source (useful when you don't need any other options) | 343 + | MPRIS_BLACKLIST | No | | Comma-delimited list of player names not to scrobble from | 344 + | MPRIS_WHITELIST | No | | Comma-delimited list of players names to ONLY scrobble from. Overrides blacklist | 345 + 346 + ### File-Based 347 + 348 + See [`mpris.json.example`](/config/mpris.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FMPRISSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fclient.json) 324 349 325 350 326 351 # Client Configurations
+1 -1
src/common/index.ts
··· 1 1 import * as path from 'path'; 2 - import {fileURLToPath} from "url"; 2 + //import {fileURLToPath} from "url"; 3 3 4 4 //const __filename = fileURLToPath(import.meta.url); 5 5 //const __dirname = path.dirname(__filename);
+2 -2
src/common/infrastructure/Atomic.ts
··· 1 1 import {Dayjs} from "dayjs"; 2 2 3 - export type SourceType = 'spotify' | 'plex' | 'tautulli' | 'subsonic' | 'jellyfin' | 'lastfm' | 'deezer' | 'ytmusic'; 4 - export const sourceTypes: SourceType[] = ['spotify', 'plex', 'tautulli', 'subsonic', 'jellyfin', 'lastfm', 'deezer', 'ytmusic']; 3 + export type SourceType = 'spotify' | 'plex' | 'tautulli' | 'subsonic' | 'jellyfin' | 'lastfm' | 'deezer' | 'ytmusic' | 'mpris'; 4 + export const sourceTypes: SourceType[] = ['spotify', 'plex', 'tautulli', 'subsonic', 'jellyfin', 'lastfm', 'deezer', 'ytmusic', 'mpris']; 5 5 6 6 export const lowGranularitySources: SourceType[] = ['subsonic','ytmusic']; 7 7
+55
src/common/infrastructure/config/source/mpris.ts
··· 1 + import {CommonSourceConfig, CommonSourceData} from "./index.js"; 2 + 3 + export const PLAYBACK_STATUS_PLAYING = 'Playing'; 4 + export const PLAYBACK_STATUS_PAUSED = 'Paused'; 5 + export const PLAYBACK_STATUS_STOPPED = 'Stopped'; 6 + 7 + export type PlaybackStatus = 'Playing' | 'Paused' | 'Stopped'; 8 + 9 + export const MPRIS_IFACE = 'org.mpris.MediaPlayer2.Player'; 10 + export const MPRIS_PATH = '/org/mpris/MediaPlayer2'; 11 + export const PROPERTIES_IFACE = 'org.freedesktop.DBus.Properties'; 12 + 13 + export interface MPRISMetadata { 14 + trackid?: string 15 + length?: number 16 + artUrl?: string 17 + album?: string 18 + albumArtist?: string[] 19 + artist?: string[] 20 + title?: string 21 + url?: string 22 + } 23 + 24 + export interface PlayerInfo { 25 + name: string 26 + status: PlaybackStatus 27 + position?: number 28 + metadata: MPRISMetadata 29 + } 30 + 31 + export interface MPRISData extends CommonSourceData { 32 + /** 33 + * DO NOT scrobble from any players that START WITH these values, case-insensitive 34 + * 35 + * @examples [["spotify","vlc"]] 36 + * */ 37 + blacklist?: string | string[] 38 + 39 + /** 40 + * ONLY from any players that START WITH these values, case-insensitive 41 + * 42 + * If whitelist is present then blacklist is ignored 43 + * 44 + * @examples [["spotify","vlc"]] 45 + * */ 46 + whitelist?: string | string[] 47 + } 48 + 49 + export interface MPRISSourceConfig extends CommonSourceConfig { 50 + data: MPRISData 51 + } 52 + 53 + export interface MPRISSourceAIOConfig extends MPRISSourceConfig { 54 + type: 'mpris' 55 + }
+3 -2
src/common/infrastructure/config/source/sources.ts
··· 6 6 import {JellySourceAIOConfig, JellySourceConfig} from "./jellyfin.js"; 7 7 import {LastFmSouceAIOConfig, LastfmSourceConfig} from "./lastfm.js"; 8 8 import {YTMusicSourceAIOConfig, YTMusicSourceConfig} from "./ytmusic.js"; 9 + import {MPRISSourceAIOConfig, MPRISSourceConfig} from "./mpris.js"; 9 10 10 - export type SourceConfig = SpotifySourceConfig | PlexSourceConfig | TautulliSourceConfig | DeezerSourceConfig | SubSonicSourceConfig | JellySourceConfig | LastfmSourceConfig | YTMusicSourceConfig; 11 + export type SourceConfig = SpotifySourceConfig | PlexSourceConfig | TautulliSourceConfig | DeezerSourceConfig | SubSonicSourceConfig | JellySourceConfig | LastfmSourceConfig | YTMusicSourceConfig | MPRISSourceConfig; 11 12 12 - export type SourceAIOConfig = SpotifySourceAIOConfig | PlexSourceAIOConfig | TautulliSourceAIOConfig | DeezerSourceAIOConfig | SubsonicSourceAIOConfig | JellySourceAIOConfig | LastFmSouceAIOConfig | YTMusicSourceAIOConfig; 13 + export type SourceAIOConfig = SpotifySourceAIOConfig | PlexSourceAIOConfig | TautulliSourceAIOConfig | DeezerSourceAIOConfig | SubsonicSourceAIOConfig | JellySourceAIOConfig | LastFmSouceAIOConfig | YTMusicSourceAIOConfig | MPRISSourceAIOConfig;
+110
src/common/schema/aio-source.json
··· 328 328 ], 329 329 "type": "object" 330 330 }, 331 + "MPRISData": { 332 + "properties": { 333 + "blacklist": { 334 + "anyOf": [ 335 + { 336 + "items": { 337 + "type": "string" 338 + }, 339 + "type": "array" 340 + }, 341 + { 342 + "type": "string" 343 + } 344 + ], 345 + "description": "DO NOT scrobble from any players that START WITH these values, case-insensitive", 346 + "examples": [ 347 + [ 348 + "spotify", 349 + "vlc" 350 + ] 351 + ] 352 + }, 353 + "maxPollRetries": { 354 + "default": 0, 355 + "description": "default # of automatic polling restarts on error", 356 + "examples": [ 357 + 1 358 + ], 359 + "type": "number" 360 + }, 361 + "maxRequestRetries": { 362 + "default": 1, 363 + "description": "default # of http request retries a source can make before error is thrown", 364 + "examples": [ 365 + 1 366 + ], 367 + "type": "number" 368 + }, 369 + "options": { 370 + "$ref": "#/definitions/Record<string,any>" 371 + }, 372 + "retryMultiplier": { 373 + "default": 1.5, 374 + "description": "default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)", 375 + "examples": [ 376 + 1.5 377 + ], 378 + "type": "number" 379 + }, 380 + "whitelist": { 381 + "anyOf": [ 382 + { 383 + "items": { 384 + "type": "string" 385 + }, 386 + "type": "array" 387 + }, 388 + { 389 + "type": "string" 390 + } 391 + ], 392 + "description": "ONLY from any players that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored", 393 + "examples": [ 394 + [ 395 + "spotify", 396 + "vlc" 397 + ] 398 + ] 399 + } 400 + }, 401 + "type": "object" 402 + }, 403 + "MPRISSourceAIOConfig": { 404 + "properties": { 405 + "clients": { 406 + "description": "Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.", 407 + "examples": [ 408 + [ 409 + "MyMalojaConfigName", 410 + "MyLastFMConfigName" 411 + ] 412 + ], 413 + "items": { 414 + "type": "string" 415 + }, 416 + "type": "array" 417 + }, 418 + "data": { 419 + "$ref": "#/definitions/MPRISData" 420 + }, 421 + "name": { 422 + "description": "Unique identifier for this source.", 423 + "type": "string" 424 + }, 425 + "type": { 426 + "enum": [ 427 + "mpris" 428 + ], 429 + "type": "string" 430 + } 431 + }, 432 + "required": [ 433 + "data", 434 + "type" 435 + ], 436 + "type": "object" 437 + }, 331 438 "PlexSourceAIOConfig": { 332 439 "properties": { 333 440 "clients": { ··· 482 589 }, 483 590 { 484 591 "$ref": "#/definitions/YTMusicSourceAIOConfig" 592 + }, 593 + { 594 + "$ref": "#/definitions/MPRISSourceAIOConfig" 485 595 } 486 596 ] 487 597 },
+110
src/common/schema/aio.json
··· 529 529 ], 530 530 "type": "object" 531 531 }, 532 + "MPRISData": { 533 + "properties": { 534 + "blacklist": { 535 + "anyOf": [ 536 + { 537 + "items": { 538 + "type": "string" 539 + }, 540 + "type": "array" 541 + }, 542 + { 543 + "type": "string" 544 + } 545 + ], 546 + "description": "DO NOT scrobble from any players that START WITH these values, case-insensitive", 547 + "examples": [ 548 + [ 549 + "spotify", 550 + "vlc" 551 + ] 552 + ] 553 + }, 554 + "maxPollRetries": { 555 + "default": 0, 556 + "description": "default # of automatic polling restarts on error", 557 + "examples": [ 558 + 1 559 + ], 560 + "type": "number" 561 + }, 562 + "maxRequestRetries": { 563 + "default": 1, 564 + "description": "default # of http request retries a source can make before error is thrown", 565 + "examples": [ 566 + 1 567 + ], 568 + "type": "number" 569 + }, 570 + "options": { 571 + "$ref": "#/definitions/Record<string,any>" 572 + }, 573 + "retryMultiplier": { 574 + "default": 1.5, 575 + "description": "default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)", 576 + "examples": [ 577 + 1.5 578 + ], 579 + "type": "number" 580 + }, 581 + "whitelist": { 582 + "anyOf": [ 583 + { 584 + "items": { 585 + "type": "string" 586 + }, 587 + "type": "array" 588 + }, 589 + { 590 + "type": "string" 591 + } 592 + ], 593 + "description": "ONLY from any players that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored", 594 + "examples": [ 595 + [ 596 + "spotify", 597 + "vlc" 598 + ] 599 + ] 600 + } 601 + }, 602 + "type": "object" 603 + }, 604 + "MPRISSourceAIOConfig": { 605 + "properties": { 606 + "clients": { 607 + "description": "Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.", 608 + "examples": [ 609 + [ 610 + "MyMalojaConfigName", 611 + "MyLastFMConfigName" 612 + ] 613 + ], 614 + "items": { 615 + "type": "string" 616 + }, 617 + "type": "array" 618 + }, 619 + "data": { 620 + "$ref": "#/definitions/MPRISData" 621 + }, 622 + "name": { 623 + "description": "Unique identifier for this source.", 624 + "type": "string" 625 + }, 626 + "type": { 627 + "enum": [ 628 + "mpris" 629 + ], 630 + "type": "string" 631 + } 632 + }, 633 + "required": [ 634 + "data", 635 + "type" 636 + ], 637 + "type": "object" 638 + }, 532 639 "MalojaClientAIOConfig": { 533 640 "properties": { 534 641 "data": { ··· 906 1013 }, 907 1014 { 908 1015 "$ref": "#/definitions/YTMusicSourceAIOConfig" 1016 + }, 1017 + { 1018 + "$ref": "#/definitions/MPRISSourceAIOConfig" 909 1019 } 910 1020 ] 911 1021 },
+103
src/common/schema/source.json
··· 24 24 }, 25 25 { 26 26 "$ref": "#/definitions/YTMusicSourceConfig" 27 + }, 28 + { 29 + "$ref": "#/definitions/MPRISSourceConfig" 27 30 } 28 31 ], 29 32 "definitions": { ··· 322 325 }, 323 326 "data": { 324 327 "$ref": "#/definitions/LastFmSourceData" 328 + }, 329 + "name": { 330 + "description": "Unique identifier for this source.", 331 + "type": "string" 332 + } 333 + }, 334 + "required": [ 335 + "data" 336 + ], 337 + "type": "object" 338 + }, 339 + "MPRISData": { 340 + "properties": { 341 + "blacklist": { 342 + "anyOf": [ 343 + { 344 + "items": { 345 + "type": "string" 346 + }, 347 + "type": "array" 348 + }, 349 + { 350 + "type": "string" 351 + } 352 + ], 353 + "description": "DO NOT scrobble from any players that START WITH these values, case-insensitive", 354 + "examples": [ 355 + [ 356 + "spotify", 357 + "vlc" 358 + ] 359 + ] 360 + }, 361 + "maxPollRetries": { 362 + "default": 0, 363 + "description": "default # of automatic polling restarts on error", 364 + "examples": [ 365 + 1 366 + ], 367 + "type": "number" 368 + }, 369 + "maxRequestRetries": { 370 + "default": 1, 371 + "description": "default # of http request retries a source can make before error is thrown", 372 + "examples": [ 373 + 1 374 + ], 375 + "type": "number" 376 + }, 377 + "options": { 378 + "$ref": "#/definitions/Record<string,any>" 379 + }, 380 + "retryMultiplier": { 381 + "default": 1.5, 382 + "description": "default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)", 383 + "examples": [ 384 + 1.5 385 + ], 386 + "type": "number" 387 + }, 388 + "whitelist": { 389 + "anyOf": [ 390 + { 391 + "items": { 392 + "type": "string" 393 + }, 394 + "type": "array" 395 + }, 396 + { 397 + "type": "string" 398 + } 399 + ], 400 + "description": "ONLY from any players that START WITH these values, case-insensitive\n\nIf whitelist is present then blacklist is ignored", 401 + "examples": [ 402 + [ 403 + "spotify", 404 + "vlc" 405 + ] 406 + ] 407 + } 408 + }, 409 + "type": "object" 410 + }, 411 + "MPRISSourceConfig": { 412 + "properties": { 413 + "clients": { 414 + "description": "Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.", 415 + "examples": [ 416 + [ 417 + "MyMalojaConfigName", 418 + "MyLastFMConfigName" 419 + ] 420 + ], 421 + "items": { 422 + "type": "string" 423 + }, 424 + "type": "array" 425 + }, 426 + "data": { 427 + "$ref": "#/definitions/MPRISData" 325 428 }, 326 429 "name": { 327 430 "description": "Unique identifier for this source.",
+195
src/sources/MPRISSource.ts
··· 1 + import dbus, {ClientInterface, Variant} from 'dbus-next'; 2 + import dayjs from "dayjs"; 3 + import { 4 + MPRIS_IFACE, 5 + MPRIS_PATH, 6 + MPRISMetadata, MPRISSourceConfig, PLAYBACK_STATUS_STOPPED, 7 + PlaybackStatus, PlayerInfo, 8 + PROPERTIES_IFACE 9 + } from "../common/infrastructure/config/source/mpris.js"; 10 + import {InternalConfig, PlayObject} from "../common/infrastructure/Atomic.js"; 11 + import MemorySource from "./MemorySource.js"; 12 + import {Notifiers} from "../notifier/Notifiers.js"; 13 + import {RecentlyPlayedOptions} from "./AbstractSource.js"; 14 + import {removeDuplicates} from "../utils.js"; 15 + 16 + 17 + export class MPRISSource extends MemorySource { 18 + 19 + declare config: MPRISSourceConfig; 20 + 21 + whitelist: string[] = []; 22 + blacklist: string[] = []; 23 + 24 + constructor(name: any, config: MPRISSourceConfig, internal: InternalConfig, notifier: Notifiers) { 25 + super('mpris', name, config, internal, notifier); 26 + this.canPoll = true; 27 + 28 + const {data: {whitelist = [], blacklist = []} = {}} = config; 29 + if(!Array.isArray(whitelist)) { 30 + this.whitelist = whitelist.split(',') 31 + } else { 32 + this.whitelist = whitelist; 33 + } 34 + if(!Array.isArray(blacklist)) { 35 + this.blacklist = blacklist.split(','); 36 + } else { 37 + this.blacklist = blacklist; 38 + } 39 + } 40 + 41 + static formatPlayObj(obj: PlayerInfo, newFromSource = false): PlayObject { 42 + const { 43 + name, 44 + position, 45 + metadata: { 46 + length, 47 + album, 48 + artist = [], 49 + albumArtist = [], 50 + title, 51 + trackid, 52 + url, 53 + } = {} 54 + } = obj; 55 + 56 + return { 57 + data: { 58 + track: title, 59 + album, 60 + artists: Array.from(new Set(artist.concat(albumArtist))), 61 + duration: length, 62 + playDate: dayjs() 63 + }, 64 + meta: { 65 + source: 'dbus', 66 + trackId: trackid, 67 + newFromSource, 68 + url: { 69 + web: url 70 + }, 71 + trackProgressPosition: position, 72 + deviceId: name, 73 + } 74 + } 75 + } 76 + 77 + initialize = async () => { 78 + // test if we can get DBus 79 + try { 80 + await this.getDBus(); 81 + return true; 82 + } catch (e) { 83 + this.logger.error('Could not get DBus interface from operating system'); 84 + this.logger.error(e); 85 + return false; 86 + } 87 + } 88 + 89 + protected getDBus = async () => { 90 + const bus = dbus.sessionBus(); 91 + const obj = await bus.getProxyObject('org.freedesktop.DBus', '/org/freedesktop/DBus'); 92 + return obj.getInterface('org.freedesktop.DBus'); 93 + } 94 + 95 + protected listAll = async () => { 96 + let iface = await this.getDBus(); 97 + let names = await iface.ListNames(); 98 + return names.filter((n) => n.startsWith('org.mpris.MediaPlayer2')) 99 + } 100 + 101 + getPlayersInfo = async (activeOnly = true): Promise<PlayerInfo[]> => { 102 + const list = await this.listAll(); 103 + 104 + let bus = dbus.sessionBus(); 105 + 106 + const playerInfos: PlayerInfo[] = []; 107 + 108 + for (const playerName of list) { 109 + let obj = await bus.getProxyObject(playerName, MPRIS_PATH); 110 + 111 + const plainPlayerName = playerName.replace('org.mpris.MediaPlayer2.', ''); 112 + //let player = obj.getInterface(MPRIS_IFACE); 113 + let props = obj.getInterface(PROPERTIES_IFACE); 114 + 115 + const pos = await this.getPlayerPosition(props); 116 + const status = await this.getPlayerStatus(props); 117 + if (status === PLAYBACK_STATUS_STOPPED && activeOnly) { 118 + continue; 119 + } 120 + const metadata = await this.getPlayerMetadata(props); 121 + playerInfos.push({ 122 + name: plainPlayerName, 123 + status, 124 + position: pos, 125 + metadata 126 + }); 127 + } 128 + return playerInfos; 129 + } 130 + 131 + protected getPlayerPosition = async (props: ClientInterface): Promise<number> => { 132 + const pos = await props.Get(MPRIS_IFACE, 'Position'); 133 + return dayjs.duration({milliseconds: Number(pos.value / 1000n)}).asSeconds(); 134 + } 135 + 136 + protected getPlayerStatus = async (props: ClientInterface): Promise<PlaybackStatus> => { 137 + const status = await props.Get(MPRIS_IFACE, 'PlaybackStatus'); 138 + return status.value as PlaybackStatus; 139 + } 140 + 141 + protected getPlayerMetadata = async (props: ClientInterface): Promise<MPRISMetadata> => { 142 + const metadata = await props.Get(MPRIS_IFACE, 'Metadata'); 143 + return this.metadataToPlain(metadata.value); 144 + } 145 + 146 + metadataToPlain = (metadataVariant): MPRISMetadata => { 147 + let metadataPlain = {}; 148 + for (let k of Object.keys(metadataVariant)) { 149 + let value = metadataVariant[k]; 150 + if (value === undefined || value === null) { 151 + //logging.warn(`ignoring a null metadata value for key ${k}`); 152 + continue; 153 + } 154 + const plainKey = k.replace(/mpris:|xesam:/, ''); 155 + if (value instanceof Variant) { 156 + if (typeof value.value === 'bigint') { 157 + // in this context we're using it as a duration (track length or playback position) 158 + metadataPlain[plainKey] = dayjs.duration({milliseconds: Number(value.value / 1000n)}).asSeconds(); 159 + } else { 160 + metadataPlain[plainKey] = value.value; 161 + } 162 + } else { 163 + metadataPlain[plainKey] = value; 164 + } 165 + } 166 + return metadataPlain; 167 + } 168 + 169 + getRecentlyPlayed = async (options: RecentlyPlayedOptions = {}) => { 170 + const infos = await this.getPlayersInfo(); 171 + let plays: PlayObject[] = []; 172 + for(const info of infos) { 173 + const lowerName = info.name.toLocaleLowerCase(); 174 + if(this.whitelist.length > 0) { 175 + if(!this.whitelist.some(x => lowerName.includes(x.toLocaleLowerCase()))) { 176 + this.logger.debug(`No name in whitelist was found in Player Name '${info.name}', skipping player`); 177 + continue; 178 + } 179 + } else if(this.blacklist.length > 0) { 180 + if(this.whitelist.some(x => lowerName.includes(x.toLocaleLowerCase()))) { 181 + this.logger.debug(`A name in blacklist was found in Player Name '${info.name}', skipping player`); 182 + continue; 183 + } 184 + } 185 + plays.push(MPRISSource.formatPlayObj(info)); 186 + } 187 + const deduped = removeDuplicates(plays); 188 + if(options.display === true) { 189 + return deduped; 190 + } 191 + return this.processRecentPlays(deduped); 192 + } 193 + } 194 + 195 +
+24 -1
src/sources/ScrobbleSources.ts
··· 1 - import {createLabelledLogger, readJson, validateJson} from "../utils.js"; 1 + import {createLabelledLogger, parseBool, readJson, validateJson} from "../utils.js"; 2 2 import SpotifySource from "./SpotifySource.js"; 3 3 import PlexSource from "./PlexSource.js"; 4 4 import TautulliSource from "./TautulliSource.js"; ··· 25 25 import YTMusicSource from "./YTMusicSource.js"; 26 26 import {YTMusicSourceConfig} from "../common/infrastructure/config/source/ytmusic.js"; 27 27 import {Notifiers} from "../notifier/Notifiers.js"; 28 + import {MPRISData, MPRISSourceConfig} from "../common/infrastructure/config/source/mpris.js"; 29 + import {MPRISSource} from "./MPRISSource.js"; 28 30 29 31 type groupedNamedConfigs = {[key: string]: ParsedConfig[]}; 30 32 ··· 229 231 }); 230 232 } 231 233 break; 234 + case 'mpris': 235 + const shouldUse = parseBool(process.env.MPRIS_ENABLE); 236 + const mp = { 237 + blacklist: process.env.MPRIS_BLACKLIST, 238 + whitelist: process.env.MPRIS_WHITELIST 239 + } 240 + if (!Object.values(mp).every(x => x === undefined) || shouldUse) { 241 + configs.push({ 242 + type: 'mpris', 243 + name: 'unnamed', 244 + source: 'ENV', 245 + mode: 'single', 246 + configureAs: defaultConfigureAs, 247 + data: mp as MPRISData 248 + }); 249 + } 250 + break; 232 251 default: 233 252 break; 234 253 } ··· 370 389 break; 371 390 case 'ytmusic': 372 391 newSource = await new YTMusicSource(name, compositeConfig as YTMusicSourceConfig, internal, notifier); 392 + break; 393 + case 'mpris': 394 + newSource = await new MPRISSource(name, compositeConfig as MPRISSourceConfig, internal, notifier); 395 + break; 373 396 default: 374 397 break; 375 398 }
+16
src/utils.ts
··· 572 572 } 573 573 return undefined; 574 574 } 575 + 576 + export function parseBool(value: any, prev: any = false): boolean { 577 + let usedVal = value; 578 + if (value === undefined || value === '') { 579 + usedVal = prev; 580 + } 581 + if(usedVal === undefined || usedVal === '') { 582 + return false; 583 + } 584 + if (typeof usedVal === 'string') { 585 + return usedVal === 'true'; 586 + } else if (typeof usedVal === 'boolean') { 587 + return usedVal; 588 + } 589 + throw new Error('Not a boolean value.'); 590 + }