diff --git a/index.js b/index.js index 10b6f2d9..39d077bc 100644 --- a/index.js +++ b/index.js @@ -28,8 +28,6 @@ if (config.get("options.disableHardwareAcceleration")) { // Adds debug features like hotkeys for triggering dev tools and reload require("electron-debug")(); -// these are the providers for the plugins, this shouldn't be hardcoded but it's temporarily -const providers = ["song-info"]; // Prevent window being garbage collected let mainWindow; autoUpdater.autoDownload = false; @@ -56,15 +54,6 @@ function loadPlugins(win) { } }); - providers.forEach(provider => { - console.log("Loaded provider - " + provider); - const providerPath = path.join(__dirname, "providers", provider, "back.js"); - fileExists(providerPath, () => { - const handle = require(providerPath); - handle(win); - }); - }); - config.plugins.getEnabled().forEach(([plugin, options]) => { console.log("Loaded plugin - " + plugin); const pluginPath = path.join(__dirname, "plugins", plugin, "back.js"); diff --git a/plugins/discord-rpc/back.js b/plugins/discord-rpc/back.js index d984a300..3e4ab629 100644 --- a/plugins/discord-rpc/back.js +++ b/plugins/discord-rpc/back.js @@ -1,41 +1,51 @@ -const Discord = require('discord-rpc'); +const Discord = require("discord-rpc"); + +const getSongInfo = require("../../providers/song-info"); + const rpc = new Discord.Client({ - transport: 'ipc' + transport: "ipc", }); -const clientId = '790655993809338398'; +// Application ID registered by @semvis123 +const clientId = "790655993809338398"; + +module.exports = (win) => { + const registerCallback = getSongInfo(win); -module.exports = win => { // If the page is ready, register the callback - win.on('ready-to-show', () => { - // Startup the rpc client - rpc.login({ - clientId - }).catch(console.error); + win.on("ready-to-show", () => { + rpc.on("ready", () => { + // Register the callback + registerCallback((songInfo) => { + // Song information changed, so lets update the rich presence + const activityInfo = { + details: songInfo.title, + state: songInfo.artist, + largeImageKey: "logo", + largeImageText: songInfo.views + " - " + songInfo.likes, + }; - // Register the callback - global.songInfo.onNewData(songInfo => { - // Song information changed, so lets update the rich presence + if (songInfo.isPaused) { + // Add an idle icon to show that the song is paused + activityInfo.smallImageKey = "idle"; + activityInfo.smallImageText = "idle/paused"; + } 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; + } - const activityInfo = { - details: songInfo.title, - state: songInfo.artist, - largeImageKey: 'logo', - largeImageText: songInfo.views + ' - ' + songInfo.likes - }; - - if (songInfo.isPaused) { - // Add an idle icon to show that the song is paused - activityInfo.smallImageKey = 'idle'; - activityInfo.smallImageText = 'idle/paused'; - } 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); + rpc.setActivity(activityInfo); + }); }); + + // Startup the rpc client + rpc + .login({ + clientId, + }) + .catch(console.error); }); }; diff --git a/plugins/notifications/back.js b/plugins/notifications/back.js index ac5e6660..d70898cc 100644 --- a/plugins/notifications/back.js +++ b/plugins/notifications/back.js @@ -1,27 +1,31 @@ -const {Notification} = require('electron'); +const { Notification } = require("electron"); -const notify = info => { - let notificationImage = 'assets/youtube-music.png'; +const getSongInfo = require("../../providers/song-info"); + +const notify = (info) => { + let notificationImage = "assets/youtube-music.png"; if (info.image) { - notificationImage = info.image.resize({height: 256, width: 256}); + notificationImage = info.image.resize({ height: 256, width: 256 }); } // Fill the notification with content const notification = { - title: info.title || 'Playing', + title: info.title || "Playing", body: info.artist, icon: notificationImage, - silent: true + silent: true, }; // Send the notification new Notification(notification).show(); }; -module.exports = win => { - win.on('ready-to-show', () => { +module.exports = (win) => { + const registerCallback = getSongInfo(win); + + win.on("ready-to-show", () => { // Register the callback for new song information - global.songInfo.onNewData(songInfo => { + registerCallback((songInfo) => { // If song is playing send notification if (!songInfo.isPaused) { notify(songInfo); diff --git a/plugins/shortcuts/back.js b/plugins/shortcuts/back.js index 5a25a379..36efd0bc 100644 --- a/plugins/shortcuts/back.js +++ b/plugins/shortcuts/back.js @@ -1,12 +1,7 @@ const { globalShortcut } = require("electron"); const electronLocalshortcut = require("electron-localshortcut"); -const { - playPause, - nextTrack, - previousTrack, - startSearch -} = require("./youtube.js"); +const getSongControls = require("../../providers/song-controls"); function _registerGlobalShortcut(webContents, shortcut, action) { globalShortcut.register(shortcut, () => { @@ -21,11 +16,13 @@ function _registerLocalShortcut(win, shortcut, action) { } function registerShortcuts(win) { + const { playPause, next, previous, search } = getSongControls(win); + _registerGlobalShortcut(win.webContents, "MediaPlayPause", playPause); - _registerGlobalShortcut(win.webContents, "MediaNextTrack", nextTrack); - _registerGlobalShortcut(win.webContents, "MediaPreviousTrack", previousTrack); - _registerLocalShortcut(win, "CommandOrControl+F", startSearch); - _registerLocalShortcut(win, "CommandOrControl+L", startSearch); + _registerGlobalShortcut(win.webContents, "MediaNextTrack", next); + _registerGlobalShortcut(win.webContents, "MediaPreviousTrack", previous); + _registerLocalShortcut(win, "CommandOrControl+F", search); + _registerLocalShortcut(win, "CommandOrControl+L", search); } module.exports = registerShortcuts; diff --git a/plugins/shortcuts/youtube.js b/plugins/shortcuts/youtube.js deleted file mode 100644 index 6dd26199..00000000 --- a/plugins/shortcuts/youtube.js +++ /dev/null @@ -1,29 +0,0 @@ -function _keyboardInput(webContents, key) { - return webContents.sendInputEvent({ - type : "keydown", - keyCode: key - }); -} - -function playPause(webContents) { - return _keyboardInput(webContents, "Space"); -} - -function nextTrack(webContents) { - return _keyboardInput(webContents, "j"); -} - -function previousTrack(webContents) { - return _keyboardInput(webContents, "k"); -} - -function startSearch(webContents) { - return _keyboardInput(webContents, "/"); -} - -module.exports = { - playPause : playPause, - nextTrack : nextTrack, - previousTrack: previousTrack, - startSearch : startSearch -}; diff --git a/plugins/touchbar/back.js b/plugins/touchbar/back.js index 5e0a41bf..87240ed0 100644 --- a/plugins/touchbar/back.js +++ b/plugins/touchbar/back.js @@ -1,15 +1,18 @@ -const {TouchBar} = require('electron'); +const { TouchBar } = require("electron"); const { TouchBarButton, TouchBarLabel, TouchBarSpacer, TouchBarSegmentedControl, - TouchBarScrubber + TouchBarScrubber, } = TouchBar; +const getSongInfo = require("../../providers/song-info"); +const getSongControls = require("../../providers/song-controls"); + // Songtitle label const songTitle = new TouchBarLabel({ - label: '' + label: "", }); // This will store the song controls once available let controls = []; @@ -22,23 +25,23 @@ const pausePlayButton = new TouchBarButton(); // The song control buttons (control functions are in the same order) const buttons = new TouchBarSegmentedControl({ - mode: 'buttons', + mode: "buttons", segments: [ new TouchBarButton({ - label: '⏮' + label: "⏮", }), pausePlayButton, new TouchBarButton({ - label: '⏭' + label: "⏭", }), new TouchBarButton({ - label: '👎' + label: "👎", }), new TouchBarButton({ - label: '👍' - }) + label: "👍", + }), ], - change: i => controls[i]() + change: (i) => controls[i](), }); // This is the touchbar object, this combines everything with proper layout @@ -46,38 +49,37 @@ const touchBar = new TouchBar({ items: [ new TouchBarScrubber({ items: [songImage, songTitle], - continuous: false + continuous: false, }), new TouchBarSpacer({ - size: 'flexible' + size: "flexible", }), - buttons - ] + buttons, + ], }); -module.exports = win => { +module.exports = (win) => { + const registerCallback = getSongInfo(win); + const { playPause, next, previous, like, dislike } = getSongControls(win); + // If the page is ready, register the callback - win.on('ready-to-show', () => { - controls = [ - global.songControls.previous, - global.songControls.pause, - global.songControls.next, - global.songControls.like, - global.songControls.dislike - ]; + win.on("ready-to-show", () => { + controls = [previous, playPause, next, like, dislike]; // Register the callback - global.songInfo.onNewData(songInfo => { + registerCallback((songInfo) => { // Song information changed, so lets update the touchBar // Set the song title songTitle.label = songInfo.title; // Changes the pause button if paused - pausePlayButton.label = songInfo.isPaused ? '▶️' : '⏸'; + pausePlayButton.label = songInfo.isPaused ? "▶️" : "⏸"; // Get image source - songImage.icon = songInfo.image ? songInfo.image.resize({height: 23}) : null; + songImage.icon = songInfo.image + ? songInfo.image.resize({ height: 23 }) + : null; win.setTouchBar(touchBar); }); diff --git a/providers/song-controls.js b/providers/song-controls.js new file mode 100644 index 00000000..20946a38 --- /dev/null +++ b/providers/song-controls.js @@ -0,0 +1,18 @@ +// This is used for to control the songs +const pressKey = (window, key) => { + window.webContents.sendInputEvent({ + type: "keydown", + keyCode: key, + }); +}; + +module.exports = (win) => { + return { + previous: () => pressKey(win, "k"), + next: () => pressKey(win, "j"), + playPause: () => pressKey(win, "space"), + like: () => pressKey(win, "_"), + dislike: () => pressKey(win, "+"), + search: () => pressKey(win, "/"), + }; +}; diff --git a/providers/song-info.js b/providers/song-info.js new file mode 100644 index 00000000..4ddf9cab --- /dev/null +++ b/providers/song-info.js @@ -0,0 +1,137 @@ +const { nativeImage } = require("electron"); + +const fetch = require("node-fetch"); + +// This selects the song title +const titleSelector = ".title.style-scope.ytmusic-player-bar"; + +// This selects the song image +const imageSelector = + "#layout > ytmusic-player-bar > div.middle-controls.style-scope.ytmusic-player-bar > img"; + +// This selects the song subinfo, this includes artist, views, likes +const subInfoSelector = + "#layout > ytmusic-player-bar > div.middle-controls.style-scope.ytmusic-player-bar > div.content-info-wrapper.style-scope.ytmusic-player-bar > span"; + +// This selects the progress bar, used for songlength and current progress +const progressSelector = "#progress-bar"; + +// Grab the title using the selector +const getTitle = (win) => { + return win.webContents + .executeJavaScript( + "document.querySelector('" + titleSelector + "').innerText" + ) + .catch((error) => { + console.log(error); + }); +}; + +// Grab the image src using the selector +const getImageSrc = (win) => { + return win.webContents + .executeJavaScript("document.querySelector('" + imageSelector + "').src") + .catch((error) => { + console.log(error); + }); +}; + +// Grab the subinfo using the selector +const getSubInfo = async (win) => { + // Get innerText of subinfo element + const subInfoString = await win.webContents.executeJavaScript( + 'document.querySelector("' + subInfoSelector + '").innerText' + ); + + // Split and clean the string + const splittedSubInfo = subInfoString.replaceAll("\n", "").split(" • "); + + // Make sure we always return 3 elements in the aray + const subInfo = []; + for (let i = 0; i < 3; i++) { + // Fill array with empty string if not defined + subInfo.push(splittedSubInfo[i] || ""); + } + + return subInfo; +}; + +// Grab the progress using the selector +const getProgress = async (win) => { + // Get max value of the progressbar element + const songDuration = await win.webContents.executeJavaScript( + 'document.querySelector("' + progressSelector + '").max' + ); + // Get current value of the progressbar element + const elapsedSeconds = await win.webContents.executeJavaScript( + 'document.querySelector("' + progressSelector + '").value' + ); + + return { songDuration, elapsedSeconds }; +}; + +// Grab the native image using the src +const getImage = async (src) => { + const result = await fetch(src); + const buffer = await result.buffer(); + return nativeImage.createFromBuffer(buffer); +}; + +const getPausedStatus = async (win) => { + const title = await win.webContents.executeJavaScript("document.title"); + return !title.includes("-"); +}; + +// Fill songInfo with empty values +const songInfo = { + title: "", + artist: "", + views: "", + likes: "", + imageSrc: "", + image: null, + isPaused: true, + songDuration: 0, + elapsedSeconds: 0, +}; + +const registerProvider = (win) => { + // This variable will be filled with the callbacks once they register + const callbacks = []; + + // This function will allow plugins to register callback that will be triggered when data changes + const registerCallback = (callback) => { + callbacks.push(callback); + }; + + win.on("page-title-updated", async () => { + // Save the old title temporarily + const oldTitle = songInfo.title; + // Get and set the new data + songInfo.title = await getTitle(win); + songInfo.isPaused = await getPausedStatus(win); + + const { songDuration, elapsedSeconds } = await getProgress(win); + songInfo.songDuration = songDuration; + songInfo.elapsedSeconds = elapsedSeconds; + + // If title changed then we do need to update other info + if (oldTitle !== songInfo.title) { + const subInfo = await getSubInfo(win); + songInfo.artist = subInfo[0]; + songInfo.views = subInfo[1]; + songInfo.likes = subInfo[2]; + songInfo.imageSrc = await getImageSrc(win); + songInfo.image = await getImage(songInfo.imageSrc); + } + + // Trigger the callbacks + callbacks.forEach((c) => { + c(songInfo); + }); + }); + + return registerCallback; +}; + +module.exports = registerProvider; diff --git a/providers/song-info/back.js b/providers/song-info/back.js deleted file mode 100644 index 117ec947..00000000 --- a/providers/song-info/back.js +++ /dev/null @@ -1,141 +0,0 @@ -const {nativeImage} = require('electron'); -const fetch = require('node-fetch'); - -// This selects the song title -const titleSelector = '.title.style-scope.ytmusic-player-bar'; - -// This selects the song image -const imageSelector = '#layout > ytmusic-player-bar > div.middle-controls.style-scope.ytmusic-player-bar > img'; - -// This selects the song subinfo, this includes artist, views, likes -const subInfoSelector = '#layout > ytmusic-player-bar > div.middle-controls.style-scope.ytmusic-player-bar > div.content-info-wrapper.style-scope.ytmusic-player-bar > span'; - -// This selects the progress bar, used for songlength and current progress -const progressSelector = '#progress-bar'; - -// This is used for to control the songs -const presskey = (window, key) => { - window.webContents.sendInputEvent({ - type: 'keydown', - keyCode: key - }); -}; - -// Grab the title using the selector -const getTitle = win => { - return win.webContents.executeJavaScript( - 'document.querySelector(\'' + titleSelector + '\').innerText' - ).catch(error => { - console.log(error); - }); -}; - -// Grab the image src using the selector -const getImageSrc = win => { - return win.webContents.executeJavaScript( - 'document.querySelector(\'' + imageSelector + '\').src' - ).catch(error => { - console.log(error); - }); -}; - -// Grab the subinfo using the selector -const getSubInfo = async win => { - // Get innerText of subinfo element - const subInfoString = await win.webContents.executeJavaScript( - 'document.querySelector("' + subInfoSelector + '").innerText'); - - // Split and clean the string - const splittedSubInfo = subInfoString.replaceAll('\n', '').split(' • '); - - // Make sure we always return 3 elements in the aray - const subInfo = []; - for (let i = 0; i < 3; i++) { - // Fill array with empty string if not defined - subInfo.push(splittedSubInfo[i] || ''); - } - - return subInfo; -}; - -// Grab the progress using the selector -const getProgress = async win => { - // Get max value of the progressbar element - const songDuration = await win.webContents.executeJavaScript( - 'document.querySelector("' + progressSelector + '").max'); - // Get current value of the progressbar element - const elapsedSeconds = await win.webContents.executeJavaScript( - 'document.querySelector("' + progressSelector + '").value'); - - return {songDuration, elapsedSeconds}; -}; - -// Grab the native image using the src -const getImage = async src => { - const result = await fetch(src); - const buffer = await result.buffer(); - return nativeImage.createFromBuffer(buffer); -}; - -const getPausedStatus = async win => { - const title = await win.webContents.executeJavaScript('document.title'); - return !title.includes('-'); -}; - -// This variable will be filled with the callbacks once they register -const callbacks = []; - -module.exports = win => { - // Fill songInfo with empty values - global.songInfo = { - title: '', - artist: '', - views: '', - likes: '', - imageSrc: '', - image: null, - isPaused: true, - songDuration: 0, - elapsedSeconds: 0 - }; - // The song control functions - global.songControls = { - previous: () => presskey(win, 'k'), - next: () => presskey(win, 'j'), - pause: () => presskey(win, 'space'), - like: () => presskey(win, '_'), - dislike: () => presskey(win, '+') - }; - - // This function will allow plugins to register callback that will be triggered when data changes - global.songInfo.onNewData = callback => { - callbacks.push(callback); - }; - - win.on('page-title-updated', async () => { - // Save the old title temporarily - const oldTitle = global.songInfo.title; - // Get and set the new data - global.songInfo.title = await getTitle(win); - global.songInfo.isPaused = await getPausedStatus(win); - - const {songDuration, elapsedSeconds} = await getProgress(win); - global.songInfo.songDuration = songDuration; - global.songInfo.elapsedSeconds = elapsedSeconds; - - // If title changed then we do need to update other info - if (oldTitle !== global.songInfo.title) { - const subInfo = await getSubInfo(win); - global.songInfo.artist = subInfo[0]; - global.songInfo.views = subInfo[1]; - global.songInfo.likes = subInfo[2]; - global.songInfo.imageSrc = await getImageSrc(win); - global.songInfo.image = await getImage(global.songInfo.imageSrc); - } - - // Trigger the callbacks - callbacks.forEach(c => { - c(global.songInfo); - }); - }); -};