"use strict";
// SPDX-License-Identifier: GPL-3.0-or-later
// myMPD (c) 2018-2024 Juergen Mang <mail@jcgames.de>
// https://github.com/jcorporation/mympd
/** @module state_js */
/**
* Clears the mpd error
* @returns {void}
*/
function clearMPDerror() {
sendAPI('MYMPD_API_PLAYER_CLEARERROR',{}, function() {
getState();
}, false);
}
/**
* Clears the jukebox error
* @returns {void}
*/
function clearJukeboxError() {
sendAPI('MYMPD_API_JUKEBOX_CLEARERROR',{}, function() {
getState();
}, false);
}
/**
* Creates the elapsed / duration counter text
* @returns {string} song counter text
*/
function getCounterText() {
return fmtSongDuration(currentState.elapsedTime) +
( currentState.totalTime > 0
? smallSpace + '/' + smallSpace + fmtSongDuration(currentState.totalTime)
: ''
);
}
/**
* Sets the song counters
* @returns {void}
*/
function setCounter() {
//progressbar in footer
//calc percent with two decimals after comma
const prct = currentState.totalTime > 0
? Math.ceil((100 / currentState.totalTime) * currentState.elapsedTime * 100) / 100
: 0;
domCache.progressBar.style.width = `${prct}vw`;
domCache.progress.style.cursor = currentState.totalTime <= 0 ? 'default' : 'pointer';
//counter
const counterText = getCounterText();
//counter in footer
domCache.counter.textContent = counterText;
//update queue card
const playingRow = elGetById('queueSongId' + currentState.currentSongId);
if (playingRow !== null) {
//progressbar and counter in queue card
if (currentState.state === 'stop') {
resetDuration(playingRow);
}
else {
setQueueCounter(playingRow, counterText);
}
}
//synced lyrics
if (showSyncedLyrics === true &&
settings.viewPlayback.fields.includes('Lyrics'))
{
const sl = elGetById('currentLyrics');
const toHighlight = sl.querySelector('[data-sec="' + currentState.elapsedTime + '"]');
const highlighted = sl.querySelector('.highlight');
if (highlighted !== toHighlight &&
toHighlight !== null)
{
toHighlight.classList.add('highlight');
if (scrollSyncedLyrics === true) {
toHighlight.scrollIntoView({behavior: "smooth"});
}
if (highlighted !== null) {
highlighted.classList.remove('highlight');
}
}
}
if (progressTimer) {
clearTimeout(progressTimer);
}
if (currentState.state === 'play') {
if (currentState.totalTime > 0 &&
currentState.totalTime < currentState.elapsedTime)
{
// this should not appear, update state
getState();
return;
}
progressTimer = setTimeout(function() {
currentState.elapsedTime += 1;
setCounter();
}, 1000);
}
}
/**
* Gets the player state
* @returns {void}
*/
function getState() {
sendAPI("MYMPD_API_PLAYER_STATE", {}, parseState, false);
}
/**
* Parses the MYMPD_API_PLAYER_STATE jsonrpc response
* @param {object} obj jsonrpc response
* @returns {void}
*/
function parseState(obj) {
if (obj.result === undefined) {
logDebug('State is undefined');
return;
}
//Get current song if songid or queueVersion has changed
//On stream updates only the queue version will change
if (currentState.currentSongId !== obj.result.currentSongId ||
currentState.queueVersion !== obj.result.queueVersion)
{
sendAPI("MYMPD_API_PLAYER_CURRENT_SONG", {}, parseCurrentSong, false);
}
//save state
currentState = obj.result;
// set state of playback controls
updatePlaybackControls();
// update playing row in current queue view
if (app.id === 'QueueCurrent') {
setPlayingRow();
}
//media session
mediaSessionSetState();
mediaSessionSetPositionState(obj.result.totalTime, obj.result.elapsedTime);
//local playback
controlLocalPlayback(currentState.state);
//queue badge in navbar
const badgeQueueItemsEl = elGetById('badgeQueueItems');
if (badgeQueueItemsEl) {
badgeQueueItemsEl.textContent = obj.result.queueLength;
}
//Set volume
parseVolume(obj);
//Set play counters
setCounter();
//clear playback card if no current song
if (obj.result.songPos === -1) {
document.title = 'myMPD';
const playbackTitleEl = elGetById('PlaybackTitle');
const footerTitleEl = elGetById('footerTitle');
playbackTitleEl.textContent = tn('Not playing');
footerTitleEl.textContent = tn('Not playing');
footerTitleEl.removeAttribute('title');
footerTitleEl.classList.remove('clickable');
playbackTitleEl.classList.remove('clickable');
elGetById('footerCover').classList.remove('clickable');
elGetById('PlaybackCover').classList.remove('clickable');
clearCurrentCover();
const pb = document.querySelectorAll('#PlaybackListTags p');
for (let i = 0, j = pb.length; i < j; i++) {
elClear(pb[i]);
}
elClearId('footerAudioFormat');
}
else {
const cff = elGetById('currentAudioFormat');
if (cff) {
elReplaceChild(
cff.querySelector('p'),
printValue('AudioFormat', obj.result.AudioFormat)
);
}
elReplaceChildId('footerAudioFormat', printValue('AudioFormat', obj.result.AudioFormat));
}
//handle error from mpd status response
toggleAlert('alertMpdStatusError', (obj.result.lastError === '' ? false : true), obj.result.lastError);
//handle jukebox error status
toggleAlert('alertJukeboxStatusError', (obj.result.lastJukeboxError === '' ? false : true), obj.result.lastJukeboxError);
//handle mpd update status
toggleAlert('alertUpdateDBState', (obj.result.updateState === 0 ? false : true), tn('Updating MPD database'));
//handle myMPD cache update status
toggleAlert('alertUpdateCacheState', obj.result.updateCacheState, tn('Updating caches'));
//check if we need to refresh the settings
if (localSettings.partition !== obj.result.partition || /* partition has changed */
settings.partition.mpdConnected === false || /* mpd is not connected */
uiEnabled === false) /* ui is disabled at startup */
{
if (elGetById('modalSettings').classList.contains('show') ||
elGetById('modalConnection').classList.contains('show') ||
elGetById('modalPlayback').classList.contains('show'))
{
//do not refresh settings, if a settings modal is open
return;
}
logDebug('Refreshing settings');
getSettings(parseSettings);
}
}
/**
* Sets the state of the playback control buttons
* @returns {void}
*/
function updatePlaybackControls() {
const prefixes = ['footer'];
if (document.querySelector('.playbackPopoverBtns') !== null) {
prefixes.push('popoverFooter');
if (currentState.songPos < 0 ||
currentState.totalTime === 0 ||
currentState.state === 'stop')
{
elDisableId('popoverFooterGotoBtn');
}
else {
elEnableId('popoverFooterGotoBtn');
}
}
for (const prefix of prefixes) {
if (currentState.state === 'stop') {
elGetById(prefix + 'PlayBtn').textContent = 'play_arrow';
domCache.progressBar.style.width = '0';
elDisableId(prefix + 'StopBtn');
elDisableId(prefix + 'NextBtn');
elDisableId(prefix + 'PrevBtn');
}
else if (currentState.state === 'play') {
elGetById(prefix + 'PlayBtn').textContent =
settings.webuiSettings.footerPlaybackControls === 'stop'
? 'stop'
: 'pause';
elEnableId(prefix + 'StopBtn');
elEnableId(prefix + 'NextBtn');
elEnableId(prefix + 'PrevBtn');
}
else {
// pause
elGetById(prefix + 'PlayBtn').textContent = 'play_arrow';
elEnableId(prefix + 'StopBtn');
elEnableId(prefix + 'NextBtn');
elEnableId(prefix + 'PrevBtn');
}
if (currentState.songPos < 0 ||
currentState.totalTime === 0 ||
currentState.state === 'stop')
{
elDisableId(prefix + 'FastRewindBtn');
elDisableId(prefix + 'FastForwardBtn');
}
else {
// enable seeking only if totalTime is known
elEnableId(prefix + 'FastRewindBtn');
elEnableId(prefix + 'FastForwardBtn');
}
if (currentState.queueLength === 0) {
// no song in queue
elDisableId(prefix + 'PlayBtn');
}
else {
elEnableId(prefix + 'PlayBtn');
}
if (currentState.nextSongPos === -1 &&
settings.partition.jukeboxMode === 'off')
{
// last song in queue and disabled jukebox
elDisableId(prefix + 'NextBtn');
}
else if (currentState.state !== 'stop') {
// next button triggers jukebox
elEnableId(prefix + 'NextBtn');
}
if (currentState.songPos < 0) {
// no current song
elDisableId(prefix + 'PrevBtn');
}
else if (currentState.state !== 'stop') {
elEnableId(prefix + 'PrevBtn');
}
}
}
/**
* Sets the background image
* @param {HTMLElement} el element for the background image
* @param {string} url background image url
* @returns {void}
*/
function setBackgroundImage(el, url) {
if (url === undefined) {
clearBackgroundImage(el);
return;
}
const bgImageUrl = subdir + '/albumart?offset=0&uri=' + myEncodeURIComponent(url);
const old = el.parentNode.querySelectorAll(el.tagName + '> div.albumartbg');
//do not update if url is the same
if (old[0] &&
getData(old[0], 'uri') === url)
{
logDebug('Background image already set for: ' + el.tagName);
return;
}
//remove old covers that are already hidden and
//update z-index of current displayed cover
for (let i = 0, j = old.length; i < j; i++) {
if (old[i].style.zIndex === '-10') {
old[i].remove();
}
else {
old[i].style.zIndex = '-10';
}
}
//add new cover and let it fade in
const div = elCreateEmpty('div', {"class": ["albumartbg"]});
if (el.tagName === 'BODY') {
div.style.filter = settings.webuiSettings.bgCssFilter;
}
div.style.backgroundImage = 'url("' + bgImageUrl + '")';
div.style.opacity = 0;
setData(div, 'uri', url);
el.insertBefore(div, el.firstChild);
//create dummy img element for preloading and fade-in
const img = new Image();
// add reference to image container
setData(img, 'div', div);
img.onload = function(event) {
//fade-in current cover
getData(event.target, 'div').style.opacity = 1;
//fade-out old cover with some overlap
setTimeout(function() {
const bgImages = el.parentNode.querySelectorAll(el.tagName + '> div.albumartbg');
for (let i = 0, j = bgImages.length; i < j; i++) {
if (bgImages[i].style.zIndex === '-10') {
bgImages[i].style.opacity = 0;
}
}
}, 800);
};
img.src = bgImageUrl;
}
/**
* Clears the background image
* @param {HTMLElement} el element for the background image
* @returns {void}
*/
function clearBackgroundImage(el) {
const old = el.parentNode.querySelectorAll(el.tagName + '> div.albumartbg');
for (let i = 0, j = old.length; i < j; i++) {
if (old[i].style.zIndex === '-10') {
old[i].remove();
}
else {
old[i].style.zIndex = '-10';
old[i].style.opacity = '0';
}
}
}
/**
* Sets the current cover in playback view and footer
* @param {string} url song uri
* @returns {void}
*/
function setCurrentCover(url) {
setBackgroundImage(elGetById('PlaybackCover'), url);
setBackgroundImage(elGetById('footerCover'), url);
if (settings.webuiSettings.bgCover === true) {
setBackgroundImage(domCache.body, url);
}
}
/**
* Clears the current cover in playback view and footer
* @returns {void}
*/
function clearCurrentCover() {
clearBackgroundImage(elGetById('PlaybackCover'));
clearBackgroundImage(elGetById('footerCover'));
if (settings.webuiSettings.bgCover === true) {
clearBackgroundImage(domCache.body);
}
}
/**
* Sets the elapsed time for the media session api
* @param {number} duration song duration
* @param {number} position elapsed time
* @returns {void}
*/
function mediaSessionSetPositionState(duration, position) {
if (features.featMediaSession === false ||
navigator.mediaSession.setPositionState === undefined)
{
return;
}
if (position < duration) {
//streams have position > duration
navigator.mediaSession.setPositionState({
duration: duration,
position: position
});
}
}
/**
* Sets the state for the media session api
* @returns {void}
*/
function mediaSessionSetState() {
if (features.featMediaSession === false) {
return;
}
const state = currentState.state === 'play'
? 'playing'
: 'paused';
logDebug('Set mediaSession.playbackState to ' + state);
navigator.mediaSession.playbackState = state;
}
/**
* Sets metadata for the media session api
* @param {string} title song title
* @param {object} artist array of artists
* @param {string} album album name
* @param {string} url song uri
* @returns {void}
*/
function mediaSessionSetMetadata(title, artist, album, url) {
if (features.featMediaSession === false) {
return;
}
const artwork = getMyMPDuri() + '/albumart-thumb?offset=0&uri=' + myEncodeURIComponent(url);
navigator.mediaSession.metadata = new MediaMetadata({
title: title,
artist: joinArray(artist),
album: album,
artwork: [{src: artwork}]
});
}