[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(sonos): Add group/device filtering

FoxxMD (Jan 22, 2026, 1:48 PM UTC) 49529af8 54eabf49

+66 -3
+5 -1
config/sonos.json.example
··· 3 3 "enable": true, 4 4 "name": "MySonos", 5 5 "data": { 6 - "host": "192.168.0.150" 6 + "host": "192.168.0.150", 7 + "devicesAllow": ["move","beam"], 8 + "devicesBlock": ["roam"], 9 + "groupsAllow": ["family"], 10 + "groupsBlock": ["bedroom"] 7 11 } 8 12 } 9 13 ]
+9 -1
docsite/docs/configuration/sources/sonos.mdx
··· 81 81 82 82 </details> 83 83 84 + ### Filtering Activity 85 + 86 + The **optional** `devices` and `groups` related properties found in the config below can be used to include or exclude Sonos devices/groups by name, case-insensitive. 87 + 84 88 ## Configuration 85 89 86 90 You **must** define the IP address of at least one Sonos/accessory in order for Multi-scrobbler to connect to your Sonos system. 87 91 88 92 <Config config="SonosSourceConfig" fileContent={JsonConfig} name="sonos"> 89 93 | Environmental Variable | Required? | Default | Description | 90 - | ---------------------- | --------- | ------- | ---------------------------------------------------------------------- | 94 + | ---------------------- | --------- | ------- | :--------------------------------------------------------------------- | 91 95 | `SONOS_HOST` | Yes | | The IP address of any active Sonos device/accessory EX `192.168.0.150` | 96 + | `SONOS_DEVICES_ALLOW` | No | | Comma-separated list of Sonos device names to scrobble from | 97 + | `SONOS_DEVICES_BLOCK` | No | | Comma-separated list of Sonos device names to disallow scrobbles from | 98 + | `SONOS_GROUPS_ALLOW` | No | | Comma-separated list of device group names to scrobble from | 99 + | `SONOS_GROUPS_BLOCK` | No | | Comma-separated list of devices group names to disallow scrobbles from | 92 100 </Config>
+18
src/backend/common/infrastructure/config/source/sonos.ts
··· 8 8 * @examples ["192.168.0.170"] 9 9 * */ 10 10 host: string 11 + 12 + /** 13 + * Only scrobble if device name contains strings from this list (case-insensitive) 14 + * */ 15 + devicesAllow?: string | string[] 16 + /** 17 + * Do not scrobble if device name contains strings from this list (case-insensitive) 18 + * */ 19 + devicesBlock?: string | string[] 20 + 21 + /** 22 + * Only scrobble if the name of a group the playing device belongs to contains strings from this list (case-insensitive) 23 + * */ 24 + groupsAllow?: string | string[] 25 + /** 26 + * Do not scrobble if the name of a group the playing device belongs to contains strings from this list (case-insensitive) 27 + * */ 28 + groupsBlock?: string | string[] 11 29 } 12 30 export interface SonosSourceConfig extends CommonSourceConfig { 13 31 data: SonosData
+4
src/backend/sources/ScrobbleSources.ts
··· 798 798 case 'sonos': { 799 799 const sonos = { 800 800 host: process.env.SONOS_HOST, 801 + devicesAllow: process.env.SONOS_DEVICES_ALLOW, 802 + devicesBlock: process.env.SONOS_DEVICES_BLOCK, 803 + groupsAllow: process.env.SONOS_GROUPS_ALLOW, 804 + groupsBlocks: process.env.SONOS_GROUPS_BLOCK 801 805 } 802 806 if (!Object.values(sonos).every(x => x === undefined)) { 803 807 configs.push({
+30 -1
src/backend/sources/SonosSource.ts
··· 19 19 import { Track } from "@svrooij/sonos/lib/models/track.js"; 20 20 import { parseDurationFromTimestamp } from "../utils/TimeUtils.js"; 21 21 import { FixedSizeList } from "fixed-size-list"; 22 - import { buildStatePlayerPlayIdententifyingInfo } from "../utils/StringUtils.js"; 22 + import { buildStatePlayerPlayIdententifyingInfo, parseArrayFromMaybeString } from "../utils/StringUtils.js"; 23 23 import { isDebugMode } from "../utils.js"; 24 24 25 25 export interface UniquePlay { ··· 46 46 uniqueDropReasons: FixedSizeList<string>; 47 47 logFilterFailure: false | 'debug' | 'warn'; 48 48 49 + devicesAllow: string[] = []; 50 + devicesBlock: string[] = []; 51 + groupsAllow: string[] = []; 52 + groupsBlock: string[] = []; 53 + 49 54 constructor(name: any, config: SonosSourceConfig, internal: InternalConfig, emitter: EventEmitter) { 50 55 const { 51 56 data, ··· 67 72 const { 68 73 options: { 69 74 logFilterFailure = (isDebugMode() ? 'debug' : 'warn'), 75 + } = {}, 76 + data: { 77 + devicesAllow = [], 78 + devicesBlock = [], 79 + groupsAllow = [], 80 + groupsBlock = [] 70 81 } = {} 71 82 } = this.config; 72 83 if (logFilterFailure !== false && !['debug', 'warn'].includes(logFilterFailure)) { ··· 75 86 this.logFilterFailure = logFilterFailure; 76 87 } 77 88 89 + this.devicesAllow = parseArrayFromMaybeString(devicesAllow, {lower: true}); 90 + this.devicesBlock = parseArrayFromMaybeString(devicesBlock, {lower: true}); 91 + this.groupsAllow = parseArrayFromMaybeString(groupsAllow, {lower: true}); 92 + this.groupsBlock = parseArrayFromMaybeString(groupsBlock, {lower: true}); 93 + 78 94 return true; 79 95 } 80 96 ··· 94 110 } 95 111 if (!data.state.positionInfo.TrackMetaData.UpnpClass.toLocaleLowerCase().includes('musictrack')) { 96 112 return `UpnpClass does include 'musictrack', found '${data.state.positionInfo.TrackMetaData.UpnpClass}'`; 113 + } 114 + 115 + if(this.devicesAllow.length > 0 && !this.devicesAllow.some(x => data.device.Name.toLocaleLowerCase().includes(x))) { 116 + return `'devicesAllow does not include a phrase found in ${data.device.Name}`; 117 + } 118 + if(this.devicesBlock.length > 0 && this.devicesBlock.some(x => data.device.Name.toLocaleLowerCase().includes(x))) { 119 + return `'devicesBlock includes a phrase found in ${data.device.Name}`; 120 + } 121 + if(this.groupsAllow.length > 0 && !this.groupsAllow.some(x => data.device.GroupName.toLocaleLowerCase().includes(x))) { 122 + return `'groupsAllow does not include a phrase found in ${data.device.GroupName}`; 123 + } 124 + if(this.groupsBlock.length > 0 && this.groupsBlock.some(x => data.device.GroupName.toLocaleLowerCase().includes(x))) { 125 + return `'groupsBlock includes a phrase found in ${data.device.GroupName}`; 97 126 } 98 127 return; 99 128 }