mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
Merge branch 'master' of github.com:th-ch/youtube-music into refactor-providers
# By TC (9) and semvis123 (2) # Via GitHub (4) and semvis123 (1) * 'master' of github.com:th-ch/youtube-music: renamed DiscordRPC to Discord Downloader plugin: log audio bitrate Disable context isolation (to load ffmpeg wasm) Use contextBridge in preload script + update navigation plugin Fix downloader plugin with context isolation Bump version Add portable target to windows builds Added Discord rich presence and added extra properties to songinfo provider Bump version Allow custom audio extensions in downloader Defensive: handle null/undefined ffmpeg args
This commit is contained in:
4
index.js
4
index.js
@ -75,7 +75,9 @@ function createMainWindow() {
|
|||||||
backgroundColor: "#000",
|
backgroundColor: "#000",
|
||||||
show: false,
|
show: false,
|
||||||
webPreferences: {
|
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"),
|
preload: path.join(__dirname, "preload.js"),
|
||||||
nodeIntegrationInSubFrames: true,
|
nodeIntegrationInSubFrames: true,
|
||||||
nativeWindowOpen: true, // window.open return Window object(like in regular browsers), not BrowserWindowProxy
|
nativeWindowOpen: true, // window.open return Window object(like in regular browsers), not BrowserWindowProxy
|
||||||
|
|||||||
14
package.json
14
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "youtube-music",
|
"name": "youtube-music",
|
||||||
"productName": "YouTube Music",
|
"productName": "YouTube Music",
|
||||||
"version": "1.8.0",
|
"version": "1.8.2",
|
||||||
"description": "YouTube Music Desktop App - including custom plugins",
|
"description": "YouTube Music Desktop App - including custom plugins",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "th-ch/youtube-music",
|
"repository": "th-ch/youtube-music",
|
||||||
@ -18,12 +18,19 @@
|
|||||||
"icon": "assets/generated/icons/mac/icon.icns"
|
"icon": "assets/generated/icons/mac/icon.icns"
|
||||||
},
|
},
|
||||||
"win": {
|
"win": {
|
||||||
"icon": "assets/generated/icons/win/icon.ico"
|
"icon": "assets/generated/icons/win/icon.ico",
|
||||||
|
"target": ["nsis", "portable"]
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"icon": "assets/generated/icons/png",
|
"icon": "assets/generated/icons/png",
|
||||||
"category": "AudioVideo",
|
"category": "AudioVideo",
|
||||||
"target": ["AppImage", "snap", "freebsd", "deb", "rpm"]
|
"target": [
|
||||||
|
"AppImage",
|
||||||
|
"snap",
|
||||||
|
"freebsd",
|
||||||
|
"deb",
|
||||||
|
"rpm"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -53,6 +60,7 @@
|
|||||||
"@ffmpeg/core": "^0.8.5",
|
"@ffmpeg/core": "^0.8.5",
|
||||||
"@ffmpeg/ffmpeg": "^0.9.6",
|
"@ffmpeg/ffmpeg": "^0.9.6",
|
||||||
"YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.8.0",
|
"YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.8.0",
|
||||||
|
"discord-rpc": "^3.1.4",
|
||||||
"downloads-folder": "^3.0.1",
|
"downloads-folder": "^3.0.1",
|
||||||
"electron-debug": "^3.1.0",
|
"electron-debug": "^3.1.0",
|
||||||
"electron-is": "^3.0.0",
|
"electron-is": "^3.0.0",
|
||||||
|
|||||||
41
plugins/discord-rpc/back.js
Normal file
41
plugins/discord-rpc/back.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
const Discord = require('discord-rpc');
|
||||||
|
const rpc = new Discord.Client({
|
||||||
|
transport: 'ipc'
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientId = '790655993809338398';
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
const { contextBridge } = require("electron");
|
||||||
|
|
||||||
const { ElementFromFile, templatePath, triggerAction } = require("../utils");
|
const { ElementFromFile, templatePath, triggerAction } = require("../utils");
|
||||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||||
const { downloadVideoToMP3 } = require("./youtube-dl");
|
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 = () => {
|
global.download = () => {
|
||||||
const videoUrl = window.location.href;
|
const videoUrl = window.location.href;
|
||||||
|
|
||||||
@ -48,6 +53,7 @@ global.download = () => {
|
|||||||
pluginOptions
|
pluginOptions
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
// });
|
||||||
|
|
||||||
function observeMenu(options) {
|
function observeMenu(options) {
|
||||||
pluginOptions = { ...pluginOptions, ...options };
|
pluginOptions = { ...pluginOptions, ...options };
|
||||||
|
|||||||
@ -54,7 +54,12 @@ const downloadVideoToMP3 = (
|
|||||||
.on("info", (info, format) => {
|
.on("info", (info, format) => {
|
||||||
videoName = info.videoDetails.title.replace("|", "").toString("ascii");
|
videoName = info.videoDetails.title.replace("|", "").toString("ascii");
|
||||||
if (is.dev()) {
|
if (is.dev()) {
|
||||||
console.log("Downloading video - name:", videoName);
|
console.log(
|
||||||
|
"Downloading video - name:",
|
||||||
|
videoName,
|
||||||
|
"- quality:",
|
||||||
|
format.audioBitrate + "kbits/s"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on("error", sendError)
|
.on("error", sendError)
|
||||||
@ -73,6 +78,7 @@ const toMP3 = async (
|
|||||||
options
|
options
|
||||||
) => {
|
) => {
|
||||||
const safeVideoName = randomBytes(32).toString("hex");
|
const safeVideoName = randomBytes(32).toString("hex");
|
||||||
|
const extension = options.extension || "mp3";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!ffmpeg.isLoaded()) {
|
if (!ffmpeg.isLoaded()) {
|
||||||
@ -87,15 +93,17 @@ const toMP3 = async (
|
|||||||
await ffmpeg.run(
|
await ffmpeg.run(
|
||||||
"-i",
|
"-i",
|
||||||
safeVideoName,
|
safeVideoName,
|
||||||
...options.ffmpegArgs,
|
...(options.ffmpegArgs || []),
|
||||||
safeVideoName + ".mp3"
|
safeVideoName + "." + extension
|
||||||
);
|
);
|
||||||
|
|
||||||
const folder = options.downloadFolder || downloadsFolder();
|
const folder = options.downloadFolder || downloadsFolder();
|
||||||
const filename = filenamify(videoName + ".mp3", { replacement: "_" });
|
const filename = filenamify(videoName + "." + extension, {
|
||||||
|
replacement: "_",
|
||||||
|
});
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(folder, filename),
|
join(folder, filename),
|
||||||
ffmpeg.FS("readFile", safeVideoName + ".mp3")
|
ffmpeg.FS("readFile", safeVideoName + "." + extension)
|
||||||
);
|
);
|
||||||
|
|
||||||
reinit();
|
reinit();
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
const { triggerAction } = require('../utils');
|
const { triggerAction } = require("../utils");
|
||||||
|
|
||||||
const CHANNEL = "navigation";
|
const CHANNEL = "navigation";
|
||||||
const ACTIONS = {
|
const ACTIONS = {
|
||||||
NEXT: "next",
|
NEXT: "next",
|
||||||
BACK: 'back',
|
BACK: "back",
|
||||||
}
|
};
|
||||||
|
|
||||||
function goToNextPage() {
|
function goToNextPage() {
|
||||||
triggerAction(CHANNEL, ACTIONS.NEXT);
|
triggerAction(CHANNEL, ACTIONS.NEXT);
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToPreviousPage() {
|
function goToPreviousPage() {
|
||||||
triggerAction(CHANNEL, ACTIONS.BACK);
|
triggerAction(CHANNEL, ACTIONS.BACK);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
CHANNEL: CHANNEL,
|
CHANNEL: CHANNEL,
|
||||||
ACTIONS: ACTIONS,
|
ACTIONS: ACTIONS,
|
||||||
global: {
|
actions: {
|
||||||
goToNextPage: goToNextPage,
|
goToNextPage: goToNextPage,
|
||||||
goToPreviousPage: goToPreviousPage,
|
goToPreviousPage: goToPreviousPage,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,23 +1,23 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const { injectCSS, listenAction } = require("../utils");
|
const { injectCSS, listenAction } = require("../utils");
|
||||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||||
|
|
||||||
function handle(win) {
|
function handle(win) {
|
||||||
injectCSS(win.webContents, path.join(__dirname, "style.css"));
|
injectCSS(win.webContents, path.join(__dirname, "style.css"));
|
||||||
listenAction(CHANNEL, (event, action) => {
|
listenAction(CHANNEL, (event, action) => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case ACTIONS.NEXT:
|
case ACTIONS.NEXT:
|
||||||
if (win.webContents.canGoForward()) {
|
if (win.webContents.canGoForward()) {
|
||||||
win.webContents.goForward();
|
win.webContents.goForward();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ACTIONS.BACK:
|
case ACTIONS.BACK:
|
||||||
if (win.webContents.canGoBack()) {
|
if (win.webContents.canGoBack()) {
|
||||||
win.webContents.goBack();
|
win.webContents.goBack();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log("Unknown action: " + action);
|
console.log("Unknown action: " + action);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const { remote } = require("electron");
|
const { contextBridge, remote } = require("electron");
|
||||||
|
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
const { fileExists } = require("./plugins/utils");
|
const { fileExists } = require("./plugins/utils");
|
||||||
@ -10,7 +10,10 @@ const plugins = config.plugins.getEnabled();
|
|||||||
plugins.forEach(([plugin, options]) => {
|
plugins.forEach(([plugin, options]) => {
|
||||||
const pluginPath = path.join(__dirname, "plugins", plugin, "actions.js");
|
const pluginPath = path.join(__dirname, "plugins", plugin, "actions.js");
|
||||||
fileExists(pluginPath, () => {
|
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) => {
|
Object.keys(actions).forEach((actionName) => {
|
||||||
global[actionName] = actions[actionName];
|
global[actionName] = actions[actionName];
|
||||||
});
|
});
|
||||||
|
|||||||
@ -13,6 +13,9 @@ const imageSelector =
|
|||||||
const subInfoSelector =
|
const subInfoSelector =
|
||||||
"#layout > ytmusic-player-bar > div.middle-controls.style-scope.ytmusic-player-bar > div.content-info-wrapper.style-scope.ytmusic-player-bar > span";
|
"#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
|
// Grab the title using the selector
|
||||||
const getTitle = (win) => {
|
const getTitle = (win) => {
|
||||||
return win.webContents
|
return win.webContents
|
||||||
@ -53,6 +56,20 @@ const getSubInfo = async (win) => {
|
|||||||
return subInfo;
|
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
|
// Grab the native image using the src
|
||||||
const getImage = async (src) => {
|
const getImage = async (src) => {
|
||||||
const result = await fetch(src);
|
const result = await fetch(src);
|
||||||
@ -74,6 +91,8 @@ const songInfo = {
|
|||||||
imageSrc: "",
|
imageSrc: "",
|
||||||
image: null,
|
image: null,
|
||||||
isPaused: true,
|
isPaused: true,
|
||||||
|
songDuration: 0,
|
||||||
|
elapsedSeconds: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const registerProvider = (win) => {
|
const registerProvider = (win) => {
|
||||||
@ -92,6 +111,10 @@ const registerProvider = (win) => {
|
|||||||
songInfo.title = await getTitle(win);
|
songInfo.title = await getTitle(win);
|
||||||
songInfo.isPaused = await getPausedStatus(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 title changed then we do need to update other info
|
||||||
if (oldTitle !== songInfo.title) {
|
if (oldTitle !== songInfo.title) {
|
||||||
const subInfo = await getSubInfo(win);
|
const subInfo = await getSubInfo(win);
|
||||||
|
|||||||
13
yarn.lock
13
yarn.lock
@ -2911,6 +2911,14 @@ dir-glob@^2.2.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
path-type "^3.0.0"
|
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:
|
dmg-builder@22.8.1:
|
||||||
version "22.8.1"
|
version "22.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-22.8.1.tgz#9b3bcbbc43e5fed232525d61a5567ea4b66085c3"
|
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-22.8.1.tgz#9b3bcbbc43e5fed232525d61a5567ea4b66085c3"
|
||||||
@ -8773,6 +8781,11 @@ ws@^7.2.3:
|
|||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
|
||||||
integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==
|
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:
|
xdg-basedir@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
|
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
|
||||||
|
|||||||
Reference in New Issue
Block a user