Skip to content

Commit

Permalink
Merge pull request #20 from svrooij/feature/small-changes
Browse files Browse the repository at this point in the history
Feature/small changes
  • Loading branch information
svrooij authored Dec 23, 2019
2 parents 2f54d63 + dde61b7 commit 26b7926
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 7 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,31 @@ I also implemented extra functionatity for each player. (mostly combining calls)
- **.SwitchToTV()** - On your playbar you can use this to switch to TV input :tv:.
- **.TogglePlayback()** - If playing or transitioning your playback is paused :arrow_forward:. If stopped or paused your playback is resumed.

You can also browse content, see [content.js](./examples/content.js)

- **.Browse({...})** - Browse local content.
- **.BrowseWithDefaults({...})** - Browse local content by only specifying the ObjectID, the rest will be set to default.
- **.GetFavoriteRadioShows({...})** - Get your favorite radio shows
- **.GetFavoriteRadioStations({...})** - Get your favorite radio stations
- **.GetFavorites({...})** - Get your favorite songs
- **.GetQueue({...})** - Get the current queue

### Shortcuts

Each **Sonos Device** has the following shortcuts (things you could also do by using one of the exposed services):

- **.GetNightMode()** - Get NightMode status (playbar)
- **.GetSpeechEnhancement()** - Get Speech enhancement status (playbar)
- **.GetZoneGroupState()** - Get current group info.
- **.GetZoneInfo()** - Get info about current player.
- **.Play()** - Start playing *.
- **.Pause()** - Pause playing *.
- **.Next()** - Go to next song (when playing the queue) *.
- **.Previous()** - Go to previous song (when playing the queue) *.
- **.SetNightMode(desiredState)** - Turn on/off nightmode on your playbar.
- **.SetRelativeVolume(adjustment)** - Change the volume relative to current.
- **.SetSpeechEnhancement(desiredState)** - Turn on/off speech enhancement on your playbar.
- **.SetVolume(newVolume)** - Change the volume.
- **.Stop()** - Stop playing (most of the time it's better to pause playback) *.
- **.SeekPosition('0:03:01')** - Go to other postion in track *.
- **.SeekTrack(3)** - Go to other track in the queue *.
Expand Down
23 changes: 23 additions & 0 deletions examples/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const SonosDevice = require('../lib').SonosDevice

const sonos = new SonosDevice(process.env.SONOS_HOST || '192.168.96.56')

// sonos.GetQueue()
// .then(resp => {
// console.log(resp)
// })
// .catch(console.error)

// Search for artist eminem
sonos.BrowseWithDefaults('A:ARTIST:eminem')
.then(async resp => {
// You will now have the artist reference in the ItemId, this can be used to do a new call.
if (resp.NumberReturned > 0) {
return sonos.BrowseWithDefaults(resp.Result[0].ItemId)
}
return resp
})
.then(resp => {
console.log(resp)
})
.catch(console.error)
5 changes: 3 additions & 2 deletions src/helpers/xml-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,10 @@ export class XmlHelper {
}

static EncodeTrackUri(trackUri: string): string {
if(trackUri.startsWith('http') && trackUri.indexOf('?') === -1)
return trackUri.replace(/ /g, '+');
if(trackUri.startsWith('http'))
return encodeURI(trackUri);

// Part below needs some work.
const index = trackUri.indexOf(':') + 1
return trackUri.substr(0, index) + this.EncodeXml(trackUri.substr(index)).replace(/:/g, '%3a');
}
Expand Down
4 changes: 3 additions & 1 deletion src/models/browse-response.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Track } from "./track";

export interface BrowseResponse {
Result: string;
Result: string | Track[];
NumberReturned: number;
TotalMatches: number;
UpdateID: number;
Expand Down
133 changes: 130 additions & 3 deletions src/sonos-device.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SonosDeviceBase } from './sonos-device-base'
import { GetZoneInfoResponse, GetZoneAttributesResponse, GetZoneGroupStateResponse, AddURIToQueueResponse } from './services'
import { PlayNotificationOptions, Alarm, TransportState, ServiceEvents, SonosEvents, PatchAlarm, PlayTtsOptions } from './models'
import { PlayNotificationOptions, Alarm, TransportState, ServiceEvents, SonosEvents, PatchAlarm, PlayTtsOptions, BrowseResponse } from './models'
import { AsyncHelper } from './helpers/async-helper'
import { ZoneHelper } from './helpers/zone-helper'
import { EventEmitter } from 'events';
Expand Down Expand Up @@ -123,6 +123,43 @@ export class SonosDevice extends SonosDeviceBase {
return this.AlarmClockService.UpdateAlarm(alarm);
})
}
/**
* Browse or search content directory
*
* @param {{ ObjectID: string; BrowseFlag: string; Filter: string; StartingIndex: number; RequestedCount: number; SortCriteria: string }} input
* @param {string} ObjectID The search query, ['A:ARTIST','A:ALBUMARTIST','A:ALBUM','A:GENRE','A:COMPOSER','A:TRACKS','A:PLAYLISTS'] with optionally ':search+query' behind it.
* @param {string} BrowseFlag 'BrowseDirectChildren' is default, could also be 'BrowseMetadata'
* @param {string} Filter Which fields should be returned '*' for all.
* @param {number} StartingIndex Where to start in the results, (could be used for paging)
* @param {number} RequestedCount How many items should be returned, 0 for all.
* @param {string} SortCriteria Sort the results based on metadata fields. '+upnp:artist,+dc:title' for sorting on artist then on title.
* @returns {Promise<BrowseResponse>}
* @memberof SonosDevice
* @see http://www.upnp.org/specs/av/UPnP-av-ContentDirectory-v1-Service.pdf
*/
public async Browse(input: { ObjectID: string; BrowseFlag: string; Filter: string; StartingIndex: number; RequestedCount: number; SortCriteria: string }): Promise<BrowseResponse> {
return this.ContentDirectoryService.Browse(input)
.then(resp => {
if(typeof resp.Result === 'string' && resp.NumberReturned > 0) {
const parsedData = XmlHelper.DecodeAndParseXml(resp.Result)['DIDL-Lite'];
const itemObject = parsedData.item || parsedData.container;
const items = Array.isArray(itemObject) ? itemObject : [itemObject];
resp.Result = items.map(i => MetadataHelper.ParseDIDLTrack(i, this.host, this.port));
}
return resp;
})
}

/**
* Same as browse but with all parameters set to default.
*
* @param {string} ObjectID The search query, ['A:ARTIST','A:ALBUMARTIST','A:ALBUM','A:GENRE','A:COMPOSER','A:TRACKS','A:PLAYLISTS'] with optionally ':search+query' behind it.
* @returns {Promise<BrowseResponse>}
* @memberof SonosDevice
*/
public async BrowseWithDefaults(ObjectID: string): Promise<BrowseResponse> {
return this.Browse({ObjectID: ObjectID, BrowseFlag: 'BrowseDirectChildren', Filter: '*', StartingIndex: 0, RequestedCount: 0, SortCriteria: '' });
}

/**
* Execute any sonos command by name, see examples/commands.js
Expand Down Expand Up @@ -167,9 +204,28 @@ export class SonosDevice extends SonosDeviceBase {

const serviceDictionary = this.executeCommandGetFunctions();
if(serviceDictionary[serviceName]) return serviceDictionary[serviceName] as unknown as {[key: string]: Function};
// Do case insensitive lookup
const checkedName = Object.keys(serviceDictionary).find(k => k.toLowerCase() === serviceName.toLowerCase())
if(checkedName !== undefined) return serviceDictionary[checkedName] as unknown as {[key: string]: Function};
return undefined;
}

public async GetFavoriteRadioShows(): Promise<BrowseResponse> {
return this.BrowseWithDefaults('R:0/1');
}

public async GetFavoriteRadioStations(): Promise<BrowseResponse> {
return this.BrowseWithDefaults('R:0/0');
}

public async GetFavorites(): Promise<BrowseResponse> {
return this.BrowseWithDefaults('FV:2');
}

public async GetQueue(): Promise<BrowseResponse> {
return this.BrowseWithDefaults('Q:0');
}

/**
* Join this device to an other group, if you know the coordinator uuid you can do .AVTransportService.SetAVTransportURI({InstanceID: 0, CurrentURI: `x-rincon:${uuid}`, CurrentURIMetaData: ''})
*
Expand Down Expand Up @@ -262,7 +318,7 @@ export class SonosDevice extends SonosDeviceBase {

if (originalPositionInfo.Track > 1 && originalMediaInfo.NrTracks > 1) {
this.debug('Selecting track %d', originalPositionInfo.Track)
await this.SeeKTrack(originalPositionInfo.Track)
await this.SeekTrack(originalPositionInfo.Track)
.catch(err => {
this.debug('Error selecting track, happens with some music services %o', err)
})
Expand Down Expand Up @@ -574,6 +630,29 @@ export class SonosDevice extends SonosDeviceBase {
//#endregion

//#region Shortcuts

/**
* Get nightmode status of playbar.
*
* @returns {Promise<boolean>}
* @memberof SonosDevice
*/
public GetNightMode(): Promise<boolean> {
return this.RenderingControlService.GetEQ({ InstanceID: 0, EQType: 'NightMode' })
.then(resp => resp.CurrentValue === 1)
}

/**
* Get Speech Enhancement status of playbar
*
* @returns {Promise<boolean>}
* @memberof SonosDevice
*/
public GetSpeechEnhancement(): Promise<boolean> {
return this.RenderingControlService.GetEQ({ InstanceID: 0, EQType: 'DialogLevel' })
.then(resp => resp.CurrentValue === 1)
}

/**
* GetZoneAttributes shortcut to .DevicePropertiesService.GetZoneAttributes()
*
Expand Down Expand Up @@ -652,8 +731,56 @@ export class SonosDevice extends SonosDeviceBase {
* @returns {Promise<boolean>}
* @memberof SonosDevice
*/
public SeeKTrack(trackNr: number): Promise<boolean> { return this.Coordinator.AVTransportService.Seek({InstanceID: 0, Unit: 'TRACK_NR', Target: trackNr.toString()}) }
public SeekTrack(trackNr: number): Promise<boolean> { return this.Coordinator.AVTransportService.Seek({InstanceID: 0, Unit: 'TRACK_NR', Target: trackNr.toString()}) }

/**
* Turn on/off night mode, on your playbar.
*
* @param {boolean} nightmode
* @returns {Promise<boolean>}
* @memberof SonosDevice
*/
public SetNightMode(nightmode: boolean): Promise<boolean> {
return this.RenderingControlService
.SetEQ({ InstanceID: 0, EQType: 'NightMode', DesiredValue: nightmode === true ? 1 : 0 })
}

/**
* Set relative volume, shortcut to .RenderingControlService.SetRelativeVolume({ InstanceID: 0, Channel: 'Master', Adjustment: volumeAdjustment })
*
* @param {number} volumeAdjustment the adjustment, positive or negative
* @returns {Promise<number>}
* @memberof SonosDevice
*/
public SetRelativeVolume(volumeAdjustment: number): Promise<number> {
return this.RenderingControlService.SetRelativeVolume({ InstanceID: 0, Channel: 'Master', Adjustment: volumeAdjustment })
.then(resp => resp.NewVolume);
}

/**
* Turn on/off speech enhancement, on your playbar,
* shortcut to .RenderingControlService.SetEQ({ InstanceID: 0, EQType: 'DialogLevel', DesiredValue: dialogLevel === true ? 1 : 0 })
*
* @param {boolean} dialogLevel
* @returns {Promise<boolean>}
* @memberof SonosDevice
*/
public SetSpeechEnhancement(dialogLevel: boolean): Promise<boolean> {
return this.RenderingControlService
.SetEQ({ InstanceID: 0, EQType: 'DialogLevel', DesiredValue: dialogLevel === true ? 1 : 0 })
}

/**
* Set the volume, shortcut to .RenderingControlService.SetVolume({InstanceID: 0, Channel: 'Master', DesiredVolume: volume});
*
* @param {number} volume new Volume (between 0 and 100)
* @returns {Promise<boolean>}
* @memberof SonosDevice
*/
public SetVolume(volume: number): Promise<boolean> {
if (volume < 0 || volume > 100) throw new Error('Volume should be between 0 and 100');
return this.RenderingControlService.SetVolume({InstanceID: 0, Channel: 'Master', DesiredVolume: volume});
}
/**
* Stop playback, shortcut to .Coordinator.AVTransportService.Stop()
*
Expand Down
8 changes: 7 additions & 1 deletion tests/xml-helper-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ import { expect } from 'chai'
import 'mocha';

describe('XmlHelper', function () {
it('Encodes a standard url correctly', function() {
it('Encodes an url correctly', function() {
const url = 'http://192.168.96.200:5601/cache/nl-NL/1af7308e4c6ac25a6620e339ba70a451b9dda8a2.mp3';
const result = XmlHelper.EncodeTrackUri(url);
expect(result).to.be.eq(url);
})
it('Encodes an url with query correctly', function() {
const url = 'http://192.168.96.200:5601/cache/nl-NL/tts?text=dit is een test tekst';
const result = XmlHelper.EncodeTrackUri(url);
const expected = 'http://192.168.96.200:5601/cache/nl-NL/tts?text=dit%20is%20een%20test%20tekst'
expect(result).to.be.eq(expected);
})
})

0 comments on commit 26b7926

Please sign in to comment.