Skip to content

Commit

Permalink
Showing 17 changed files with 494 additions and 452 deletions.
22 changes: 10 additions & 12 deletions index.js
Original file line number Diff line number Diff line change
@@ -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,12 +75,21 @@ function createMainWindow() {
backgroundColor: "#000",
show: false,
webPreferences: {
nodeIntegration: isTesting(), // Only necessary when testing with Spectron
// 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
enableRemoteModule: true,
affinity: "main-window", // main window, and addition windows should work in one process
...(isTesting()
? {
// Only necessary when testing with Spectron
contextIsolation: false,
nodeIntegration: true,
}
: undefined),
},
frame: !is.macOS(),
titleBarStyle: is.macOS() ? "hiddenInset" : "default",
19 changes: 10 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "youtube-music",
"productName": "YouTube Music",
"version": "1.7.5",
"version": "1.8.2",
"description": "YouTube Music Desktop App - including custom plugins",
"license": "MIT",
"repository": "th-ch/youtube-music",
@@ -18,7 +18,8 @@
"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",
@@ -55,30 +56,30 @@
"npm": "Please use yarn and not npm"
},
"dependencies": {
"@cliqz/adblocker-electron": "^1.18.8",
"@ffmpeg/core": "^0.8.4",
"@ffmpeg/ffmpeg": "^0.9.5",
"@cliqz/adblocker-electron": "^1.19.0",
"@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-is": "^3.0.0",
"electron-localshortcut": "^3.2.1",
"electron-store": "^6.0.1",
"electron-updater": "^4.3.5",
"electron-updater": "^4.3.6",
"filenamify": "^4.2.0",
"node-fetch": "^2.6.1",
"ytdl-core": "^4.1.1"
"ytdl-core": "^4.1.2"
},
"devDependencies": {
"electron": "^10.1.3",
"electron": "^11.1.1",
"electron-builder": "^22.8.1",
"electron-devtools-installer": "^3.1.1",
"electron-icon-maker": "0.0.5",
"get-port": "^5.1.1",
"jest": "^26.4.2",
"rimraf": "^3.0.2",
"spectron": "^12.0.0",
"spectron": "^13.0.0",
"xo": "^0.33.1"
},
"resolutions": {
76 changes: 43 additions & 33 deletions plugins/discord-rpc/back.js
Original file line number Diff line number Diff line change
@@ -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);

// Register the callback
global.songInfo.onNewData(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);
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);
});
};
6 changes: 6 additions & 0 deletions plugins/downloader/front.js
Original file line number Diff line number Diff line change
@@ -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 };
18 changes: 13 additions & 5 deletions plugins/downloader/youtube-dl.js
Original file line number Diff line number Diff line change
@@ -54,7 +54,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)
@@ -73,6 +78,7 @@ const toMP3 = async (
options
) => {
const safeVideoName = randomBytes(32).toString("hex");
const extension = options.extension || "mp3";

try {
if (!ffmpeg.isLoaded()) {
@@ -87,15 +93,17 @@ const toMP3 = async (
await ffmpeg.run(
"-i",
safeVideoName,
...options.ffmpegArgs,
safeVideoName + ".mp3"
...(options.ffmpegArgs || []),
safeVideoName + "." + extension
);

const folder = options.downloadFolder || downloadsFolder();
const filename = filenamify(videoName + ".mp3", { replacement: "_" });
const filename = filenamify(videoName + "." + extension, {
replacement: "_",
});
writeFileSync(
join(folder, filename),
ffmpeg.FS("readFile", safeVideoName + ".mp3")
ffmpeg.FS("readFile", safeVideoName + "." + extension)
);

reinit();
24 changes: 12 additions & 12 deletions plugins/navigation/actions.js
Original file line number Diff line number Diff line change
@@ -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,
},
};
8 changes: 4 additions & 4 deletions plugins/navigation/back.js
Original file line number Diff line number Diff line change
@@ -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);
}
});
22 changes: 13 additions & 9 deletions plugins/notifications/back.js
Original file line number Diff line number Diff line change
@@ -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);
17 changes: 7 additions & 10 deletions plugins/shortcuts/back.js
Original file line number Diff line number Diff line change
@@ -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;
29 changes: 0 additions & 29 deletions plugins/shortcuts/youtube.js

This file was deleted.

54 changes: 28 additions & 26 deletions plugins/touchbar/back.js
Original file line number Diff line number Diff line change
@@ -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,62 +25,61 @@ 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
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);
});
7 changes: 5 additions & 2 deletions preload.js
Original file line number Diff line number Diff line change
@@ -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 changes: 18 additions & 0 deletions providers/song-controls.js
Original file line number Diff line number Diff line change
@@ -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 changes: 137 additions & 0 deletions providers/song-info.js
Original file line number Diff line number Diff line change
@@ -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;
141 changes: 0 additions & 141 deletions providers/song-info/back.js

This file was deleted.

1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -43,6 +43,7 @@ Install the `youtube-music-bin` package from the AUR. For AUR installation instr
- **Auto confirm when paused**: when the "Continue Watching?" modal appears, automatically click "Yes"
- **Hide video player**: no video in the interface when playing music
- **Notifications**: display a notification when a song starts playing
- **Touchbar**: custom TouchBar layout for macOS

## Dev

347 changes: 187 additions & 160 deletions yarn.lock

Large diffs are not rendered by default.

0 comments on commit 39dc229

Please sign in to comment.