Skip to content

Commit

Permalink
Merge pull request #100 from svrooij/bug/numeric-album-skipped
Browse files Browse the repository at this point in the history
fix: Metadata finally fixed ans support fo deezer
  • Loading branch information
svrooij committed Dec 30, 2020
2 parents fe635cb + b4c8de0 commit 146bb98
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 37 deletions.
5 changes: 5 additions & 0 deletions docs/sonos-device/methods.md
Expand Up @@ -62,6 +62,11 @@ This library can guess the required metadata for certain track uri's. This is do

Currently supported url's for metadata guessing:

- `deezer:album:123456` - A Deezer album by id
- `deezer:artistTopTracks:123456` - A Deezer artist top tracks
- `deezer:playlist:123456` - A Deezer playlist by id
- `deezer:track:123456` - A Deezer track by id
- `sonos:playlist:7` - A saved sonos playlist by number.
- `spotify:track:0GiWi4EkPduFWHQyhiKpRB` - Regular spotify track.
- `spotify:artistRadio:72qVrKXRp9GeFQOesj0Pmv` - Spotify artist radio (has to be added to queue).
- `spotify:artistTopTracks:72qVrKXRp9GeFQOesj0Pmv` - Spotify artist top tracks (has to be added to queue).
Expand Down
74 changes: 74 additions & 0 deletions src/helpers/metadata-helper.ts
Expand Up @@ -120,6 +120,16 @@ export default class MetadataHelper {
track.CdUdn = 'RINCON_AssociatedZPUDN';
return track;
}
if (trackUri.startsWith('file:///jffs/settings/savedqueues.rsq#') || trackUri.startsWith('sonos:playlist:')) {
const queueId = trackUri.match(/\d+/g);
if (queueId?.length === 1) {
track.TrackUri = `file:///jffs/settings/savedqueues.rsq#${queueId[0]}`;
track.UpnpClass = 'object.container.playlistContainer';
track.ItemId = `SQ:${queueId[0]}`;
track.CdUdn = 'RINCON_AssociatedZPUDN';
return track;
}
}
if (trackUri.startsWith('x-rincon-playlist')) {
const parentMatch = /.*#(.*)\/.*/g.exec(trackUri);
if (parentMatch === null) throw new Error('ParentID not found');
Expand Down Expand Up @@ -161,11 +171,44 @@ export default class MetadataHelper {
track.CdUdn = 'SA_RINCON40967_X_#Svc40967-0-Token';
return track;
}

if (trackUri.startsWith('x-rincon-cpcontainer:1004006calbum-')) { // Deezer Album
const numbers = trackUri.match(/\d+/g);
if (numbers && numbers.length >= 2) {
return MetadataHelper.deezerMetadata('album', numbers[1]);
}
}

if (trackUri.startsWith('x-rincon-cpcontainer:10fe206ctracks-artist-')) { // Deezer Artists Top Tracks
const numbers = trackUri.match(/\d+/g);
if (numbers && numbers.length >= 3) {
return MetadataHelper.deezerMetadata('artistTopTracks', numbers[2]);
}
}

if (trackUri.startsWith('x-rincon-cpcontainer:1006006cplaylist_spotify%3aplaylist-')) { // Deezer Playlist
const numbers = trackUri.match(/\d+/g);
if (numbers && numbers.length >= 3) {
return MetadataHelper.deezerMetadata('playlist', numbers[2]);
}
}

if (trackUri.startsWith('x-sonos-http:tr%3a') && trackUri.includes('sid=2')) { // Deezer Track
const numbers = trackUri.match(/\d+/g);
if (numbers && numbers.length >= 2) {
return MetadataHelper.deezerMetadata('track', numbers[1]);
}
}

const parts = trackUri.split(':');
if (parts.length === 3 && parts[0] === 'spotify') {
return MetadataHelper.guessSpotifyMetadata(trackUri, parts[1], spotifyRegion);
}

if (parts.length === 3 && parts[0] === 'deezer') {
return MetadataHelper.deezerMetadata(parts[1], parts[2]);
}

if (parts.length === 2 && parts[0] === 'radio' && parts[1].startsWith('s')) {
const [, stationId] = parts;
track.UpnpClass = 'object.item.audioItem.audioBroadcast';
Expand Down Expand Up @@ -231,6 +274,37 @@ export default class MetadataHelper {
return track;
}

private static deezerMetadata(kind: 'album' | 'artistTopTracks' | 'playlist' | 'track' | unknown, id: string, region = '519'): Track | undefined {
const track: Track = {
CdUdn: `SA_RINCON${region}_X_#Svc${region}-0-Token`,
};
switch (kind) {
case 'album':
track.TrackUri = `x-rincon-cpcontainer:1004006calbum-${id}?sid=2&flags=108&sn=23`;
track.UpnpClass = 'object.container.album.musicAlbum.#HERO';
track.ItemId = `1004006calbum-${id}`;
break;
case 'artistTopTracks':
track.TrackUri = `x-rincon-cpcontainer:10fe206ctracks-artist-${id}?sid=2&flags=8300&sn=23`;
track.UpnpClass = 'object.container.#DEFAULT';
track.ItemId = `10fe206ctracks-artist-${id}`;
break;
case 'playlist':
track.TrackUri = `x-rincon-cpcontainer:1006006cplaylist_spotify%3aplaylist-${id}?sid=2&flags=108&sn=23`;
track.UpnpClass = 'object.container.playlistContainer.#DEFAULT';
track.ItemId = `1006006cplaylist_spotify%3aplaylist-${id}`;
break;
case 'track':
track.TrackUri = `x-sonos-http:tr:${id}.mp3?sid=2&flags=8224&sn=23`;
track.UpnpClass = 'object.item.audioItem.musicTrack.#DEFAULT';
track.ItemId = `10032020tr%3a${id}`;
break;
default:
return undefined;
}
return track;
}

private static GetUpnpClass(parentID: string): string {
switch (parentID) {
case 'A:ALBUMS':
Expand Down
17 changes: 4 additions & 13 deletions src/helpers/xml-helper.ts
Expand Up @@ -31,10 +31,12 @@ export default class XmlHelper {
* @memberof XmlHelper
*/
static DecodeHtml(text: unknown): string | undefined {
if (typeof text !== 'string' || text === '') {
if (typeof text === 'undefined' || (typeof text === 'string' && text === '')) {
return undefined;
}

if (typeof text !== 'string') {
return XmlHelper.DecodeHtml(`${text}`);
}
return XmlHelper.htmlEntities.decode(text);
}

Expand Down Expand Up @@ -78,17 +80,6 @@ export default class XmlHelper {
return XmlHelper.xmlEntities.encode(xml);
}

static EncodeXmlUndefined(xml: unknown): string | undefined {
if (typeof xml === 'undefined') {
return undefined;
}
if (typeof xml === 'string') {
return xml === '' ? undefined : XmlHelper.xmlEntities.encode(xml);
}

return XmlHelper.EncodeXml(`${xml}`);
}

static EncodeTrackUri(trackUri: string): string {
if (trackUri.startsWith('http')) return encodeURI(trackUri);
if (
Expand Down
6 changes: 3 additions & 3 deletions src/sonos-device.ts
Expand Up @@ -7,7 +7,7 @@ import {
GetZoneInfoResponse, GetZoneAttributesResponse, GetZoneGroupStateResponse, AddURIToQueueResponse, AVTransportServiceEvent, RenderingControlServiceEvent, MusicService, AccountData,
} from './services';
import {
PlayNotificationOptions, Alarm, TransportState, ServiceEvents, SonosEvents, PatchAlarm, PlayTtsOptions, BrowseResponse,
PlayNotificationOptions, Alarm, TransportState, ServiceEvents, SonosEvents, PatchAlarm, PlayTtsOptions, BrowseResponse, ZoneGroup, ZoneMember,
} from './models';
import { StrongSonosEvents } from './models/strong-sonos-events';
import AsyncHelper from './helpers/async-helper';
Expand Down Expand Up @@ -259,12 +259,12 @@ export default class SonosDevice extends SonosDeviceBase {
this.debug('JoinGroup(%s)', otherDevice);
const zones = await this.ZoneGroupTopologyService.GetParsedZoneGroupState();

const groupToJoin = zones.find((z) => z.members.some((m) => m.name.toLowerCase() === otherDevice.toLowerCase()));
const groupToJoin = zones.find((z: ZoneGroup) => z.members.some((m) => m.name.toLowerCase() === otherDevice.toLowerCase()));
if (groupToJoin === undefined) {
throw new Error(`Player '${otherDevice}' isn't found!`);
}

if (groupToJoin.members.some((m) => m.uuid === this.Uuid)) {
if (groupToJoin.members.some((m: ZoneMember) => m.uuid === this.Uuid)) {
return Promise.resolve(false); // Already in the group.
}
return await this.AVTransportService.SetAVTransportURI({ InstanceID: 0, CurrentURI: `x-rincon:${groupToJoin.coordinator.uuid}`, CurrentURIMetaData: '' });
Expand Down
77 changes: 77 additions & 0 deletions tests/helpers/metadata-helper.test.ts
Expand Up @@ -17,6 +17,70 @@ describe('MetadataHelper', () => {
});

describe('GuessTrack', () => {
it('Guess metadata for file:///jffs/settings/savedqueues.rsq#7', () => {
const track = MetadataHelper.GuessTrack('file:///jffs/settings/savedqueues.rsq#7');
expect(track).to.have.property('ItemId', 'SQ:7');
expect(track).to.have.property('TrackUri', 'file:///jffs/settings/savedqueues.rsq#7');
});

it('Guess metadata for sonos:playlist:7', () => {
const track = MetadataHelper.GuessTrack('sonos:playlist:7');
expect(track).to.have.property('ItemId', 'SQ:7');
expect(track).to.have.property('TrackUri', 'file:///jffs/settings/savedqueues.rsq#7');
});

it('Not guess metadata for sonos:playlist:a', () => {
const track = MetadataHelper.GuessTrack('sonos:playlist:a');
expect(track).to.be.undefined;
});
});

describe('GuessTrack -> Deezer', () => {
it('Guess metadata for deezer:album:169734362', () => {
const track = MetadataHelper.GuessTrack('deezer:album:169734362');
expect(track).to.have.property('TrackUri', 'x-rincon-cpcontainer:1004006calbum-169734362?sid=2&flags=108&sn=23');
});

it('Guess metadata for x-rincon-cpcontainer:1004006calbum-169734362?sid=2&flags=108&sn=23', () => {
const track = MetadataHelper.GuessTrack('x-rincon-cpcontainer:1004006calbum-169734362?sid=2&flags=108&sn=23');
expect(track).to.have.property('TrackUri', 'x-rincon-cpcontainer:1004006calbum-169734362?sid=2&flags=108&sn=23');
expect(track).to.have.property('UpnpClass', 'object.container.album.musicAlbum.#HERO');
});

it('Guess metadata for deezer:artistTopTracks:6049784', () => {
const track = MetadataHelper.GuessTrack('deezer:artistTopTracks:6049784');
expect(track).to.have.property('TrackUri', 'x-rincon-cpcontainer:10fe206ctracks-artist-6049784?sid=2&flags=8300&sn=23');
});

it('Guess metadata for x-rincon-cpcontainer:10fe206ctracks-artist-6049784?sid=2&flags=8300&sn=23', () => {
const track = MetadataHelper.GuessTrack('x-rincon-cpcontainer:10fe206ctracks-artist-6049784?sid=2&flags=8300&sn=23');
expect(track).to.have.property('TrackUri', 'x-rincon-cpcontainer:10fe206ctracks-artist-6049784?sid=2&flags=8300&sn=23');
expect(track).to.have.property('UpnpClass', 'object.container.#DEFAULT');
});

it('Guess metadata for deezer:playlist:1371651955', () => {
const track = MetadataHelper.GuessTrack('deezer:playlist:1371651955');
expect(track).to.have.property('TrackUri', 'x-rincon-cpcontainer:1006006cplaylist_spotify%3aplaylist-1371651955?sid=2&flags=108&sn=23');
});

it('Guess metadata for x-rincon-cpcontainer:1006006cplaylist_spotify%3aplaylist-1371651955?sid=2&flags=108&sn=23', () => {
const track = MetadataHelper.GuessTrack('x-rincon-cpcontainer:1006006cplaylist_spotify%3aplaylist-1371651955?sid=2&flags=108&sn=23');
expect(track).to.have.property('TrackUri', 'x-rincon-cpcontainer:1006006cplaylist_spotify%3aplaylist-1371651955?sid=2&flags=108&sn=23');
});

it('Guess metadata for deezer:track:1121931512', () => {
const track = MetadataHelper.GuessTrack('deezer:track:1121931512');
expect(track).to.have.property('TrackUri', 'x-sonos-http:tr:1121931512.mp3?sid=2&flags=8224&sn=23');
});

it('Guess metadata for x-sonos-http:tr%3a1121931512.mp3?sid=2&flags=8224&sn=23', () => {
const track = MetadataHelper.GuessTrack('x-sonos-http:tr%3a1121931512.mp3?sid=2&flags=8224&sn=23');
expect(track).to.have.property('TrackUri', 'x-sonos-http:tr:1121931512.mp3?sid=2&flags=8224&sn=23');
expect(track).to.have.property('UpnpClass', 'object.item.audioItem.musicTrack.#DEFAULT');
});
});

describe('GuessTrack -> Spotify', () => {
it('Guess metadata for Spotify artist top tracks', () => {
const track = MetadataHelper.GuessTrack('spotify:artistTopTracks:72qVrKXRp9GeFQOesj0Pmv');
expect(track).to.be.an('object');
Expand Down Expand Up @@ -100,6 +164,19 @@ describe('MetadataHelper', () => {
done();
});

it('decodes numeric album', (done) => {
const album = 19;
const artist = 'Adele';
const didl = {
'upnp:album': album,
'dc:creator': artist
};
const result = MetadataHelper.ParseDIDLTrack(didl, 'fake_host');
expect(result).to.has.property('Album','19');
expect(result).to.has.property('Artist', artist);
done();
});

it('decodes spotify album art uri correctly', (done) => {
const hostName = 'fake_host'
const albumArt = '/getaa?s=1&u=x-sonos-spotify%3aspotify%253atrack%253a0WS5DKZ6QnHvFYBk8lRRm0%3fsid%3d9%26flags%3d8224%26sn%3d7';
Expand Down
21 changes: 0 additions & 21 deletions tests/helpers/xml-helper.test.ts
Expand Up @@ -62,27 +62,6 @@ describe('XmlHelper', () => {
});
});

describe('EncodeXmlUndefined()', () => {

it('returns undefined when input is undefined', () => {
const xml = undefined;
const result = XmlHelper.EncodeXmlUndefined(xml);
expect(result).to.be.undefined;
});

it('returns undefined when input is empty string', () => {
const xml = '';
const result = XmlHelper.EncodeXmlUndefined(xml);
expect(result).to.be.undefined;
});

it('returns string when input is number', () => {
const xml = 19;
const result = XmlHelper.EncodeXmlUndefined(xml);
expect(result).to.be.string('19');
})
});

describe('EncodeTrackUri()', () => {

it('doesn\'t encode an uri starting with x-rincon-mp3radio', () => {
Expand Down

0 comments on commit 146bb98

Please sign in to comment.