From 183bad43f662ffc294be57548720eb09c9fa0cbb Mon Sep 17 00:00:00 2001 From: Constantin Piber Date: Sun, 15 Aug 2021 12:20:37 +0200 Subject: [PATCH 1/7] Fix discord clearActivity, menu The callback sends multiple events, in particular two pause when going to the next song, so the timeout wasn't properly cleared. Add menu buttons for the two options --- plugins/discord/back.js | 13 +++++++++---- plugins/discord/menu.js | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 plugins/discord/menu.js diff --git a/plugins/discord/back.js b/plugins/discord/back.js index c6faadd7..2f75c430 100644 --- a/plugins/discord/back.js +++ b/plugins/discord/back.js @@ -16,6 +16,10 @@ module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => { 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; @@ -31,16 +35,17 @@ module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => { ].join(' || '), }; + // stop the clear activity timout + clearTimeout(clearActivity); + 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(), activityTimoutTime||10000); + clearActivity = setTimeout(() => rpc.clearActivity().catch(console.error), activityTimoutTime || 10000); } else { - // stop the clear activity timout - clearTimeout(clearActivity); // Add the start and end time of the song const songStartTime = Date.now() - songInfo.elapsedSeconds * 1000; activityInfo.startTimestamp = songStartTime; @@ -48,7 +53,7 @@ module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => { songStartTime + songInfo.songDuration * 1000; } - rpc.setActivity(activityInfo); + rpc.setActivity(activityInfo).catch(console.error); }); }); diff --git a/plugins/discord/menu.js b/plugins/discord/menu.js new file mode 100644 index 00000000..d0af48e8 --- /dev/null +++ b/plugins/discord/menu.js @@ -0,0 +1,21 @@ +const { setOptions } = require("../../config/plugins"); +const { edit } = require("../../config"); + +module.exports = (win, options) => [ + { + 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", + click: () => { + // open config.json + edit(); + }, + }, +]; From 39014572180798772c0a9a464c2bf43fb7d417ce Mon Sep 17 00:00:00 2001 From: Constantin Piber Date: Tue, 17 Aug 2021 10:09:11 +0200 Subject: [PATCH 2/7] Discord add menu button for clearing activity --- plugins/discord/back.js | 2 ++ plugins/discord/menu.js | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/plugins/discord/back.js b/plugins/discord/back.js index 2f75c430..57d9902a 100644 --- a/plugins/discord/back.js +++ b/plugins/discord/back.js @@ -61,3 +61,5 @@ module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => { rpc.login({ clientId }).catch(console.error); }); }; + +module.exports.clear = () => rpc.clearActivity(); diff --git a/plugins/discord/menu.js b/plugins/discord/menu.js index d0af48e8..96e25e8c 100644 --- a/plugins/discord/menu.js +++ b/plugins/discord/menu.js @@ -1,7 +1,14 @@ const { setOptions } = require("../../config/plugins"); const { edit } = require("../../config"); +const { clear } = require("./back"); module.exports = (win, options) => [ + { + label: "Clear activity", + click: () => { + clear(); + }, + }, { label: "Clear activity after timeout", type: "checkbox", From 36bc9c62b0e964dd8cbc1ad964ae570c0fe7566a Mon Sep 17 00:00:00 2001 From: Constantin Piber Date: Wed, 18 Aug 2021 21:39:40 +0200 Subject: [PATCH 3/7] Discord timeout 0 clear activity directly --- plugins/discord/back.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/discord/back.js b/plugins/discord/back.js index 57d9902a..c0808e4e 100644 --- a/plugins/discord/back.js +++ b/plugins/discord/back.js @@ -38,6 +38,12 @@ module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => { // stop the clear activity timout clearTimeout(clearActivity); + // clear directly if timeout is 0 + if (songInfo.isPaused && activityTimoutEnabled && activityTimoutTime === 0) { + rpc.clearActivity().catch(console.error); + return; + } + if (songInfo.isPaused) { // Add an idle icon to show that the song is paused activityInfo.smallImageKey = "idle"; From b5fd6b4969a318b3738583e7f33eb2c0cf295237 Mon Sep 17 00:00:00 2001 From: Constantin Piber Date: Fri, 27 Aug 2021 16:32:55 +0200 Subject: [PATCH 4/7] Discord add reconnecting functionality Clear rpc on disconnect Add menu button to reconnect --- plugins/discord/back.js | 183 +++++++++++++++++++++++++++------------- plugins/discord/menu.js | 52 +++++++----- providers/song-info.js | 11 +++ 3 files changed, 168 insertions(+), 78 deletions(-) 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); }; From ef66612cc87e749f702a96248099d7178cad8ae7 Mon Sep 17 00:00:00 2001 From: Constantin Piber Date: Fri, 27 Aug 2021 17:07:23 +0200 Subject: [PATCH 5/7] Discord show error dialog on reconnect error --- plugins/discord/back.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/discord/back.js b/plugins/discord/back.js index 6a8dd007..8f0cc614 100644 --- a/plugins/discord/back.js +++ b/plugins/discord/back.js @@ -1,5 +1,6 @@ const Discord = require("discord-rpc"); -const { dev } = require("electron-is") +const { dev } = require("electron-is"); +const { dialog } = require("electron"); const registerCallback = require("../../providers/song-info"); @@ -32,7 +33,8 @@ const resetInfo = () => { refreshCallbacks.forEach(cb => cb()); }; -const connect = () => { +let window; +const connect = (showErr = false) => { if (info.rpc) { if (dev()) console.log('Attempted to connect with active RPC object'); @@ -58,6 +60,7 @@ const connect = () => { info.rpc.login({ clientId }).catch(err => { resetInfo(); if (dev()) console.error(err); + if (showErr) dialog.showMessageBox(window, { title: 'Connection failed', message: err.message || String(err), type: 'error' }); }); }; @@ -68,6 +71,7 @@ let clearActivity; let updateActivity; module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => { + window = win; // We get multiple events // Next song: PAUSE(n), PAUSE(n+1), PLAY(n+1) // Skip time: PAUSE(N), PLAY(N) From 587818b91ed0730bb75c421f27405a3e1038c4e4 Mon Sep 17 00:00:00 2001 From: Constantin Piber Date: Tue, 5 Oct 2021 10:10:36 +0200 Subject: [PATCH 6/7] Add type, clear on close --- plugins/discord/back.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/discord/back.js b/plugins/discord/back.js index 8f0cc614..478af101 100644 --- a/plugins/discord/back.js +++ b/plugins/discord/back.js @@ -97,7 +97,10 @@ module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => { } // Song information changed, so lets update the rich presence + // @see https://discord.com/developers/docs/topics/gateway#activity-object + // not all options are transfered through https://github.com/discordjs/RPC/blob/6f83d8d812c87cb7ae22064acd132600407d7d05/src/client.js#L518-530 const activityInfo = { + type: 2, // Listening, addressed in https://github.com/discordjs/RPC/pull/149 details: songInfo.title, state: songInfo.artist, largeImageKey: "logo", @@ -133,6 +136,7 @@ module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => { registerCallback(updateActivity); connect(); }); + win.on("close", () => module.exports.clear()); }; module.exports.clear = () => { From b9dbd8bd4def9e9f43f25946228ed495f3365417 Mon Sep 17 00:00:00 2001 From: Constantin Piber Date: Mon, 11 Oct 2021 15:02:24 +0200 Subject: [PATCH 7/7] Allow disable listen along (#426) --- config/defaults.js | 3 ++- config/store.js | 5 +++++ plugins/discord/back.js | 10 +++++----- plugins/discord/menu.js | 9 +++++++++ 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/config/defaults.js b/config/defaults.js index f0533241..552a4995 100644 --- a/config/defaults.js +++ b/config/defaults.js @@ -45,7 +45,8 @@ const defaultConfig = { discord: { enabled: false, activityTimoutEnabled: true, // if enabled, the discord rich presence gets cleared when music paused after the time specified below - activityTimoutTime: 10 * 60 * 1000 // 10 minutes + activityTimoutTime: 10 * 60 * 1000, // 10 minutes + listenAlong: true, // add a "listen along" button to rich presence }, notifications: { enabled: false, diff --git a/config/store.js b/config/store.js index 9c0de1e4..f66c1f9a 100644 --- a/config/store.js +++ b/config/store.js @@ -3,6 +3,11 @@ const Store = require("electron-store"); const defaults = require("./defaults"); const migrations = { + ">=1.13.0": (store) => { + if (store.get("plugins.discord.listenAlong") === undefined) { + store.set("plugins.discord.listenAlong", true); + } + }, ">=1.11.0": (store) => { if (store.get("options.resumeOnStart") === undefined) { store.set("options.resumeOnStart", true); diff --git a/plugins/discord/back.js b/plugins/discord/back.js index 478af101..3c3ab5fd 100644 --- a/plugins/discord/back.js +++ b/plugins/discord/back.js @@ -70,7 +70,7 @@ let clearActivity; */ let updateActivity; -module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => { +module.exports = (win, {activityTimoutEnabled, activityTimoutTime, listenAlong}) => { window = win; // We get multiple events // Next song: PAUSE(n), PAUSE(n+1), PLAY(n+1) @@ -106,11 +106,11 @@ module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => { largeImageKey: "logo", largeImageText: [ songInfo.uploadDate, - songInfo.views.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " views" + songInfo.views.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " views", ].join(' || '), - buttons: [ + buttons: listenAlong ? [ { label: "Listen Along", url: songInfo.url }, - ], + ] : undefined, }; if (songInfo.isPaused) { @@ -119,7 +119,7 @@ module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => { 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); + 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; diff --git a/plugins/discord/menu.js b/plugins/discord/menu.js index 3e8d97e9..9750dabe 100644 --- a/plugins/discord/menu.js +++ b/plugins/discord/menu.js @@ -29,6 +29,15 @@ module.exports = (win, options, refreshMenu) => { setOptions('discord', options); }, }, + { + label: "Listen Along", + type: "checkbox", + checked: options.listenAlong, + click: (item) => { + options.listenAlong = item.checked; + setOptions('discord', options); + }, + }, { label: "Set timeout time in config", // open config.json