diff --git a/plugins/discord/back.js b/plugins/discord/back.js index c0808e4e..7ba1ea1a 100644 --- a/plugins/discord/back.js +++ b/plugins/discord/back.js @@ -1,71 +1,140 @@ const Discord = require("discord-rpc"); +const { dev } = require("electron-is") const registerCallback = require("../../providers/song-info"); -const rpc = new Discord.Client({ - transport: "ipc", -}); - // Application ID registered by @semvis123 const clientId = "790655993809338398"; -let clearActivity; +/** + * @typedef {Object} Info + * @property {import('discord-rpc').Client} rpc + * @property {boolean} ready + * @property {import('../../providers/song-info').SongInfo} lastSongInfo + */ +/** + * @type {Info} + */ +const info = { + rpc: null, + ready: false, + lastSongInfo: null, +}; +/** + * @type {(() => void)[]} + */ +const refreshCallbacks = []; +const resetInfo = () => { + info.rpc = null; + info.ready = false; + clearTimeout(clearActivity); + if (dev()) console.log("discord disconnected"); + refreshCallbacks.forEach(cb => cb()); +}; -module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => { - // If the page is ready, register the callback - win.once("ready-to-show", () => { - rpc.once("ready", () => { - // Register the callback - // - // We get multiple events - // Next song: PAUSE(n), PAUSE(n+1), PLAY(n+1) - // Skip time: PAUSE(N), PLAY(N) - registerCallback((songInfo) => { - if (songInfo.title.length === 0 && songInfo.artist.length === 0) { - return; - } - // Song information changed, so lets update the rich presence - const activityInfo = { - details: songInfo.title, - state: songInfo.artist, - largeImageKey: "logo", - largeImageText: [ - songInfo.uploadDate, - songInfo.views.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " views" - ].join(' || '), - }; +const connect = () => { + if (info.rpc) { + if (dev()) + console.log('Attempted to connect with active RPC object'); + return; + } - // stop the clear activity timout - clearTimeout(clearActivity); + info.rpc = new Discord.Client({ + transport: "ipc", + }); + info.ready = false; - // clear directly if timeout is 0 - if (songInfo.isPaused && activityTimoutEnabled && activityTimoutTime === 0) { - rpc.clearActivity().catch(console.error); - return; - } + info.rpc.once("connected", () => { + if (dev()) console.log("discord connected"); + refreshCallbacks.forEach(cb => cb()); + }); + info.rpc.once("ready", () => { + info.ready = true; + if (info.lastSongInfo) updateActivity(info.lastSongInfo) + }); + info.rpc.once("disconnected", resetInfo); - if (songInfo.isPaused) { - // Add an idle icon to show that the song is paused - activityInfo.smallImageKey = "idle"; - activityInfo.smallImageText = "idle/paused"; - // Set start the timer so the activity gets cleared after a while if enabled - if (activityTimoutEnabled) - clearActivity = setTimeout(() => rpc.clearActivity().catch(console.error), activityTimoutTime || 10000); - } else { - // Add the start and end time of the song - const songStartTime = Date.now() - songInfo.elapsedSeconds * 1000; - activityInfo.startTimestamp = songStartTime; - activityInfo.endTimestamp = - songStartTime + songInfo.songDuration * 1000; - } - - rpc.setActivity(activityInfo).catch(console.error); - }); - }); - - // Startup the rpc client - rpc.login({ clientId }).catch(console.error); + // Startup the rpc client + info.rpc.login({ clientId }).catch(err => { + resetInfo(); + if (dev()) console.error(err); }); }; -module.exports.clear = () => rpc.clearActivity(); +let clearActivity; +/** + * @type {import('../../providers/song-info').songInfoCallback} + */ +let updateActivity; + +module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => { + // We get multiple events + // Next song: PAUSE(n), PAUSE(n+1), PLAY(n+1) + // Skip time: PAUSE(N), PLAY(N) + updateActivity = songInfo => { + if (songInfo.title.length === 0 && songInfo.artist.length === 0) { + return; + } + info.lastSongInfo = songInfo; + + // stop the clear activity timout + clearTimeout(clearActivity); + + // stop early if discord connection is not ready + // do this after clearTimeout to avoid unexpected clears + if (!info.rpc || !info.ready) { + return; + } + + // clear directly if timeout is 0 + if (songInfo.isPaused && activityTimoutEnabled && activityTimoutTime === 0) { + info.rpc.clearActivity().catch(console.error); + return; + } + + // Song information changed, so lets update the rich presence + const activityInfo = { + details: songInfo.title, + state: songInfo.artist, + largeImageKey: "logo", + largeImageText: [ + songInfo.uploadDate, + songInfo.views.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " views" + ].join(' || '), + }; + + if (songInfo.isPaused) { + // Add an idle icon to show that the song is paused + activityInfo.smallImageKey = "idle"; + activityInfo.smallImageText = "idle/paused"; + // Set start the timer so the activity gets cleared after a while if enabled + if (activityTimoutEnabled) + clearActivity = setTimeout(() => info.rpc.clearActivity().catch(console.error), activityTimoutTime || 10000); + } else { + // Add the start and end time of the song + const songStartTime = Date.now() - songInfo.elapsedSeconds * 1000; + activityInfo.startTimestamp = songStartTime; + activityInfo.endTimestamp = + songStartTime + songInfo.songDuration * 1000; + } + + info.rpc.setActivity(activityInfo).catch(console.error); + }; + + // If the page is ready, register the callback + win.once("ready-to-show", () => { + registerCallback(updateActivity); + connect(); + }); +}; + +module.exports.clear = () => { + if (info.rpc) info.rpc.clearActivity(); + clearTimeout(clearActivity); +}; +module.exports.connect = connect; +module.exports.registerRefresh = (cb) => refreshCallbacks.push(cb); +/** + * @type {Info} + */ +module.exports.info = Object.defineProperties({}, Object.keys(info).reduce((o, k) => ({ ...o, [k]: { enumerable: true, get: () => info[k] } }), {})); diff --git a/plugins/discord/menu.js b/plugins/discord/menu.js index 96e25e8c..3e8d97e9 100644 --- a/plugins/discord/menu.js +++ b/plugins/discord/menu.js @@ -1,28 +1,38 @@ const { setOptions } = require("../../config/plugins"); const { edit } = require("../../config"); -const { clear } = require("./back"); +const { clear, info, connect, registerRefresh } = require("./back"); -module.exports = (win, options) => [ - { - label: "Clear activity", - click: () => { - clear(); +let hasRegisterred = false; + +module.exports = (win, options, refreshMenu) => { + if (!hasRegisterred) { + registerRefresh(refreshMenu); + hasRegisterred = true; + } + + return [ + { + label: info.rpc !== null ? "Connected" : "Reconnect", + enabled: info.rpc === null, + click: connect, }, - }, - { - label: "Clear activity after timeout", - type: "checkbox", - checked: options.activityTimoutEnabled, - click: (item) => { - options.activityTimoutEnabled = item.checked; - setOptions('discord', options); + { + label: "Clear activity", + click: clear, }, - }, - { - label: "Set timeout time in config", - click: () => { + { + label: "Clear activity after timeout", + type: "checkbox", + checked: options.activityTimoutEnabled, + click: (item) => { + options.activityTimoutEnabled = item.checked; + setOptions('discord', options); + }, + }, + { + label: "Set timeout time in config", // open config.json - edit(); + click: edit, }, - }, -]; + ]; +}; diff --git a/providers/song-info.js b/providers/song-info.js index 08eaf944..11b2abeb 100644 --- a/providers/song-info.js +++ b/providers/song-info.js @@ -40,6 +40,9 @@ const getArtist = async (win) => { } // Fill songInfo with empty values +/** + * @typedef {songInfo} SongInfo + */ const songInfo = { title: "", artist: "", @@ -71,6 +74,14 @@ const handleData = async (responseText, win) => { const callbacks = []; // This function will allow plugins to register callback that will be triggered when data changes +/** + * @callback songInfoCallback + * @param {songInfo} songInfo + * @returns {void} + */ +/** + * @param {songInfoCallback} callback + */ const registerCallback = (callback) => { callbacks.push(callback); };