Source: api.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 api_js */

/**
 * This messages are hidden from notifications.
 */
/** @type {object} */
const ignoreMessages = [
    'ok',
    'No current song',
    'No lyrics found'
];

/**
 * Sends a JSON-RPC API request to the selected partition and handles the response.
 * @param {string} method jsonrpc api method
 * @param {object} params jsonrpc parameters
 * @param {Function} callback callback function
 * @param {boolean} onerror true = execute callback also on error
 * @returns {void}
 */
function sendAPI(method, params, callback, onerror) {
    sendAPIpartition(localSettings.partition, method, params, callback, onerror);
}

/**
 * Updates the jsonrpc error object
 * @param {number} id jsonrpc id
 * @param {string} method myMPD api method
 * @param {string} error the error message
 * @returns {void}
 */
function setJsonRpcError(id, method, error) {
    jsonRpcError.id = id;
    jsonRpcError.error.method = method;
    jsonRpcError.error.message = error;
}

/**
 * Sends a JSON-RPC API request and handles the response.
 * @param {string} partition partition endpoint
 * @param {string} method jsonrpc api method
 * @param {object} params jsonrpc parameters
 * @param {Function} callback callback function
 * @param {boolean} onerror true = execute callback also on error
 * @returns {Promise<void>}
 */
async function sendAPIpartition(partition, method, params, callback, onerror) {
    if (APImethods[method] === undefined) {
        logError('Method "' + method + '" is not defined');
    }
    if (settings.pin === true &&
        session.token === '' &&
        session.timeout < getTimestamp() &&
        APImethods[method].protected === true)
    {
        logDebug('Request must be authorized but we have no session');
        enterPin(method, params, callback, onerror);
        return;
    }

    logDebug('Send API request: ' + method);
    const uri = subdir + '/api/' + partition;
    const headers = {'Content-Type': 'application/json'};
    if (session.token !== '') {
        headers['X-myMPD-Session'] = session.token;
    }
    // generate uniq id for this request
    const id = generateJsonrpcId();
    // fetch response
    let response = null;
    try {
        response = await fetch(uri, {
            method: 'POST',
            mode: 'same-origin',
            credentials: 'same-origin',
            cache: 'no-store',
            redirect: 'follow',
            headers: headers,
            body: JSON.stringify(
                {"jsonrpc": "2.0", "id": id, "method": method, "params": params}
            )
        });
    }
    catch(error) {
        showNotification(tn('API error') + '\n' +
            tn('Error accessing %{uri}', {"uri": uri}),
            'general', 'error'
        );
        logError('Error posting to ' + uri);
        logError(error);
        if (onerror === true) {
            setJsonRpcError(id, method, tn("Error posting to %{uri}", {"uri": uri}));
            callback(jsonRpcError);
        }
        return;
    }

    if (response.redirected === true) {
        logError('Request was redirect, reloading application');
        window.location.reload();
        return;
    }
    if (response.status === 403 &&
        method !== 'MYMPD_API_SESSION_VALIDATE')
    {
        //myMPD session authentication
        logDebug('Authorization required for ' + method);
        enterPin(method, params, callback, onerror);
        return;
    }
    if (response.ok === false) {
        showNotification(tn('API error') + '\n' +
            tn('Error accessing %{uri}', {"uri": uri}) + '\n' +
            tn('Response code: %{code}', {"code": response.status + ' - ' + response.statusText}),
            'general', 'error'
        );
        logError('Error accessing ' + uri + ', code ' + response.status + ' - ' + response.statusText);
        if (onerror === true) {
            setJsonRpcError(id, method, tn("Response error: %{status}", response.status + ' - ' + response.statusText));
            callback(jsonRpcError);
        }
        return;
    }
    
    //successful http response - extend session
    if (settings.pin === true &&
        session.token !== '' &&
        APImethods[method].protected === true)
    {
        //session was extended through request
        session.timeout = getTimestamp() + sessionLifetime;
        resetSessionTimer();
    }

    //parse response
    let obj;
    try {
        obj = await response.json();
    }
    catch(error) {
        showNotification(tn('API error') + '\n' +
            tn('Failed to parse response from %{uri}', {"uri": uri}),
            'general', 'error'
        );
        logError('Failed to parse response from ' + uri);
        logError(error);
        if (onerror === true) {
            setJsonRpcError(id, method, tn("Failed to parse response from %{uri}", {"uri": uri}));
            callback(jsonRpcError);
        }
        return;
    }
    checkAPIresponse(obj, callback, onerror);
}

/**
 * Validates the JSON-RPC API response and calls the callback function
 * @param {object} obj parsed json rpc response object
 * @param {Function} callback callback function
 * @param {boolean} onerror true = execute callback also on error
 * @returns {void}
 */
function checkAPIresponse(obj, callback, onerror) {
    logDebug('Got API response: ' + JSON.stringify(obj));
    myMPDready = true;

    if (obj.error &&
        typeof obj.error.message === 'string')
    {
        if (obj.error.method === 'GENERAL_API_NOT_READY') {
            myMPDready = false;
            toggleUI();
        }
        else {
            //show and log message
            showNotification(tn(obj.error.message, obj.error.data), obj.error.facility, obj.error.severity);
            logSeverity(obj.error.severity, JSON.stringify(obj));
        }
    }
    else if (obj.result &&
             typeof obj.result.message === 'string')
    {
        //show message
        if (ignoreMessages.includes(obj.result.message) === false) {
            showNotification(tn(obj.result.message, obj.result.data), obj.result.facility, obj.result.severity);
        }
    }
    else if (obj.result &&
             typeof obj.result.method === 'string')
    {
        //result is used in callback
    }
    else {
        //remaining results are invalid
        logError('Got invalid API response: ' + JSON.stringify(obj));
        //set generic error
        setJsonRpcError(0, "MYMPD_API_UNKNOWN", tn("Invalid response"));
        obj = jsonRpcError;
    }
    if (callback !== undefined &&
        typeof(callback) === 'function')
    {
        if (obj.result !== undefined ||
            onerror === true)
        {
            logDebug('Calling ' + callback.name);
            callback(obj);
        }
        else {
            logDebug('Result is undefined, skip calling ' + callback.name);
        }
    }
}

/**
 * Gets the callback for an jsonrpc method.
 * Used for async jsonrpc responses.
 * @param {string} method jsonrpc method
 * @returns {Function} the function that can parse the response, or null
 */
function getResponseCallback(method) {
    switch(method) {
        default:
            return null;
    }
}

/**
 * Generates a uniq jsonrpcid, keeping the clientId the same.
 * Wraps around the requestId.
 * @returns {number} the jsonrpcid
 */
function generateJsonrpcId() {
    jsonrpcRequestId = jsonrpcRequestId === 999
        ? 0
        : ++jsonrpcRequestId;
    return jsonrpcClientId * 1000 + jsonrpcRequestId;
}