From 9be3e1afe91f0aa3419040bba65e7b3b83b469c6 Mon Sep 17 00:00:00 2001 From: Sem Vissscher Date: Mon, 21 Dec 2020 18:18:34 +0100 Subject: [PATCH 1/7] Globalized the songinfo and song controls, and changed the pause/play button. Globalized the songinfo and song controls, and changed the pause/play button. The songInfo file should eventually get another location, because it isn't really a plugin. --- plugins/songInfo/back.js | 120 ++++++++++++++++++++++++++++++ plugins/touchbar/back.js | 153 ++++++++++++++++----------------------- 2 files changed, 184 insertions(+), 89 deletions(-) create mode 100644 plugins/songInfo/back.js diff --git a/plugins/songInfo/back.js b/plugins/songInfo/back.js new file mode 100644 index 000000000..94d7618ae --- /dev/null +++ b/plugins/songInfo/back.js @@ -0,0 +1,120 @@ +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 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 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 = async win => { + // Fill songInfo with empty values + global.songInfo = { + title: '', + artist: '', + views: '', + likes: '', + imageSrc: '', + image: null, + isPaused: true + }; + // The song control funcions + 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); + + // 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); + }); + }); +}; diff --git a/plugins/touchbar/back.js b/plugins/touchbar/back.js index e165950ff..5e0a41bf7 100644 --- a/plugins/touchbar/back.js +++ b/plugins/touchbar/back.js @@ -1,6 +1,4 @@ -const { - TouchBar, nativeImage -} = require('electron'); +const {TouchBar} = require('electron'); const { TouchBarButton, TouchBarLabel, @@ -8,103 +6,80 @@ const { TouchBarSegmentedControl, TouchBarScrubber } = TouchBar; -const fetch = require('node-fetch'); -// This selects the song title -const titleSelector = '.title.style-scope.ytmusic-player-bar'; +// Songtitle label +const songTitle = new TouchBarLabel({ + label: '' +}); +// This will store the song controls once available +let controls = []; -// This selects the song image -const imageSelector = '#layout > ytmusic-player-bar > div.middle-controls.style-scope.ytmusic-player-bar > img'; +// This will store the song image once available +const songImage = {}; -// These keys will be used to go backwards, pause, skip songs, like songs, dislike songs -const keys = ['k', 'space', 'j', '_', '+']; +// Pause/play button +const pausePlayButton = new TouchBarButton(); -const presskey = (window, key) => { - window.webContents.sendInputEvent({ - type: 'keydown', - keyCode: key - }); -}; +// The song control buttons (control functions are in the same order) +const buttons = new TouchBarSegmentedControl({ + mode: 'buttons', + segments: [ + new TouchBarButton({ + label: '⏮' + }), + pausePlayButton, + new TouchBarButton({ + label: '⏭' + }), + new TouchBarButton({ + label: '👎' + }), + new TouchBarButton({ + label: '👍' + }) + ], + change: i => controls[i]() +}); -// 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 getImage = win => { - return win.webContents.executeJavaScript( - 'document.querySelector(\'' + imageSelector + '\').src' - ).catch(error => { - console.log(error); - }); -}; +// This is the touchbar object, this combines everything with proper layout +const touchBar = new TouchBar({ + items: [ + new TouchBarScrubber({ + items: [songImage, songTitle], + continuous: false + }), + new TouchBarSpacer({ + size: 'flexible' + }), + buttons + ] +}); module.exports = win => { - // Songtitle label - const songTitle = new TouchBarLabel({ - label: '' - }); - - // This will store the song image once available - const songImage = {}; + // 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 + ]; - // The song control buttons (keys to press are in the same order) - const buttons = new TouchBarSegmentedControl({ - mode: 'buttons', - segments: [ - new TouchBarButton({ - label: '⏮' - }), - new TouchBarButton({ - label: '⏯️' - }), - new TouchBarButton({ - label: '⏭' - }), - new TouchBarButton({ - label: '👎' - }), - new TouchBarButton({ - label: '👍' - }) - ], - change: i => presskey(win, keys[i]) - }); - - // This is the touchbar object, this combines everything with proper layout - const touchBar = new TouchBar({ - items: [ - new TouchBarScrubber({ - items: [songImage, songTitle], - continuous: false - }), - new TouchBarSpacer({ - size: 'flexible' - }), - buttons - ] - }); + // Register the callback + global.songInfo.onNewData(songInfo => { + // Song information changed, so lets update the touchBar - // If the page title changes, update touchbar and song title - win.on('page-title-updated', async () => { - // Set the song title - songTitle.label = await getTitle(win); + // Set the song title + songTitle.label = songInfo.title; - // Get image source - const imageSrc = await getImage(win); + // Changes the pause button if paused + pausePlayButton.label = songInfo.isPaused ? '▶️' : '⏸'; - // Fetch and set song image - await fetch(imageSrc) - .then(response => response.buffer()) - .then(data => { - songImage.icon = nativeImage.createFromBuffer(data).resize({height: 23}); - }); + // Get image source + songImage.icon = songInfo.image ? songInfo.image.resize({height: 23}) : null; - win.setTouchBar(touchBar); + win.setTouchBar(touchBar); + }); }); }; From ee239da64740861f9acb14483d048bf98fe4fb4e Mon Sep 17 00:00:00 2001 From: Sem Vissscher Date: Mon, 21 Dec 2020 21:11:00 +0100 Subject: [PATCH 2/7] removed unnecessary await keyword --- plugins/songInfo/back.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/songInfo/back.js b/plugins/songInfo/back.js index 94d7618ae..a9bed9133 100644 --- a/plugins/songInfo/back.js +++ b/plugins/songInfo/back.js @@ -70,7 +70,7 @@ const getPausedStatus = async win => { // This variable will be filled with the callbacks once they register const callbacks = []; -module.exports = async win => { +module.exports = win => { // Fill songInfo with empty values global.songInfo = { title: '', From 5bffdbd6285a6816749c467d6e912d14748f9959 Mon Sep 17 00:00:00 2001 From: Sem Vissscher Date: Mon, 21 Dec 2020 21:35:02 +0100 Subject: [PATCH 3/7] Simplifies the notification plugin to use the globalized song info --- plugins/notifications/actions.js | 18 ------- plugins/notifications/back.js | 38 +++++++------- plugins/notifications/front.js | 86 -------------------------------- 3 files changed, 18 insertions(+), 124 deletions(-) delete mode 100644 plugins/notifications/actions.js delete mode 100644 plugins/notifications/front.js diff --git a/plugins/notifications/actions.js b/plugins/notifications/actions.js deleted file mode 100644 index 15e3f4296..000000000 --- a/plugins/notifications/actions.js +++ /dev/null @@ -1,18 +0,0 @@ -const { triggerAction } = require("../utils"); - -const CHANNEL = "notification"; -const ACTIONS = { - NOTIFICATION: "notification", -}; - -function notify(info) { - triggerAction(CHANNEL, ACTIONS.NOTIFICATION, info); -} - -module.exports = { - CHANNEL, - ACTIONS, - global: { - notify, - }, -}; diff --git a/plugins/notifications/back.js b/plugins/notifications/back.js index 54dac482f..108456def 100644 --- a/plugins/notifications/back.js +++ b/plugins/notifications/back.js @@ -1,33 +1,31 @@ -const { nativeImage, Notification } = require("electron"); - -const { listenAction } = require("../utils"); -const { ACTIONS, CHANNEL } = require("./actions.js"); +const {Notification} = require('electron'); function notify(info) { - let notificationImage = "assets/youtube-music.png"; + let notificationImage = 'assets/youtube-music.png'; + if (info.image) { - notificationImage = nativeImage.createFromDataURL(info.image); + 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(); } -function listenAndNotify() { - listenAction(CHANNEL, (event, action, imageSrc) => { - switch (action) { - case ACTIONS.NOTIFICATION: - notify(imageSrc); - break; - default: - console.log("Unknown action: " + action); - } +module.exports = win => { + win.on('ready-to-show', () => { + // Register the callback for new song information + global.songInfo.onNewData(songInfo => { + // If song is playing send notification + if (!songInfo.isPaused) { + notify(songInfo); + } + }); }); -} - -module.exports = listenAndNotify; +}; diff --git a/plugins/notifications/front.js b/plugins/notifications/front.js deleted file mode 100644 index ed07c7018..000000000 --- a/plugins/notifications/front.js +++ /dev/null @@ -1,86 +0,0 @@ -let videoElement = null; -let image = null; - -const observer = new MutationObserver((mutations, observer) => { - if (!videoElement) { - videoElement = document.querySelector("video"); - } - - if (!image) { - image = document.querySelector(".ytmusic-player-bar.image"); - } - - if (videoElement !== null && image !== null) { - observer.disconnect(); - let notificationImage = null; - - videoElement.addEventListener("play", () => { - notify({ - title: getTitle(), - artist: getArtist(), - image: notificationImage, - }); - }); - - image.addEventListener("load", () => { - notificationImage = null; - const imageInBase64 = convertImageToBase64(image); - if (image && image.complete && image.naturalHeight !== 0) { - notificationImage = imageInBase64; - } - }); - } -}); - -// Convert an image (DOM element) to base64 string -const convertImageToBase64 = (image, size = 256) => { - image.setAttribute("crossorigin", "anonymous"); - - const c = document.createElement("canvas"); - c.height = size; - c.width = size; - - const ctx = c.getContext("2d"); - ctx.drawImage( - image, - 0, - 0, - image.naturalWidth, - image.naturalHeight, - 0, - 0, - c.width, - c.height - ); - - const imageInBase64 = c.toDataURL(); - return imageInBase64; -}; - -const getTitle = () => { - const title = document.querySelector(".title.ytmusic-player-bar").textContent; - return title; -}; - -const getArtist = () => { - const bar = document.querySelectorAll(".subtitle.ytmusic-player-bar")[0]; - let artist; - - if (bar.querySelectorAll(".yt-simple-endpoint.yt-formatted-string")[0]) { - artist = bar.querySelectorAll(".yt-simple-endpoint.yt-formatted-string")[0] - .textContent; - } else if (bar.querySelectorAll(".byline.ytmusic-player-bar")[0]) { - artist = bar.querySelectorAll(".byline.ytmusic-player-bar")[0].textContent; - } - - return artist; -}; - -const observeVideoAndThumbnail = () => { - observer.observe(document, { - childList: true, - subtree: true, - }); -}; - -module.exports = observeVideoAndThumbnail; From 588e0019d69d5478a98a78d5a1773bf013c3bdc9 Mon Sep 17 00:00:00 2001 From: semvis123 Date: Mon, 21 Dec 2020 22:11:23 +0100 Subject: [PATCH 4/7] Changed function style in notifications --- plugins/notifications/back.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/notifications/back.js b/plugins/notifications/back.js index 108456def..ac5e66608 100644 --- a/plugins/notifications/back.js +++ b/plugins/notifications/back.js @@ -1,6 +1,6 @@ const {Notification} = require('electron'); -function notify(info) { +const notify = info => { let notificationImage = 'assets/youtube-music.png'; if (info.image) { @@ -16,7 +16,7 @@ function notify(info) { }; // Send the notification new Notification(notification).show(); -} +}; module.exports = win => { win.on('ready-to-show', () => { From 5d8904388441bff8656dda1bac47eb77718f1b0e Mon Sep 17 00:00:00 2001 From: semvis123 Date: Sun, 10 Jan 2021 20:31:55 +0100 Subject: [PATCH 5/7] fixed typo, plugins/songInfo/back.js Co-authored-by: th-ch --- plugins/songInfo/back.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/songInfo/back.js b/plugins/songInfo/back.js index a9bed9133..c7c78a19a 100644 --- a/plugins/songInfo/back.js +++ b/plugins/songInfo/back.js @@ -81,7 +81,7 @@ module.exports = win => { image: null, isPaused: true }; - // The song control funcions + // The song control functions global.songControls = { previous: () => presskey(win, 'k'), next: () => presskey(win, 'j'), From 69f486d53f1630d26a725c4b032c47828033d183 Mon Sep 17 00:00:00 2001 From: semvis123 Date: Sun, 10 Jan 2021 21:22:01 +0100 Subject: [PATCH 6/7] moved the song info file and removed the capital letters in folder name --- {plugins/songInfo => providers/song-info}/back.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {plugins/songInfo => providers/song-info}/back.js (100%) diff --git a/plugins/songInfo/back.js b/providers/song-info/back.js similarity index 100% rename from plugins/songInfo/back.js rename to providers/song-info/back.js From 3a5d9bd973bdd67e77f8a7687c1430245a9490bd Mon Sep 17 00:00:00 2001 From: semvis123 Date: Sun, 10 Jan 2021 21:37:50 +0100 Subject: [PATCH 7/7] Loads providers before plugins --- index.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/index.js b/index.js index bf3663ec7..814f3b764 100644 --- a/index.js +++ b/index.js @@ -28,6 +28,8 @@ 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; @@ -54,6 +56,15 @@ 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");