mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 18:41:47 +00:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 644950ccc0 | |||
| d848937e6a | |||
| 446aa8becc | |||
| d52dbee13c | |||
| ed09304ed7 | |||
| 2861473097 | |||
| 25fd48697b | |||
| aec542e95e | |||
| eae95befe1 | |||
| f0200e7b38 | |||
| 70775f4988 | |||
| 074840ef56 | |||
| b54c501eeb | |||
| 6d587cb432 | |||
| 39c8031cd7 | |||
| 9ad1dad6df | |||
| 79e8fc2fac | |||
| 67c4422eb8 | |||
| 18df1223af | |||
| f7cbf2c221 | |||
| 87d2693e2b | |||
| de1e4196d9 | |||
| 9110e79c16 | |||
| 0743034de0 | |||
| f1ddb92886 | |||
| a8ce87f2cc | |||
| 86a329a61b |
@ -12,6 +12,7 @@ const defaultConfig = {
|
||||
startAtLogin: false,
|
||||
disableHardwareAcceleration: false,
|
||||
restartOnConfigChanges: false,
|
||||
trayClickPlayPause: false,
|
||||
},
|
||||
plugins: {
|
||||
// Enabled plugins
|
||||
|
||||
15
index.js
15
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");
|
||||
@ -86,7 +75,9 @@ function createMainWindow() {
|
||||
backgroundColor: "#000",
|
||||
show: false,
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
// TODO: re-enable contextIsolation once it can work with ffmepg.wasm
|
||||
// Possible bundling? https://github.com/ffmpegwasm/ffmpeg.wasm/issues/126
|
||||
contextIsolation: false,
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
nodeIntegrationInSubFrames: true,
|
||||
nativeWindowOpen: true, // window.open return Window object(like in regular browsers), not BrowserWindowProxy
|
||||
|
||||
9
menu.js
9
menu.js
@ -116,6 +116,15 @@ const mainMenuTemplate = (win) => [
|
||||
config.set("options.appVisible", false);
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Play/Pause on click",
|
||||
type: "checkbox",
|
||||
checked: config.get("options.trayClickPlayPause"),
|
||||
click: (item) => {
|
||||
config.set("options.trayClickPlayPause", item.checked);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ type: "separator" },
|
||||
|
||||
16
package.json
16
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "youtube-music",
|
||||
"productName": "YouTube Music",
|
||||
"version": "1.8.1",
|
||||
"version": "1.9.0",
|
||||
"description": "YouTube Music Desktop App - including custom plugins",
|
||||
"license": "MIT",
|
||||
"repository": "th-ch/youtube-music",
|
||||
@ -18,12 +18,19 @@
|
||||
"icon": "assets/generated/icons/mac/icon.icns"
|
||||
},
|
||||
"win": {
|
||||
"icon": "assets/generated/icons/win/icon.ico"
|
||||
"icon": "assets/generated/icons/win/icon.ico",
|
||||
"target": ["nsis", "portable"]
|
||||
},
|
||||
"linux": {
|
||||
"icon": "assets/generated/icons/png",
|
||||
"category": "AudioVideo",
|
||||
"target": ["AppImage", "snap", "freebsd", "deb", "rpm"]
|
||||
"target": [
|
||||
"AppImage",
|
||||
"snap",
|
||||
"freebsd",
|
||||
"deb",
|
||||
"rpm"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
@ -53,8 +60,9 @@
|
||||
"@ffmpeg/core": "^0.8.5",
|
||||
"@ffmpeg/ffmpeg": "^0.9.6",
|
||||
"YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.8.0",
|
||||
"discord-rpc": "^3.1.4",
|
||||
"downloads-folder": "^3.0.1",
|
||||
"electron-debug": "^3.1.0",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-is": "^3.0.0",
|
||||
"electron-localshortcut": "^3.2.1",
|
||||
"electron-store": "^6.0.1",
|
||||
|
||||
51
plugins/discord/back.js
Normal file
51
plugins/discord/back.js
Normal file
@ -0,0 +1,51 @@
|
||||
const Discord = require("discord-rpc");
|
||||
|
||||
const getSongInfo = require("../../providers/song-info");
|
||||
|
||||
const rpc = new Discord.Client({
|
||||
transport: "ipc",
|
||||
});
|
||||
|
||||
// Application ID registered by @semvis123
|
||||
const clientId = "790655993809338398";
|
||||
|
||||
module.exports = (win) => {
|
||||
const registerCallback = getSongInfo(win);
|
||||
|
||||
// If the page is ready, register the callback
|
||||
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,
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
// Startup the rpc client
|
||||
rpc
|
||||
.login({
|
||||
clientId,
|
||||
})
|
||||
.catch(console.error);
|
||||
});
|
||||
};
|
||||
@ -1,6 +1,7 @@
|
||||
const CHANNEL = "downloader";
|
||||
const ACTIONS = {
|
||||
ERROR: "error",
|
||||
METADATA: "metadata",
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
@ -2,6 +2,7 @@ const { join } = require("path");
|
||||
|
||||
const { dialog } = require("electron");
|
||||
|
||||
const getSongInfo = require("../../providers/song-info");
|
||||
const { injectCSS, listenAction } = require("../utils");
|
||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||
|
||||
@ -16,14 +17,26 @@ const sendError = (win, err) => {
|
||||
dialog.showMessageBox(dialogOpts);
|
||||
};
|
||||
|
||||
let metadata = {};
|
||||
|
||||
function handle(win) {
|
||||
injectCSS(win.webContents, join(__dirname, "style.css"));
|
||||
const registerCallback = getSongInfo(win);
|
||||
registerCallback((info) => {
|
||||
metadata = {
|
||||
...info,
|
||||
image: info.image ? info.image.toDataURL() : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
listenAction(CHANNEL, (event, action, error) => {
|
||||
switch (action) {
|
||||
case ACTIONS.ERROR:
|
||||
sendError(win, error);
|
||||
break;
|
||||
case ACTIONS.METADATA:
|
||||
event.returnValue = JSON.stringify(metadata);
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown action: " + action);
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
const { contextBridge } = require("electron");
|
||||
|
||||
const { ElementFromFile, templatePath, triggerAction } = require("../utils");
|
||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||
const { downloadVideoToMP3 } = require("./youtube-dl");
|
||||
@ -28,6 +30,9 @@ const reinit = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: re-enable once contextIsolation is set to true
|
||||
// contextBridge.exposeInMainWorld("downloader", {
|
||||
// download: () => {
|
||||
global.download = () => {
|
||||
const videoUrl = window.location.href;
|
||||
|
||||
@ -48,6 +53,7 @@ global.download = () => {
|
||||
pluginOptions
|
||||
);
|
||||
};
|
||||
// });
|
||||
|
||||
function observeMenu(options) {
|
||||
pluginOptions = { ...pluginOptions, ...options };
|
||||
|
||||
@ -12,6 +12,9 @@ const filenamify = require("filenamify");
|
||||
const FFmpeg = require("@ffmpeg/ffmpeg/dist/ffmpeg.min");
|
||||
const ytdl = require("ytdl-core");
|
||||
|
||||
const { triggerActionSync } = require("../utils");
|
||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||
|
||||
const { createFFmpeg } = FFmpeg;
|
||||
const ffmpeg = createFFmpeg({
|
||||
log: false,
|
||||
@ -54,7 +57,12 @@ const downloadVideoToMP3 = (
|
||||
.on("info", (info, format) => {
|
||||
videoName = info.videoDetails.title.replace("|", "").toString("ascii");
|
||||
if (is.dev()) {
|
||||
console.log("Downloading video - name:", videoName);
|
||||
console.log(
|
||||
"Downloading video - name:",
|
||||
videoName,
|
||||
"- quality:",
|
||||
format.audioBitrate + "kbits/s"
|
||||
);
|
||||
}
|
||||
})
|
||||
.on("error", sendError)
|
||||
@ -88,6 +96,7 @@ const toMP3 = async (
|
||||
await ffmpeg.run(
|
||||
"-i",
|
||||
safeVideoName,
|
||||
...getFFmpegMetadataArgs(),
|
||||
...(options.ffmpegArgs || []),
|
||||
safeVideoName + "." + extension
|
||||
);
|
||||
@ -107,6 +116,20 @@ const toMP3 = async (
|
||||
}
|
||||
};
|
||||
|
||||
const getFFmpegMetadataArgs = () => {
|
||||
const metadata = JSON.parse(triggerActionSync(CHANNEL, ACTIONS.METADATA));
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
return [
|
||||
"-metadata",
|
||||
`title=${metadata.title}`,
|
||||
"-metadata",
|
||||
`artist=${metadata.artist}`,
|
||||
];
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
downloadVideoToMP3,
|
||||
};
|
||||
|
||||
@ -1,24 +1,24 @@
|
||||
const { triggerAction } = require('../utils');
|
||||
const { triggerAction } = require("../utils");
|
||||
|
||||
const CHANNEL = "navigation";
|
||||
const ACTIONS = {
|
||||
NEXT: "next",
|
||||
BACK: 'back',
|
||||
}
|
||||
NEXT: "next",
|
||||
BACK: "back",
|
||||
};
|
||||
|
||||
function goToNextPage() {
|
||||
triggerAction(CHANNEL, ACTIONS.NEXT);
|
||||
triggerAction(CHANNEL, ACTIONS.NEXT);
|
||||
}
|
||||
|
||||
function goToPreviousPage() {
|
||||
triggerAction(CHANNEL, ACTIONS.BACK);
|
||||
triggerAction(CHANNEL, ACTIONS.BACK);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CHANNEL: CHANNEL,
|
||||
ACTIONS: ACTIONS,
|
||||
global: {
|
||||
goToNextPage: goToNextPage,
|
||||
goToPreviousPage: goToPreviousPage,
|
||||
}
|
||||
CHANNEL: CHANNEL,
|
||||
ACTIONS: ACTIONS,
|
||||
actions: {
|
||||
goToNextPage: goToNextPage,
|
||||
goToPreviousPage: goToPreviousPage,
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
const path = require("path");
|
||||
|
||||
const { injectCSS, listenAction } = require("../utils");
|
||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||
|
||||
function handle(win) {
|
||||
injectCSS(win.webContents, path.join(__dirname, "style.css"));
|
||||
listenAction(CHANNEL, (event, action) => {
|
||||
switch (action) {
|
||||
case ACTIONS.NEXT:
|
||||
case ACTIONS.NEXT:
|
||||
if (win.webContents.canGoForward()) {
|
||||
win.webContents.goForward();
|
||||
}
|
||||
break;
|
||||
case ACTIONS.BACK:
|
||||
case ACTIONS.BACK:
|
||||
if (win.webContents.canGoBack()) {
|
||||
win.webContents.goBack();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
default:
|
||||
console.log("Unknown action: " + action);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -4,15 +4,15 @@ const path = require("path");
|
||||
const { ipcMain, ipcRenderer } = require("electron");
|
||||
|
||||
// Creates a DOM element from a HTML string
|
||||
module.exports.ElementFromHtml = html => {
|
||||
var template = document.createElement("template");
|
||||
html = html.trim(); // Never return a text node of whitespace as the result
|
||||
template.innerHTML = html;
|
||||
module.exports.ElementFromHtml = (html) => {
|
||||
var template = document.createElement("template");
|
||||
html = html.trim(); // Never return a text node of whitespace as the result
|
||||
template.innerHTML = html;
|
||||
return template.content.firstChild;
|
||||
};
|
||||
|
||||
// Creates a DOM element from a HTML file
|
||||
module.exports.ElementFromFile = filepath => {
|
||||
module.exports.ElementFromFile = (filepath) => {
|
||||
return module.exports.ElementFromHtml(fs.readFileSync(filepath, "utf8"));
|
||||
};
|
||||
|
||||
@ -24,12 +24,16 @@ module.exports.triggerAction = (channel, action, ...args) => {
|
||||
return ipcRenderer.send(channel, action, ...args);
|
||||
};
|
||||
|
||||
module.exports.triggerActionSync = (channel, action, ...args) => {
|
||||
return ipcRenderer.sendSync(channel, action, ...args);
|
||||
};
|
||||
|
||||
module.exports.listenAction = (channel, callback) => {
|
||||
return ipcMain.on(channel, callback);
|
||||
};
|
||||
|
||||
module.exports.fileExists = (path, callbackIfExists) => {
|
||||
fs.access(path, fs.F_OK, err => {
|
||||
fs.access(path, fs.F_OK, (err) => {
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
@ -45,10 +49,10 @@ module.exports.injectCSS = (webContents, filepath) => {
|
||||
};
|
||||
|
||||
module.exports.getAllPlugins = () => {
|
||||
const isDirectory = source => fs.lstatSync(source).isDirectory();
|
||||
const isDirectory = (source) => fs.lstatSync(source).isDirectory();
|
||||
return fs
|
||||
.readdirSync(__dirname)
|
||||
.map(name => path.join(__dirname, name))
|
||||
.map((name) => path.join(__dirname, name))
|
||||
.filter(isDirectory)
|
||||
.map(name => path.basename(name));
|
||||
.map((name) => path.basename(name));
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
const path = require("path");
|
||||
|
||||
const { remote } = require("electron");
|
||||
const { contextBridge, remote } = require("electron");
|
||||
|
||||
const config = require("./config");
|
||||
const { fileExists } = require("./plugins/utils");
|
||||
@ -10,7 +10,10 @@ const plugins = config.plugins.getEnabled();
|
||||
plugins.forEach(([plugin, options]) => {
|
||||
const pluginPath = path.join(__dirname, "plugins", plugin, "actions.js");
|
||||
fileExists(pluginPath, () => {
|
||||
const actions = require(pluginPath).global || {};
|
||||
const actions = require(pluginPath).actions || {};
|
||||
|
||||
// TODO: re-enable once contextIsolation is set to true
|
||||
// contextBridge.exposeInMainWorld(plugin + "Actions", actions);
|
||||
Object.keys(actions).forEach((actionName) => {
|
||||
global[actionName] = actions[actionName];
|
||||
});
|
||||
|
||||
18
providers/song-controls.js
Normal file
18
providers/song-controls.js
Normal file
@ -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, "/"),
|
||||
};
|
||||
};
|
||||
137
providers/song-info.js
Normal file
137
providers/song-info.js
Normal file
@ -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;
|
||||
@ -1,120 +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 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);
|
||||
});
|
||||
});
|
||||
};
|
||||
24
tray.js
24
tray.js
@ -4,7 +4,7 @@ const { Menu, nativeImage, Tray } = require("electron");
|
||||
|
||||
const config = require("./config");
|
||||
const { mainMenuTemplate } = require("./menu");
|
||||
const { clickInYoutubeMusic } = require("./utils/youtube-music");
|
||||
const getSongControls = require("./providers/song-controls");
|
||||
|
||||
// Prevent tray being garbage collected
|
||||
let tray;
|
||||
@ -15,6 +15,7 @@ module.exports.setUpTray = (app, win) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { playPause, next, previous } = getSongControls(win);
|
||||
const iconPath = path.join(__dirname, "assets", "youtube-music-tray.png");
|
||||
let trayIcon = nativeImage.createFromPath(iconPath).resize({
|
||||
width: 16,
|
||||
@ -24,35 +25,30 @@ module.exports.setUpTray = (app, win) => {
|
||||
tray.setToolTip("Youtube Music");
|
||||
tray.setIgnoreDoubleClickEvents(true);
|
||||
tray.on("click", () => {
|
||||
win.isVisible() ? win.hide() : win.show();
|
||||
if (config.get("options.trayClickPlayPause")) {
|
||||
playPause();
|
||||
} else {
|
||||
win.isVisible() ? win.hide() : win.show();
|
||||
}
|
||||
});
|
||||
|
||||
const trayMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: "Play/Pause",
|
||||
click: () => {
|
||||
clickInYoutubeMusic(
|
||||
win,
|
||||
"#left-controls > div > paper-icon-button.play-pause-button.style-scope.ytmusic-player-bar"
|
||||
);
|
||||
playPause();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Next",
|
||||
click: () => {
|
||||
clickInYoutubeMusic(
|
||||
win,
|
||||
"#left-controls > div > paper-icon-button.next-button.style-scope.ytmusic-player-bar"
|
||||
);
|
||||
next();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Previous",
|
||||
click: () => {
|
||||
clickInYoutubeMusic(
|
||||
win,
|
||||
"#left-controls > div > paper-icon-button.previous-button.style-scope.ytmusic-player-bar"
|
||||
);
|
||||
previous();
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
const clickInYoutubeMusic = (win, selector) => {
|
||||
win.webContents.executeJavaScript(
|
||||
`document.querySelector("${selector}").click();`,
|
||||
true
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = { clickInYoutubeMusic };
|
||||
21
yarn.lock
21
yarn.lock
@ -2911,6 +2911,14 @@ dir-glob@^2.2.2:
|
||||
dependencies:
|
||||
path-type "^3.0.0"
|
||||
|
||||
discord-rpc@^3.1.4:
|
||||
version "3.1.4"
|
||||
resolved "https://registry.yarnpkg.com/discord-rpc/-/discord-rpc-3.1.4.tgz#6d449a682e6a0dec4f0444d5f36f9ebfabaccf91"
|
||||
integrity sha512-QaBu+gHica2SzgRAmTpuJ4J8DX9+fDwAqhvaie3hcbkU9WPqewEPh21pWdd/7vTI/JNuapU7PFm2ZKg3BTkbGg==
|
||||
dependencies:
|
||||
node-fetch "^2.6.1"
|
||||
ws "^7.3.1"
|
||||
|
||||
dmg-builder@22.8.1:
|
||||
version "22.8.1"
|
||||
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-22.8.1.tgz#9b3bcbbc43e5fed232525d61a5567ea4b66085c3"
|
||||
@ -3032,10 +3040,10 @@ electron-chromedriver@^11.0.0:
|
||||
"@electron/get" "^1.12.2"
|
||||
extract-zip "^2.0.0"
|
||||
|
||||
electron-debug@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/electron-debug/-/electron-debug-3.1.0.tgz#0df17297487fa3c82344d810812853bf67f0bd69"
|
||||
integrity sha512-SWEqLj4MgfV3tGuO5eBLQ5/Nr6M+KPxsnE0bUJZvQebGJus6RAcdmvd7L+l0Ji31h2mmrN23l2tHFtCa2FvurA==
|
||||
electron-debug@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/electron-debug/-/electron-debug-3.2.0.tgz#46a15b555c3b11872218c65ea01d058aa0814920"
|
||||
integrity sha512-7xZh+LfUvJ52M9rn6N+tPuDw6oRAjxUj9SoxAZfJ0hVCXhZCsdkrSt7TgXOiWiEOBgEV8qwUIO/ScxllsPS7ow==
|
||||
dependencies:
|
||||
electron-is-dev "^1.1.0"
|
||||
electron-localshortcut "^3.1.0"
|
||||
@ -8773,6 +8781,11 @@ ws@^7.2.3:
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
|
||||
integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==
|
||||
|
||||
ws@^7.3.1:
|
||||
version "7.4.2"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.2.tgz#782100048e54eb36fe9843363ab1c68672b261dd"
|
||||
integrity sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==
|
||||
|
||||
xdg-basedir@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
|
||||
|
||||
Reference in New Issue
Block a user