diff --git a/index.js b/index.js index 4d5fb9d6..39d077bc 100644 --- a/index.js +++ b/index.js @@ -75,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 diff --git a/package.json b/package.json index 42aa63bf..dda5ca6a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "youtube-music", "productName": "YouTube Music", - "version": "1.8.0", + "version": "1.8.2", "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,6 +60,7 @@ "@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", diff --git a/plugins/discord-rpc/back.js b/plugins/discord-rpc/back.js new file mode 100644 index 00000000..d984a300 --- /dev/null +++ b/plugins/discord-rpc/back.js @@ -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); + }); + }); +}; diff --git a/plugins/downloader/front.js b/plugins/downloader/front.js index 00459281..732fd161 100644 --- a/plugins/downloader/front.js +++ b/plugins/downloader/front.js @@ -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 }; diff --git a/plugins/downloader/youtube-dl.js b/plugins/downloader/youtube-dl.js index 3b0c7688..28f9836d 100644 --- a/plugins/downloader/youtube-dl.js +++ b/plugins/downloader/youtube-dl.js @@ -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(); diff --git a/plugins/navigation/actions.js b/plugins/navigation/actions.js index 83947a63..69c8536b 100644 --- a/plugins/navigation/actions.js +++ b/plugins/navigation/actions.js @@ -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, + }, }; diff --git a/plugins/navigation/back.js b/plugins/navigation/back.js index 54ee18a7..6d0d0a2a 100644 --- a/plugins/navigation/back.js +++ b/plugins/navigation/back.js @@ -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); } }); diff --git a/preload.js b/preload.js index 362c9170..c3d51d16 100644 --- a/preload.js +++ b/preload.js @@ -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]; }); diff --git a/providers/song-info.js b/providers/song-info.js index 1c3333e0..4ddf9cab 100644 --- a/providers/song-info.js +++ b/providers/song-info.js @@ -13,6 +13,9 @@ const imageSelector = 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 @@ -53,6 +56,20 @@ const getSubInfo = async (win) => { 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); @@ -74,6 +91,8 @@ const songInfo = { imageSrc: "", image: null, isPaused: true, + songDuration: 0, + elapsedSeconds: 0, }; const registerProvider = (win) => { @@ -92,6 +111,10 @@ const registerProvider = (win) => { 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); diff --git a/yarn.lock b/yarn.lock index f41365da..f6cc45c3 100644 --- a/yarn.lock +++ b/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" @@ -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"