Source: extensions/streamersonglist/streamersonglist.js

/**
 *      StreamRoller Copyright 2023 "SilenusTA https://www.twitch.tv/olddepressedgamer"
 * 
 *      StreamRoller is an all in one streaming solution designed to give a single
 *      'second monitor' control page and allow easy integration for configuring
 *      content (ie. tweets linked to chat, overlays triggered by messages, hue lights
 *      controlled by donations etc)
 * 
 *      This program is free software: you can redistribute it and/or modify
 *      it under the terms of the GNU Affero General Public License as published
 *      by the Free Software Foundation, either version 3 of the License, or
 *      (at your option) any later version.
 * 
 *      This program is distributed in the hope that it will be useful,
 *      but WITHOUT ANY WARRANTY; without even the implied warranty of
 *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *      GNU Affero General Public License for more details.
 * 
 *      You should have received a copy of the GNU Affero General Public License
 *      along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
/**
 * @extension StreamerSongList
 * Connects to the StreamerSongList service to allow adding/removing/viewing songlist data.
 * Mostly used by music streamers to queue music for viewers.
 */
// ############################# STREAMERSONGLIST.js ##############################
// Provides streamer songlist functionality
// ---------------------------- creation --------------------------------------
// Author: Silenus aka twitch.tv/OldDepressedGamer
// GitHub: https://github.com/SilenusTA/StreamRoller
// Date: 25-May-2023
// --------------------------- functionality ----------------------------------
// Current functionality:
//
// ----------------------------- notes ----------------------------------------
// ============================================================================

// ============================================================================
//                           IMPORTS/VARIABLES
// ============================================================================
// logger will allow you to log messages in the same format as the system messages
import * as logger from "../../backend/data_center/modules/logger.js";
// extension helper provides some functions to save you having to write them.
import * as fs from "fs";
import fetch from 'node-fetch';
import io from 'socket.io-client';
import sr_api from "../../backend/data_center/public/streamroller-message-api.cjs";
// these lines are a fix so that ES6 has access to dirname etc
import { dirname } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));

const localConfig = {
    SYSTEM_LOGGING_TAG: "[EXTENSION]",
    DataCenterSocket: null,
    ssl_client: null,
    status: {
        connected: false,
    },
    pollSongQueueHandle: null,
    pollSongListHandle: null,
    songlist: [],
    songQueue: [],
    currentsong: "",
    username: "",
    clientId: "",
    userId: "",
    streamerId: "",
    // set to ignore first load messages
    firstload: true
};
const default_serverConfig = {
    __version__: 0.4,
    extensionname: "streamersonglist",
    channel: "STREAMERSONGLIST_CHANNEL",
    sslURI: "https://api.streamersonglist.com",
    enablestreamersonglist: "off",
    streamersonglistname: "",
    pollSongQueueTimeout: 180000, // check for updated queue every 3 minutes in case the socket goes down
    pollSongListTimeout: 300000, // check for updated songs every 5 minutes in case the socket goes down
    heartBeatTimeout: 5000,
    //credentials variable names to use (in credentials modal)
    credentialscount: "4",
    cred1name: "username",
    cred1value: "",
    cred2name: "clientId",
    cred2value: "",
    cred3name: "userId",
    cred3value: "",
    cred4name: "streamerId",
    cred4value: "",
};
let serverConfig = structuredClone(default_serverConfig);

const SSL_SOCKET_EVENTS = {
    JOIN_ROOM: 'join-room',
    LEAVE_ROOM: 'leave-room',
    NEW_SONG: 'new-song',
    RELOAD_SONG_LIST: 'reload-song-list',
    UPDATE_SONG: 'update-song',
    UPDATE_SONGS: 'update-songs',
    DELETE_SONG: 'delete-song',
    UPDATE_QUEUE_LIST: 'queue-update',
    RELOAD_SAVED_QUEUE_LIST: 'reload-saved-queue',
    QUEUE_MESSAGE: 'queue-event',
    NEW_PLAYHISTORY: 'new-playhistory',
    UPDATE_PLAYHISTORY: 'update-playhistory',
    DELETE_PLAYHISTORY: 'delete-playhistory',
    UPDATE_STREAMER: 'update-streamer',
    UPDATE_ATTRIBUTES: 'update-attributes',
};
// events we want to reload songlist or queue on 
const SSL_RELOAD_EVENTS =
    ['queue-event', 'reload-song-list']

const triggersandactions =
{
    extensionname: serverConfig.extensionname,
    description: "Streamer songlist (SSL) is a tool for streamers to keep track of songs in a queue that viewers requests <a href='https://www.streamersonglist.com/'>SSL Website</a>",
    version: "0.2",
    channel: serverConfig.channel,
    // these are messages we can sendout that other extensions might want to use to trigger an action
    triggers:
        [
            {
                name: "SSLSongAddedToQueue",
                displaytitle: "Song Added To Queue",
                description: "Song was added to queue",
                messagetype: "trigger_SongAddedToQueue",
                parameters: {
                    songName: "",
                    textMessage: ""
                }
            }
            ,
            {
                name: "SSLCurrentSongChanged",
                displaytitle: "Current Song Changed",
                description: "Current song changed",
                messagetype: "trigger_CurrentSongChange",
                parameters: {
                    songName: "",
                    textMessage: ""
                }
            }

        ],
    // these are messages we can receive to perform an action
    actions:
        [
            {
                name: "SSLAddSongToQueue",
                displaytitle: "Add Song To Queue",
                description: "Add a song to the queue",
                messagetype: "action_AddSongToQueue",
                parameters: { songName: "" }
            }
            ,
            {
                name: "SSLPlaySong",
                displaytitle: "Mark Song as played",
                description: "Mark a song as played",
                messagetype: "action_MarkSongAsPlayed",
                parameters: { songName: "" }
            }
        ],
}
// ============================================================================
//                           FUNCTION: initialise
// ============================================================================
/**
 * Starts the extension using the given data.
 * @param {object:Express} app 
 * @param {string} host 
 * @param {number} port 
 * @param {number} heartbeat 
 */
function initialise (app, host, port, heartbeat)
{
    localConfig.heartBeatTimeout = heartbeat
    try
    {
        localConfig.DataCenterSocket = sr_api.setupConnection(onDataCenterMessage, onDataCenterConnect, onDataCenterDisconnect, host, port);
    } catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".initialise", "localConfig.DataCenterSocket connection failed:", err);
    }
}

// ============================================================================
//                           FUNCTION: onDataCenterDisconnect
// ============================================================================
/**
 * Disconnection message sent from the server
 * @param {String} reason 
 */
function onDataCenterDisconnect (reason)
{
    // do something here when disconnt happens if you want to handle them
    logger.log(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterDisconnect", reason);
}
// ============================================================================
//                           FUNCTION: onDataCenterConnect
// ============================================================================
// Description: Received connect message
// Parameters: socket 
// ----------------------------- notes ----------------------------------------
// When we connect to the StreamRoller server the first time (or if we reconnect)
// we will get this function called.
// it is also a good place to create/join channels we wish to use for data
// monitoring/sending on.
// ===========================================================================
/**
 * Connection message handler
 * @param {*} socket 
 */
function onDataCenterConnect (socket)
{
    // Request our credentials from the server
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("RequestCredentials", serverConfig.extensionname));
    // Request our config from the server
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("RequestConfig", serverConfig.extensionname));
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("CreateChannel", serverConfig.extensionname, serverConfig.channel));
    localConfig.heartBeatHandle = setTimeout(heartBeatCallback, serverConfig.heartBeatTimeout);
}
// ============================================================================
//                           FUNCTION: onDataCenterMessage
// ============================================================================
/**
 * receives message from the socket
 * @param {data} server_packet 
 */
function onDataCenterMessage (server_packet)
{
    if (server_packet.type === "ConfigFile")
    {
        if (server_packet.data != "" && server_packet.to === serverConfig.extensionname)
        {
            logger.info(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage", "Received config");
            if (server_packet.data.__version__ != default_serverConfig.__version__)
            {
                serverConfig = structuredClone(default_serverConfig);
                console.log("\x1b[31m" + serverConfig.extensionname + " ConfigFile Updated", "The config file has been Updated to the latest version v" + default_serverConfig.__version__ + ". Your settings may have changed" + "\x1b[0m");
            }
            else
                serverConfig = structuredClone(server_packet.data);

            pollSongQueueCallback();
            pollSongListCallback();
            SaveConfigToServer();
        }
    }
    else if (server_packet.type === "CredentialsFile")
    {
        if (server_packet.to === serverConfig.extensionname && server_packet.data && server_packet.data != "")
        {
            localConfig.username = server_packet.data.username;
            localConfig.clientId = server_packet.data.clientId;
            localConfig.userId = server_packet.data.userId;
            localConfig.streamerId = server_packet.data.streamerId;
            if (serverConfig.enablestreamersonglist == "on")
                joinSslServer()
        }
    }
    else if (server_packet.type === "ExtensionMessage")
    {
        let extension_packet = server_packet.data;
        if (extension_packet.type === "RequestSettingsWidgetSmallCode")
            SendSettingsWidgetSmall(extension_packet.from);
        else if (extension_packet.type === "RequestCredentialsModalsCode")
            SendCredentialsModal(extension_packet.from);
        else if (extension_packet.type === "SettingsWidgetSmallData")
        {

            if (extension_packet.to === serverConfig.extensionname)
            {
                if (extension_packet.data.streamersonglistname != localConfig.username)
                {
                    localConfig.username = extension_packet.data.streamersonglistname;
                    serverConfig.streamersonglistname = extension_packet.data.streamersonglistname
                    serverConfig.enablestreamersonglist = "off"
                }
                // if we have enabled/disabled connection
                if (serverConfig.enablestreamersonglist != extension_packet.data.enablestreamersonglist)
                {
                    // TURNING OFF SSL
                    //we are currently enabled so lets stop polling
                    if (serverConfig.enablestreamersonglist == "on")
                    {
                        localConfig.status.connected = false;
                        localConfig.songlist = [];
                        localConfig.songQueue = [];
                        localConfig.currentsong = "";
                        clearTimeout(localConfig.pollSongQueueTimeout);
                        sendSongQueue(extension_packet.from);
                        serverConfig.enablestreamersonglist = "off";
                    }
                    // TURNING ON SSL
                    //currently disabled so lets start
                    else
                    {
                        localConfig.status.connected = true;
                        serverConfig.enablestreamersonglist = "on";
                        pollSongQueueCallback();
                        pollSongListCallback();
                    }
                }

                serverConfig.enablestreamersonglist = "off";
                for (const [key, value] of Object.entries(extension_packet.data))
                    serverConfig[key] = value;
                SaveConfigToServer();
            }
            //update anyone who is showing our code at the moment
            SendSettingsWidgetSmall("");
            if (serverConfig.enablestreamersonglist == "on")
                joinSslServer()
        }
        else if (extension_packet.type === "RequestQueue")
        {
            sendSongQueue(extension_packet.from);
        }
        else if (extension_packet.type === "RequestSonglist")
        {
            sendSonglist(extension_packet.from);
        }
        else if (extension_packet.type === "action_AddSongToQueue")
        {
            if (extension_packet.data.songName)
                addSongToQueuebyName(extension_packet.data.songName)
            else
                addSongToQueueById(extension_packet.data);
        }
        else if (extension_packet.type === "action_MarkSongAsPlayed")
        {
            markSongAsPlayed(extension_packet.data);
        }
        else if (extension_packet.type === "RemoveSongFromQueue")
        {
            removeSongFromQueue(extension_packet.data);
        }
        else if (extension_packet.type === "SendTriggerAndActions")
        {
            if (serverConfig.enablestreamersonglist == "on")
            {
                sendTriggersAndActions(server_packet.from)
            }
        }
        else if (extension_packet.type === "RequestSettingsWidgetLargeCode"
            || extension_packet.type === "TriggerAndActions")
        {
            // we don't handle these yet
        }
        else
            logger.warn(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage", "received unhandled ExtensionMessage ", server_packet);
    }
    else if (server_packet.type === "UnknownChannel")
    {
        logger.info(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage", "Channel " + server_packet.data + " doesn't exist, scheduling rejoin");
        // channel might not exist yet, extension might still be starting up so lets rescehuled the join attempt
        setTimeout(() =>
        {
            // resent the register command to see if the extension is up and running yet
            sr_api.sendMessage(localConfig.DataCenterSocket,
                sr_api.ServerPacket(
                    "JoinChannel", serverConfig.extensionname, server_packet.data
                ));
        }, 5000);

    }
    else if (server_packet.type === "InvalidMessage")
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage",
            "InvalidMessage ", server_packet.data.error, server_packet);
    }
    else if (server_packet.type === "ChannelJoined"
        || server_packet.type === "ChannelCreated"
        || server_packet.type === "ChannelLeft"
        || server_packet.type === "LoggingLevel"
        || server_packet.type === "ExtensionMessage"
    )
    {
        // just a blank handler for items we are not using to avoid message from the catchall
    }
    // ------------------------------------------------ unknown message type received -----------------------------------------------
    else
        logger.warn(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
            ".onDataCenterMessage", "Unhandled message type", server_packet.type);
}
// ===========================================================================
//                           FUNCTION: sendTriggersAndActions
// ===========================================================================
/**
 * Sends out triggers and actions out to the given extension name
 * @param {string} from 
 */
function sendTriggersAndActions (from)
{
    sr_api.sendMessage(localConfig.DataCenterSocket,
        sr_api.ServerPacket("ExtensionMessage",
            serverConfig.extensionname,
            sr_api.ExtensionPacket(
                "TriggerAndActions",
                serverConfig.extensionname,
                triggersandactions,
                "",
                from
            ),
            "",
            from
        )
    )
}
// ===========================================================================
//                           FUNCTION: SendSettingsWidgetSmall
// ===========================================================================
/**
 * send some modal code to be displayed on the admin page or somewhere else
 * this is done as part of the webpage request for modal message we get from 
 * extension. It is a way of getting some user feedback via submitted forms
 * from a page that supports the modal system
 * @param {String} tochannel 
 */
function SendSettingsWidgetSmall (tochannel)
{
    // read our modal file
    fs.readFile(__dirname + "/streamersonglistsettingswidgetsmall.html", function (err, filedata)
    {
        if (err)
            logger.err(localConfig.SYSTEM_LOGGING_TAG + localConfig.EXTENSION_NAME +
                ".SendSettingsWidgetSmall", "failed to load modal", err);
        //throw err;
        else
        {
            let modalstring = filedata.toString();
            for (const [key, value] of Object.entries(serverConfig))
            {
                if (value === "on")
                    modalstring = modalstring.replace(key + "checked", "checked");
                // replace text strings
                else if (typeof (value) == "string")
                    modalstring = modalstring.replace(key + "text", value);
            }
            // send the modified modal data to the server
            sr_api.sendMessage(localConfig.DataCenterSocket,
                sr_api.ServerPacket(
                    "ExtensionMessage", // this type of message is just forwarded on to the extension
                    serverConfig.extensionname,
                    sr_api.ExtensionPacket(
                        "SettingsWidgetSmallCode", // message type
                        serverConfig.extensionname, //our name
                        modalstring,// data
                        "",
                        tochannel,
                        serverConfig.channel
                    ),
                    "",
                    tochannel // in this case we only need the "to" channel as we will send only to the requester
                ))
        }
    });
}
// ===========================================================================
//                           FUNCTION: SendCredentialsModal
// ===========================================================================
/**
 * Send our CredentialsModal to whoever requested it
 * @param {String} extensionname 
 */
function SendCredentialsModal (extensionname)
{

    fs.readFile(__dirname + "/streamersonglistcredentialsmodal.html", function (err, filedata)
    {
        if (err)
            logger.err(localConfig.SYSTEM_LOGGING_TAG + localConfig.EXTENSION_NAME +
                ".SendCredentialsModal", "failed to load modal", err);
        //throw err;
        else
        {
            let modalstring = filedata.toString();
            // first lets update our modal to the current settings
            for (const [key, value] of Object.entries(serverConfig))
            {
                // true values represent a checkbox so replace the "[key]checked" values with checked
                if (value === "on")
                    modalstring = modalstring.replace(key + "checked", "checked");
                else if (typeof (value) == "string" || typeof (value) == "number")
                    modalstring = modalstring.replace(key + "text", value);
            }
            // send the modal data to the server
            sr_api.sendMessage(localConfig.DataCenterSocket,
                sr_api.ServerPacket("ExtensionMessage",
                    serverConfig.extensionname,
                    sr_api.ExtensionPacket(
                        "CredentialsModalCode",
                        serverConfig.extensionname,
                        modalstring,
                        "",
                        extensionname,
                        serverConfig.channel
                    ),
                    "",
                    extensionname)
            )
        }
    });
}
// ============================================================================
//                           FUNCTION: SaveConfigToServer
// ============================================================================
/**
 * save config file to the server
 */
function SaveConfigToServer ()
{
    // saves our serverConfig to the server so we can load it again next time we startup
    sr_api.sendMessage(localConfig.DataCenterSocket, sr_api.ServerPacket(
        "SaveConfig",
        serverConfig.extensionname,
        serverConfig))
}
// ============================================================================
//                      FUNCTION: joinSslServer
//                        Join the Song list server for updates
// ============================================================================
/**
 * Joins the StreamerSonglist service
 */
function joinSslServer ()
{
    // if we already have a connection lets remove that one (so we can start a new one with any changed data/creds)
    if (localConfig.ssl_client != null)
    {
        localConfig.ssl_client.disconnect();
        localConfig.ssl_client == null;
    }
    localConfig.ssl_client = io(serverConfig.sslURI, {
        transports: ["websocket"],
        reconnection: true,
    });
    localConfig.ssl_client.on('connect', () =>
    {
        // add all our handlers
        for (const [key] of Object.entries(SSL_SOCKET_EVENTS))
        {
            localConfig.ssl_client.on(SSL_SOCKET_EVENTS[key], (msg) =>
            {
                logger.extra(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage", "StreamerSonglist socket callback received ", SSL_SOCKET_EVENTS[key]);
                if ("QUEUE_MESSAGE" == key)
                {
                    if (msg.added)
                    {
                        let triggertosend = findtriggerByMessageType("trigger_SongAddedToQueue")
                        triggertosend.parameters.songName = msg.title
                        triggertosend.parameters.textMessage = "Song Added to queue: " + msg.title
                        sr_api.sendMessage(localConfig.DataCenterSocket,
                            sr_api.ServerPacket("ChannelData",
                                serverConfig.extensionname,
                                sr_api.ExtensionPacket(
                                    "trigger_SongAddedToQueue",
                                    serverConfig.extensionname,
                                    triggertosend,
                                    serverConfig.channel),
                                serverConfig.channel
                            ),
                        );
                    }
                    fetchSongQueue();

                }
                else if (SSL_RELOAD_EVENTS.includes(SSL_SOCKET_EVENTS[key]) && serverConfig.enablestreamersonglist == "on")
                {
                    logger.log(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage:ssl_client.on message", "StreamerSonglist socket callback received, updating songs and queue: ", SSL_SOCKET_EVENTS[key]);
                    fetchSongList();
                    fetchSongQueue();
                }
            });
        }

        try
        {
            localConfig.ssl_client.emit("join-room", localConfig.streamerId);
        } catch (error)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".onDataCenterMessage:ssl_client.join room", "Failed to join room");
        }
    });

    // perform a fetch of the lists in case we get asked for them later
    fetchSongList()
    fetchSongQueue()
    // start our times if we havent alreadly
    pollSongQueueCallback();
    pollSongListCallback();
}
// ============================================================================
//                      FUNCTION: sendSongQueue
// ============================================================================
/**
 * Sends the current songQueue to the given extension name
 * @param {string} extensionsname 
 */
function sendSongQueue (extensionsname)
{
    if (serverConfig.enablestreamersonglist == "on")
    {
        sr_api.sendMessage(localConfig.DataCenterSocket,
            sr_api.ServerPacket(
                "ExtensionMessage",
                serverConfig.extensionname,
                sr_api.ExtensionPacket(
                    "SongQueue",
                    serverConfig.extensionname,
                    localConfig.songQueue,
                    "",
                    extensionsname
                ),
                "",
                extensionsname
            ));
    }
}
// ============================================================================
//                     FUNCTION: outputSongQueue
// ============================================================================
/**
 * Sends the current songQueue out on our channel
 */
function outputSongQueue ()
{
    if (serverConfig.enablestreamersonglist == "on")
    {
        sr_api.sendMessage(localConfig.DataCenterSocket,
            sr_api.ServerPacket("ChannelData",
                serverConfig.extensionname,
                sr_api.ExtensionPacket(
                    "SongQueue",
                    serverConfig.extensionname,
                    localConfig.songQueue,
                    serverConfig.channel),
                serverConfig.channel
            ),
        );
    }
}
// ============================================================================
//                           FUNCTION: fetchSongQueue
// ============================================================================
/**
 * Fetches the current song queue from the StreamerSonglist server
 */
function fetchSongQueue ()
{
    if (serverConfig.enablestreamersonglist == "on")
    {
        fetch(`${serverConfig.sslURI}/v1/streamers/${localConfig.username}/queue`, {
            headers: { 'Client-ID': localConfig.clientId, },
        })
            .then(response =>
            {
                if (!response.ok)
                    throw new Error('Request failed with status ' + response.status);
                return response.json();
            })
            .then(data =>
            {

                localConfig.songQueue = data;
                if (localConfig.songQueue.list.length > 0 && localConfig.currentsong != localConfig.songQueue.list[0].song.title)
                {
                    localConfig.currentsong = localConfig.songQueue.list[0].song.title
                    sendCurrentSongChange("")
                }
                else if (localConfig.songQueue.list.length == 0)
                {

                    localConfig.currentsong = ""
                    sendCurrentSongChange("")
                }

                outputSongQueue();
                localConfig.status.connected = true;
            })
            .catch(e =>
            {
                localConfig.status.connected = false;
                logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".fetchSongQueue", "Error getting songs queue", e.message);
            });
    }
}
// ============================================================================
//                       FUNCTION: sendSonglist
// ============================================================================
/**
 * Sends the full songlist to given extensionextension
 * @param {string} extension 
 */
function sendSonglist (extension)
{
    if (serverConfig.enablestreamersonglist == "on")
    {
        sr_api.sendMessage(localConfig.DataCenterSocket,
            sr_api.ServerPacket(
                "ExtensionMessage",
                serverConfig.extensionname,
                sr_api.ExtensionPacket(
                    "SongList",
                    serverConfig.extensionname,
                    localConfig.songlist,
                    "",
                    extension
                ),
                "",
                extension
            ));
    }
}
// ============================================================================
//                       FUNCTION: sendCurrentSongChange
//                      send songlist to extension
// ============================================================================
/**
 * Sends a trigger_CurrentSongChange message to given extensionextension
 * @param {string} extension 
 */
function sendCurrentSongChange (extension)
{
    if (serverConfig.enablestreamersonglist == "on")
    {
        let songtext = ""
        if (localConfig.currentsong != "")
            songtext = "Current song now: " + localConfig.currentsong

        let triggertosend = findtriggerByMessageType("trigger_CurrentSongChange")
        triggertosend.parameters.songName = localConfig.currentsong
        triggertosend.parameters.textMessage = songtext
        if (extension != "")
        {
            sr_api.sendMessage(localConfig.DataCenterSocket,
                sr_api.ServerPacket(
                    "ExtensionMessage",
                    serverConfig.extensionname,
                    sr_api.ExtensionPacket(
                        "trigger_CurrentSongChange",
                        serverConfig.extensionname,
                        triggertosend,
                        serverConfig.channel,
                        extension
                    ),
                    serverConfig.channel,
                    extension
                ));
        }
        else
        {
            sr_api.sendMessage(localConfig.DataCenterSocket,
                sr_api.ServerPacket(
                    "ChannelData",
                    serverConfig.extensionname,
                    sr_api.ExtensionPacket(
                        "trigger_CurrentSongChange",
                        serverConfig.extensionname,
                        triggertosend,
                        serverConfig.channel
                    ),
                    serverConfig.channel
                ));
        }
    }
}
// ============================================================================
//                           FUNCTION: outputSongList
// ============================================================================
/**
 * Outputs the full songlist on our channel
 */
function outputSongList ()
{
    if (serverConfig.enablestreamersonglist == "on")
    {
        sr_api.sendMessage(localConfig.DataCenterSocket,
            sr_api.ServerPacket("ChannelData",
                serverConfig.extensionname,
                sr_api.ExtensionPacket(
                    "SongList",
                    serverConfig.extensionname,
                    localConfig.songlist,
                    serverConfig.channel),
                serverConfig.channel
            ),
        );
    }
}
// ============================================================================
//                           FUNCTION: fetchSongList
// ============================================================================
/**
 * Fetches the full songlist from the StreamerSonglist server
 */
function fetchSongList ()
{
    if (serverConfig.enablestreamersonglist == "on")
    {
        fetch(`${serverConfig.sslURI}/v1/streamers/${localConfig.username}/songs`, {
            headers: { 'Client-ID': localConfig.clientId, },
        })
            .then(response =>
            {
                if (!response.ok)
                    throw new Error('Request failed with status ' + response.status);
                return response.json();
            })
            .then(data =>
            {
                localConfig.songlist = data;
                outputSongList();
                localConfig.status.connected = true;
            })
            .catch(e =>
            {
                localConfig.status.connected = false;
                logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".fetchSongList", "Error getting songs queue", e.message);
            });
    }
}
// ============================================================================
//                           FUNCTION: addSongToQueuebyName
// ============================================================================
/**
 * Adds a song by name to the songlist queue on the server
 * @param {string} songName 
 */
function addSongToQueuebyName (songName)
{
    if (serverConfig.enablestreamersonglist == "on")
    {
        let found = false
        localConfig.songlist.items.forEach((song) =>
        {
            if (songName.toLowerCase().indexOf(song.title.toLowerCase()) > -1)
            {
                addSongToQueueById(song.id)
                found = true;
            }
        })
        if (!found)
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".addSongToQueuebyName", "Unknown Song", songName);
    }
}
// ============================================================================
//                           FUNCTION: addSongToQueue
// ============================================================================
/**
 * Adds a song by id to the songlist queue on the server
 * @param {number} songId 
 */
function addSongToQueueById (songId)
{
    if (serverConfig.enablestreamersonglist == "on")
    {
        fetch(`${serverConfig.sslURI}/v1/streamers/${localConfig.streamerId}/queue/${songId}/request`, {
            method: 'POST',
            headers: {
                "accept": "application/json",
                "Authorization": "Bearer " + localConfig.clientId,
                "origin": "StreamRoller",
                "source": "StreamRoller",
            }
        })
            .then(response =>
            {
                if (!response.ok)
                    throw new Error('Request failed with status ' + response.status);
                return response.json();
            })
            .then(data =>
            {
                fetchSongQueue();
                localConfig.status.connected = true;
            })
            .catch(e =>
            {
                localConfig.status.connected = false;
                logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".addSongToQueueById", "Error adding song", e.message);
            });
    }
}

// ============================================================================
//                           FUNCTION: removeSongFromQueue
// ============================================================================
/**
 * Removes a songqueue song using it's queueId
 * @param {number} queueId Queue id of item to remove
 */
function removeSongFromQueue (queueId)
{
    if (serverConfig.enablestreamersonglist == "on")
    {
        //const url = `${serverConfig.sslURI}/v1/streamers/${localConfig.streamerId}/queue/${queueId}`
        const headers = {
            "accept": "application/json",
            "Content-Type": "application/json",
            "Authorization": "Bearer " + localConfig.clientId,
            "origin": "StreamRoller",
            "queueId": queueId
        };
        fetch(`${serverConfig.sslURI}/v1/streamers/${localConfig.streamerId}/queue/${queueId}`, { method: 'DELETE', headers: headers })
            .then(response =>
            {
                if (!response.ok)
                    throw new Error('Request failed with status ' + response.status, response.statusText);
                return response.json();
            })
            .then(data =>
            {
                fetchSongQueue()
            })
            .catch(e =>
            {
                logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".removeSongFromQueue", "Error removing song", e.message);
            });
    }
}

// ============================================================================
//                           FUNCTION: markSongAsPlayed
// ============================================================================
/**
 * Mark a song in the current queue as being played
 * @param {number} queueId 
 */
function markSongAsPlayed (queueId)
{
    if (serverConfig.enablestreamersonglist == "on")
    {
        fetch(`${serverConfig.sslURI}/v1/streamers/${localConfig.streamerId}/queue/${queueId}/played`, {
            method: 'POST',
            headers: {
                "accept": "application/json",
                "Authorization": "Bearer " + localConfig.clientId,
                "origin": "StreamRoller"
            }
        })
            .then(response =>
            {
                if (!response.ok)
                {
                    throw new Error('Request failed with status ' + response.status);
                }
                return response.json();
            })
            .then(data =>
            {
                fetchSongQueue()
            })
            .catch(e =>
            {
                logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".markSongAsPlayed", "Error Marking song as played", e.message);
            });
    }
}

// ============================================================================
//                           FUNCTION: saveQueue
// ============================================================================
function saveQueue (queue)
{
    if (serverConfig.enablestreamersonglist == "on")
    {
        fetch(`${serverConfig.sslURI}/v1/streamers/${localConfig.streamerId}/queue`, {
            method: 'PUT',
            headers: {
                'Content-Type': 'application/json',
                'Client-ID': localConfig.clientId,
            },
            body: JSON.stringify(queue),
        })
            .then(response =>
            {
                if (!response.ok)
                    throw new Error('Request failed with status ' + response.status);
            })
            .catch(e =>
            {
                logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".saveQueue", "Error saving queue", e.message);
            });
    }
}
// ============================================================================
//                           FUNCTION: pollSongQueueCallback
// ============================================================================
/**
 * Callback from the song queue timer
 */
function pollSongQueueCallback ()
{
    if (serverConfig.enablestreamersonglist == "on" && localConfig.username != "")//&& localConfig.clientId != "")
    {
        try
        {
            fetchSongQueue();
        }
        catch (err)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".pollSongQueueCallback", "callback failed:", err.message);
        }
    }
    if (localConfig.pollSongQueueHandle)
        clearTimeout(localConfig.pollSongQueueHandle)
    localConfig.pollSongQueueHandle = setTimeout(pollSongQueueCallback, serverConfig.pollSongQueueTimeout)
}
// ============================================================================
//                           FUNCTION: pollSongListCallback
// ============================================================================
/**
 * Timer for polling the songlist
 */
function pollSongListCallback ()
{
    if (serverConfig.enablestreamersonglist == "on" && localConfig.username != "" && localConfig.clientId != "")
    {
        try
        {
            fetchSongList()
        }
        catch (err)
        {
            logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".pollSongListCallback", "callback failed:", err.message);
        }
    }
    if (localConfig.pollSongListHandle)
        clearTimeout(localConfig.pollSongListHandle)
    localConfig.pollSongListHandle = setTimeout(pollSongListCallback, serverConfig.pollSongListTimeout)
}
// ============================================================================
//                           FUNCTION: heartBeat
// ============================================================================
/**
 * Sends out heartbeat messages so other extensions can see our status
 */
function heartBeatCallback ()
{
    try
    {
        sr_api.sendMessage(localConfig.DataCenterSocket,
            sr_api.ServerPacket("ChannelData",
                serverConfig.extensionname,
                sr_api.ExtensionPacket(
                    "HeartBeat",
                    serverConfig.extensionname,
                    localConfig.status,
                    serverConfig.channel),
                serverConfig.channel
            ),
        );
    }
    catch (err)
    {
        logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname + ".heartBeatCallback", "callback failed:", err.message);
    }
    localConfig.heartBeatHandle = setTimeout(heartBeatCallback, serverConfig.heartBeatTimeout)
}
// ============================================================================
//                           FUNCTION: findtriggerByMessageType
// ============================================================================
/**
 * Find a trigger by messagetype
 * @param {string} messagetype 
 * @returns trigger
 */
function findtriggerByMessageType (messagetype)
{
    for (let i = 0; i < triggersandactions.triggers.length; i++)
    {
        if (triggersandactions.triggers[i].messagetype.toLowerCase() == messagetype.toLowerCase()) return triggersandactions.triggers[i];
    }
    logger.err(localConfig.SYSTEM_LOGGING_TAG + serverConfig.extensionname +
        ".findtriggerByMessageType", "failed to find action", messagetype);
}
// ============================================================================
//                                  EXPORTS
// Note that initialise is mandatory to allow the server to start this extension
// ============================================================================
export { initialise, triggersandactions };