Source: modalSongDetails.js

"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 modalSongDetails_js */

/**
 * Initialization function for song elements
 * @returns {void}
 */
function initModalSongDetails() {
    elGetById('modalSongDetailsTagsList').addEventListener('click', function(event) {
        if (event.target.nodeName === 'A') {
            if (event.target.id === 'calcFingerprint') {
                sendAPI("MYMPD_API_SONG_FINGERPRINT", {
                    "uri": getData(event.target, 'uri')
                }, parseFingerprint, true);
                event.preventDefault();
                const spinner = elCreateEmpty('div', {"class": ["spinner-border", "spinner-border-sm"]});
                elHide(event.target);
                event.target.parentNode.appendChild(spinner);
            }
            else if (event.target.classList.contains('external') === true) {
                 //do nothing, link opens in new browser window
            }
            else if (getData(event.target.parentNode, 'tag') !== undefined) {
                uiElements.modalSongDetails.hide();
                event.preventDefault();
                gotoBrowse(event);
            }
        }
        else if (event.target.nodeName === 'BUTTON') {
            switch(event.target.id) {
                case 'gotoContainingFolder': {
                    uiElements.modalSongDetails.hide();
                    event.preventDefault();
                    appGoto('Browse', 'Filesystem', undefined, 0, undefined, getData(event.target, 'folder'), {'tag': '', 'desc': false}, '', '', 0);
                    break;
                }
                case 'downloadSong': {
                    const href = getData(event.target, 'href');
                    window.open(href, '_blank');
                    break;
                }
                default: {
                    //song vote buttons
                    const cmd = getData(event.target.parentNode, 'href');
                    if (cmd !== undefined) {
                        parseCmd(event, cmd);
                    }
                }
            }
        }
    }, false);
}

/**
 * Shows the song details modal
 * @param {string} uri song uri
 * @returns {void}
 */
function songDetails(uri) {
    setUpdateViewId('modalSongDetailsTagsList');
    sendAPI("MYMPD_API_SONG_DETAILS", {
        "uri": uri
    }, parseSongDetails, true);
    uiElements.modalSongDetails.show();
}

/**
 * Parses the MYMPD_API_SONG_FINGERPRINT jsonrpc response
 * @param {object} obj jsonrpc response
 * @returns {void}
 */
function parseFingerprint(obj) {
    if (obj.error) {
        elReplaceChildId('fingerprint',
            elCreateTextTn('div', {"class": ["alert", "alert-danger"]}, obj.error.message, obj.error.data)
        );
        return;
    }
    const textarea = elCreateEmpty('textarea', {"class": ["form-control", "font-monospace", "breakAll"], "rows": 5});
    textarea.value = obj.result.fingerprint;
    elReplaceChildId('fingerprint', textarea);
}

/**
 * Adds a row to the song details modal
 * @param {string} thContent text for th
 * @param {HTMLElement | Node | string} tdContent content element for td
 * @returns {HTMLElement} created row
 */
function songDetailsRow(thContent, tdContent) {
    const td = elCreateEmpty('td', {});
    if (typeof tdContent === 'object') {
        td.appendChild(tdContent);
    }
    else {
        td.textContent = tdContent;
    }
    const tr = elCreateNodes('tr', {}, [
        elCreateTextTn('th', {}, thContent),
        td
    ]);
    return tr;
}

/**
 * Parses the MYMPD_API_SONG_DETAILS jsonrpc response
 * @param {object} obj jsonrpc response
 * @returns {void}
 */
function parseSongDetails(obj) {
    const modal = elGetById('modalSongDetails');
    setData(modal, 'uri', obj.result.uri);
    modal.querySelector('.album-cover').style.backgroundImage = getCssImageUri('/albumart?offset=0&uri=' + myEncodeURIComponent(obj.result.uri));

    const elH1s = modal.querySelectorAll('h1');
    for (let i = 0, j = elH1s.length; i < j; i++) {
        elH1s[i].textContent = obj.result.Title;
    }
    const table = elGetById('modalSongDetailsTagsList');
    const tbody = table.querySelector('tbody');
    elClear(tbody);
    if (checkResult(obj, table, 'table') === false) {
        return;
    }
    for (let i = 0, j = settings.tagList.length; i < j; i++) {
        if (settings.tagList[i] === 'Title' ||
            isEmptyTag(obj.result[settings.tagList[i]]) === true)
        {
            continue;
        }
        const tr = elCreateEmpty('tr', {});
        tr.appendChild(
            elCreateTextTn('th', {}, settings.tagList[i])
        );
        const td = elCreateEmpty('td', {});
        setData(td, 'tag', settings.tagList[i]);
        setData(td, 'name', obj.result[settings.tagList[i]]);
        if (settings.tagList[i] === 'Album') {
            setData(td, 'AlbumId', obj.result.AlbumId);
        }
        if (settings.tagListBrowse.includes(settings.tagList[i]) &&
            isEmptyTag(obj.result[settings.tagList[i]]) === false)
        {
            if (typeof obj.result[settings.tagList[i]] === 'string') {
                td.appendChild(
                    elCreateText('a', {"class": ["text-success"], "href": "#"}, obj.result[settings.tagList[i]])
                );
            }
            else {
                td.appendChild(
                    elCreateText('a', {"class": ["text-success"], "href": "#"}, obj.result[settings.tagList[i]].join(', '))
                );
            }
        }
        else if (settings.tagList[i].indexOf('MUSICBRAINZ') === 0) {
            td.appendChild(
                printValue(settings.tagList[i], obj.result[settings.tagList[i]])
            );
        }
        else {
            td.textContent = obj.result[settings.tagList[i]];
        }
        tr.appendChild(td);
        tbody.appendChild(tr);
    }
    tbody.appendChild(songDetailsRow('Duration', fmtDuration(obj.result.Duration)));
    //resolves cuesheet virtual tracks
    const rUri = cuesheetUri(obj.result.uri);
    let isCuesheet = false;
    if (rUri !== obj.result.uri) {
        isCuesheet = true;
    }

    const shortName = basename(rUri, false) + (isCuesheet === true ? ' (' + cuesheetTrack(obj.result.uri) + ')' : '');
    const openFolderBtn = elCreateText('button', {"id": "gotoContainingFolder", "class": ["btn", "btn-secondary", "mi"],
        "data-title-phrase": "Open folder"}, 'folder_open');
    setData(openFolderBtn, 'folder', dirname(obj.result.uri));
    let downloadBtn = null;
    if (features.featLibrary === true) {
        downloadBtn = elCreateText('button', {"id": "downloadSong","class": ["btn", "btn-secondary", "mi"],
            "data-title-phrase": "Download"}, 'file_download');
        setData(downloadBtn, 'href', myEncodeURI(subdir + '/browse/music/' + rUri));
    }
    tbody.appendChild(
        songDetailsRow('Filename',
            elCreateNodes('div', {}, [
                elCreateText('p', {"class": ["text-break", "mb-1"], "title": rUri}, shortName),
                elCreateNodes('div', {"class": ["input-group", "mb-1"]}, [
                    elCreateEmpty('input', {"class": ["form-control"], "value": rUri}),
                    openFolderBtn,
                    downloadBtn
                ])
            ])
        )
    );

    tbody.appendChild(songDetailsRow('AudioFormat', printValue('AudioFormat', obj.result.AudioFormat)));
    tbody.appendChild(songDetailsRow('Filetype', filetype(rUri, true)));
    tbody.appendChild(songDetailsRow('Last-Modified', printValue('Last-Modified', obj.result['Last-Modified'])));
    if (features.featDbAdded === true) {
        tbody.appendChild(songDetailsRow('Added', printValue('Added', obj.result.Added)));
    }
    //fingerprint command is not supported for cuesheet virtual tracks
    if (features.featFingerprint === true &&
        isCuesheet === false)
    {
        const a = elCreateTextTn('a', {"class": ["text-success"], "id": "calcFingerprint", "href": "#"}, 'Calculate');
        setData(a, 'uri', obj.result.uri);
        tbody.appendChild(songDetailsRow('Fingerprint', a));
        tbody.lastChild.lastChild.setAttribute('id', 'fingerprint');
    }
    if (obj.result.bookletPath !== '') {
        tbody.appendChild(
            songDetailsRow('Booklet',
                elCreateTextTn('a', {"class": ["text-success"],
                    "href": myEncodeURI(subdir + obj.result.bookletPath), "target": "_blank"}, 'Download')
            )
        );
    }
    if (obj.result.infoTxtPath !== '') {
        const infoTxtEl = elCreateTextTn('span', {"class": ["text-success", "clickable"]}, 'Show');
        setData(infoTxtEl, 'uri', obj.result.infoTxtPath);
        infoTxtEl.addEventListener('click', function(event) {
            showInfoTxt(event.target);
        }, false);
        tbody.appendChild(
            songDetailsRow('Album info', infoTxtEl)
        );
    }
    if (features.featStickers === true) {
        tbody.appendChild(
            elCreateNode('tr', {},
                elCreateNode('th', {"colspan": "2", "class": ["pt-3"]},
                    elCreateTextTn('h5', {}, 'Statistics')
                )
            )
        );
        for (const sticker of stickerListSongs) {
            if (sticker === 'like' ||
                sticker === 'rating')
            {
                if (sticker === 'like' &&
                    features.featLike === true)
                {
                    const grp = createLike(obj.result.like, "song");
                    setData(grp, 'href', {"cmd": "voteLike", "options": ["target"]});
                    setData(grp, 'uri', obj.result.uri);
                    tbody.appendChild(
                        elCreateNodes('tr', {}, [
                            elCreateTextTn('th', {}, 'Like'),
                            elCreateNode('td', {}, grp)
                        ])
                    );
                }
                else if (sticker === 'rating' &&
                    features.featRating === true)
                {
                    const grp = createStarRating(obj.result.rating, "song");
                    setData(grp, 'href', {"cmd": "voteRating", "options": ["target"]});
                    setData(grp, 'uri', obj.result.uri);
                    tbody.appendChild(
                        elCreateNodes('tr', {}, [
                            elCreateTextTn('th', {}, 'Stars'),
                            elCreateNode('td', {}, grp)
                        ])
                    );
                }
            }
            else if (sticker === 'elapsed') {
                const div = elCreateNodes('div', {'class': ['row']}, [
                    elCreateEmpty('div', {'class': ['col', 'colMaxContent']}),
                    elCreateNode('div', {'class': ['col']}, printValue(sticker, obj.result[sticker], obj.result))
                ]);
                if (obj.result[sticker] > 0 &&
                    obj.result[sticker] < obj.result.Duration)
                {
                    const resumeBtn = pEl.resumeBtn.cloneNode(true);
                    resumeBtn.classList.add("me-3");
                    div.firstElementChild.appendChild(resumeBtn);
                    setData(resumeBtn, 'uri', obj.result.uri);
                    new BSN.Dropdown(resumeBtn.firstElementChild);
                    resumeBtn.lastElementChild.firstElementChild.addEventListener('click', function(event) {
                        clickResumeSong(event);
                    }, false);
                }
                tbody.appendChild(
                    songDetailsRow(sticker, div)
                );
            }
            else {
                tbody.appendChild(
                    songDetailsRow(sticker, printValue(sticker, obj.result[sticker]))
                );
            }
        }
        tbody.appendChild(
            elCreateNode('tr', {},
                elCreateText('th', {"colspan": "2", "class": ["pt-3"]}, 'Sticker')
            )
        );
        let i = 0;
        for (const key in obj.result.sticker) {
            tbody.appendChild(
                songDetailsRow(key, obj.result.sticker[key])
            );
            i++;
        }
        if (i === 0) {
            tbody.appendChild(
                elCreateNode('tr', {},
                    elCreateTextTn('td', {"colspan": "2",}, 'No user defined stickers')
                )
            );
        }
    }
    unsetUpdateViewId('modalSongDetailsTagsList');
    //populate other tabs
    if (features.featLyrics === true) {
        elClearId('modalSongDetailsTabLyricsText');
        getLyrics(obj.result.uri, elGetById('modalSongDetailsTabLyricsText'));
    }
    getComments(obj.result.uri);
    const imgEl = elGetById('modalSongDetailsTabPics');
    createImgCarousel(imgEl, 'modalSongDetailsPicsCarousel', obj.result.uri, obj.result.images, obj.result.embeddedImageCount);
}

/**
 * Gets the song comments
 * @param {string} uri song uri
 * @returns {void}
 */
function getComments(uri) {
    setUpdateViewId('modalSongDetailsCommentsList');
    sendAPI("MYMPD_API_SONG_COMMENTS", {
        "uri": uri
    }, parseComments, true);
}

/**
 * Gets the song comments
 * @param {object} obj jsonrpc response
 * @returns {void}
 */
function parseComments(obj) {
    const table = elGetById('modalSongDetailsCommentsList');
    const tbody = table.querySelector('tbody');
    elClear(tbody);
    if (checkResult(obj, table, 'table') === false) {
        return;
    }
    for (const key in obj.result.data) {
        tbody.appendChild(
            elCreateNodes('tr', {}, [
                elCreateText('td', {}, key),
                elCreateText('td', {}, obj.result.data[key])
            ])
        );
    }
    unsetUpdateView(table);
}

/**
 * Adds the song in current song detail modal to the queue
 * @param {string} action one of appendQueue, appendPlayQueue,
 *                               insertAfterCurrentQueue, replaceQueue,
 *                               replacePlayQueue, addToHome
 * @returns {void}
 */
//eslint-disable-next-line no-unused-vars
function songDetailAddTo(action) {
    const uri = getDataId('modalSongDetails', 'uri');
    const type = 'song';
    switch(action) {
        case 'appendQueue':
            appendQueue(type, [uri]);
            break;
        case 'appendPlayQueue':
            appendPlayQueue(type, [uri]);
            break;
        case 'insertAfterCurrentQueue':
            insertAfterCurrentQueue(type, [uri], null);
            break;
        case 'replaceQueue':
            replaceQueue(type, [uri]);
            break;
        case 'replacePlayQueue':
            replacePlayQueue(type, [uri]);
            break;
        case 'addToHome':
            addPlistToHome(uri, type, uri);
            break;
        default:
            logError('Invalid action: ' + action);
    }
}

/**
 * Shows the sticker edit dialog from song details modal
 * @returns {void}
 */
//eslint-disable-next-line no-unused-vars
function songDetailStickerEdit() {
    const uri = getDataId('modalSongDetails', 'uri');
    showStickerModal(uri, 'song');
}