iobroker.squeezeboxrpc
Version:
ioBroker Logitech Squeezebox Adapter over JSON/RPC-Protocol
1,218 lines (1,207 loc) • 42.9 kB
JavaScript
'use strict';
const { ioUtil } = require('./ioUtil');
/**
* Represents a Squeezebox player within a server environment.
*
* @param server - The server instance that manages the player.
* @param playerdata - Initial data related to the player.
*
* Properties include:
* - server: The server instance.
* - adapter: Adapter for server communication.
* - ioUtil: Utility for IO operations.
* - fullStatus: String representing full status tags.
* - smallStatus: String representing small status tags.
* - statePath: Path to the player's state within the server.
* - btnStatePath: Path to the player's button states.
* - currentStates: Object holding the current states of the player.
* - observers: List of observer functions for state changes.
* - playername: Name of the player.
* - playerid: Unique identifier for the player.
* - playerindex: Index of the player.
* - remote: Indicator for remote control capability.
* - statuscounter: Counter for status updates.
* - connected: Connection status of the player.
* - log: Object for logging information.
* - islogsilly: Flag for verbose logging.
* - islogdebug: Flag for debug logging.
* - TPE2Handling: Handling mode for TPE2 frames.
* - FORBIDDEN_CHARS: Regular expression for forbidden characters in player names.
* - sbPlayerStatusMain: Main status attributes of the player.
* - sbPlayerStatusLoop: Loop status attributes of the player.
* - sbPlayerButtons: Button commands available for the player.
* - sbPlayerStatusPlaylist: Playlist status attributes of the player.
*
* Methods include:
* - init: Initializes player settings and starts observation.
* - sanitizePlayername: Sanitizes the player name.
* - doObserverPlayer: Manages player status observation.
* - getPlayerUpdateStatus: Updates player status.
* - doAlarmsUpdateStatus: Updates alarm status.
* - doPlayerUpdateStatus: Updates player data based on server responses.
* - doPlayerStatus: Sets player status from initial data.
* - checkPlayerdataStates: Ensures all player data states are initialized.
* - doStateChange: Handles state changes based on updates.
* - createArtworkUrl: Generates a URL for player artwork.
* - disconnect: Handles player disconnection.
* - connect: Handles player connection.
* - createState: Creates a state object for the player.
* - createObject: Creates a device or state object.
* - request: Sends a request to the server.
* - setTPE2Handling: Sets the TPE2 handling mode.
* - setState: Updates the state of the player.
* - createFolder: Creates a folder object.
* - createDevice: Creates a device object.
* - existsObjectAsync: Checks if an object exists asynchronously.
* - convertState: Converts state values based on type.
* - logsilly: Logs messages at a verbose level.
* - logdebug: Logs debug messages.
* - logerror: Logs error messages.
* - loginfo: Logs informational messages.
*/
function ioSBPlayer(server, playerdata) {
this.server = server;
this.adapter = server.adapter;
this.ioUtil = new ioUtil(
server.adapter,
this.adapter.config.outputplayerdebug,
this.adapter.config.outputplayersilly,
);
this.fullStatus = 'tags:cgABbehldiqtyrSuoKLNJ';
this.smallStatus = 'tags:uB';
this.statePath = this.server.PlayersStatePath;
this.btnStatePath = 'Buttons';
this.currentStates = {};
this.observers = [];
this.playername = '';
this.playerid = '';
this.playerindex = 0;
this.remote = 0;
this.statuscounter = 0;
this.connected = 0;
this.log = {};
this.islogsilly = this.adapter.config.outputplayersilly;
this.islogdebug = this.adapter.config.outputplayerdebug;
this.TPE2Handling = 1;
this.FORBIDDEN_CHARS = /[^\d\w_]+/gm;
//test the regex: https://regex101.com/r/Ed0WhH/1
this.sbPlayerStatusMain = {
player_name: {
name: 'Playername',
read: true,
write: false,
type: 'string',
role: 'info.name',
exist: false,
},
playerid: {
name: 'PlayerID',
read: true,
write: false,
type: 'string',
role: 'info.name',
exist: false,
},
player_connected: {
name: 'Connected',
read: true,
write: false,
type: 'number',
role: 'value',
exist: false,
},
player_ip: {
name: 'IP',
read: true,
write: false,
type: 'string',
role: 'info.ip',
exist: false,
},
power: {
name: 'Power',
read: true,
write: true,
type: 'number',
role: 'switch',
exist: false,
min: 0,
max: 1,
},
mode: {
name: 'Mode',
read: true,
write: false,
type: 'string',
role: 'media.state',
exist: false,
},
time: {
name: 'Time',
read: true,
write: false,
type: 'number',
role: 'media.elapsed',
exist: false,
},
rate: {
name: 'Rate',
read: true,
write: false,
type: 'number',
role: 'value',
exist: false,
},
sync_slaves: {
name: 'SyncSlaves',
read: true,
write: false,
type: 'string',
role: 'value',
exist: false,
},
sync_master: {
name: 'SyncMaster',
read: true,
write: false,
type: 'string',
role: 'value',
exist: false,
},
'mixer volume': {
name: 'Volume',
read: true,
write: true,
type: 'number',
role: 'level.volume',
exist: false,
min: 0,
max: 100,
},
'playlist repeat': {
name: 'PlaylistRepeat',
read: true,
write: true,
type: 'number',
role: 'media.mode.repeat',
exist: false,
},
'playlist shuffle': {
name: 'PlaylistShuffle',
read: true,
write: true,
type: 'number',
role: 'media.mode.shuffle',
exist: false,
},
remote: {
name: 'Remote',
read: true,
write: false,
type: 'number',
role: 'value',
exist: false,
},
playlist: {
name: 'Playlist',
read: true,
write: false,
type: 'string',
role: 'value',
exist: false,
},
playlist_cur_index: {
name: 'PlaylistCurrentIndex',
read: true,
write: true,
type: 'string',
role: 'value',
exist: false,
},
alarms: {
name: 'Alarms',
read: true,
write: false,
type: 'string',
role: 'value',
exist: false,
},
state: {
name: 'state',
read: true,
write: true,
type: 'number',
role: 'media.state',
exist: false,
min: 0,
max: 2,
},
};
this.sbPlayerStatusLoop = {
duration: {
name: 'Duration',
read: true,
write: false,
type: 'number',
role: 'media.duration',
exist: false,
},
artwork_url: {
name: 'ArtworkUrl',
read: true,
write: false,
type: 'string',
role: 'media.cover',
exist: false,
},
bitrate: {
name: 'Bitrate',
read: true,
write: false,
type: 'string',
role: 'media.bitrate',
exist: false,
},
album: {
name: 'Album',
read: true,
write: false,
type: 'string',
role: 'media.album',
exist: false,
},
coverid: {
name: 'ArtworkUrl',
read: true,
write: false,
type: 'string',
role: 'value',
exist: true,
},
genre: {
name: 'Genre',
read: true,
write: false,
type: 'string',
role: 'media.genre',
exist: false,
},
type: {
name: 'Type',
read: true,
write: false,
type: 'string',
role: 'value',
exist: false,
},
title: {
name: 'Title',
read: true,
write: false,
type: 'string',
role: 'media.title',
exist: false,
},
artist: {
name: 'Artist',
read: true,
write: false,
type: 'string',
role: 'media.artist',
exist: false,
},
albumartist: {
name: 'Albumartist',
read: true,
write: false,
type: 'string',
role: 'media.artist',
exist: false,
},
trackartist: {
name: 'Trackartist',
read: true,
write: false,
type: 'string',
role: 'media.artist',
exist: false,
},
band: {
name: 'Band',
read: true,
write: false,
type: 'string',
role: 'media.artist',
exist: false,
},
url: {
name: 'Url',
read: true,
write: false,
type: 'string',
role: 'media.url',
exist: false,
},
remote_title: {
name: 'RadioName',
read: true,
write: false,
type: 'string',
role: 'value',
exist: false,
},
};
this.sbPlayerButtons = {
cmdPlayFavorite: {
name: 'cmdPlayFavorite',
read: true,
write: true,
type: 'string',
role: 'state',
exist: false,
def: ' ',
},
cmdPlayUrl: {
name: 'cmdPlayUrl',
read: true,
write: true,
type: 'string',
role: 'state',
exist: false,
def: ' ',
},
cmdGeneral: {
name: 'cmdGeneral',
read: true,
write: true,
type: 'string',
role: 'state',
exist: false,
def: ' ',
},
cmdGoTime: {
name: 'cmdGoTime',
read: true,
write: true,
type: 'string',
role: 'state',
exist: false,
def: ' ',
},
forward: {
name: 'btnForward',
read: true,
write: true,
type: 'boolean',
role: 'button.forward',
exist: false,
def: false,
},
preset_1: {
name: 'btnPreset_1',
read: true,
write: true,
type: 'boolean',
role: 'button',
exist: false,
def: false,
},
preset_2: {
name: 'btnPreset_2',
read: true,
write: true,
type: 'boolean',
role: 'button',
exist: false,
def: false,
},
preset_3: {
name: 'btnPreset_3',
read: true,
write: true,
type: 'boolean',
role: 'button',
exist: false,
def: false,
},
preset_4: {
name: 'btnPreset_4',
read: true,
write: true,
type: 'boolean',
role: 'button',
exist: false,
def: false,
},
preset_5: {
name: 'btnPreset_5',
read: true,
write: true,
type: 'boolean',
role: 'button',
exist: false,
def: false,
},
preset_6: {
name: 'btnPreset_6',
read: true,
write: true,
type: 'boolean',
role: 'button',
exist: false,
def: false,
},
rewind: {
name: 'btnRewind',
read: true,
write: true,
type: 'boolean',
role: 'button.reverse',
exist: false,
def: false,
},
};
this.sbPlayerStatusPlaylist = {
'playlist index': {
name: 'index',
read: true,
write: false,
type: 'number',
role: 'value',
exist: false,
},
id: {
name: 'id',
read: true,
write: false,
type: 'number',
role: 'value',
exist: false,
},
url: {
name: 'url',
read: true,
write: false,
type: 'string',
role: 'value',
exist: false,
},
title: {
name: 'title',
read: true,
write: false,
type: 'string',
role: 'value',
exist: false,
},
artwork_url: {
name: 'ArtworkUrl',
read: true,
write: false,
type: 'string',
role: 'media.cover',
exist: false,
},
coverid: {
name: 'ArtworkUrl',
read: true,
write: false,
type: 'string',
role: 'value',
exist: true,
},
type: {
name: 'Type',
read: true,
write: false,
type: 'string',
role: 'value',
exist: false,
},
bitrate: {
name: 'Bitrate',
read: true,
write: false,
type: 'string',
role: 'media.bitrate',
exist: false,
},
duration: {
name: 'Duration',
read: true,
write: false,
type: 'number',
role: 'media.duration',
exist: false,
},
artist: {
name: 'Artist',
read: true,
write: false,
type: 'string',
role: 'media.artist',
exist: false,
},
album: {
name: 'Album',
read: true,
write: false,
type: 'string',
role: 'media.album',
exist: false,
},
remote_title: {
name: 'RadioName',
read: true,
write: false,
type: 'string',
role: 'value',
exist: false,
},
};
/**
* Initializes the player with the provided player data.
*
* @param playerdata - Initial data related to the player.
* @async
* @returns - A promise that resolves after initializing the player settings and starting observation.
*
* This function performs the following tasks:
* - Sets the initial player status using the provided player data.
* - Logs the discovery of a new player using its name and ID.
* - Creates a folder and device object for the player in the server's state path.
* - Starts observing player status updates with a delay based on the player's index.
*/
this.init = async function (playerdata) {
this.doPlayerStatus(playerdata);
this.ioUtil.logdebug(`New Player found: ${this.playername} with id ${this.playerid}`);
await this.createFolder(this.statePath);
await this.createDevice(this.playername, this.statePath);
const delay = this.playerindex || 0;
setTimeout(this.doObserverPlayer.bind(this), delay * 190);
};
this.sanitizePlayername = playername => playername.replace(this.FORBIDDEN_CHARS, '_');
/**
* Starts observing player status updates.
*
* Logs the start of observing player updates, calls the function to request the player status,
* and schedules the next call to itself with a delay based on the configured player refresh interval.
*/
this.doObserverPlayer = function () {
this.ioUtil.logsilly('doObserverPlayer');
this.getPlayerUpdateStatus();
this.observers['player'] = setTimeout(this.doObserverPlayer.bind(this), this.adapter.config.playerrefresh);
};
/**
* Requests the player status, choosing between full and small status updates
* in an alternating pattern.
*
* @description
* Requests the player status using the "status" command, with a counter
* cycling between 0 and 9. When the counter is 0, the full player status
* is requested using the fullStatus parameter. Otherwise, the last player
* status is requested using the smallStatus parameter. The
* doPlayerUpdateStatus function is called with the result.
* Additionally, the alarms are requested using the "alarms" command with
* the filter "all". The doAlarmsUpdateStatus function is called with the
* result.
*/
this.getPlayerUpdateStatus = () => {
this.ioUtil.logsilly('getPlayerUpdate');
if (this.statuscounter == 0) {
this.request(this.playerid, ['status', '0', '999', this.fullStatus], this.doPlayerUpdateStatus.bind(this));
this.request(this.playerid, ['alarms', '0', '999', 'filter:all'], this.doAlarmsUpdateStatus.bind(this));
} else {
this.request(this.playerid, ['status', '-', '1', this.smallStatus], this.doPlayerUpdateStatus.bind(this));
}
this.statuscounter += 1;
if (this.statuscounter > 9) {
this.statuscounter = 0;
}
};
/**
* Updates the alarm status for the player.
*
* @description
* This function is responsible for updating the alarm status of the player
* by logging the operation, serializing the result of the alarms request
* into a JSON string, and setting the state of the alarms in the player's
* status using the converted state value. It utilizes the player's main
* status attributes and the corresponding state path.
* @param result - The result object from the alarms request,
* containing the alarm data to be processed and updated.
*/
this.doAlarmsUpdateStatus = function (result) {
this.ioUtil.logsilly('doAlarmsUpdateStatus');
const alarmdata = JSON.stringify(result.result);
this.setState(
this.sbPlayerStatusMain['alarms'].name,
this.convertState(this.sbPlayerStatusMain['alarms'], alarmdata),
this.statePath,
this.playername,
);
};
this.doPlayerUpdateStatus = result => {
this.ioUtil.logsilly('doPlayerUpdateStatus');
const fullStatus = result.params[1][3] == this.fullStatus;
let playerdata = result.result;
for (const key in this.sbPlayerStatusMain) {
let value = '';
if (key == 'state') {
continue;
}
if (key == 'playlist') {
continue;
}
if (key == 'playerid') {
continue;
}
if (key == 'alarms') {
continue;
}
if (Object.prototype.hasOwnProperty.call(playerdata, key)) {
value = playerdata[key];
if (key == 'name') {
this.playername = this.sanitizePlayername(value);
}
if (key == 'playerindex') {
this.playerindex = parseInt(value);
}
if (key == 'remote') {
this.remote = parseInt(value);
}
if (key == 'mode') {
if (value == 'play') {
this.setState('state', 1, this.statePath, this.playername);
}
if (value == 'pause') {
this.setState('state', 0, this.statePath, this.playername);
}
if (value == 'stop') {
this.setState('state', 2, this.statePath, this.playername);
}
}
} else {
//if (key == "remote") value = 0;
if (key == 'remote') {
this.remote = 0;
}
}
this.setState(
this.sbPlayerStatusMain[key].name,
this.convertState(this.sbPlayerStatusMain[key], value),
this.statePath,
this.playername,
);
}
this.setState(
this.sbPlayerStatusMain['playerid'].name,
this.convertState(this.sbPlayerStatusMain['playerid'], this.playerid),
this.statePath,
this.playername,
);
if (!result.result.playlist_loop) {
return;
}
//todo
playerdata = result.result.playlist_loop.find(
function (result, el) {
return el['playlist index'] == result.playlist_cur_index;
}.bind(this, result.result),
);
if (playerdata) {
for (const key in this.sbPlayerStatusLoop) {
let value = '';
if (Object.prototype.hasOwnProperty.call(playerdata, key)) {
value = playerdata[key];
if (key == 'coverid') {
if (value[0] != '-') {
value = this.createArtworkUrl(value);
} else {
continue;
}
}
if (key == 'artwork_url' && playerdata['coverid'] && playerdata['coverid'][0] != '-') {
continue;
}
if (key == 'artwork_url') {
const regex = /imageproxy\/(.*)\/[^/]/gm;
const m = regex.exec(value);
if (m && m.index > 0) {
value = decodeURIComponent(m[1]);
} else if (value === 'html/images/radio.png') {
value = `http://${this.adapter.config.server}:${
this.adapter.config.port
}/html/images/radio.png`;
} else if (value === 'html/images/cover.png') {
value = `http://${this.adapter.config.server}:${
this.adapter.config.port
}/html/images/cover.png`;
} else if (value === 'html/images/favorites.png') {
value = `http://${this.adapter.config.server}:${
this.adapter.config.port
}/html/images/favorites.png`;
}
}
this.setState(
this.sbPlayerStatusLoop[key].name,
this.convertState(this.sbPlayerStatusLoop[key], value),
this.statePath,
this.playername,
);
} else {
if (key == 'remote_title' && this.remote == 0) {
value = '';
}
if (fullStatus) {
this.setState(
this.sbPlayerStatusLoop[key].name,
this.convertState(this.sbPlayerStatusLoop[key], value),
this.statePath,
this.playername,
);
}
}
}
if (
playerdata['trackartist'] !== undefined &&
playerdata['trackartist'] !== null &&
playerdata['trackartist'] !== '' &&
(playerdata['artist'] === undefined || playerdata['artist'] === null || playerdata['artist'] === '')
) {
this.setState(
this.sbPlayerStatusLoop['artist'].name,
this.convertState(this.sbPlayerStatusLoop['artist'], playerdata['trackartist']),
this.statePath,
this.playername,
);
}
}
if (fullStatus) {
const playlist = result.result.playlist_loop;
const pla = [];
for (const playlistkey in playlist) {
const playlistitem = playlist[playlistkey];
const pli = {};
for (const key in this.sbPlayerStatusPlaylist) {
if (Object.prototype.hasOwnProperty.call(playlistitem, key)) {
let value = playlistitem[key];
if (key == 'coverid') {
if (value[0] != '-') {
value = this.createArtworkUrl(value);
} else {
continue;
}
}
if (key == 'artwork_url' && playlistitem['coverid'] && playlistitem['coverid'][0] != '-') {
continue;
}
if (key == 'artwork_url') {
const regex = /imageproxy\/(.*)\/[^/]/gm;
const m = regex.exec(value);
if (m && m.index > 0) {
value = decodeURIComponent(m[1]);
}
}
pli[this.sbPlayerStatusPlaylist[key].name] = value;
}
}
pla.push(pli);
}
if (this.adapter.config.useplaylist) {
this.setState(
this.sbPlayerStatusMain['playlist'].name,
this.convertState(this.sbPlayerStatusMain['playlist'], JSON.stringify(pla)),
this.statePath,
this.playername,
);
} else {
this.setState(
this.sbPlayerStatusMain['playlist'].name,
this.convertState(this.sbPlayerStatusMain['playlist'], ''),
this.statePath,
this.playername,
);
}
}
return;
};
this.doPlayerStatus = playerdata => {
this.ioUtil.logsilly('doPlayerStatus');
if (playerdata.playerid) {
this.playerid = playerdata.playerid;
}
if (playerdata.name) {
this.playername = this.sanitizePlayername(playerdata.name);
}
if (playerdata.playerindex) {
this.playerindex = playerdata.playerindex;
}
if (playerdata.connected) {
this.connected = playerdata.connected;
}
this.checkPlayerdataStates(playerdata);
};
this.checkPlayerdataStates = playerdata => {
this.ioUtil.logsilly('checkPlayerdataStates');
if (!playerdata) {
return;
}
if (playerdata.name) {
this.playername = this.sanitizePlayername(playerdata.name);
}
for (const key in this.sbPlayerStatusMain) {
const stateTemplate = this.sbPlayerStatusMain[key];
if (key == 'playlist' && !this.adapter.config.useplaylist) {
continue;
}
if (!stateTemplate.exist) {
this.sbPlayerStatusMain[key] = this.createObject(stateTemplate, this.statePath, this.playername);
}
}
for (const key in this.sbPlayerStatusLoop) {
//if (key == "albumartist") key = "artist";
const stateTemplate = this.sbPlayerStatusLoop[key];
if (!stateTemplate.exist) {
this.sbPlayerStatusLoop[key] = this.createObject(stateTemplate, this.statePath, this.playername);
}
}
for (const key in this.sbPlayerButtons) {
const stateTemplate = this.sbPlayerButtons[key];
if (!stateTemplate.exist) {
stateTemplate.create = true;
this.sbPlayerButtons[key] = this.createObject(stateTemplate, this.statePath, this.playername);
}
}
};
/************* ✨ Codeium Command ⭐ *************/
/**
* Handles state changes for player.
*
* @param idParts - The state identifier split into parts.
* @param state - The new state object; can be null if the state was deleted.
*
* The function logs the state change, shifts the idParts array and invokes the
* appropriate state change handler based on the first part of the id.
* It returns early if id, state, or state.ack is falsy.
*
* Supported state changes:
* - Volume: Sets the volume of the player.
* - Power: Sets the power state of the player.
* - state: Sets the state of the player (0 = off, 1 = play, 2 = stop)
* - PlaylistRepeat: Sets the repeat state of the playlist (0 = off, 1 = repeat all, 2 = repeat one)
* - PlaylistShuffle: Sets the shuffle state of the playlist (0 = off, 1 = shuffle, 2 = shuffle all)
* - cmdGoTime: Sets the current time of the player in seconds.
* - PlaylistCurrentIndex: Sets the current index of the playlist.
* - cmdPlayFavorite: Plays a favorite with the given item_id.
* - cmdPlayUrl: Plays a URL.
* - cmdGeneral: Executes a general command (e.g. a button press).
* - btnForward: Jump forward.
* - btnRewind: Jump backward.
* - btnPreset_X: Executes the preset X (e.g. preset_1.single).
*/
/****** 4364f116-3e7c-49b0-821a-4798e59f5e85 *******/
this.doStateChange = function (idParts, state) {
this.ioUtil.logsilly('doPlayerStateChange');
idParts.shift();
if (idParts[0] == 'Volume') {
if (state.val != null) {
this.request(this.playerid, ['mixer', 'volume', Math.round(state.val)]);
this.setState(idParts[0], Math.round(state.val), this.statePath, this.playername, false);
}
}
if (idParts[0] == 'Power') {
if (state.val == 0) {
this.request(this.playerid, ['power', 0]);
}
if (state.val == 1) {
this.request(this.playerid, ['power', 1]);
}
}
if (idParts[0] == 'state') {
if (state.val == 0) {
this.request(this.playerid, ['pause', '1']);
}
if (state.val == 1) {
this.request(this.playerid, ['play', '2']);
}
if (state.val == 2) {
this.request(this.playerid, ['stop']);
}
}
if (idParts[0] == 'PlaylistRepeat') {
if (state.val == 0) {
this.request(this.playerid, ['playlist', 'repeat', '0']);
}
if (state.val == 1) {
this.request(this.playerid, ['playlist', 'repeat', '1']);
}
if (state.val == 2) {
this.request(this.playerid, ['playlist', 'repeat', '2']);
}
}
if (idParts[0] == 'PlaylistShuffle') {
if (state.val == 0) {
this.request(this.playerid, ['playlist', 'shuffle', '0']);
}
if (state.val == 1) {
this.request(this.playerid, ['playlist', 'shuffle', '1']);
}
if (state.val == 2) {
this.request(this.playerid, ['playlist', 'shuffle', '2']);
}
}
if (idParts[0] == 'cmdGoTime') {
// state.val needs to be a string, although the time is set in seconds
// when settings cmdGoTime via ioBroker.simple-api its not possible to submit a number as string
// if we receive a number, we need to cast this to a string
if (typeof state.val === 'number') {
state.val = state.val.toString();
}
if (state.val.trim() !== '' && !isNaN(state.val.trim())) {
this.request(this.playerid, ['time', state.val.trim()]);
this.setState(idParts[0], ' ', this.statePath, this.playername, false);
}
}
if (idParts[0] == 'PlaylistCurrentIndex') {
if (state.val.trim() !== '' && !isNaN(state.val.trim())) {
this.request(this.playerid, ['playlist', 'index', state.val.trim()]);
}
}
if (idParts[0] == 'cmdPlayFavorite') {
if (state.val !== ' ') {
this.request(this.playerid, ['favorites', 'playlist', 'play', `item_id:${state.val}`]);
this.setState(idParts[0], ' ', this.statePath, this.playername, false);
}
}
if (idParts[0] == 'cmdPlayUrl') {
if (state.val !== ' ') {
this.request(this.playerid, ['playlist', 'play', state.val]);
this.setState(idParts[0], ' ', this.statePath, this.playername, false);
}
}
if (idParts[0] == 'cmdGeneral') {
if (state.val !== ' ') {
try {
const cmd = JSON.parse(`[${state.val}]`);
this.request(this.playerid, cmd);
} catch {
this.ioUtil.logerror(`cmdGeneral, Parameter error: ${state.val}`);
}
this.setState(idParts[0], ' ', this.statePath, this.playername, false);
}
}
if (idParts[0] == 'btnForward') {
if (state.val) {
this.request(this.playerid, ['button', 'jump_fwd']);
this.setState(idParts[0], false, this.statePath, this.playername, false);
}
}
if (idParts[0] == 'btnRewind') {
if (state.val) {
this.request(this.playerid, ['button', 'jump_rew']);
this.setState(idParts[0], false, this.statePath, this.playername, false);
}
}
if (idParts[0].startsWith('btnPreset_')) {
const name = `preset_${idParts[0].split('_')[1]}.single`;
if (state.val) {
this.request(this.playerid, ['button', name]);
this.setState(idParts[0], false, this.statePath, this.playername, false);
}
}
};
/**
* Returns a URL for a given artworkid.
*
* @param artworkid The artworkid
* @returns The URL for the artwork
*/
this.createArtworkUrl = function (artworkid) {
return `http://${this.adapter.config.server}:${this.adapter.config.port}/music/${artworkid}/cover.jpg`;
};
this.disconnect = () => {
/**
* Disconnects the player from the server.
*
* Logs the disconnection event, removes the player observer and clears the
* power state, and sets the connected flag to 0.
*/
this.ioUtil.logdebug(`Player ${this.playername} disconnected with id ${this.playerid}`);
clearInterval(this.observers['player']);
this.setState('Power', 0, this.statePath, this.playername, false);
delete this.observers['player'];
this.connected = 0;
};
/**
* Connects the player to the server.
*
* Logs the connection event, starts the player observer and sets the
* connected flag to 1.
*/
this.connect = () => {
this.ioUtil.logdebug(`Player ${this.playername} connected with id ${this.playerid}`);
this.doObserverPlayer();
this.connected = 1;
};
/**
* Creates a state object for the player if it does not already exist.
*
* @param stateTemplate - The template containing the common properties of the state.
* @param [level1path] - The first level path to prepend to the state name, can be false or empty.
* @param [level2path] - The second level path to prepend to the state name, can be false or empty.
* @returns The state template with an 'exist' property set to true.
*/
this.createState = function (stateTemplate, level1path = false, level2path = false) {
const name = (level1path ? `${level1path}.` : '') + (level2path ? `${level2path}.` : '') + stateTemplate.name;
this.ioUtil.logsilly(`Create state ${name}`);
if (!this.currentStates[name]) {
this.adapter.createState(level1path, level2path, stateTemplate.name, stateTemplate);
}
stateTemplate.exist = true;
return stateTemplate;
};
/**
* Creates a state object in the ioBroker system if it does not already exist.
*
* @param stateTemplate - The template containing the common properties of the state.
* @param level1path - The first level path to prepend to the state name, can be null or empty.
* @param level2path - The second level path to prepend to the state name, can be null or empty.
* @param [callback] - Optional callback function, called when the object is created.
* @returns The state template with an 'exist' property set to true.
*/
this.createObject = function (stateTemplate, level1path, level2path, callback) {
this.ioUtil.logdebug(`createObject ${stateTemplate.name}`);
const name = (level1path ? `${level1path}.` : '') + (level2path ? `${level2path}.` : '') + stateTemplate.name;
this.adapter.getObject(name, (err, obj) => {
const newobj = {
type: 'state',
common: stateTemplate,
native: {},
};
if (!obj) {
callback ? this.adapter.setObject(name, newobj, callback) : this.adapter.setObject(name, newobj);
} else {
if (callback) {
callback();
}
}
});
stateTemplate.exist = true;
return stateTemplate;
};
/**
* Makes a request to the Logitech Media Server.
*
* @param playerid - The player ID to request from, or an empty string for the server.
* @param params - The parameters to pass to the request.
* @param [callback] - The function to call with the result of the request.
*
* If the request is successful, calls the callback function with the result.
* If the request fails, logs an error and does not call the callback function.
*/
this.request = function (playerid, params, callback) {
if (this.connected) {
this.server.request(playerid, params, callback);
}
};
/**
* Sets the TPE2 handling value for the player.
*
* @param value - The value to set for TPE2 handling.
*
* This function updates the TPE2Handling property of the player to
* the provided value, which determines how the TPE2 tag is used
* in handling album artist metadata.
*/
this.setTPE2Handling = function (value) {
this.TPE2Handling = value;
};
this.setState = function (name, value, level1path, level2path, check = true) {
name = (level1path ? `${level1path}.` : '') + (level2path ? `${level2path}.` : '') + name;
if (this.currentStates[name] !== value && check) {
this.currentStates[name] = value;
if (!name.includes('Time')) {
this.ioUtil.logsilly(`setState name: ${name} value: ${value}`);
}
this.adapter.setState(name, value, true);
}
if (!check) {
this.currentStates[name] = value;
this.ioUtil.logsilly(`setState name: ${name} value: ${value}`);
this.adapter.setState(name, value, true);
}
};
this.createFolder = async function (foldername, level1path, level2path) {
const id = (level1path ? `${level1path}.` : '') + (level2path ? `${level2path}.` : '') + foldername;
this.ioUtil.logsilly(`createFolder ${id}`);
if (await this.existsObjectAsync(id)) {
this.ioUtil.logsilly(`Folder exists: ${id}`);
} else {
const obj = {
type: 'folder',
common: {
name: foldername,
},
native: {},
};
this.adapter.setObject(id, obj);
}
};
this.createDevice = async function (devicename, level1path, level2path) {
const id = (level1path ? `${level1path}.` : '') + (level2path ? `${level2path}.` : '') + devicename;
this.ioUtil.logsilly(`createDevice ${id}`);
if (await this.existsObjectAsync(id)) {
this.ioUtil.logsilly(`Device exists: ${id}`);
} else {
const obj = {
type: 'device',
common: {
name: devicename,
},
native: {},
};
this.adapter.setObject(id, obj);
}
};
this.existsObjectAsync = function (id) {
return new Promise((resolve, reject) => {
id = `${this.adapter.namespace}.${id}`;
this.adapter.getForeignObject(id, (err, obj) => {
if (err) {
reject(err);
} else {
resolve(!!obj);
}
});
});
};
this.convertState = function (stateTemplate, value) {
if (stateTemplate.type == 'string') {
return String(value);
}
if (stateTemplate.type == 'number') {
return Number(value);
}
this.ioUtil.logdebug(`Missing conversion function for type ${stateTemplate.type} Please report.`);
return value;
};
this.logsilly = s => {
if (this.islogsilly) {
this.adapter.log.silly(s);
}
};
this.logdebug = s => {
if (this.islogdebug) {
this.adapter.log.debug(s);
}
};
this.logerror = function (s) {
this.adapter.log.error(s);
};
this.loginfo = s => {
this.adapter.log.info(s);
};
this.init(playerdata);
}
module.exports = ioSBPlayer;