UNPKG

neeo_driver_squeezebox

Version:

This driver adds Squeezebox / Logitech Media Server support to NEEO

316 lines (286 loc) 12.4 kB
const isArray = require('isarray'); /** * This controller uses the NeeoApi internaly to defines button and handler simultaneously. */ class Controller { constructor( device, player ) { this.device = device; this.player = player; this._mappings = {}; this._settings = { pollingDelay: 1 // 1 second } this._status = {}; this._cache = {}; this._lastUpdate = {}; } /** * @function addDefaultButtonHandler Adds a default button handler. If the command is not defined a warning is emitted. * @returns this */ addDefaultButtonHandler(){ this.device.addButtonHander( ( commandName ) => { const handler = this.getHandler( commandName ); if( handler ) handler.apply( this ); else console.warn('The command '+commandName+' is not defined.'); } ); return this; } /** * @function addButton * @param {string|Array} buttonNameOrButtonArrayNames the name of the button used for NEEO mapping and command handling. * @param {function} commandHandle the handler of the button when it is pressed on NEEO remote. * @param {string} buttonLabel the label of the button to display on NEEO remote (Optionnal). * @description Adds a button and a related handle by the button name convetion. The button label can be overriden. * @returns this. */ addButton( buttonNameOrButtonArrayNames, commandHandler, buttonLabel ){ if( isArray(buttonNameOrButtonArrayNames)){ buttonNameOrButtonArrayNames.forEach( buttonName => { this.addMapping( buttonName, commandHandler); this.addDeviceButton( buttonName, buttonLabel ); }); } else { this.addMapping( buttonNameOrButtonArrayNames, commandHandler); this.addDeviceButton( buttonNameOrButtonArrayNames, buttonLabel ); } return this; } addMapping( commandName, commandHandler) { if( this._mappings.hasOwnProperty(commandName)) throw new Error('A command named '+commandName+' has already been defined.') this._mappings[commandName] = commandHandler; return this; } addDeviceButton( buttonName, buttonLabel ){ this.device.addButton({name: buttonName, label: buttonLabel || buttonName }); return this; } /** * @function addFavorites * @param {array} favorites * @description Adds an array of favorites items. A favorite should have a property name and a proprty itemId, which is the SqueezeServer index of the favorite. */ addFavorites( favorites ){ favorites.forEach( p => this.addButton( p.name, () => this.player.playFavorite( p.itemId ) ) ); return this; } /** * @function addSpotify * @param {array} spotify * @description Adds the spotify configuration. Currently, only spotify playlists are supported. */ addSpotify( spotify ){ spotify.playlists.forEach( p => this.addButton( p.name, () => this.player.playlistPlay( p.user, p.itemId ) ) ); return this; } addNavigationButtons(){ return this.addButton(['SKIP BACKWARD','CHANNEL DOWN','CURSOR LEFT'], () => this.player.playIndex(-1, cb => { this.updateStatus(); this.updateSong(); } ) ) .addButton(['SKIP FORWARD','CHANNEL UP', 'CURSOR RIGHT'], () => this.player.playIndex(1, cb => { this.updateStatus(); this.updateSong(); } ) ); } /** * @function addDurationSlider * @param {string} label The name of label in front of the component. Defaults to Duration. * @description Adds a duration slider which can be used to seek track seconds on the remote. */ addDurationSlider( label ){ this.device.addSlider({ name: 'duration', label: label || 'Duration', range:[0,100], unit: '%' }, { setter: (deviceId,value) => { const durationInSeconds = (value * this._status.duration)/100; this.player.seek( durationInSeconds ); }, getter: () => (this._status.time / this._status.duration) * 100 } ); return this; } /** * @function addTrackLabels * @description Adds track information labels as shortcuts. Album, Artist and Title. */ addTrackLabels({ albumLabel, artistLabel, titleLabel }){ this.device .addTextLabel({ name: 'artistname', label: artistLabel||'Artist' }, () => this._cache.artist ) .addTextLabel({ name: 'albumname', label: albumLabel||'Album' }, () => this._cache.album ) .addTextLabel({ name: 'titlename', label: titleLabel||'Title' }, () => this._cache.title ); return this; } addCurrentTrackCover(){ this.device.addImageUrl({ name: 'albumcover', size: 'large'}, ( deviceId ) => this._cache.cover ); return this; } addVolumeActions(){ return this.addButton('MUTE TOGGLE', () => this.player.toggleMute() ) .addButton( 'VOLUME UP', () => { this._status.volume = (this._status.volume + 3 >= 100) ? 100 : this._status.volume + 3; this.player.setVolume( this._status.volume ); } ) .addButton( 'VOLUME DOWN', () => { this._status.volume = (this._status.volume - 3 <= 0) ? 0 : this._status.volume - 3; this.player.setVolume( this._status.volume ); }); } addBasicActions(){ return this.addButton('CURSOR ENTER', () => { if( this._status.mode == 'play') this.player.pause(); else this.player.play(); } ) .addButton('PLAY', () => this.player.play() ) .addButton('PAUSE', () => this.player.pause() ) .addButton('STOP', () => this.player.stop() ); } /** * @function addPowerStateManagement * @description Add power state management to the device. * The power state is updated according to the real power state of the squeezebox. */ addPowerStateManagement(){ this.addButton('POWER OFF', () => this.player.power( 0 ) ).addButton('POWER ON', () => this.player.power( 1 ) ) this.device.addPowerStateSensor({ getter: () => !!this._cache.power }); const setUpdateCallbackReference = function ( updateCallback, optionalCallbackFunctions ) { this.sendComponentUpdate = updateCallback; if (optionalCallbackFunctions && optionalCallbackFunctions.powerOnNotificationFunction) { this.markDeviceOn = optionalCallbackFunctions.powerOnNotificationFunction; } if (optionalCallbackFunctions && optionalCallbackFunctions.powerOffNotificationFunction) { this.markDeviceOff = optionalCallbackFunctions.powerOffNotificationFunction; } setInterval( this.update.bind(this), this._settings.pollingDelay * 1000 ); }; this.device.registerSubscriptionFunction( setUpdateCallbackReference.bind(this) ); return this; } /** * @function getHandler * @param {string} commandName * @description Gets the handler of the given command * @return {function} The button handler function or null if not found. */ getHandler( commandName ) { if( !this._mappings.hasOwnProperty(commandName)){ return null; }; return this._mappings[commandName]; } /** * This is called internaly to update the state of the driver from the player state. */ update(){ this.updateStatus().then( this.updateStatusComponents.bind( this ) ); this.updateSong().then( this.updateSongComponents.bind( this ) ); } /** * Updates the player status */ updateStatus(){ return new Promise( (resolve, reject) => { this.player.getStatus( ({result}) => { this._status = result; this._status.volume = result['mixer volume']; resolve( this._status ); }); }); } /** * Updates the internal cache of player remote meta. */ updateSong(){ return new Promise( (resolve, reject) => { this.player.getCurrentRemoteMeta( ({result}) => { if( result){ this._cache = result; this._cache.cover = this.player.address + ":"+ this.player.port + result.artwork_url; resolve( this._cache ); } else { this.player.getAlbum(({result}) => { this._cache.album = result; this.player.getArtist(({result}) => { this._cache.artist = result; this.player.getCurrentTitle(({result}) =>{ this._cache.artist = result; resolve( this._cache ); } ); } ); } ); } } ); } ); } /** * Updates power mode and sliders */ updateStatusComponents(){ const uniqueDeviceId = 'default'; //TODO: gets the device id // Powet state management if( this._status.power === 1 ){ if(this.markDeviceOn){ this.markDeviceOn(uniqueDeviceId); } } else { if(this.markDeviceOff) { this.markDeviceOff(uniqueDeviceId); } } // Stops the interval increment by one second for the slider update. // It will be restarted below if needed. if( this.durationInterval ){ clearInterval( this.durationInterval ); this.durationInterval = null; } if( this._status.mode == 'play' ){ if( this.sendComponentUpdate ) { // Slider duration management let value = this._cache['duration'] = Math.round( this._status.time / this._status.duration * 100 ); if( this._settings.pollingDelay >= 2 ) { const durationIncrement = (1 / this._status.duration) * 100 this.durationInterval = setInterval( () => { this.sendComponentUpdate({ uniqueDeviceId, component: 'duration', value }); value += durationIncrement; }, 1 * 1000 ); } else { // If the polling is every seconds, updates the slider directly... if( this._shouldSendUpdate('duration')) this.sendComponentUpdate({ uniqueDeviceId, component: 'duration', value }); } } } } updateSongComponents() { const uniqueDeviceId = 'default'; //TODO: gets the device id if(this.sendComponentUpdate ) { if( this._shouldSendUpdate('artist')) this.sendComponentUpdate({ uniqueDeviceId, component: 'artistname', value: this._cache.artist || "" }); if( this._shouldSendUpdate('album')) this.sendComponentUpdate({ uniqueDeviceId, component: 'albumname', value: this._cache.album || "" }); if( this._shouldSendUpdate('title')) this.sendComponentUpdate({ uniqueDeviceId, component: 'titlename', value: this._cache.title || "" }); if( this._shouldSendUpdate('cover')) this.sendComponentUpdate({ uniqueDeviceId, component: 'albumcover', value: this._cache.cover || "" }); } } _shouldSendUpdate( propertyName ){ // If the cache does not contains the property, don't send update. if( !this._cache[propertyName] ) return false; // if the cache is different from the previous update, sends the update to the brain. if( this._lastUpdate[propertyName] != this._cache[propertyName] ){ this._lastUpdate[propertyName] = this._cache[propertyName]; return true; } return false; } } module.exports.build = function( device, player ){ return new Controller( device, player ); };