Merge pull request #102 from semvis123/master

Globalized the song info and song controls, and updated Touch Bar for it.
This commit is contained in:
th-ch
2021-01-10 22:15:11 +01:00
committed by GitHub
6 changed files with 214 additions and 214 deletions

View File

@ -28,6 +28,8 @@ if (config.get("options.disableHardwareAcceleration")) {
// Adds debug features like hotkeys for triggering dev tools and reload // Adds debug features like hotkeys for triggering dev tools and reload
require("electron-debug")(); 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 // Prevent window being garbage collected
let mainWindow; let mainWindow;
autoUpdater.autoDownload = false; 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]) => { config.plugins.getEnabled().forEach(([plugin, options]) => {
console.log("Loaded plugin - " + plugin); console.log("Loaded plugin - " + plugin);
const pluginPath = path.join(__dirname, "plugins", plugin, "back.js"); const pluginPath = path.join(__dirname, "plugins", plugin, "back.js");

View File

@ -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,
},
};

View File

@ -1,33 +1,31 @@
const { nativeImage, Notification } = require("electron"); const {Notification} = require('electron');
const { listenAction } = require("../utils"); const notify = info => {
const { ACTIONS, CHANNEL } = require("./actions.js"); let notificationImage = 'assets/youtube-music.png';
function notify(info) {
let notificationImage = "assets/youtube-music.png";
if (info.image) { if (info.image) {
notificationImage = nativeImage.createFromDataURL(info.image); notificationImage = info.image.resize({height: 256, width: 256});
} }
// Fill the notification with content
const notification = { const notification = {
title: info.title || "Playing", title: info.title || 'Playing',
body: info.artist, body: info.artist,
icon: notificationImage, icon: notificationImage,
silent: true, silent: true
}; };
// Send the notification
new Notification(notification).show(); new Notification(notification).show();
} };
function listenAndNotify() { module.exports = win => {
listenAction(CHANNEL, (event, action, imageSrc) => { win.on('ready-to-show', () => {
switch (action) { // Register the callback for new song information
case ACTIONS.NOTIFICATION: global.songInfo.onNewData(songInfo => {
notify(imageSrc); // If song is playing send notification
break; if (!songInfo.isPaused) {
default: notify(songInfo);
console.log("Unknown action: " + action); }
} });
}); });
} };
module.exports = listenAndNotify;

View File

@ -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;

View File

@ -1,6 +1,4 @@
const { const {TouchBar} = require('electron');
TouchBar, nativeImage
} = require('electron');
const { const {
TouchBarButton, TouchBarButton,
TouchBarLabel, TouchBarLabel,
@ -8,103 +6,80 @@ const {
TouchBarSegmentedControl, TouchBarSegmentedControl,
TouchBarScrubber TouchBarScrubber
} = TouchBar; } = TouchBar;
const fetch = require('node-fetch');
// This selects the song title // Songtitle label
const titleSelector = '.title.style-scope.ytmusic-player-bar'; const songTitle = new TouchBarLabel({
label: ''
});
// This will store the song controls once available
let controls = [];
// This selects the song image // This will store the song image once available
const imageSelector = '#layout > ytmusic-player-bar > div.middle-controls.style-scope.ytmusic-player-bar > img'; const songImage = {};
// These keys will be used to go backwards, pause, skip songs, like songs, dislike songs // Pause/play button
const keys = ['k', 'space', 'j', '_', '+']; const pausePlayButton = new TouchBarButton();
const presskey = (window, key) => { // The song control buttons (control functions are in the same order)
window.webContents.sendInputEvent({ const buttons = new TouchBarSegmentedControl({
type: 'keydown', mode: 'buttons',
keyCode: key segments: [
}); new TouchBarButton({
}; label: '⏮'
}),
pausePlayButton,
new TouchBarButton({
label: '⏭'
}),
new TouchBarButton({
label: '👎'
}),
new TouchBarButton({
label: '👍'
})
],
change: i => controls[i]()
});
// Grab the title using the selector // This is the touchbar object, this combines everything with proper layout
const getTitle = win => { const touchBar = new TouchBar({
return win.webContents.executeJavaScript( items: [
'document.querySelector(\'' + titleSelector + '\').innerText' new TouchBarScrubber({
).catch(error => { items: [songImage, songTitle],
console.log(error); continuous: false
}); }),
}; new TouchBarSpacer({
size: 'flexible'
// Grab the image src using the selector }),
const getImage = win => { buttons
return win.webContents.executeJavaScript( ]
'document.querySelector(\'' + imageSelector + '\').src' });
).catch(error => {
console.log(error);
});
};
module.exports = win => { module.exports = win => {
// Songtitle label // If the page is ready, register the callback
const songTitle = new TouchBarLabel({ win.on('ready-to-show', () => {
label: '' controls = [
}); global.songControls.previous,
global.songControls.pause,
global.songControls.next,
global.songControls.like,
global.songControls.dislike
];
// This will store the song image once available // Register the callback
const songImage = {}; global.songInfo.onNewData(songInfo => {
// Song information changed, so lets update the touchBar
// The song control buttons (keys to press are in the same order) // Set the song title
const buttons = new TouchBarSegmentedControl({ songTitle.label = songInfo.title;
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 // Changes the pause button if paused
const touchBar = new TouchBar({ pausePlayButton.label = songInfo.isPaused ? '▶️' : '⏸';
items: [
new TouchBarScrubber({
items: [songImage, songTitle],
continuous: false
}),
new TouchBarSpacer({
size: 'flexible'
}),
buttons
]
});
// If the page title changes, update touchbar and song title // Get image source
win.on('page-title-updated', async () => { songImage.icon = songInfo.image ? songInfo.image.resize({height: 23}) : null;
// Set the song title
songTitle.label = await getTitle(win);
// Get image source win.setTouchBar(touchBar);
const imageSrc = await getImage(win); });
// Fetch and set song image
await fetch(imageSrc)
.then(response => response.buffer())
.then(data => {
songImage.icon = nativeImage.createFromBuffer(data).resize({height: 23});
});
win.setTouchBar(touchBar);
}); });
}; };

120
providers/song-info/back.js Normal file
View File

@ -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 = win => {
// Fill songInfo with empty values
global.songInfo = {
title: '',
artist: '',
views: '',
likes: '',
imageSrc: '',
image: null,
isPaused: true
};
// 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);
// 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);
});
});
};