Skip to content

Commit

Permalink
fix: Support for Stream URL (#193)
Browse files Browse the repository at this point in the history
* fix: Support for stream urls fixed #192 
* feat: Support for Group ID Fixed #182

Co-authored-by: H. Klages <[email protected]>
Co-authored-by: Stephan van Rooij <[email protected]
  • Loading branch information
svrooij and hklages authored Mar 30, 2024
1 parent 40cd032 commit e26cf9a
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 10 deletions.
1 change: 1 addition & 0 deletions docs/sonos-device/methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Currently supported url's for metadata guessing:
- `spotify:playlist:37i9dQZF1DXcx1szy2g67M` - Spotify playlist (has to be added to queue).
- `spotify:user:spotify:playlist:37i9dQZF1DX1htCFhfVtyK` - Spotify playlist of a different user (summer rewind from user spotify in this case).
- `radio:s113577` - Tunein radio station.
- `x-rincon-mp3radio://https://....` - Direct stream url (mp3 or acc).

This library will guess the metadata automatically for the following methods, but you can also use the **MetadataHelper**, yourself.

Expand Down
5 changes: 4 additions & 1 deletion examples/use-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ manager.InitializeFromDevice(process.env.SONOS_HOST || '192.168.96.56')
.then(console.log)
.then(() => {
manager.Devices.forEach(d => {
console.log('Device %s (%s) is joined in %s', d.Name, d.Uuid, d.GroupName)
console.log('Device %s (%s) is joined in %s (%s)', d.Name, d.Uuid, d.GroupName, d.GroupId)
d.Events.on(SonosEvents.Coordinator, Uuid => {
console.log('Coordinator for %s changed to %s', d.Name, Uuid)
})
d.Events.on(SonosEvents.GroupName, newName => {
console.log('Group name for %s changed to %s', d.Name, newName)
})
d.Events.on(SonosEvents.GroupId, id => {
console.log('Group Id for %s changed to %s', d.Name, id)
})
d.Events.on(SonosEvents.CurrentTrack, uri => {
console.log('Current Track for %s %s', d.Name, uri)
})
Expand Down
13 changes: 11 additions & 2 deletions src/helpers/metadata-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ export default class MetadataHelper {
const uri = Array.isArray(didlItem['upnp:albumArtURI']) ? didlItem['upnp:albumArtURI'][0] : didlItem['upnp:albumArtURI'];
// Github user @hklages discovered that the album uri sometimes doesn't work because of encoding:
// See https://github.com/svrooij/node-sonos-ts/issues/93 if you found and album art uri that doesn't work.
const art = (uri as string).replace(/&amp;/gi, '&'); // .replace(/%25/g, '%').replace(/%3a/gi, ':');
track.AlbumArtUri = art.startsWith('http') ? art : `http://${host}:${port}${art}`;
if (typeof uri === 'string' && uri.length > 0) {
const art = (uri as string).replace(/&amp;/gi, '&'); // .replace(/%25/g, '%').replace(/%3a/gi, ':');
track.AlbumArtUri = art.startsWith('http') ? art : `http://${host}:${port}${art}`;
}
}

if (didlItem.res) {
Expand Down Expand Up @@ -150,6 +152,13 @@ export default class MetadataHelper {
return track;
}

if (trackUri.startsWith('x-rincon-mp3radio://http')) {
track.TrackUri = trackUri;
track.ItemId = '-1';
//track.UpnpClass = 'object.item.audioItem.audioBroadcast';
return track;
}

if (trackUri.startsWith('x-rincon-cpcontainer:1006206ccatalog')) { // Amazon prime container
track.TrackUri = trackUri;
track.ItemId = trackUri.replace('x-rincon-cpcontainer:', '');
Expand Down
4 changes: 4 additions & 0 deletions src/models/sonos-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export enum SonosEvents {
* This event is emitted if the coordinator of this device changed
*/
Coordinator = 'coordinator',
/**
* This event is emitted if the group id changed
*/
GroupId = 'groupid',
/**
* This event is emitted if the groupname changes.
*/
Expand Down
1 change: 1 addition & 0 deletions src/models/strong-sonos-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type StrongSonosEvents = {
volume: (volume: number) => void;

coordinator: (uuid: string) => void;
groupid: (id: string) => void;
groupname: (name: string) => void;

subscriptionError: (error: EventsError) => void;
Expand Down
26 changes: 24 additions & 2 deletions src/sonos-device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export default class SonosDevice extends SonosDeviceBase {

private groupName: string | undefined;

private groupId: string | undefined;

private coordinator: SonosDevice | undefined;

/**
Expand All @@ -48,11 +50,12 @@ export default class SonosDevice extends SonosDeviceBase {
* @param {({coordinator?: SonosDevice; name: string; managerEvents: EventEmitter} | undefined)} [groupConfig=undefined] groupConfig is used by the SonosManager to setup group change events.
* @memberof SonosDevice
*/
constructor(host: string, port = 1400, uuid: string | undefined = undefined, name: string | undefined = undefined, groupConfig: { coordinator?: SonosDevice; name: string; managerEvents: EventEmitter } | undefined = undefined) {
constructor(host: string, port = 1400, uuid: string | undefined = undefined, name: string | undefined = undefined, groupConfig: { coordinator?: SonosDevice; name: string; managerEvents: EventEmitter, groupId?: string } | undefined = undefined) {
super(host, port, uuid);
this.name = name;
if (groupConfig) {
this.groupName = groupConfig.name;
this.groupId = groupConfig.groupId;
if (groupConfig.coordinator !== undefined && uuid !== groupConfig.coordinator.uuid) {
this.coordinator = groupConfig.coordinator;
this.handleCoordinatorSimpleStateEvent(this.coordinator.currentTransportState === TransportState.Playing ? TransportState.Playing : TransportState.Stopped);
Expand Down Expand Up @@ -900,7 +903,7 @@ export default class SonosDevice extends SonosDeviceBase {
// #region Group stuff
private boundHandleGroupUpdate = this.handleGroupUpdate.bind(this);

private handleGroupUpdate(data: { coordinator: SonosDevice | undefined; name: string }): void {
private handleGroupUpdate(data: { coordinator: SonosDevice | undefined; name: string; groupdId: string | undefined }): void {
if (data.coordinator && data.coordinator?.uuid !== this.Uuid && (!this.coordinator || this.coordinator?.Uuid !== data.coordinator?.Uuid)) {
this.debug('Coordinator changed for %s', this.uuid);
this.coordinator?.events?.removeListener('simpleTransportState', this.boundHandleCoordinatorSimpleStateEvent);
Expand Down Expand Up @@ -932,6 +935,14 @@ export default class SonosDevice extends SonosDeviceBase {
this.events.emit(SonosEvents.GroupName, this.groupName);
}
}

if (data.groupdId && data.groupdId !== this.groupId) {
this.groupId = data.groupdId;
this.debug('GroupId changed for %s to %s', this.uuid, this.groupId);
if (this.events !== undefined) {
this.events.emit(SonosEvents.GroupId, this.groupId);
}
}
}

/**
Expand All @@ -955,6 +966,17 @@ export default class SonosDevice extends SonosDeviceBase {
public get GroupName(): string | undefined {
return this.groupName;
}

/**
* Get the GroupId, if device is created by the SonosManager.
*
* @readonly
* @type {(string | undefined)}
* @memberof SonosDevice
*/
public get GroupId(): string | undefined {
return this.groupId;
}
// #endregion

// #region Properties
Expand Down
10 changes: 5 additions & 5 deletions src/sonos-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,12 @@ export default class SonosManager {

private InitializeWithGroups(groups: ZoneGroup[]): boolean {
groups.forEach((g) => {
const coordinator = new SonosDevice(g.coordinator.host, g.coordinator.port, g.coordinator.uuid, g.coordinator.name, { name: g.name, managerEvents: this.events });
const coordinator = new SonosDevice(g.coordinator.host, g.coordinator.port, g.coordinator.uuid, g.coordinator.name, { name: g.name, managerEvents: this.events, groupId: g.groupId });
if (this.devices.findIndex((v) => v.Uuid === coordinator.Uuid) === -1) this.devices.push(coordinator);
g.members.forEach((m) => {
// Check if device exists
if (this.devices.findIndex((v) => v.Uuid === m.uuid) === -1) {
this.devices.push(new SonosDevice(m.host, m.port, m.uuid, m.name, { coordinator: m.uuid === g.coordinator.uuid ? undefined : coordinator, name: g.name, managerEvents: this.events }));
this.devices.push(new SonosDevice(m.host, m.port, m.uuid, m.name, { coordinator: m.uuid === g.coordinator.uuid ? undefined : coordinator, name: g.name, managerEvents: this.events, groupId: g.groupId }));
}
});
});
Expand All @@ -122,7 +122,7 @@ export default class SonosManager {
data.ZoneGroupState.forEach((g) => {
let coordinator = this.devices.find((d) => d.Uuid === g.coordinator.uuid);
if (coordinator === undefined) {
coordinator = new SonosDevice(g.coordinator.host, g.coordinator.port, g.coordinator.uuid, g.coordinator.name, { coordinator: undefined, name: g.name, managerEvents: this.events });
coordinator = new SonosDevice(g.coordinator.host, g.coordinator.port, g.coordinator.uuid, g.coordinator.name, { coordinator: undefined, name: g.name, managerEvents: this.events, groupId: g.groupId });
this.devices.push(coordinator);
this.events.emit('NewDevice', coordinator);
}
Expand All @@ -131,13 +131,13 @@ export default class SonosManager {
g.members
.filter((m) => !this.devices.some((d) => d.Uuid === m.uuid))
.forEach((m) => {
const newDevice = new SonosDevice(m.host, m.port, m.uuid, m.name, { coordinator: m.uuid === g.coordinator.uuid ? undefined : coordinator, name: g.name, managerEvents: this.events });
const newDevice = new SonosDevice(m.host, m.port, m.uuid, m.name, { coordinator: m.uuid === g.coordinator.uuid ? undefined : coordinator, name: g.name, managerEvents: this.events, groupId: g.groupId });
this.devices.push(newDevice);
this.events.emit('NewDevice', newDevice);
});

g.members.forEach((m) => {
this.events.emit(m.uuid, { coordinator: g.coordinator.uuid === m.uuid ? undefined : coordinator, name: g.name });
this.events.emit(m.uuid, { coordinator: g.coordinator.uuid === m.uuid ? undefined : coordinator, name: g.name, groupId: g.groupId });
});
});
}
Expand Down
40 changes: 40 additions & 0 deletions tests/sonos-device-metadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { expect } from 'chai'
import { EventEmitter } from 'events';
import { randomUUID } from 'crypto';
import SonosDevice from '../src/sonos-device'
import { TestHelpers } from './test-helpers';
import SonosEventListener from '../src/sonos-event-listener';
import { SmapiClient } from '../src/musicservices/smapi-client';
import { PlayMode, Repeat, TransportState } from '../src/models';


describe('SonosDevice Metadata', () => {
describe('SetTransportUri', () => {
// This tests fails for some reason, not sure is Artist radio ever worked.
it.skip('should work with artist radio', async() => {
const trackUri = 'spotify:artistRadio:72qVrKXRp9GeFQOesj0Pmv'
const scope = TestHelpers.mockRequestToService('/MediaRenderer/AVTransport/Control',
'AVTransport',
'SetAVTransportURI',
'<InstanceID>0</InstanceID><CurrentURI>x-sonosapi-radio:spotify%3aartistRadio%3a72qVrKXRp9GeFQOesj0Pmv?sid=9&amp;flags=8300&amp;sn=2</CurrentURI><CurrentURIMetaData>&lt;DIDL-Lite xmlns:dc=&quot;http://purl.org/dc/elements/1.1/&quot; xmlns:upnp=&quot;urn:schemas-upnp-org:metadata-1-0/upnp/&quot; xmlns:r=&quot;urn:schemas-rinconnetworks-com:metadata-1-0/&quot; xmlns=&quot;urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/&quot;&gt;&lt;item id=&quot;100c206cspotify%3aartistRadio%3a72qVrKXRp9GeFQOesj0Pmv&quot; parentID=&quot;10052064spotify%3aartist%3a72qVrKXRp9GeFQOesj0Pmv&quot; restricted=&quot;true&quot;&gt;&lt;dc:title&gt;Guus Meeuwis Radio&lt;/dc:title&gt;&lt;upnp:class&gt;object.item.audioItem.audioBroadcast.#artistRadio&lt;/upnp:class&gt;&lt;dc:creator&gt;Guus Meeuwis&lt;/dc:creator&gt;&lt;upnp:albumArtURI&gt;https://i.scdn.co/image/ab6761610000e5ebf5eda5ecd45cdbc2cc3c184a&lt;/upnp:albumArtURI&gt;&lt;r:albumArtist&gt;Guus Meeuwis&lt;/r:albumArtist&gt;&lt;r:description&gt;Guus Meeuwis&lt;/r:description&gt;&lt;desc id=&quot;cdudn&quot; nameSpace=&quot;urn:schemas-rinconnetworks-com:metadata-1-0/&quot;&gt;SA_RINCON2311_X_#Svc2311-0-Token&lt;/desc&gt;&lt;/item&gt;&lt;/DIDL-Lite&gt;</CurrentURIMetaData>'
)
const device = new SonosDevice(TestHelpers.testHost, 1400);
const result = await device.SetAVTransportURI(trackUri);
expect(scope.isDone()).to.be.true;
expect(result).to.be.true
})

it('should work with a stream url', async() => {
const trackUri = 'x-rincon-mp3radio://https://kexp.streamguys1.com/kexp160.aac'
const scope = TestHelpers.mockRequestToService('/MediaRenderer/AVTransport/Control',
'AVTransport',
'SetAVTransportURI',
'<InstanceID>0</InstanceID><CurrentURI>x-rincon-mp3radio://https://kexp.streamguys1.com/kexp160.aac</CurrentURI><CurrentURIMetaData>&lt;DIDL-Lite xmlns:dc=&quot;http://purl.org/dc/elements/1.1/&quot; xmlns:upnp=&quot;urn:schemas-upnp-org:metadata-1-0/upnp/&quot; xmlns:r=&quot;urn:schemas-rinconnetworks-com:metadata-1-0/&quot; xmlns=&quot;urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/&quot;&gt;&lt;item id=&quot;-1&quot; restricted=&quot;true&quot;&gt;&lt;desc id=&quot;cdudn&quot; nameSpace=&quot;urn:schemas-rinconnetworks-com:metadata-1-0/&quot;&gt;RINCON_AssociatedZPUDN&lt;/desc&gt;&lt;/item&gt;&lt;/DIDL-Lite&gt;</CurrentURIMetaData>'
)
const device = new SonosDevice(TestHelpers.testHost, 1400);
const result = await device.SetAVTransportURI(trackUri);
expect(scope.isDone()).to.be.true;
expect(result).to.be.true
})
})
});

0 comments on commit e26cf9a

Please sign in to comment.