mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 02:31:45 +00:00
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:
11
index.js
11
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");
|
||||
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -1,33 +1,31 @@
|
||||
const { nativeImage, Notification } = require("electron");
|
||||
const {Notification} = require('electron');
|
||||
|
||||
const { listenAction } = require("../utils");
|
||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||
const notify = info => {
|
||||
let notificationImage = 'assets/youtube-music.png';
|
||||
|
||||
function notify(info) {
|
||||
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;
|
||||
};
|
||||
|
||||
@ -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;
|
||||
@ -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: ''
|
||||
});
|
||||
// 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
|
||||
];
|
||||
|
||||
// This will store the song image once available
|
||||
const songImage = {};
|
||||
// Register the callback
|
||||
global.songInfo.onNewData(songInfo => {
|
||||
// Song information changed, so lets update the touchBar
|
||||
|
||||
// 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])
|
||||
});
|
||||
// Set the song title
|
||||
songTitle.label = songInfo.title;
|
||||
|
||||
// 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
|
||||
]
|
||||
});
|
||||
// Changes the pause button if paused
|
||||
pausePlayButton.label = songInfo.isPaused ? '▶️' : '⏸';
|
||||
|
||||
// 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);
|
||||
// Get image source
|
||||
songImage.icon = songInfo.image ? songInfo.image.resize({height: 23}) : null;
|
||||
|
||||
// Get image source
|
||||
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);
|
||||
win.setTouchBar(touchBar);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
120
providers/song-info/back.js
Normal file
120
providers/song-info/back.js
Normal 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);
|
||||
});
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user