From b6f9404ff5d294451393e120f5a66fda2a6746cf Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Fri, 3 Mar 2023 21:43:38 +0200 Subject: [PATCH 01/33] download using youtubei,js instead of ytdl-core --- index.js | 5 - package.json | 4 +- plugins/downloader/back-downloader.js | 193 ++++++++++++++++++++++++ plugins/downloader/back.js | 59 ++------ plugins/downloader/front.js | 24 +-- plugins/downloader/menu.js | 42 +++--- plugins/downloader/utils.js | 6 +- plugins/downloader/youtube-dl.js | 206 -------------------------- yarn.lock | 98 +++++++++--- 9 files changed, 308 insertions(+), 329 deletions(-) create mode 100644 plugins/downloader/back-downloader.js delete mode 100644 plugins/downloader/youtube-dl.js diff --git a/index.js b/index.js index f00660a1..19a2e2f9 100644 --- a/index.js +++ b/index.js @@ -41,11 +41,6 @@ if(config.get("options.singleInstanceLock")){ }); } -app.commandLine.appendSwitch( - "js-flags", - // WebAssembly flags - "--experimental-wasm-threads" -); app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer"); // Required for downloader app.allowRendererProcessReuse = true; // https://github.com/electron/electron/issues/18397 if (config.get("options.disableHardwareAcceleration")) { diff --git a/package.json b/package.json index b7afae26..1c1eaf3b 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,7 @@ "electron-store": "^8.1.0", "electron-unhandled": "^4.0.1", "electron-updater": "^5.3.0", + "file-type": "^18.2.1", "filenamify": "^4.3.0", "howler": "^2.2.3", "html-to-text": "^9.0.3", @@ -133,10 +134,11 @@ "md5": "^2.3.0", "mpris-service": "^2.1.2", "node-fetch": "^2.6.8", + "node-id3": "^0.2.6", "node-notifier": "^10.0.1", "simple-youtube-age-restriction-bypass": "https://gitpkg.now.sh/api/pkg.tgz?url=zerodytrash/Simple-YouTube-Age-Restriction-Bypass&commit=v2.5.4", "vudio": "^2.1.1", - "youtubei.js": "^2.9.0", + "youtubei.js": "^3.1.1", "ytdl-core": "^4.11.1", "ytpl": "^2.3.0" }, diff --git a/plugins/downloader/back-downloader.js b/plugins/downloader/back-downloader.js new file mode 100644 index 00000000..d96182be --- /dev/null +++ b/plugins/downloader/back-downloader.js @@ -0,0 +1,193 @@ +const { existsSync, mkdirSync, createWriteStream, writeFileSync } = require('fs'); +const { ipcMain, app } = require("electron"); +const { join } = require("path"); + +const { Innertube, UniversalCache, Utils } = require('youtubei.js'); +const filenamify = require("filenamify"); +const id3 = require('node-id3').Promise; + +const { sendError } = require("./back"); +const { presets } = require('./utils'); + +ffmpegWriteTags +/** @type {Innertube} */ +let yt; +let options; + +module.exports = async (options_) => { + options = options_; + yt = await Innertube.create({ cache: new UniversalCache(false), generate_session_locally: true }); + ipcMain.handle("download-song", (_, url) => downloadSong(url)); +}; + +async function downloadSong(url, playlistFolder = undefined) { + const metadata = await getMetadata(url); + + const stream = await yt.download(metadata.id, { + type: 'audio', // audio, video or video+audio + quality: 'best', // best, bestefficiency, 144p, 240p, 480p, 720p and so on. + format: 'any' // media container format + }); + + console.info(`Downloading ${metadata.artist} - ${metadata.title} {${metadata.id}}...`); + + const iterableStream = Utils.streamToIterable(stream); + + const dir = playlistFolder || options.downloadFolder || app.getPath("downloads"); + const name = `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`; + + const extension = presets[options.preset]?.extension || 'mp3'; + + const filename = filenamify(`${name}.${extension}`, { + replacement: "_", + maxLength: 255, + }); + const filePath = join(dir, filename); + + if (!existsSync(dir)) { + mkdirSync(dir); + } + + if (!presets[options.preset]) { + await toMP3(iterableStream, filePath, metadata); + console.info('writing id3 tags...'); // DELETE + await writeID3(filePath, metadata).then(() => console.info('done writing id3 tags!')); // DELETE + } else { + const file = createWriteStream(filePath); + //stream.pipeTo(file); + for await (const chunk of iterableStream) { + file.write(chunk); + } + ffmpegWriteTags(filePath, metadata, presets[options.preset]?.ffmpegArgs); + } + + console.info(`${filePath} - Done!`, '\n'); +} +module.exports.downloadSong = downloadSong; + +function getIdFromUrl(url) { + const match = url.match(/v=([^&]+)/); + return match ? match[1] : null; +} + +async function getMetadata(url) { + const id = getIdFromUrl(url); + const info = await yt.music.getInfo(id); + //console.log('got info:' + JSON.stringify(info, null, 2)); // DELETE + + return { + id: info.basic_info.id, + title: info.basic_info.title, + artist: info.basic_info.author, + album: info.player_overlays?.browser_media_session?.album?.text, + image: info.basic_info.thumbnail[0].url, + }; +} + +const { getImage } = require("../../providers/song-info"); +const { cropMaxWidth } = require("./utils"); + +async function writeID3(filePath, metadata) { + const tags = { + title: metadata.title, + artist: metadata.artist, + album: metadata.album, + image: { + mime: "image/png", + type: { + id: 3, + name: "front cover" + }, + description: "", + imageBuffer: cropMaxWidth(await getImage(metadata.image))?.toPNG(), + } + // TODO: lyrics + }; + + await id3.write(tags, filePath); +} + +const { randomBytes } = require("crypto"); +const Mutex = require("async-mutex").Mutex; +const ffmpeg = require("@ffmpeg/ffmpeg").createFFmpeg({ + log: false, + logger: () => { }, // console.log, + progress: () => { }, // console.log, +}); + +const ffmpegMutex = new Mutex(); + +async function toMP3(stream, filePath, metadata, extension = "mp3") { + const chunks = []; + for await (const chunk of stream) { + chunks.push(chunk); + } + const buffer = Buffer.concat(chunks); + const safeVideoName = randomBytes(32).toString("hex"); + const releaseFFmpegMutex = await ffmpegMutex.acquire(); + + try { + if (!ffmpeg.isLoaded()) { + // sendFeedback("Loading…", 2); // indefinite progress bar after download + await ffmpeg.load(); + } + + // sendFeedback("Preparing file…"); + ffmpeg.FS("writeFile", safeVideoName, buffer); + + // sendFeedback("Converting…"); + + await ffmpeg.run( + "-i", + safeVideoName, + ...getFFmpegMetadataArgs(metadata), + safeVideoName + "." + extension + ); + + const fileBuffer = ffmpeg.FS("readFile", safeVideoName + "." + extension); + + await writeID3(fileBuffer, metadata); + + // sendFeedback("Saving…"); + + writeFileSync(filePath, fileBuffer); + } catch (e) { + sendError(e); + } finally { + releaseFFmpegMutex(); + } +} + +async function ffmpegWriteTags(filePath, metadata, ffmpegArgs = []) { + const releaseFFmpegMutex = await ffmpegMutex.acquire(); + + try { + if (!ffmpeg.isLoaded()) { + await ffmpeg.load(); + } + + await ffmpeg.run( + "-i", + filePath, + ...getFFmpegMetadataArgs(metadata), + ...ffmpegArgs, + filePath + ); + } catch (e) { + sendError(e); + } finally { + releaseFFmpegMutex(); + } +} + +function getFFmpegMetadataArgs(metadata) { + if (!metadata) { + return; + } + + return [ + ...(metadata.title ? ["-metadata", `title=${metadata.title}`] : []), + ...(metadata.artist ? ["-metadata", `artist=${metadata.artist}`] : []), + ...(metadata.album ? ["-metadata", `album=${metadata.album}`] : []), + ]; +}; diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index daea62b2..c10a812f 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -12,7 +12,9 @@ const { isEnabled } = require("../../config/plugins"); const { getImage } = require("../../providers/song-info"); const { fetchFromGenius } = require("../lyrics-genius/back"); -const sendError = (win, error) => { +let win = {}; + +const sendError = (error) => { win.setProgressBar(-1); // close progress bar dialog.showMessageBox({ type: "info", @@ -25,8 +27,13 @@ const sendError = (win, error) => { let nowPlayingMetadata = {}; -function handle(win) { + +function handle(win_, options) { + win = win_; injectCSS(win.webContents, join(__dirname, "style.css")); + + require("./back-downloader")(options); + registerCallback((info) => { nowPlayingMetadata = info; }); @@ -34,7 +41,7 @@ function handle(win) { listenAction(CHANNEL, (event, action, arg) => { switch (action) { case ACTIONS.ERROR: // arg = error - sendError(win, arg); + sendError(arg); break; case ACTIONS.METADATA: event.returnValue = JSON.stringify(nowPlayingMetadata); @@ -46,52 +53,6 @@ function handle(win) { console.log("Unknown action: " + action); } }); - - ipcMain.on("add-metadata", async (event, filePath, songBuffer, currentMetadata) => { - let fileBuffer = songBuffer; - const songMetadata = currentMetadata.imageSrcYTPL ? // This means metadata come from ytpl.getInfo(); - { - ...currentMetadata, - image: cropMaxWidth(await getImage(currentMetadata.imageSrcYTPL)) - } : - { ...nowPlayingMetadata, ...currentMetadata }; - - try { - const coverBuffer = songMetadata.image && !songMetadata.image.isEmpty() ? - songMetadata.image.toPNG() : null; - - const writer = new ID3Writer(songBuffer); - - // Create the metadata tags - writer - .setFrame("TIT2", songMetadata.title) - .setFrame("TPE1", [songMetadata.artist]); - if (coverBuffer) { - writer.setFrame("APIC", { - type: 3, - data: coverBuffer, - description: "" - }); - } - if (isEnabled("lyrics-genius")) { - const lyrics = await fetchFromGenius(songMetadata); - if (lyrics) { - writer.setFrame("USLT", { - description: lyrics, - lyrics: lyrics, - }); - } - } - writer.addTag(); - fileBuffer = Buffer.from(writer.arrayBuffer); - } catch (error) { - sendError(win, error); - } - - writeFileSync(filePath, fileBuffer); - // Notify the youtube-dl file - event.reply("add-metadata-done"); - }); } module.exports = handle; diff --git a/plugins/downloader/front.js b/plugins/downloader/front.js index 095d4968..97d73397 100644 --- a/plugins/downloader/front.js +++ b/plugins/downloader/front.js @@ -4,7 +4,6 @@ const { defaultConfig } = require("../../config"); const { getSongMenu } = require("../../providers/dom-elements"); const { ElementFromFile, templatePath, triggerAction } = require("../utils"); const { ACTIONS, CHANNEL } = require("./actions.js"); -const { downloadVideoToMP3 } = require("./youtube-dl"); let menu = null; let progress = null; @@ -61,28 +60,9 @@ global.download = () => { videoUrl = metadata.url || window.location.href; } - downloadVideoToMP3( - videoUrl, - (feedback, ratio = undefined) => { - if (!progress) { - console.warn("Cannot update progress"); - } else { - progress.innerHTML = feedback; - } - if (ratio) { - triggerAction(CHANNEL, ACTIONS.PROGRESS, ratio); - } - }, - (error) => { - triggerAction(CHANNEL, ACTIONS.ERROR, error); - reinit(); - }, - reinit, - pluginOptions, - metadata - ); + ipcRenderer.invoke('download-song', videoUrl).finally(() => triggerAction(CHANNEL, ACTIONS.PROGRESS, -1)); + return; }; -// }); function observeMenu(options) { pluginOptions = { ...pluginOptions, ...options }; diff --git a/plugins/downloader/menu.js b/plugins/downloader/menu.js index 622370a2..b133394e 100644 --- a/plugins/downloader/menu.js +++ b/plugins/downloader/menu.js @@ -9,6 +9,7 @@ const filenamify = require('filenamify'); const { setMenuOptions } = require("../../config/plugins"); const { sendError } = require("./back"); +const { downloadSong } = require("./back-downloader"); const { defaultMenuDownloadLabel, getFolder, presets, setBadge } = require("./utils"); let downloadLabel = defaultMenuDownloadLabel; @@ -62,7 +63,7 @@ module.exports = (win, options) => { options.preset = preset; setMenuOptions("downloader", options); }, - checked: options.preset === preset || presets[preset] === undefined, + checked: options.preset === preset, })), }, ]; @@ -81,7 +82,7 @@ async function downloadPlaylist(givenUrl, win, options) { || getPlaylistID(new URL(playingUrl)); if (!playlistId) { - sendError(win, new Error("No playlist ID found")); + sendError(new Error("No playlist ID found")); return; } @@ -92,18 +93,15 @@ async function downloadPlaylist(givenUrl, win, options) { limit: options.playlistMaxItems || Infinity, }); } catch (e) { - sendError(win, e); + sendError(e); return; } - const safePlaylistTitle = filenamify(playlist.title, {replacement: ' '}); + const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' }); const folder = getFolder(options.downloadFolder); const playlistFolder = join(folder, safePlaylistTitle); if (existsSync(playlistFolder)) { - sendError( - win, - new Error(`The folder ${playlistFolder} already exists`) - ); + sendError(new Error(`The folder ${playlistFolder} already exists`)); return; } mkdirSync(playlistFolder, { recursive: true }); @@ -128,24 +126,30 @@ async function downloadPlaylist(givenUrl, win, options) { setBadge(playlist.items.length); let dirWatcher = chokidar.watch(playlistFolder); - dirWatcher.on('add', () => { - downloadCount += 1; - if (downloadCount >= playlist.items.length) { + const closeDirWatcher = () => { + if (dirWatcher) { win.setProgressBar(-1); // close progress bar setBadge(0); // close badge counter dirWatcher.close().then(() => (dirWatcher = null)); + } + }; + dirWatcher.on('add', () => { + downloadCount += 1; + if (downloadCount >= playlist.items.length) { + closeDirWatcher(); } else { win.setProgressBar(downloadCount / playlist.items.length); setBadge(playlist.items.length - downloadCount); } }); - playlist.items.forEach((song) => { - win.webContents.send( - "downloader-download-playlist", - song.url, - safePlaylistTitle, - options - ); - }); + try { + for (const song of playlist.items) { + await downloadSong(song.url, playlistFolder).catch((e) => sendError(e)); + } + } catch (e) { + sendError(e); + } finally { + closeDirWatcher(); + } } diff --git a/plugins/downloader/utils.js b/plugins/downloader/utils.js index 70160188..0569a6d1 100644 --- a/plugins/downloader/utils.js +++ b/plugins/downloader/utils.js @@ -1,7 +1,7 @@ -const electron = require("electron"); +const { app } = require("electron"); const is = require('electron-is'); -module.exports.getFolder = customFolder => customFolder || electron.app.getPath("downloads"); +module.exports.getFolder = customFolder => customFolder || app.getPath("downloads"); module.exports.defaultMenuDownloadLabel = "Download playlist"; const orderedQualityList = ["maxresdefault", "hqdefault", "mqdefault", "sdddefault"]; @@ -41,6 +41,6 @@ module.exports.presets = { module.exports.setBadge = n => { if (is.linux() || is.macOS()) { - electron.app.setBadgeCount(n); + app.setBadgeCount(n); } } diff --git a/plugins/downloader/youtube-dl.js b/plugins/downloader/youtube-dl.js deleted file mode 100644 index 8a7b0bf1..00000000 --- a/plugins/downloader/youtube-dl.js +++ /dev/null @@ -1,206 +0,0 @@ -const { randomBytes } = require("crypto"); -const { join } = require("path"); - -const Mutex = require("async-mutex").Mutex; -const { ipcRenderer } = require("electron"); -const is = require("electron-is"); -const filenamify = require("filenamify"); - -// Workaround for "Automatic publicPath is not supported in this browser" -// See https://github.com/cypress-io/cypress/issues/18435#issuecomment-1048863509 -const script = document.createElement("script"); -document.body.appendChild(script); -script.src = " "; // single space and not the empty string - -// Browser version of FFmpeg (in renderer process) instead of loading @ffmpeg/ffmpeg -// because --js-flags cannot be passed in the main process when the app is packaged -// See https://github.com/electron/electron/issues/22705 -const FFmpeg = require("@ffmpeg/ffmpeg/dist/ffmpeg.min"); -const ytdl = require("ytdl-core"); - -const { triggerAction, triggerActionSync } = require("../utils"); -const { ACTIONS, CHANNEL } = require("./actions.js"); -const { presets, urlToJPG } = require("./utils"); -const { cleanupName } = require("../../providers/song-info"); - -const { createFFmpeg } = FFmpeg; -const ffmpeg = createFFmpeg({ - log: false, - logger: () => {}, // console.log, - progress: () => {}, // console.log, -}); -const ffmpegMutex = new Mutex(); - -const downloadVideoToMP3 = async ( - videoUrl, - sendFeedback, - sendError, - reinit, - options, - metadata = undefined, - subfolder = "" -) => { - sendFeedback("Downloading…"); - - if (metadata === null) { - const { videoDetails } = await ytdl.getInfo(videoUrl); - const thumbnails = videoDetails?.thumbnails; - metadata = { - artist: - videoDetails?.media?.artist || - cleanupName(videoDetails?.author?.name) || - "", - title: videoDetails?.media?.song || videoDetails?.title || "", - imageSrcYTPL: thumbnails ? - urlToJPG(thumbnails[thumbnails.length - 1].url, videoDetails?.videoId) - : "" - } - } - - let videoName = "YouTube Music - Unknown title"; - let videoReadableStream; - try { - videoReadableStream = ytdl(videoUrl, { - filter: "audioonly", - quality: "highestaudio", - highWaterMark: 32 * 1024 * 1024, // 32 MB - requestOptions: { maxRetries: 3 }, - }); - } catch (err) { - sendError(err); - return; - } - - const chunks = []; - videoReadableStream - .on("data", (chunk) => { - chunks.push(chunk); - }) - .on("progress", (_chunkLength, downloaded, total) => { - const ratio = downloaded / total; - const progress = Math.floor(ratio * 100); - sendFeedback("Download: " + progress + "%", ratio); - }) - .on("info", (info, format) => { - videoName = info.videoDetails.title.replaceAll("|", "").toString("ascii"); - if (is.dev()) { - console.log( - "Downloading video - name:", - videoName, - "- quality:", - format.audioBitrate + "kbits/s" - ); - } - }) - .on("error", sendError) - .on("end", async () => { - const buffer = Buffer.concat(chunks); - await toMP3( - videoName, - buffer, - sendFeedback, - sendError, - reinit, - options, - metadata, - subfolder - ); - }); -}; - -const toMP3 = async ( - videoName, - buffer, - sendFeedback, - sendError, - reinit, - options, - existingMetadata = undefined, - subfolder = "" -) => { - const convertOptions = { ...presets[options.preset], ...options }; - const safeVideoName = randomBytes(32).toString("hex"); - const extension = convertOptions.extension || "mp3"; - const releaseFFmpegMutex = await ffmpegMutex.acquire(); - - try { - if (!ffmpeg.isLoaded()) { - sendFeedback("Loading…", 2); // indefinite progress bar after download - await ffmpeg.load(); - } - - sendFeedback("Preparing file…"); - ffmpeg.FS("writeFile", safeVideoName, buffer); - - sendFeedback("Converting…"); - const metadata = existingMetadata || getMetadata(); - await ffmpeg.run( - "-i", - safeVideoName, - ...getFFmpegMetadataArgs(metadata), - ...(convertOptions.ffmpegArgs || []), - safeVideoName + "." + extension - ); - - const folder = options.downloadFolder || await ipcRenderer.invoke('getDownloadsFolder'); - const name = metadata.title - ? `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}` - : videoName; - const filename = filenamify(name + "." + extension, { - replacement: "_", - maxLength: 255, - }); - - const filePath = join(folder, subfolder, filename); - const fileBuffer = ffmpeg.FS("readFile", safeVideoName + "." + extension); - - // Add the metadata - sendFeedback("Adding metadata…"); - ipcRenderer.send("add-metadata", filePath, fileBuffer, { - artist: metadata.artist, - title: metadata.title, - imageSrcYTPL: metadata.imageSrcYTPL - }); - ipcRenderer.once("add-metadata-done", reinit); - } catch (e) { - sendError(e); - } finally { - releaseFFmpegMutex(); - } -}; - -const getMetadata = () => { - return JSON.parse(triggerActionSync(CHANNEL, ACTIONS.METADATA)); -}; - -const getFFmpegMetadataArgs = (metadata) => { - if (!metadata) { - return; - } - - return [ - ...(metadata.title ? ["-metadata", `title=${metadata.title}`] : []), - ...(metadata.artist ? ["-metadata", `artist=${metadata.artist}`] : []), - ]; -}; - -module.exports = { - downloadVideoToMP3, -}; - -ipcRenderer.on( - "downloader-download-playlist", - (_, url, playlistFolder, options) => { - downloadVideoToMP3( - url, - () => {}, - (error) => { - triggerAction(CHANNEL, ACTIONS.ERROR, error); - }, - () => {}, - options, - null, - playlistFolder - ); - } -); diff --git a/yarn.lock b/yarn.lock index a973a9d6..93a342b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -734,13 +734,6 @@ __metadata: languageName: node linkType: hard -"@protobuf-ts/runtime@npm:^2.7.0": - version: 2.8.2 - resolution: "@protobuf-ts/runtime@npm:2.8.2" - checksum: ab322e832bfb347b271a8862b8ef3db27ffa380f9c49f94acb410534586a282ebd8af96d4459f959ad0fe5fbf34183f3f4fe512e50c9a4331b742a7445b16c92 - languageName: node - linkType: hard - "@remusao/guess-url-type@npm:^1.1.2": version: 1.2.1 resolution: "@remusao/guess-url-type@npm:1.2.1" @@ -3860,6 +3853,17 @@ __metadata: languageName: node linkType: hard +"file-type@npm:^18.2.1": + version: 18.2.1 + resolution: "file-type@npm:18.2.1" + dependencies: + readable-web-to-node-stream: ^3.0.2 + strtok3: ^7.0.0 + token-types: ^5.0.1 + checksum: bbc9381292e96a72ecd892f9f5e1a9a8d3f9717955841346e55891acfe099135bfa149f7dad51f35ee52b5e7e0a1a02d7375061b2800758011682c2e9d96953e + languageName: node + linkType: hard + "file-uri-to-path@npm:1.0.0": version: 1.0.0 resolution: "file-uri-to-path@npm:1.0.0" @@ -4709,6 +4713,15 @@ __metadata: languageName: node linkType: hard +"iconv-lite@npm:0.6.2": + version: 0.6.2 + resolution: "iconv-lite@npm:0.6.2" + dependencies: + safer-buffer: ">= 2.1.2 < 3.0.0" + checksum: 03e03eb9fc003bc94f7956849f747258e57c162760259d76d1e67483058cad854a4b681b635e21e3ec41f4bd15ceed1b4a350f890565d680343442c5b139fa8a + languageName: node + linkType: hard + "iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" @@ -5339,12 +5352,12 @@ __metadata: languageName: node linkType: hard -"jintr@npm:^0.3.1": - version: 0.3.1 - resolution: "jintr@npm:0.3.1" +"jintr@npm:^0.4.1": + version: 0.4.1 + resolution: "jintr@npm:0.4.1" dependencies: acorn: ^8.8.0 - checksum: 1fb2454904461c3bbe6b55251dce4ac352fb3b94803773e3d8925ede4a907b5d52a2f30f3f76757c770e1785f34a3665d5cffd710c3ae99837cd157762130a24 + checksum: 9dd5932be611aa926dba90e3b1bf09afbdc8864a128dbba53f5ee8461f0ac27955fca780dfd4cbb1575e6873d0d74dd346127554a4b2cae01986fe12aad3ba09 languageName: node linkType: hard @@ -6319,6 +6332,15 @@ __metadata: languageName: node linkType: hard +"node-id3@npm:^0.2.6": + version: 0.2.6 + resolution: "node-id3@npm:0.2.6" + dependencies: + iconv-lite: 0.6.2 + checksum: 9f3ba9d42f4d52348bb2f88dbcdd63ee8fd513dc7c01481d6b10082b83d0f1ce696f920c9bff0e3f2b00486c25fe49c3f93a56d54813809b7edc9ab14b1383d6 + languageName: node + linkType: hard + "node-notifier@npm:^10.0.1": version: 10.0.1 resolution: "node-notifier@npm:10.0.1" @@ -6837,6 +6859,13 @@ __metadata: languageName: node linkType: hard +"peek-readable@npm:^5.0.0": + version: 5.0.0 + resolution: "peek-readable@npm:5.0.0" + checksum: bef5ceb50586eb42e14efba274ac57ffe97f0ed272df9239ce029f688f495d9bf74b2886fa27847c706a9db33acda4b7d23bbd09a2d21eb4c2a54da915117414 + languageName: node + linkType: hard + "pend@npm:~1.2.0": version: 1.2.0 resolution: "pend@npm:1.2.0" @@ -7277,7 +7306,7 @@ __metadata: languageName: node linkType: hard -"readable-web-to-node-stream@npm:^3.0.0": +"readable-web-to-node-stream@npm:^3.0.0, readable-web-to-node-stream@npm:^3.0.2": version: 3.0.2 resolution: "readable-web-to-node-stream@npm:3.0.2" dependencies: @@ -8051,6 +8080,16 @@ __metadata: languageName: node linkType: hard +"strtok3@npm:^7.0.0": + version: 7.0.0 + resolution: "strtok3@npm:7.0.0" + dependencies: + "@tokenizer/token": ^0.3.0 + peek-readable: ^5.0.0 + checksum: 2ebe7ad8f2aea611dec6742cf6a42e82764892a362907f7ce493faf334501bf981ce21c828dcc300457e6d460dc9c34d644ededb3b01dcb9e37559203cf1748c + languageName: node + linkType: hard + "sumchecker@npm:^3.0.1": version: 3.0.1 resolution: "sumchecker@npm:3.0.1" @@ -8238,6 +8277,16 @@ __metadata: languageName: node linkType: hard +"token-types@npm:^5.0.1": + version: 5.0.1 + resolution: "token-types@npm:5.0.1" + dependencies: + "@tokenizer/token": ^0.3.0 + ieee754: ^1.2.1 + checksum: 32780123bc6ce8b6a2231d860445c994a02a720abf38df5583ea957aa6626873cd1c4dd8af62314da4cf16ede00c379a765707a3b06f04b8808c38efdae1c785 + languageName: node + linkType: hard + "tough-cookie@npm:~2.5.0": version: 2.5.0 resolution: "tough-cookie@npm:2.5.0" @@ -8483,12 +8532,12 @@ __metadata: languageName: node linkType: hard -"undici@npm:^5.7.0": - version: 5.18.0 - resolution: "undici@npm:5.18.0" +"undici@npm:^5.19.1": + version: 5.20.0 + resolution: "undici@npm:5.20.0" dependencies: busboy: ^1.6.0 - checksum: 74e0f357c376c745fcca612481a5742866ae36086ad387e626255f4c4a15fc5357d9d0fa4355271b6a633d50f5556c3e85720844680c44861c66e23afca7245f + checksum: 25412a785b2bd0b12f0bb0ec47ef00aa7a611ca0e570cb7af97cffe6a42e0d78e4b15190363a43771e9002defc3c6647c1b2d52201b3f64e2196819db4d150d3 languageName: node linkType: hard @@ -9023,6 +9072,7 @@ __metadata: electron-store: ^8.1.0 electron-unhandled: ^4.0.1 electron-updater: ^5.3.0 + file-type: ^18.2.1 filenamify: ^4.3.0 howler: ^2.2.3 html-to-text: ^9.0.3 @@ -9032,26 +9082,26 @@ __metadata: mpris-service: ^2.1.2 node-fetch: ^2.6.8 node-gyp: ^9.3.1 + node-id3: ^0.2.6 node-notifier: ^10.0.1 playwright: ^1.29.2 simple-youtube-age-restriction-bypass: "https://gitpkg.now.sh/api/pkg.tgz?url=zerodytrash/Simple-YouTube-Age-Restriction-Bypass&commit=v2.5.4" vudio: ^2.1.1 xo: ^0.53.1 - youtubei.js: ^2.9.0 + youtubei.js: ^3.1.1 ytdl-core: ^4.11.1 ytpl: ^2.3.0 languageName: unknown linkType: soft -"youtubei.js@npm:^2.9.0": - version: 2.9.0 - resolution: "youtubei.js@npm:2.9.0" +"youtubei.js@npm:^3.1.1": + version: 3.1.1 + resolution: "youtubei.js@npm:3.1.1" dependencies: - "@protobuf-ts/runtime": ^2.7.0 - jintr: ^0.3.1 + jintr: ^0.4.1 linkedom: ^0.14.12 - undici: ^5.7.0 - checksum: 0b9d86c1ec7297ee41b9013d6cb951976d82b2775d9d9d5abf0447d7acb9f36b07ebb689710bf8ccfa256a6f56088f49b699fb1a3e5bac2b0ea7d2daa508c109 + undici: ^5.19.1 + checksum: 1280e2ddacec3034ee8e1b398ba80662a6854e184416d3484119e7cf47b69ab2e58b4f1efdf468dcad3e50bdc7bd42b6ee66b95660ffb521efb5f0634ef60fb7 languageName: node linkType: hard From ec6107138d1beb670ea9a4a6373da78185fe226f Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sat, 4 Mar 2023 00:42:39 +0200 Subject: [PATCH 02/33] remove `node-id3` --- package.json | 3 - plugins/downloader/back-downloader.js | 71 ++++++++++++++-------- plugins/downloader/back.js | 10 +--- yarn.lock | 84 +-------------------------- 4 files changed, 51 insertions(+), 117 deletions(-) diff --git a/package.json b/package.json index 1c1eaf3b..6fa8f076 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,6 @@ "electron-store": "^8.1.0", "electron-unhandled": "^4.0.1", "electron-updater": "^5.3.0", - "file-type": "^18.2.1", "filenamify": "^4.3.0", "howler": "^2.2.3", "html-to-text": "^9.0.3", @@ -134,12 +133,10 @@ "md5": "^2.3.0", "mpris-service": "^2.1.2", "node-fetch": "^2.6.8", - "node-id3": "^0.2.6", "node-notifier": "^10.0.1", "simple-youtube-age-restriction-bypass": "https://gitpkg.now.sh/api/pkg.tgz?url=zerodytrash/Simple-YouTube-Age-Restriction-Bypass&commit=v2.5.4", "vudio": "^2.1.1", "youtubei.js": "^3.1.1", - "ytdl-core": "^4.11.1", "ytpl": "^2.3.0" }, "devDependencies": { diff --git a/plugins/downloader/back-downloader.js b/plugins/downloader/back-downloader.js index d96182be..9cf89c96 100644 --- a/plugins/downloader/back-downloader.js +++ b/plugins/downloader/back-downloader.js @@ -4,12 +4,16 @@ const { join } = require("path"); const { Innertube, UniversalCache, Utils } = require('youtubei.js'); const filenamify = require("filenamify"); -const id3 = require('node-id3').Promise; +const ID3Writer = require("browser-id3-writer"); + +const { fetchFromGenius } = require("../lyrics-genius/back"); +const { isEnabled } = require("../../config/plugins"); +const { getImage } = require("../../providers/song-info"); +const { cropMaxWidth } = require("./utils"); const { sendError } = require("./back"); const { presets } = require('./utils'); -ffmpegWriteTags /** @type {Innertube} */ let yt; let options; @@ -49,16 +53,17 @@ async function downloadSong(url, playlistFolder = undefined) { } if (!presets[options.preset]) { - await toMP3(iterableStream, filePath, metadata); + const fileBuffer = await toMP3(iterableStream, filePath, metadata); console.info('writing id3 tags...'); // DELETE - await writeID3(filePath, metadata).then(() => console.info('done writing id3 tags!')); // DELETE + writeFileSync(filePath, await writeID3(fileBuffer, metadata)); + console.info('done writing id3 tags!'); // DELETE } else { const file = createWriteStream(filePath); //stream.pipeTo(file); for await (const chunk of iterableStream) { file.write(chunk); } - ffmpegWriteTags(filePath, metadata, presets[options.preset]?.ffmpegArgs); + await ffmpegWriteTags(filePath, metadata, presets[options.preset]?.ffmpegArgs); } console.info(`${filePath} - Done!`, '\n'); @@ -84,29 +89,45 @@ async function getMetadata(url) { }; } -const { getImage } = require("../../providers/song-info"); -const { cropMaxWidth } = require("./utils"); +async function writeID3(buffer, metadata) { + try { + const nativeImage = cropMaxWidth(await getImage(metadata.image)); + const coverBuffer = nativeImage && !nativeImage.isEmpty() ? + nativeImage.toPNG() : null; -async function writeID3(filePath, metadata) { - const tags = { - title: metadata.title, - artist: metadata.artist, - album: metadata.album, - image: { - mime: "image/png", - type: { - id: 3, - name: "front cover" - }, - description: "", - imageBuffer: cropMaxWidth(await getImage(metadata.image))?.toPNG(), + const writer = new ID3Writer(buffer); + + // Create the metadata tags + writer + .setFrame("TIT2", metadata.title) + .setFrame("TPE1", [metadata.artist]); + if (metadata.album) { + writer.setFrame("TALB", metadata.album); } - // TODO: lyrics - }; - - await id3.write(tags, filePath); + if (coverBuffer) { + writer.setFrame("APIC", { + type: 3, + data: coverBuffer, + description: "", + }); + } + if (isEnabled("lyrics-genius")) { + const lyrics = await fetchFromGenius(metadata); + if (lyrics) { + writer.setFrame("USLT", { + description: '', + lyrics: lyrics, + }); + } + } + writer.addTag(); + return Buffer.from(writer.arrayBuffer); + } catch (e) { + sendError(e); + } } + const { randomBytes } = require("crypto"); const Mutex = require("async-mutex").Mutex; const ffmpeg = require("@ffmpeg/ffmpeg").createFFmpeg({ @@ -150,7 +171,7 @@ async function toMP3(stream, filePath, metadata, extension = "mp3") { // sendFeedback("Saving…"); - writeFileSync(filePath, fileBuffer); + return fileBuffer; } catch (e) { sendError(e); } finally { diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index c10a812f..379d6271 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -1,21 +1,17 @@ -const { writeFileSync } = require("fs"); const { join } = require("path"); -const ID3Writer = require("browser-id3-writer"); -const { dialog, ipcMain } = require("electron"); +const { dialog } = require("electron"); const registerCallback = require("../../providers/song-info"); const { injectCSS, listenAction } = require("../utils"); -const { cropMaxWidth } = require("./utils"); const { ACTIONS, CHANNEL } = require("./actions.js"); -const { isEnabled } = require("../../config/plugins"); -const { getImage } = require("../../providers/song-info"); -const { fetchFromGenius } = require("../lyrics-genius/back"); let win = {}; const sendError = (error) => { win.setProgressBar(-1); // close progress bar + + console.error(error); dialog.showMessageBox({ type: "info", buttons: ["OK"], diff --git a/yarn.lock b/yarn.lock index 93a342b9..929d11d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3853,17 +3853,6 @@ __metadata: languageName: node linkType: hard -"file-type@npm:^18.2.1": - version: 18.2.1 - resolution: "file-type@npm:18.2.1" - dependencies: - readable-web-to-node-stream: ^3.0.2 - strtok3: ^7.0.0 - token-types: ^5.0.1 - checksum: bbc9381292e96a72ecd892f9f5e1a9a8d3f9717955841346e55891acfe099135bfa149f7dad51f35ee52b5e7e0a1a02d7375061b2800758011682c2e9d96953e - languageName: node - linkType: hard - "file-uri-to-path@npm:1.0.0": version: 1.0.0 resolution: "file-uri-to-path@npm:1.0.0" @@ -4713,15 +4702,6 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.6.2": - version: 0.6.2 - resolution: "iconv-lite@npm:0.6.2" - dependencies: - safer-buffer: ">= 2.1.2 < 3.0.0" - checksum: 03e03eb9fc003bc94f7956849f747258e57c162760259d76d1e67483058cad854a4b681b635e21e3ec41f4bd15ceed1b4a350f890565d680343442c5b139fa8a - languageName: node - linkType: hard - "iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" @@ -5833,16 +5813,6 @@ __metadata: languageName: node linkType: hard -"m3u8stream@npm:^0.8.6": - version: 0.8.6 - resolution: "m3u8stream@npm:0.8.6" - dependencies: - miniget: ^4.2.2 - sax: ^1.2.4 - checksum: b8f61c1101dd3ad873ff2f3d0e9e6a5139ad17e20990b89ae67f2585043bc2b727151ed85f3e58aabc8a1a95af28e1ee26f69af6ac9e8841ff68129eae2f5ac5 - languageName: node - linkType: hard - "make-fetch-happen@npm:^10.0.3": version: 10.2.1 resolution: "make-fetch-happen@npm:10.2.1" @@ -6332,15 +6302,6 @@ __metadata: languageName: node linkType: hard -"node-id3@npm:^0.2.6": - version: 0.2.6 - resolution: "node-id3@npm:0.2.6" - dependencies: - iconv-lite: 0.6.2 - checksum: 9f3ba9d42f4d52348bb2f88dbcdd63ee8fd513dc7c01481d6b10082b83d0f1ce696f920c9bff0e3f2b00486c25fe49c3f93a56d54813809b7edc9ab14b1383d6 - languageName: node - linkType: hard - "node-notifier@npm:^10.0.1": version: 10.0.1 resolution: "node-notifier@npm:10.0.1" @@ -6859,13 +6820,6 @@ __metadata: languageName: node linkType: hard -"peek-readable@npm:^5.0.0": - version: 5.0.0 - resolution: "peek-readable@npm:5.0.0" - checksum: bef5ceb50586eb42e14efba274ac57ffe97f0ed272df9239ce029f688f495d9bf74b2886fa27847c706a9db33acda4b7d23bbd09a2d21eb4c2a54da915117414 - languageName: node - linkType: hard - "pend@npm:~1.2.0": version: 1.2.0 resolution: "pend@npm:1.2.0" @@ -7306,7 +7260,7 @@ __metadata: languageName: node linkType: hard -"readable-web-to-node-stream@npm:^3.0.0, readable-web-to-node-stream@npm:^3.0.2": +"readable-web-to-node-stream@npm:^3.0.0": version: 3.0.2 resolution: "readable-web-to-node-stream@npm:3.0.2" dependencies: @@ -7603,7 +7557,7 @@ __metadata: languageName: node linkType: hard -"sax@npm:>=0.6.0, sax@npm:^1.1.3, sax@npm:^1.2.4": +"sax@npm:>=0.6.0, sax@npm:^1.2.4": version: 1.2.4 resolution: "sax@npm:1.2.4" checksum: d3df7d32b897a2c2f28e941f732c71ba90e27c24f62ee918bd4d9a8cfb3553f2f81e5493c7f0be94a11c1911b643a9108f231dd6f60df3fa9586b5d2e3e9e1fe @@ -8080,16 +8034,6 @@ __metadata: languageName: node linkType: hard -"strtok3@npm:^7.0.0": - version: 7.0.0 - resolution: "strtok3@npm:7.0.0" - dependencies: - "@tokenizer/token": ^0.3.0 - peek-readable: ^5.0.0 - checksum: 2ebe7ad8f2aea611dec6742cf6a42e82764892a362907f7ce493faf334501bf981ce21c828dcc300457e6d460dc9c34d644ededb3b01dcb9e37559203cf1748c - languageName: node - linkType: hard - "sumchecker@npm:^3.0.1": version: 3.0.1 resolution: "sumchecker@npm:3.0.1" @@ -8277,16 +8221,6 @@ __metadata: languageName: node linkType: hard -"token-types@npm:^5.0.1": - version: 5.0.1 - resolution: "token-types@npm:5.0.1" - dependencies: - "@tokenizer/token": ^0.3.0 - ieee754: ^1.2.1 - checksum: 32780123bc6ce8b6a2231d860445c994a02a720abf38df5583ea957aa6626873cd1c4dd8af62314da4cf16ede00c379a765707a3b06f04b8808c38efdae1c785 - languageName: node - linkType: hard - "tough-cookie@npm:~2.5.0": version: 2.5.0 resolution: "tough-cookie@npm:2.5.0" @@ -9072,7 +9006,6 @@ __metadata: electron-store: ^8.1.0 electron-unhandled: ^4.0.1 electron-updater: ^5.3.0 - file-type: ^18.2.1 filenamify: ^4.3.0 howler: ^2.2.3 html-to-text: ^9.0.3 @@ -9082,14 +9015,12 @@ __metadata: mpris-service: ^2.1.2 node-fetch: ^2.6.8 node-gyp: ^9.3.1 - node-id3: ^0.2.6 node-notifier: ^10.0.1 playwright: ^1.29.2 simple-youtube-age-restriction-bypass: "https://gitpkg.now.sh/api/pkg.tgz?url=zerodytrash/Simple-YouTube-Age-Restriction-Bypass&commit=v2.5.4" vudio: ^2.1.1 xo: ^0.53.1 youtubei.js: ^3.1.1 - ytdl-core: ^4.11.1 ytpl: ^2.3.0 languageName: unknown linkType: soft @@ -9105,17 +9036,6 @@ __metadata: languageName: node linkType: hard -"ytdl-core@npm:^4.11.1": - version: 4.11.2 - resolution: "ytdl-core@npm:4.11.2" - dependencies: - m3u8stream: ^0.8.6 - miniget: ^4.2.2 - sax: ^1.1.3 - checksum: 57df38b5b1e4955db0e0c0be8d185f34de9eaee102ad1281d69de91628230cc84e8d46d278409eafa68c4aab4085a0f9fe8de30e9ea8644e011e20cae7f37c0e - languageName: node - linkType: hard - "ytpl@npm:^2.3.0": version: 2.3.0 resolution: "ytpl@npm:2.3.0" From 54d3f925e6199e97956db443bfa1fffa10b554e8 Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sat, 4 Mar 2023 01:53:19 +0200 Subject: [PATCH 03/33] add trackId to album downloads --- plugins/downloader/back-downloader.js | 18 +++++++++--------- plugins/downloader/menu.js | 9 ++++++++- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/plugins/downloader/back-downloader.js b/plugins/downloader/back-downloader.js index 9cf89c96..63923301 100644 --- a/plugins/downloader/back-downloader.js +++ b/plugins/downloader/back-downloader.js @@ -24,8 +24,9 @@ module.exports = async (options_) => { ipcMain.handle("download-song", (_, url) => downloadSong(url)); }; -async function downloadSong(url, playlistFolder = undefined) { +async function downloadSong(url, playlistFolder = undefined, trackId = undefined) { const metadata = await getMetadata(url); + metadata.trackId = trackId; const stream = await yt.download(metadata.id, { type: 'audio', // audio, video or video+audio @@ -53,7 +54,7 @@ async function downloadSong(url, playlistFolder = undefined) { } if (!presets[options.preset]) { - const fileBuffer = await toMP3(iterableStream, filePath, metadata); + const fileBuffer = await toMP3(iterableStream, metadata); console.info('writing id3 tags...'); // DELETE writeFileSync(filePath, await writeID3(fileBuffer, metadata)); console.info('done writing id3 tags!'); // DELETE @@ -78,7 +79,6 @@ function getIdFromUrl(url) { async function getMetadata(url) { const id = getIdFromUrl(url); const info = await yt.music.getInfo(id); - //console.log('got info:' + JSON.stringify(info, null, 2)); // DELETE return { id: info.basic_info.id, @@ -120,6 +120,9 @@ async function writeID3(buffer, metadata) { }); } } + if (metadata.trackId) { + writer.setFrame("TRCK", metadata.trackId); + } writer.addTag(); return Buffer.from(writer.arrayBuffer); } catch (e) { @@ -138,7 +141,7 @@ const ffmpeg = require("@ffmpeg/ffmpeg").createFFmpeg({ const ffmpegMutex = new Mutex(); -async function toMP3(stream, filePath, metadata, extension = "mp3") { +async function toMP3(stream, metadata, extension = "mp3") { const chunks = []; for await (const chunk of stream) { chunks.push(chunk); @@ -165,13 +168,9 @@ async function toMP3(stream, filePath, metadata, extension = "mp3") { safeVideoName + "." + extension ); - const fileBuffer = ffmpeg.FS("readFile", safeVideoName + "." + extension); - - await writeID3(fileBuffer, metadata); - // sendFeedback("Saving…"); - return fileBuffer; + return ffmpeg.FS("readFile", safeVideoName + "." + extension); } catch (e) { sendError(e); } finally { @@ -210,5 +209,6 @@ function getFFmpegMetadataArgs(metadata) { ...(metadata.title ? ["-metadata", `title=${metadata.title}`] : []), ...(metadata.artist ? ["-metadata", `artist=${metadata.artist}`] : []), ...(metadata.album ? ["-metadata", `album=${metadata.album}`] : []), + ...(metadata.trackId ? ["-metadata", `track=${metadata.trackId}`] : []), ]; }; diff --git a/plugins/downloader/menu.js b/plugins/downloader/menu.js index b133394e..81959e35 100644 --- a/plugins/downloader/menu.js +++ b/plugins/downloader/menu.js @@ -96,6 +96,10 @@ async function downloadPlaylist(givenUrl, win, options) { sendError(e); return; } + let isAlbum = playlist.title.startsWith('Album - '); + if (isAlbum) { + playlist.title = playlist.title.slice(8); + } const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' }); const folder = getFolder(options.downloadFolder); @@ -143,9 +147,12 @@ async function downloadPlaylist(givenUrl, win, options) { } }); + let counter = 1; + try { for (const song of playlist.items) { - await downloadSong(song.url, playlistFolder).catch((e) => sendError(e)); + const trackId = isAlbum ? counter++ : undefined; + await downloadSong(song.url, playlistFolder, trackId).catch((e) => sendError(e)); } } catch (e) { sendError(e); From 099e5d8491e68f09730d670bdabcb0f4e26a18bf Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sat, 4 Mar 2023 11:57:56 +0200 Subject: [PATCH 04/33] add download feedback and progress --- package.json | 1 - plugins/downloader/back-downloader.js | 214 +++++++++++++++++++++----- plugins/downloader/back.js | 6 +- plugins/downloader/front.js | 12 +- plugins/downloader/menu.js | 127 +-------------- plugins/downloader/utils.js | 4 + yarn.lock | 89 +---------- 7 files changed, 199 insertions(+), 254 deletions(-) diff --git a/package.json b/package.json index 6fa8f076..f8f40050 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,6 @@ "browser-id3-writer": "^4.4.0", "butterchurn": "^2.6.7", "butterchurn-presets": "^2.4.7", - "chokidar": "^3.5.3", "custom-electron-prompt": "^1.5.1", "custom-electron-titlebar": "^4.1.5", "electron-better-web-request": "^1.0.1", diff --git a/plugins/downloader/back-downloader.js b/plugins/downloader/back-downloader.js index 63923301..5e64ffb3 100644 --- a/plugins/downloader/back-downloader.js +++ b/plugins/downloader/back-downloader.js @@ -1,38 +1,67 @@ const { existsSync, mkdirSync, createWriteStream, writeFileSync } = require('fs'); -const { ipcMain, app } = require("electron"); const { join } = require("path"); -const { Innertube, UniversalCache, Utils } = require('youtubei.js'); -const filenamify = require("filenamify"); -const ID3Writer = require("browser-id3-writer"); - const { fetchFromGenius } = require("../lyrics-genius/back"); const { isEnabled } = require("../../config/plugins"); const { getImage } = require("../../providers/song-info"); -const { cropMaxWidth } = require("./utils"); - const { sendError } = require("./back"); -const { presets } = require('./utils'); +const { presets, cropMaxWidth, getFolder, setBadge, sendFeedback: sendFeedback_ } = require('./utils'); + +const { ipcMain, app, dialog } = require("electron"); +const is = require("electron-is"); +const { Innertube, UniversalCache, Utils } = require('youtubei.js'); +const ytpl = require("ytpl"); // REPLACE with youtubei getplaylist https://github.com/LuanRT/YouTube.js#getplaylistid + +const filenamify = require("filenamify"); +const ID3Writer = require("browser-id3-writer"); +const { randomBytes } = require("crypto"); +const Mutex = require("async-mutex").Mutex; +const ffmpeg = require("@ffmpeg/ffmpeg").createFFmpeg({ + log: false, + logger: () => { }, // console.log, + progress: () => { }, // console.log, +}); +const ffmpegMutex = new Mutex(); /** @type {Innertube} */ let yt; let options; +let win; +let playingUrl = undefined; -module.exports = async (options_) => { +module.exports = async (win_, options_) => { options = options_; + win = win_; yt = await Innertube.create({ cache: new UniversalCache(false), generate_session_locally: true }); ipcMain.handle("download-song", (_, url) => downloadSong(url)); + ipcMain.on("video-src-changed", async (_, data) => { + playingUrl = JSON.parse(data)?.microformat?.microformatDataRenderer?.urlCanonical; + }); + ipcMain.on("download-playlist-request", async (_event, url) => downloadPlaylist(url, win, options)); }; -async function downloadSong(url, playlistFolder = undefined, trackId = undefined) { +async function downloadSong(url, playlistFolder = undefined, trackId = undefined, increasePlaylistProgress = ()=>{}) { + const sendFeedback = (message, progress) => { + if (!playlistFolder) { + sendFeedback_(win, message); + if (!isNaN(progress)) { + win.setProgressBar(progress); + } + } + }; + + sendFeedback(`Downloading...`, 2); const metadata = await getMetadata(url); metadata.trackId = trackId; - const stream = await yt.download(metadata.id, { + const download_options = { type: 'audio', // audio, video or video+audio quality: 'best', // best, bestefficiency, 144p, 240p, 480p, 720p and so on. format: 'any' // media container format - }); + }; + + const format = metadata.info.chooseFormat(download_options); + const stream = await metadata.info.download(download_options); console.info(`Downloading ${metadata.artist} - ${metadata.title} {${metadata.id}}...`); @@ -54,30 +83,32 @@ async function downloadSong(url, playlistFolder = undefined, trackId = undefined } if (!presets[options.preset]) { - const fileBuffer = await toMP3(iterableStream, metadata); - console.info('writing id3 tags...'); // DELETE - writeFileSync(filePath, await writeID3(fileBuffer, metadata)); - console.info('done writing id3 tags!'); // DELETE + const fileBuffer = await toMP3(iterableStream, metadata, format.content_length, sendFeedback, increasePlaylistProgress); + writeFileSync(filePath, await writeID3(fileBuffer, metadata, sendFeedback)); } else { const file = createWriteStream(filePath); - //stream.pipeTo(file); + let downloaded = 0; + let total = format.content_length; + for await (const chunk of iterableStream) { + downloaded += chunk.length; + const ratio = downloaded / total; + const progress = Math.floor(ratio * 100); + sendFeedback("Download: " + progress + "%", ratio); + increasePlaylistProgress(ratio); file.write(chunk); } await ffmpegWriteTags(filePath, metadata, presets[options.preset]?.ffmpegArgs); + sendFeedback(null, -1); } + sendFeedback(null, -1); console.info(`${filePath} - Done!`, '\n'); } module.exports.downloadSong = downloadSong; -function getIdFromUrl(url) { - const match = url.match(/v=([^&]+)/); - return match ? match[1] : null; -} - async function getMetadata(url) { - const id = getIdFromUrl(url); + const id = url.match(/v=([^&]+)/)?.[1]; const info = await yt.music.getInfo(id); return { @@ -86,11 +117,14 @@ async function getMetadata(url) { artist: info.basic_info.author, album: info.player_overlays?.browser_media_session?.album?.text, image: info.basic_info.thumbnail[0].url, + info }; } -async function writeID3(buffer, metadata) { +async function writeID3(buffer, metadata, sendFeedback) { try { + sendFeedback("Writing ID3 tags..."); + const nativeImage = cropMaxWidth(await getImage(metadata.image)); const coverBuffer = nativeImage && !nativeImage.isEmpty() ? nativeImage.toPNG() : null; @@ -130,36 +164,33 @@ async function writeID3(buffer, metadata) { } } - -const { randomBytes } = require("crypto"); -const Mutex = require("async-mutex").Mutex; -const ffmpeg = require("@ffmpeg/ffmpeg").createFFmpeg({ - log: false, - logger: () => { }, // console.log, - progress: () => { }, // console.log, -}); - -const ffmpegMutex = new Mutex(); - -async function toMP3(stream, metadata, extension = "mp3") { +async function toMP3(stream, metadata, content_length, sendFeedback, increasePlaylistProgress = () => { }, extension = "mp3") { const chunks = []; + let downloaded = 0; + let total = content_length; for await (const chunk of stream) { + downloaded += chunk.length; chunks.push(chunk); + const ratio = downloaded / total; + const progress = Math.floor(ratio * 100); + sendFeedback("Download: " + progress + "%", ratio); + increasePlaylistProgress(ratio); } + sendFeedback("Loading…", 2); // indefinite progress bar after download + const buffer = Buffer.concat(chunks); const safeVideoName = randomBytes(32).toString("hex"); const releaseFFmpegMutex = await ffmpegMutex.acquire(); try { if (!ffmpeg.isLoaded()) { - // sendFeedback("Loading…", 2); // indefinite progress bar after download await ffmpeg.load(); } - // sendFeedback("Preparing file…"); + sendFeedback("Preparing file…"); ffmpeg.FS("writeFile", safeVideoName, buffer); - // sendFeedback("Converting…"); + sendFeedback("Converting…"); await ffmpeg.run( "-i", @@ -168,9 +199,9 @@ async function toMP3(stream, metadata, extension = "mp3") { safeVideoName + "." + extension ); - // sendFeedback("Saving…"); + sendFeedback("Saving…"); - return ffmpeg.FS("readFile", safeVideoName + "." + extension); + return ffmpeg.FS("readFile", safeVideoName + "." + extension); } catch (e) { sendError(e); } finally { @@ -212,3 +243,104 @@ function getFFmpegMetadataArgs(metadata) { ...(metadata.trackId ? ["-metadata", `track=${metadata.trackId}`] : []), ]; }; + +// Playlist radio modifier needs to be cut from playlist ID +const INVALID_PLAYLIST_MODIFIER = 'RDAMPL'; + +const getPlaylistID = aURL => { + const result = aURL?.searchParams.get("list") || aURL?.searchParams.get("playlist"); + if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) { + return result.slice(6) + } + return result; +}; + +async function downloadPlaylist(givenUrl) { + if (givenUrl) { + try { + givenUrl = new URL(givenUrl); + } catch { + givenUrl = undefined; + }; + } + const playlistId = getPlaylistID(givenUrl) + || getPlaylistID(new URL(win.webContents.getURL())) + || getPlaylistID(new URL(playingUrl)); + + if (!playlistId) { + sendError(new Error("No playlist ID found")); + return; + } + + const sendFeedback = message => sendFeedback_(win, message); + + console.log(`trying to get playlist ID: '${playlistId}'`); + sendFeedback("Getting playlist info…"); + let playlist; + try { + playlist = await ytpl(playlistId, { + limit: options.playlistMaxItems || Infinity, + }); + } catch (e) { + sendError(e); + return; + } + let isAlbum = playlist.title.startsWith('Album - '); + if (isAlbum) { + playlist.title = playlist.title.slice(8); + } + const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' }); + + const folder = getFolder(options.downloadFolder); + const playlistFolder = join(folder, safePlaylistTitle); + if (existsSync(playlistFolder)) { + sendError(new Error(`The folder ${playlistFolder} already exists`)); + return; + } + mkdirSync(playlistFolder, { recursive: true }); + + dialog.showMessageBox({ + type: "info", + buttons: ["OK"], + title: "Started Download", + message: `Downloading Playlist "${playlist.title}"`, + detail: `(${playlist.items.length} songs)`, + }); + + if (is.dev()) { + console.log( + `Downloading playlist "${playlist.title}" - ${playlist.items.length} songs (${playlistId})` + ); + } + + win.setProgressBar(2); // starts with indefinite bar + + setBadge(playlist.items.length); + + let counter = 1; + + const progressStep = 1 / playlist.items.length; + + const increaseProgress = (itemPercentage) => { + const currentProgress = (counter - 1) / playlist.items.length; + const newProgress = currentProgress + (progressStep * itemPercentage); + win.setProgressBar(newProgress); + }; + + try { + for (const song of playlist.items) { + sendFeedback(`Downloading ${counter}/${playlist.items.length}...`); + const trackId = isAlbum ? counter : undefined; + await downloadSong(song.url, playlistFolder, trackId, increaseProgress).catch((e) => sendError(e)); + win.setProgressBar(counter / playlist.items.length); + setBadge(playlist.items.length - counter); + counter++; + } + } catch (e) { + sendError(e); + } finally { + win.setProgressBar(-1); // close progress bar + setBadge(0); // close badge counter + sendFeedback(); // clear feedback + } +} diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index 379d6271..bb980675 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -4,12 +4,16 @@ const { dialog } = require("electron"); const registerCallback = require("../../providers/song-info"); const { injectCSS, listenAction } = require("../utils"); +const { setBadge, sendFeedback } = require("./utils"); const { ACTIONS, CHANNEL } = require("./actions.js"); let win = {}; const sendError = (error) => { win.setProgressBar(-1); // close progress bar + setBadge(0); // close badge + sendFeedback(); // reset feedback + console.error(error); dialog.showMessageBox({ @@ -28,7 +32,7 @@ function handle(win_, options) { win = win_; injectCSS(win.webContents, join(__dirname, "style.css")); - require("./back-downloader")(options); + require("./back-downloader")(win, options); registerCallback((info) => { nowPlayingMetadata = info; diff --git a/plugins/downloader/front.js b/plugins/downloader/front.js index 97d73397..e534ed71 100644 --- a/plugins/downloader/front.js +++ b/plugins/downloader/front.js @@ -40,7 +40,7 @@ const baseUrl = defaultConfig.url; // contextBridge.exposeInMainWorld("downloader", { // download: () => { global.download = () => { - triggerAction(CHANNEL, ACTIONS.PROGRESS, 2); // starts with indefinite progress bar + //triggerAction(CHANNEL, ACTIONS.PROGRESS, 2); // starts with indefinite progress bar let metadata; let videoUrl = getSongMenu() // selector of first button which is always "Start Radio" @@ -60,7 +60,7 @@ global.download = () => { videoUrl = metadata.url || window.location.href; } - ipcRenderer.invoke('download-song', videoUrl).finally(() => triggerAction(CHANNEL, ACTIONS.PROGRESS, -1)); + ipcRenderer.invoke('download-song', videoUrl)//.finally(() => triggerAction(CHANNEL, ACTIONS.PROGRESS, -1)); return; }; @@ -73,6 +73,14 @@ function observeMenu(options) { subtree: true, }); }, { once: true, passive: true }) + + ipcRenderer.on('downloader-feedback', (_, feedback)=> { + if (!progress) { + console.warn("Cannot update progress"); + } else { + progress.innerHTML = feedback || "Download"; + } + }); } module.exports = observeMenu; diff --git a/plugins/downloader/menu.js b/plugins/downloader/menu.js index 81959e35..dd572693 100644 --- a/plugins/downloader/menu.js +++ b/plugins/downloader/menu.js @@ -1,41 +1,12 @@ -const { existsSync, mkdirSync } = require("fs"); -const { join } = require("path"); - -const { dialog, ipcMain } = require("electron"); -const is = require("electron-is"); -const ytpl = require("ytpl"); -const chokidar = require('chokidar'); -const filenamify = require('filenamify'); +const { dialog } = require("electron"); const { setMenuOptions } = require("../../config/plugins"); -const { sendError } = require("./back"); -const { downloadSong } = require("./back-downloader"); -const { defaultMenuDownloadLabel, getFolder, presets, setBadge } = require("./utils"); +const { downloadPlaylist } = require("./back-downloader"); +const { defaultMenuDownloadLabel, getFolder, presets } = require("./utils"); let downloadLabel = defaultMenuDownloadLabel; -let playingUrl = undefined; -let callbackIsRegistered = false; - -// Playlist radio modifier needs to be cut from playlist ID -const INVALID_PLAYLIST_MODIFIER = 'RDAMPL'; - -const getPlaylistID = aURL => { - const result = aURL?.searchParams.get("list") || aURL?.searchParams.get("playlist"); - if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) { - return result.slice(6) - } - return result; -}; module.exports = (win, options) => { - if (!callbackIsRegistered) { - ipcMain.on("video-src-changed", async (_, data) => { - playingUrl = JSON.parse(data)?.microformat?.microformatDataRenderer?.urlCanonical; - }); - ipcMain.on("download-playlist-request", async (_event, url) => downloadPlaylist(url, win, options)); - callbackIsRegistered = true; - } - return [ { label: downloadLabel, @@ -68,95 +39,3 @@ module.exports = (win, options) => { }, ]; }; - -async function downloadPlaylist(givenUrl, win, options) { - if (givenUrl) { - try { - givenUrl = new URL(givenUrl); - } catch { - givenUrl = undefined; - }; - } - const playlistId = getPlaylistID(givenUrl) - || getPlaylistID(new URL(win.webContents.getURL())) - || getPlaylistID(new URL(playingUrl)); - - if (!playlistId) { - sendError(new Error("No playlist ID found")); - return; - } - - console.log(`trying to get playlist ID: '${playlistId}'`); - let playlist; - try { - playlist = await ytpl(playlistId, { - limit: options.playlistMaxItems || Infinity, - }); - } catch (e) { - sendError(e); - return; - } - let isAlbum = playlist.title.startsWith('Album - '); - if (isAlbum) { - playlist.title = playlist.title.slice(8); - } - const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' }); - - const folder = getFolder(options.downloadFolder); - const playlistFolder = join(folder, safePlaylistTitle); - if (existsSync(playlistFolder)) { - sendError(new Error(`The folder ${playlistFolder} already exists`)); - return; - } - mkdirSync(playlistFolder, { recursive: true }); - - dialog.showMessageBox({ - type: "info", - buttons: ["OK"], - title: "Started Download", - message: `Downloading Playlist "${playlist.title}"`, - detail: `(${playlist.items.length} songs)`, - }); - - if (is.dev()) { - console.log( - `Downloading playlist "${playlist.title}" - ${playlist.items.length} songs (${playlistId})` - ); - } - - win.setProgressBar(2); // starts with indefinite bar - - let downloadCount = 0; - setBadge(playlist.items.length); - - let dirWatcher = chokidar.watch(playlistFolder); - const closeDirWatcher = () => { - if (dirWatcher) { - win.setProgressBar(-1); // close progress bar - setBadge(0); // close badge counter - dirWatcher.close().then(() => (dirWatcher = null)); - } - }; - dirWatcher.on('add', () => { - downloadCount += 1; - if (downloadCount >= playlist.items.length) { - closeDirWatcher(); - } else { - win.setProgressBar(downloadCount / playlist.items.length); - setBadge(playlist.items.length - downloadCount); - } - }); - - let counter = 1; - - try { - for (const song of playlist.items) { - const trackId = isAlbum ? counter++ : undefined; - await downloadSong(song.url, playlistFolder, trackId).catch((e) => sendError(e)); - } - } catch (e) { - sendError(e); - } finally { - closeDirWatcher(); - } -} diff --git a/plugins/downloader/utils.js b/plugins/downloader/utils.js index 0569a6d1..def066cb 100644 --- a/plugins/downloader/utils.js +++ b/plugins/downloader/utils.js @@ -4,6 +4,10 @@ const is = require('electron-is'); module.exports.getFolder = customFolder => customFolder || app.getPath("downloads"); module.exports.defaultMenuDownloadLabel = "Download playlist"; +module.exports.sendFeedback = (win, message) => { + win.webContents.send("downloader-feedback", message); +}; + const orderedQualityList = ["maxresdefault", "hqdefault", "mqdefault", "sdddefault"]; module.exports.urlToJPG = (imgUrl, videoId) => { if (!imgUrl || imgUrl.includes(".jpg")) return imgUrl; diff --git a/yarn.lock b/yarn.lock index 929d11d3..971b652a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1365,16 +1365,6 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:~3.1.2": - version: 3.1.3 - resolution: "anymatch@npm:3.1.3" - dependencies: - normalize-path: ^3.0.0 - picomatch: ^2.0.4 - checksum: 3e044fd6d1d26545f235a9fe4d7a534e2029d8e59fa7fd9f2a6eb21230f6b5380ea1eaf55136e60cbf8e613544b3b766e7a6fa2102e2a3a117505466e3025dc2 - languageName: node - linkType: hard - "app-builder-bin@npm:4.0.0": version: 4.0.0 resolution: "app-builder-bin@npm:4.0.0" @@ -1682,13 +1672,6 @@ __metadata: languageName: node linkType: hard -"binary-extensions@npm:^2.0.0": - version: 2.2.0 - resolution: "binary-extensions@npm:2.2.0" - checksum: ccd267956c58d2315f5d3ea6757cf09863c5fc703e50fbeb13a7dc849b812ef76e3cf9ca8f35a0c48498776a7478d7b4a0418e1e2b8cb9cb9731f2922aaad7f8 - languageName: node - linkType: hard - "bindings@npm:^1.2.1": version: 1.5.0 resolution: "bindings@npm:1.5.0" @@ -1754,7 +1737,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.2, braces@npm:~3.0.2": +"braces@npm:^3.0.2": version: 3.0.2 resolution: "braces@npm:3.0.2" dependencies: @@ -2065,25 +2048,6 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.5.3": - version: 3.5.3 - resolution: "chokidar@npm:3.5.3" - dependencies: - anymatch: ~3.1.2 - braces: ~3.0.2 - fsevents: ~2.3.2 - glob-parent: ~5.1.2 - is-binary-path: ~2.1.0 - is-glob: ~4.0.1 - normalize-path: ~3.0.0 - readdirp: ~3.6.0 - dependenciesMeta: - fsevents: - optional: true - checksum: b49fcde40176ba007ff361b198a2d35df60d9bb2a5aab228279eb810feae9294a6b4649ab15981304447afe1e6ffbf4788ad5db77235dc770ab777c6e771980c - languageName: node - linkType: hard - "chownr@npm:^2.0.0": version: 2.0.0 resolution: "chownr@npm:2.0.0" @@ -4102,25 +4066,6 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:~2.3.2": - version: 2.3.2 - resolution: "fsevents@npm:2.3.2" - dependencies: - node-gyp: latest - checksum: 97ade64e75091afee5265e6956cb72ba34db7819b4c3e94c431d4be2b19b8bb7a2d4116da417950c3425f17c8fe693d25e20212cac583ac1521ad066b77ae31f - conditions: os=darwin - languageName: node - linkType: hard - -"fsevents@patch:fsevents@~2.3.2#~builtin": - version: 2.3.2 - resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin::version=2.3.2&hash=df0bf1" - dependencies: - node-gyp: latest - conditions: os=darwin - languageName: node - linkType: hard - "function-bind@npm:^1.1.1": version: 1.1.1 resolution: "function-bind@npm:1.1.1" @@ -4247,7 +4192,7 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": +"glob-parent@npm:^5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" dependencies: @@ -4896,15 +4841,6 @@ __metadata: languageName: node linkType: hard -"is-binary-path@npm:~2.1.0": - version: 2.1.0 - resolution: "is-binary-path@npm:2.1.0" - dependencies: - binary-extensions: ^2.0.0 - checksum: 84192eb88cff70d320426f35ecd63c3d6d495da9d805b19bc65b518984b7c0760280e57dbf119b7e9be6b161784a5a673ab2c6abe83abb5198a432232ad5b35c - languageName: node - linkType: hard - "is-boolean-object@npm:^1.1.0": version: 1.1.2 resolution: "is-boolean-object@npm:1.1.2" @@ -5016,7 +4952,7 @@ __metadata: languageName: node linkType: hard -"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3": version: 4.0.3 resolution: "is-glob@npm:4.0.3" dependencies: @@ -6363,13 +6299,6 @@ __metadata: languageName: node linkType: hard -"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": - version: 3.0.0 - resolution: "normalize-path@npm:3.0.0" - checksum: 88eeb4da891e10b1318c4b2476b6e2ecbeb5ff97d946815ffea7794c31a89017c70d7f34b3c2ebf23ef4e9fc9fb99f7dffe36da22011b5b5c6ffa34f4873ec20 - languageName: node - linkType: hard - "normalize-url@npm:^6.0.1": version: 6.1.0 resolution: "normalize-url@npm:6.1.0" @@ -6860,7 +6789,7 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.3.1": +"picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" checksum: 050c865ce81119c4822c45d3c84f1ced46f93a0126febae20737bd05ca20589c564d6e9226977df859ed5e03dc73f02584a2b0faad36e896936238238b0446cf @@ -7269,15 +7198,6 @@ __metadata: languageName: node linkType: hard -"readdirp@npm:~3.6.0": - version: 3.6.0 - resolution: "readdirp@npm:3.6.0" - dependencies: - picomatch: ^2.2.1 - checksum: 1ced032e6e45670b6d7352d71d21ce7edf7b9b928494dcaba6f11fba63180d9da6cd7061ebc34175ffda6ff529f481818c962952004d273178acd70f7059b320 - languageName: node - linkType: hard - "redent@npm:^4.0.0": version: 4.0.0 resolution: "redent@npm:4.0.0" @@ -8991,7 +8911,6 @@ __metadata: browser-id3-writer: ^4.4.0 butterchurn: ^2.6.7 butterchurn-presets: ^2.4.7 - chokidar: ^3.5.3 custom-electron-prompt: ^1.5.1 custom-electron-titlebar: ^4.1.5 del-cli: ^5.0.0 From 2cbc73d2ed5a3da86227ba9daf70f8324c8a8df8 Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sat, 4 Mar 2023 12:17:27 +0200 Subject: [PATCH 05/33] remove unused imports --- plugins/downloader/actions.js | 11 - plugins/downloader/back-downloader.js | 346 ------------------------ plugins/downloader/back.js | 369 +++++++++++++++++++++++--- plugins/downloader/front.js | 17 +- plugins/downloader/menu.js | 2 +- 5 files changed, 341 insertions(+), 404 deletions(-) delete mode 100644 plugins/downloader/actions.js delete mode 100644 plugins/downloader/back-downloader.js diff --git a/plugins/downloader/actions.js b/plugins/downloader/actions.js deleted file mode 100644 index 0d6c3426..00000000 --- a/plugins/downloader/actions.js +++ /dev/null @@ -1,11 +0,0 @@ -const CHANNEL = "downloader"; -const ACTIONS = { - ERROR: "error", - METADATA: "metadata", - PROGRESS: "progress", -}; - -module.exports = { - CHANNEL: CHANNEL, - ACTIONS: ACTIONS, -}; diff --git a/plugins/downloader/back-downloader.js b/plugins/downloader/back-downloader.js deleted file mode 100644 index 5e64ffb3..00000000 --- a/plugins/downloader/back-downloader.js +++ /dev/null @@ -1,346 +0,0 @@ -const { existsSync, mkdirSync, createWriteStream, writeFileSync } = require('fs'); -const { join } = require("path"); - -const { fetchFromGenius } = require("../lyrics-genius/back"); -const { isEnabled } = require("../../config/plugins"); -const { getImage } = require("../../providers/song-info"); -const { sendError } = require("./back"); -const { presets, cropMaxWidth, getFolder, setBadge, sendFeedback: sendFeedback_ } = require('./utils'); - -const { ipcMain, app, dialog } = require("electron"); -const is = require("electron-is"); -const { Innertube, UniversalCache, Utils } = require('youtubei.js'); -const ytpl = require("ytpl"); // REPLACE with youtubei getplaylist https://github.com/LuanRT/YouTube.js#getplaylistid - -const filenamify = require("filenamify"); -const ID3Writer = require("browser-id3-writer"); -const { randomBytes } = require("crypto"); -const Mutex = require("async-mutex").Mutex; -const ffmpeg = require("@ffmpeg/ffmpeg").createFFmpeg({ - log: false, - logger: () => { }, // console.log, - progress: () => { }, // console.log, -}); -const ffmpegMutex = new Mutex(); - -/** @type {Innertube} */ -let yt; -let options; -let win; -let playingUrl = undefined; - -module.exports = async (win_, options_) => { - options = options_; - win = win_; - yt = await Innertube.create({ cache: new UniversalCache(false), generate_session_locally: true }); - ipcMain.handle("download-song", (_, url) => downloadSong(url)); - ipcMain.on("video-src-changed", async (_, data) => { - playingUrl = JSON.parse(data)?.microformat?.microformatDataRenderer?.urlCanonical; - }); - ipcMain.on("download-playlist-request", async (_event, url) => downloadPlaylist(url, win, options)); -}; - -async function downloadSong(url, playlistFolder = undefined, trackId = undefined, increasePlaylistProgress = ()=>{}) { - const sendFeedback = (message, progress) => { - if (!playlistFolder) { - sendFeedback_(win, message); - if (!isNaN(progress)) { - win.setProgressBar(progress); - } - } - }; - - sendFeedback(`Downloading...`, 2); - const metadata = await getMetadata(url); - metadata.trackId = trackId; - - const download_options = { - type: 'audio', // audio, video or video+audio - quality: 'best', // best, bestefficiency, 144p, 240p, 480p, 720p and so on. - format: 'any' // media container format - }; - - const format = metadata.info.chooseFormat(download_options); - const stream = await metadata.info.download(download_options); - - console.info(`Downloading ${metadata.artist} - ${metadata.title} {${metadata.id}}...`); - - const iterableStream = Utils.streamToIterable(stream); - - const dir = playlistFolder || options.downloadFolder || app.getPath("downloads"); - const name = `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`; - - const extension = presets[options.preset]?.extension || 'mp3'; - - const filename = filenamify(`${name}.${extension}`, { - replacement: "_", - maxLength: 255, - }); - const filePath = join(dir, filename); - - if (!existsSync(dir)) { - mkdirSync(dir); - } - - if (!presets[options.preset]) { - const fileBuffer = await toMP3(iterableStream, metadata, format.content_length, sendFeedback, increasePlaylistProgress); - writeFileSync(filePath, await writeID3(fileBuffer, metadata, sendFeedback)); - } else { - const file = createWriteStream(filePath); - let downloaded = 0; - let total = format.content_length; - - for await (const chunk of iterableStream) { - downloaded += chunk.length; - const ratio = downloaded / total; - const progress = Math.floor(ratio * 100); - sendFeedback("Download: " + progress + "%", ratio); - increasePlaylistProgress(ratio); - file.write(chunk); - } - await ffmpegWriteTags(filePath, metadata, presets[options.preset]?.ffmpegArgs); - sendFeedback(null, -1); - } - - sendFeedback(null, -1); - console.info(`${filePath} - Done!`, '\n'); -} -module.exports.downloadSong = downloadSong; - -async function getMetadata(url) { - const id = url.match(/v=([^&]+)/)?.[1]; - const info = await yt.music.getInfo(id); - - return { - id: info.basic_info.id, - title: info.basic_info.title, - artist: info.basic_info.author, - album: info.player_overlays?.browser_media_session?.album?.text, - image: info.basic_info.thumbnail[0].url, - info - }; -} - -async function writeID3(buffer, metadata, sendFeedback) { - try { - sendFeedback("Writing ID3 tags..."); - - const nativeImage = cropMaxWidth(await getImage(metadata.image)); - const coverBuffer = nativeImage && !nativeImage.isEmpty() ? - nativeImage.toPNG() : null; - - const writer = new ID3Writer(buffer); - - // Create the metadata tags - writer - .setFrame("TIT2", metadata.title) - .setFrame("TPE1", [metadata.artist]); - if (metadata.album) { - writer.setFrame("TALB", metadata.album); - } - if (coverBuffer) { - writer.setFrame("APIC", { - type: 3, - data: coverBuffer, - description: "", - }); - } - if (isEnabled("lyrics-genius")) { - const lyrics = await fetchFromGenius(metadata); - if (lyrics) { - writer.setFrame("USLT", { - description: '', - lyrics: lyrics, - }); - } - } - if (metadata.trackId) { - writer.setFrame("TRCK", metadata.trackId); - } - writer.addTag(); - return Buffer.from(writer.arrayBuffer); - } catch (e) { - sendError(e); - } -} - -async function toMP3(stream, metadata, content_length, sendFeedback, increasePlaylistProgress = () => { }, extension = "mp3") { - const chunks = []; - let downloaded = 0; - let total = content_length; - for await (const chunk of stream) { - downloaded += chunk.length; - chunks.push(chunk); - const ratio = downloaded / total; - const progress = Math.floor(ratio * 100); - sendFeedback("Download: " + progress + "%", ratio); - increasePlaylistProgress(ratio); - } - sendFeedback("Loading…", 2); // indefinite progress bar after download - - const buffer = Buffer.concat(chunks); - const safeVideoName = randomBytes(32).toString("hex"); - const releaseFFmpegMutex = await ffmpegMutex.acquire(); - - try { - if (!ffmpeg.isLoaded()) { - await ffmpeg.load(); - } - - sendFeedback("Preparing file…"); - ffmpeg.FS("writeFile", safeVideoName, buffer); - - sendFeedback("Converting…"); - - await ffmpeg.run( - "-i", - safeVideoName, - ...getFFmpegMetadataArgs(metadata), - safeVideoName + "." + extension - ); - - sendFeedback("Saving…"); - - return ffmpeg.FS("readFile", safeVideoName + "." + extension); - } catch (e) { - sendError(e); - } finally { - releaseFFmpegMutex(); - } -} - -async function ffmpegWriteTags(filePath, metadata, ffmpegArgs = []) { - const releaseFFmpegMutex = await ffmpegMutex.acquire(); - - try { - if (!ffmpeg.isLoaded()) { - await ffmpeg.load(); - } - - await ffmpeg.run( - "-i", - filePath, - ...getFFmpegMetadataArgs(metadata), - ...ffmpegArgs, - filePath - ); - } catch (e) { - sendError(e); - } finally { - releaseFFmpegMutex(); - } -} - -function getFFmpegMetadataArgs(metadata) { - if (!metadata) { - return; - } - - return [ - ...(metadata.title ? ["-metadata", `title=${metadata.title}`] : []), - ...(metadata.artist ? ["-metadata", `artist=${metadata.artist}`] : []), - ...(metadata.album ? ["-metadata", `album=${metadata.album}`] : []), - ...(metadata.trackId ? ["-metadata", `track=${metadata.trackId}`] : []), - ]; -}; - -// Playlist radio modifier needs to be cut from playlist ID -const INVALID_PLAYLIST_MODIFIER = 'RDAMPL'; - -const getPlaylistID = aURL => { - const result = aURL?.searchParams.get("list") || aURL?.searchParams.get("playlist"); - if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) { - return result.slice(6) - } - return result; -}; - -async function downloadPlaylist(givenUrl) { - if (givenUrl) { - try { - givenUrl = new URL(givenUrl); - } catch { - givenUrl = undefined; - }; - } - const playlistId = getPlaylistID(givenUrl) - || getPlaylistID(new URL(win.webContents.getURL())) - || getPlaylistID(new URL(playingUrl)); - - if (!playlistId) { - sendError(new Error("No playlist ID found")); - return; - } - - const sendFeedback = message => sendFeedback_(win, message); - - console.log(`trying to get playlist ID: '${playlistId}'`); - sendFeedback("Getting playlist info…"); - let playlist; - try { - playlist = await ytpl(playlistId, { - limit: options.playlistMaxItems || Infinity, - }); - } catch (e) { - sendError(e); - return; - } - let isAlbum = playlist.title.startsWith('Album - '); - if (isAlbum) { - playlist.title = playlist.title.slice(8); - } - const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' }); - - const folder = getFolder(options.downloadFolder); - const playlistFolder = join(folder, safePlaylistTitle); - if (existsSync(playlistFolder)) { - sendError(new Error(`The folder ${playlistFolder} already exists`)); - return; - } - mkdirSync(playlistFolder, { recursive: true }); - - dialog.showMessageBox({ - type: "info", - buttons: ["OK"], - title: "Started Download", - message: `Downloading Playlist "${playlist.title}"`, - detail: `(${playlist.items.length} songs)`, - }); - - if (is.dev()) { - console.log( - `Downloading playlist "${playlist.title}" - ${playlist.items.length} songs (${playlistId})` - ); - } - - win.setProgressBar(2); // starts with indefinite bar - - setBadge(playlist.items.length); - - let counter = 1; - - const progressStep = 1 / playlist.items.length; - - const increaseProgress = (itemPercentage) => { - const currentProgress = (counter - 1) / playlist.items.length; - const newProgress = currentProgress + (progressStep * itemPercentage); - win.setProgressBar(newProgress); - }; - - try { - for (const song of playlist.items) { - sendFeedback(`Downloading ${counter}/${playlist.items.length}...`); - const trackId = isAlbum ? counter : undefined; - await downloadSong(song.url, playlistFolder, trackId, increaseProgress).catch((e) => sendError(e)); - win.setProgressBar(counter / playlist.items.length); - setBadge(playlist.items.length - counter); - counter++; - } - } catch (e) { - sendError(e); - } finally { - win.setProgressBar(-1); // close progress bar - setBadge(0); // close badge counter - sendFeedback(); // clear feedback - } -} diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index bb980675..5adde56a 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -1,18 +1,38 @@ +const { existsSync, mkdirSync, createWriteStream, writeFileSync } = require('fs'); const { join } = require("path"); -const { dialog } = require("electron"); +const { fetchFromGenius } = require("../lyrics-genius/back"); +const { isEnabled } = require("../../config/plugins"); +const { getImage } = require("../../providers/song-info"); +const { injectCSS } = require("../utils"); +const { presets, cropMaxWidth, getFolder, setBadge, sendFeedback: sendFeedback_ } = require('./utils'); -const registerCallback = require("../../providers/song-info"); -const { injectCSS, listenAction } = require("../utils"); -const { setBadge, sendFeedback } = require("./utils"); -const { ACTIONS, CHANNEL } = require("./actions.js"); +const { ipcMain, app, dialog } = require("electron"); +const is = require("electron-is"); +const { Innertube, UniversalCache, Utils } = require('youtubei.js'); +const ytpl = require("ytpl"); // REPLACE with youtubei getplaylist https://github.com/LuanRT/YouTube.js#getplaylistid -let win = {}; +const filenamify = require("filenamify"); +const ID3Writer = require("browser-id3-writer"); +const { randomBytes } = require("crypto"); +const Mutex = require("async-mutex").Mutex; +const ffmpeg = require("@ffmpeg/ffmpeg").createFFmpeg({ + log: false, + logger: () => { }, // console.log, + progress: () => { }, // console.log, +}); +const ffmpegMutex = new Mutex(); + +/** @type {Innertube} */ +let yt; +let options; +let win; +let playingUrl = undefined; const sendError = (error) => { win.setProgressBar(-1); // close progress bar setBadge(0); // close badge - sendFeedback(); // reset feedback + sendFeedback_(win); // reset feedback console.error(error); @@ -25,35 +45,320 @@ const sendError = (error) => { }); }; -let nowPlayingMetadata = {}; +module.exports = async (win_, options_) => { + options = options_; + win = win_; + injectCSS(win.webContents, join(__dirname, "style.css")); + yt = await Innertube.create({ cache: new UniversalCache(false), generate_session_locally: true }); + ipcMain.handle("download-song", (_, url) => downloadSong(url)); + ipcMain.on("video-src-changed", async (_, data) => { + playingUrl = JSON.parse(data)?.microformat?.microformatDataRenderer?.urlCanonical; + }); + ipcMain.on("download-playlist-request", async (_event, url) => downloadPlaylist(url, win, options)); +}; -function handle(win_, options) { - win = win_; - injectCSS(win.webContents, join(__dirname, "style.css")); +async function downloadSong(url, playlistFolder = undefined, trackId = undefined, increasePlaylistProgress = ()=>{}) { + const sendFeedback = (message, progress) => { + if (!playlistFolder) { + sendFeedback_(win, message); + if (!isNaN(progress)) { + win.setProgressBar(progress); + } + } + }; - require("./back-downloader")(win, options); + sendFeedback(`Downloading...`, 2); + const metadata = await getMetadata(url); + metadata.trackId = trackId; - registerCallback((info) => { - nowPlayingMetadata = info; - }); + const download_options = { + type: 'audio', // audio, video or video+audio + quality: 'best', // best, bestefficiency, 144p, 240p, 480p, 720p and so on. + format: 'any' // media container format + }; - listenAction(CHANNEL, (event, action, arg) => { - switch (action) { - case ACTIONS.ERROR: // arg = error - sendError(arg); - break; - case ACTIONS.METADATA: - event.returnValue = JSON.stringify(nowPlayingMetadata); - break; - case ACTIONS.PROGRESS: // arg = progress - win.setProgressBar(arg); - break; - default: - console.log("Unknown action: " + action); - } - }); + const format = metadata.info.chooseFormat(download_options); + const stream = await metadata.info.download(download_options); + + console.info(`Downloading ${metadata.artist} - ${metadata.title} [${metadata.id}]`); + + const iterableStream = Utils.streamToIterable(stream); + + const dir = playlistFolder || options.downloadFolder || app.getPath("downloads"); + const name = `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`; + + const extension = presets[options.preset]?.extension || 'mp3'; + + const filename = filenamify(`${name}.${extension}`, { + replacement: "_", + maxLength: 255, + }); + const filePath = join(dir, filename); + + if (!existsSync(dir)) { + mkdirSync(dir); + } + + if (!presets[options.preset]) { + const fileBuffer = await toMP3(iterableStream, metadata, format.content_length, sendFeedback, increasePlaylistProgress); + writeFileSync(filePath, await writeID3(fileBuffer, metadata, sendFeedback)); + } else { + const file = createWriteStream(filePath); + let downloaded = 0; + let total = format.content_length; + + for await (const chunk of iterableStream) { + downloaded += chunk.length; + const ratio = downloaded / total; + const progress = Math.floor(ratio * 100); + sendFeedback("Download: " + progress + "%", ratio); + increasePlaylistProgress(ratio); + file.write(chunk); + } + await ffmpegWriteTags(filePath, metadata, presets[options.preset]?.ffmpegArgs); + sendFeedback(null, -1); + } + + sendFeedback(null, -1); + console.info(`Saved download to ${filePath}`); +} +module.exports.downloadSong = downloadSong; + +async function getMetadata(url) { + const id = url.match(/v=([^&]+)/)?.[1]; + const info = await yt.music.getInfo(id); + + return { + id: info.basic_info.id, + title: info.basic_info.title, + artist: info.basic_info.author, + album: info.player_overlays?.browser_media_session?.album?.text, + image: info.basic_info.thumbnail[0].url, + info + }; } -module.exports = handle; -module.exports.sendError = sendError; +async function writeID3(buffer, metadata, sendFeedback) { + try { + sendFeedback("Writing ID3 tags..."); + + const nativeImage = cropMaxWidth(await getImage(metadata.image)); + const coverBuffer = nativeImage && !nativeImage.isEmpty() ? + nativeImage.toPNG() : null; + + const writer = new ID3Writer(buffer); + + // Create the metadata tags + writer + .setFrame("TIT2", metadata.title) + .setFrame("TPE1", [metadata.artist]); + if (metadata.album) { + writer.setFrame("TALB", metadata.album); + } + if (coverBuffer) { + writer.setFrame("APIC", { + type: 3, + data: coverBuffer, + description: "", + }); + } + if (isEnabled("lyrics-genius")) { + const lyrics = await fetchFromGenius(metadata); + if (lyrics) { + writer.setFrame("USLT", { + description: '', + lyrics: lyrics, + }); + } + } + if (metadata.trackId) { + writer.setFrame("TRCK", metadata.trackId); + } + writer.addTag(); + return Buffer.from(writer.arrayBuffer); + } catch (e) { + sendError(e); + } +} + +async function toMP3(stream, metadata, content_length, sendFeedback, increasePlaylistProgress = () => { }, extension = "mp3") { + const chunks = []; + let downloaded = 0; + let total = content_length; + for await (const chunk of stream) { + downloaded += chunk.length; + chunks.push(chunk); + const ratio = downloaded / total; + const progress = Math.floor(ratio * 100); + sendFeedback("Download: " + progress + "%", ratio); + increasePlaylistProgress(ratio); + } + sendFeedback("Loading…", 2); // indefinite progress bar after download + + const buffer = Buffer.concat(chunks); + const safeVideoName = randomBytes(32).toString("hex"); + const releaseFFmpegMutex = await ffmpegMutex.acquire(); + + try { + if (!ffmpeg.isLoaded()) { + await ffmpeg.load(); + } + + sendFeedback("Preparing file…"); + ffmpeg.FS("writeFile", safeVideoName, buffer); + + sendFeedback("Converting…"); + + await ffmpeg.run( + "-i", + safeVideoName, + ...getFFmpegMetadataArgs(metadata), + safeVideoName + "." + extension + ); + + sendFeedback("Saving…"); + + return ffmpeg.FS("readFile", safeVideoName + "." + extension); + } catch (e) { + sendError(e); + } finally { + releaseFFmpegMutex(); + } +} + +async function ffmpegWriteTags(filePath, metadata, ffmpegArgs = []) { + const releaseFFmpegMutex = await ffmpegMutex.acquire(); + + try { + if (!ffmpeg.isLoaded()) { + await ffmpeg.load(); + } + + await ffmpeg.run( + "-i", + filePath, + ...getFFmpegMetadataArgs(metadata), + ...ffmpegArgs, + filePath + ); + } catch (e) { + sendError(e); + } finally { + releaseFFmpegMutex(); + } +} + +function getFFmpegMetadataArgs(metadata) { + if (!metadata) { + return; + } + + return [ + ...(metadata.title ? ["-metadata", `title=${metadata.title}`] : []), + ...(metadata.artist ? ["-metadata", `artist=${metadata.artist}`] : []), + ...(metadata.album ? ["-metadata", `album=${metadata.album}`] : []), + ...(metadata.trackId ? ["-metadata", `track=${metadata.trackId}`] : []), + ]; +}; + +// Playlist radio modifier needs to be cut from playlist ID +const INVALID_PLAYLIST_MODIFIER = 'RDAMPL'; + +const getPlaylistID = aURL => { + const result = aURL?.searchParams.get("list") || aURL?.searchParams.get("playlist"); + if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) { + return result.slice(6) + } + return result; +}; + +async function downloadPlaylist(givenUrl) { + if (givenUrl) { + try { + givenUrl = new URL(givenUrl); + } catch { + givenUrl = undefined; + }; + } + const playlistId = getPlaylistID(givenUrl) + || getPlaylistID(new URL(win.webContents.getURL())) + || getPlaylistID(new URL(playingUrl)); + + if (!playlistId) { + sendError(new Error("No playlist ID found")); + return; + } + + const sendFeedback = message => sendFeedback_(win, message); + + console.log(`trying to get playlist ID: '${playlistId}'`); + sendFeedback("Getting playlist info…"); + let playlist; + try { + playlist = await ytpl(playlistId, { + limit: options.playlistMaxItems || Infinity, + }); + } catch (e) { + sendError(e); + return; + } + let isAlbum = playlist.title.startsWith('Album - '); + if (isAlbum) { + playlist.title = playlist.title.slice(8); + } + const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' }); + + const folder = getFolder(options.downloadFolder); + const playlistFolder = join(folder, safePlaylistTitle); + if (existsSync(playlistFolder)) { + sendError(new Error(`The folder ${playlistFolder} already exists`)); + return; + } + mkdirSync(playlistFolder, { recursive: true }); + + dialog.showMessageBox({ + type: "info", + buttons: ["OK"], + title: "Started Download", + message: `Downloading Playlist "${playlist.title}"`, + detail: `(${playlist.items.length} songs)`, + }); + + if (is.dev()) { + console.log( + `Downloading playlist "${playlist.title}" - ${playlist.items.length} songs (${playlistId})` + ); + } + + win.setProgressBar(2); // starts with indefinite bar + + setBadge(playlist.items.length); + + let counter = 1; + + const progressStep = 1 / playlist.items.length; + + const increaseProgress = (itemPercentage) => { + const currentProgress = (counter - 1) / playlist.items.length; + const newProgress = currentProgress + (progressStep * itemPercentage); + win.setProgressBar(newProgress); + }; + + try { + for (const song of playlist.items) { + sendFeedback(`Downloading ${counter}/${playlist.items.length}...`); + const trackId = isAlbum ? counter : undefined; + await downloadSong(song.url, playlistFolder, trackId, increaseProgress).catch((e) => sendError(e)); + win.setProgressBar(counter / playlist.items.length); + setBadge(playlist.items.length - counter); + counter++; + } + } catch (e) { + sendError(e); + } finally { + win.setProgressBar(-1); // close progress bar + setBadge(0); // close badge counter + sendFeedback(); // clear feedback + } +} diff --git a/plugins/downloader/front.js b/plugins/downloader/front.js index e534ed71..3e50a743 100644 --- a/plugins/downloader/front.js +++ b/plugins/downloader/front.js @@ -2,8 +2,7 @@ const { ipcRenderer } = require("electron"); const { defaultConfig } = require("../../config"); const { getSongMenu } = require("../../providers/dom-elements"); -const { ElementFromFile, templatePath, triggerAction } = require("../utils"); -const { ACTIONS, CHANNEL } = require("./actions.js"); +const { ElementFromFile, templatePath } = require("../utils"); let menu = null; let progress = null; @@ -25,22 +24,12 @@ const observer = new MutationObserver(() => { progress = document.querySelector("#ytmcustom-download"); }); -const reinit = () => { - triggerAction(CHANNEL, ACTIONS.PROGRESS, -1); // closes progress bar - if (!progress) { - console.warn("Cannot update progress"); - } else { - progress.innerHTML = "Download"; - } -}; - const baseUrl = defaultConfig.url; // TODO: re-enable once contextIsolation is set to true // contextBridge.exposeInMainWorld("downloader", { // download: () => { global.download = () => { - //triggerAction(CHANNEL, ACTIONS.PROGRESS, 2); // starts with indefinite progress bar let metadata; let videoUrl = getSongMenu() // selector of first button which is always "Start Radio" @@ -60,7 +49,7 @@ global.download = () => { videoUrl = metadata.url || window.location.href; } - ipcRenderer.invoke('download-song', videoUrl)//.finally(() => triggerAction(CHANNEL, ACTIONS.PROGRESS, -1)); + ipcRenderer.invoke('download-song', videoUrl); return; }; @@ -74,7 +63,7 @@ function observeMenu(options) { }); }, { once: true, passive: true }) - ipcRenderer.on('downloader-feedback', (_, feedback)=> { + ipcRenderer.on('downloader-feedback', (_, feedback) => { if (!progress) { console.warn("Cannot update progress"); } else { diff --git a/plugins/downloader/menu.js b/plugins/downloader/menu.js index dd572693..439a5b6e 100644 --- a/plugins/downloader/menu.js +++ b/plugins/downloader/menu.js @@ -1,7 +1,7 @@ const { dialog } = require("electron"); const { setMenuOptions } = require("../../config/plugins"); -const { downloadPlaylist } = require("./back-downloader"); +const { downloadPlaylist } = require("./back"); const { defaultMenuDownloadLabel, getFolder, presets } = require("./utils"); let downloadLabel = defaultMenuDownloadLabel; From a31e59fbc70cb23b4572b88fde33754e88ff3407 Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sat, 4 Mar 2023 13:39:49 +0200 Subject: [PATCH 06/33] fix download button showing when it shouldn't --- plugins/downloader/front.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/downloader/front.js b/plugins/downloader/front.js index 3e50a743..d5cc19f2 100644 --- a/plugins/downloader/front.js +++ b/plugins/downloader/front.js @@ -18,7 +18,7 @@ const observer = new MutationObserver(() => { } if (menu.contains(downloadButton)) return; const menuUrl = document.querySelector('tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint')?.href; - if (menuUrl && !menuUrl.includes('watch?')) return; + if (!menuUrl?.includes('watch?')) return; menu.prepend(downloadButton); progress = document.querySelector("#ytmcustom-download"); From 560e323893650b7df53182e48d6ba9cdd9d101d2 Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sat, 4 Mar 2023 13:40:03 +0200 Subject: [PATCH 07/33] lint --- plugins/downloader/back.js | 62 ++++++++++++++++++------------------- plugins/downloader/front.js | 3 +- plugins/downloader/utils.js | 12 ------- 3 files changed, 32 insertions(+), 45 deletions(-) diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index 5adde56a..a316de7c 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -30,19 +30,19 @@ let win; let playingUrl = undefined; const sendError = (error) => { - win.setProgressBar(-1); // close progress bar - setBadge(0); // close badge - sendFeedback_(win); // reset feedback + win.setProgressBar(-1); // close progress bar + setBadge(0); // close badge + sendFeedback_(win); // reset feedback - console.error(error); - dialog.showMessageBox({ - type: "info", - buttons: ["OK"], - title: "Error in download!", - message: "Argh! Apologies, download failed…", - detail: error.toString(), - }); + console.error(error); + dialog.showMessageBox({ + type: "info", + buttons: ["OK"], + title: "Error in download!", + message: "Argh! Apologies, download failed…", + detail: error.toString(), + }); }; module.exports = async (win_, options_) => { @@ -51,14 +51,14 @@ module.exports = async (win_, options_) => { injectCSS(win.webContents, join(__dirname, "style.css")); yt = await Innertube.create({ cache: new UniversalCache(false), generate_session_locally: true }); - ipcMain.handle("download-song", (_, url) => downloadSong(url)); + ipcMain.on("download-song", (_, url) => downloadSong(url)); ipcMain.on("video-src-changed", async (_, data) => { playingUrl = JSON.parse(data)?.microformat?.microformatDataRenderer?.urlCanonical; }); ipcMain.on("download-playlist-request", async (_event, url) => downloadPlaylist(url, win, options)); }; -async function downloadSong(url, playlistFolder = undefined, trackId = undefined, increasePlaylistProgress = ()=>{}) { +async function downloadSong(url, playlistFolder = undefined, trackId = undefined, increasePlaylistProgress = () => { }) { const sendFeedback = (message, progress) => { if (!playlistFolder) { sendFeedback_(win, message); @@ -69,7 +69,11 @@ async function downloadSong(url, playlistFolder = undefined, trackId = undefined }; sendFeedback(`Downloading...`, 2); - const metadata = await getMetadata(url); + + const id = url.match(/v=([^&]+)/)?.[1]; + const info = await yt.music.getInfo(id); + + const metadata = getMetadata(info); metadata.trackId = trackId; const download_options = { @@ -78,8 +82,8 @@ async function downloadSong(url, playlistFolder = undefined, trackId = undefined format: 'any' // media container format }; - const format = metadata.info.chooseFormat(download_options); - const stream = await metadata.info.download(download_options); + const format = info.chooseFormat(download_options); + const stream = await info.download(download_options); console.info(`Downloading ${metadata.artist} - ${metadata.title} [${metadata.id}]`); @@ -121,23 +125,17 @@ async function downloadSong(url, playlistFolder = undefined, trackId = undefined } sendFeedback(null, -1); - console.info(`Saved download to ${filePath}`); + console.info(`Done: "${filePath}"`); } module.exports.downloadSong = downloadSong; -async function getMetadata(url) { - const id = url.match(/v=([^&]+)/)?.[1]; - const info = await yt.music.getInfo(id); - - return { - id: info.basic_info.id, - title: info.basic_info.title, - artist: info.basic_info.author, - album: info.player_overlays?.browser_media_session?.album?.text, - image: info.basic_info.thumbnail[0].url, - info - }; -} +const getMetadata = (info) => ({ + id: info.basic_info.id, + title: info.basic_info.title, + artist: info.basic_info.author, + album: info.player_overlays?.browser_media_session?.album?.text, + image: info.basic_info.thumbnail[0].url, +}); async function writeID3(buffer, metadata, sendFeedback) { try { @@ -349,7 +347,9 @@ async function downloadPlaylist(givenUrl) { for (const song of playlist.items) { sendFeedback(`Downloading ${counter}/${playlist.items.length}...`); const trackId = isAlbum ? counter : undefined; - await downloadSong(song.url, playlistFolder, trackId, increaseProgress).catch((e) => sendError(e)); + await downloadSong(song.url, playlistFolder, trackId, increaseProgress) + .catch((e) => sendError(`Error downloading "${song.author} - ${song.title}":\n ${e}`)); + win.setProgressBar(counter / playlist.items.length); setBadge(playlist.items.length - counter); counter++; diff --git a/plugins/downloader/front.js b/plugins/downloader/front.js index d5cc19f2..3113f029 100644 --- a/plugins/downloader/front.js +++ b/plugins/downloader/front.js @@ -49,8 +49,7 @@ global.download = () => { videoUrl = metadata.url || window.location.href; } - ipcRenderer.invoke('download-song', videoUrl); - return; + ipcRenderer.send('download-song', videoUrl); }; function observeMenu(options) { diff --git a/plugins/downloader/utils.js b/plugins/downloader/utils.js index def066cb..6b9c449b 100644 --- a/plugins/downloader/utils.js +++ b/plugins/downloader/utils.js @@ -8,18 +8,6 @@ module.exports.sendFeedback = (win, message) => { win.webContents.send("downloader-feedback", message); }; -const orderedQualityList = ["maxresdefault", "hqdefault", "mqdefault", "sdddefault"]; -module.exports.urlToJPG = (imgUrl, videoId) => { - if (!imgUrl || imgUrl.includes(".jpg")) return imgUrl; - //it will almost never get further than hqdefault - for (const quality of orderedQualityList) { - if (imgUrl.includes(quality)) { - return `https://img.youtube.com/vi/${videoId}/${quality}.jpg`; - } - } - return `https://img.youtube.com/vi/${videoId}/default.jpg`; -} - module.exports.cropMaxWidth = (image) => { const imageSize = image.getSize(); // standart youtube artwork width with margins from both sides is 280 + 720 + 280 From e4b1d38f8520f15cbd50bc740d01105ac1fea9aa Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sat, 4 Mar 2023 13:51:18 +0200 Subject: [PATCH 08/33] dont add album to metadata if it's ===`N/A` --- plugins/downloader/back.js | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index a316de7c..6e9cbe42 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -74,6 +74,7 @@ async function downloadSong(url, playlistFolder = undefined, trackId = undefined const info = await yt.music.getInfo(id); const metadata = getMetadata(info); + if (metadata.album === 'N/A') metadata.album = ''; metadata.trackId = trackId; const download_options = { From a2eb3a3319c749d55559a4826d22001a7af019c6 Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sat, 4 Mar 2023 14:16:22 +0200 Subject: [PATCH 09/33] helpful error when trying to download a private or "mixed for you" playlist --- plugins/downloader/back.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index 6e9cbe42..09aa0e06 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -299,7 +299,7 @@ async function downloadPlaylist(givenUrl) { limit: options.playlistMaxItems || Infinity, }); } catch (e) { - sendError(e); + sendError("Error getting playlist info: make sure it isn't a private or \"Mixed for you\" playlist\n\n" + e); return; } let isAlbum = playlist.title.startsWith('Album - '); @@ -349,7 +349,7 @@ async function downloadPlaylist(givenUrl) { sendFeedback(`Downloading ${counter}/${playlist.items.length}...`); const trackId = isAlbum ? counter : undefined; await downloadSong(song.url, playlistFolder, trackId, increaseProgress) - .catch((e) => sendError(`Error downloading "${song.author} - ${song.title}":\n ${e}`)); + .catch((e) => sendError(`Error downloading "${song.author.name} - ${song.title}":\n ${e}`)); win.setProgressBar(counter / playlist.items.length); setBadge(playlist.items.length - counter); From cd41f093be9623121e23e0ee8b35d777d6e13fe4 Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sat, 4 Mar 2023 15:15:01 +0200 Subject: [PATCH 10/33] fix download button not showing first time menu is opened --- plugins/downloader/front.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/downloader/front.js b/plugins/downloader/front.js index 3113f029..cb9b167f 100644 --- a/plugins/downloader/front.js +++ b/plugins/downloader/front.js @@ -11,6 +11,8 @@ const downloadButton = ElementFromFile( ); let pluginOptions = {}; +let doneFirstLoad = false; + const observer = new MutationObserver(() => { if (!menu) { menu = getSongMenu(); @@ -18,10 +20,13 @@ const observer = new MutationObserver(() => { } if (menu.contains(downloadButton)) return; const menuUrl = document.querySelector('tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint')?.href; - if (!menuUrl?.includes('watch?')) return; + if (!menuUrl?.includes('watch?') && doneFirstLoad) return; menu.prepend(downloadButton); progress = document.querySelector("#ytmcustom-download"); + + if (doneFirstLoad) return; + setTimeout(() => doneFirstLoad ||= true, 500); }); const baseUrl = defaultConfig.url; From 7b3280c12bb9338409ac43a041adf55404743c20 Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sat, 4 Mar 2023 15:17:54 +0200 Subject: [PATCH 11/33] add `skip existing files` option --- plugins/downloader/back.js | 36 ++++++++++++++++++++++-------------- plugins/downloader/menu.js | 9 +++++++++ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index 09aa0e06..b6a16f48 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -77,6 +77,22 @@ async function downloadSong(url, playlistFolder = undefined, trackId = undefined if (metadata.album === 'N/A') metadata.album = ''; metadata.trackId = trackId; + const dir = playlistFolder || options.downloadFolder || app.getPath("downloads"); + const name = `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`; + + const extension = presets[options.preset]?.extension || 'mp3'; + + const filename = filenamify(`${name}.${extension}`, { + replacement: "_", + maxLength: 255, + }); + const filePath = join(dir, filename); + + if (options.skipExisting && existsSync(filePath)) { + sendFeedback(null, -1); + return; + } + const download_options = { type: 'audio', // audio, video or video+audio quality: 'best', // best, bestefficiency, 144p, 240p, 480p, 720p and so on. @@ -90,17 +106,6 @@ async function downloadSong(url, playlistFolder = undefined, trackId = undefined const iterableStream = Utils.streamToIterable(stream); - const dir = playlistFolder || options.downloadFolder || app.getPath("downloads"); - const name = `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`; - - const extension = presets[options.preset]?.extension || 'mp3'; - - const filename = filenamify(`${name}.${extension}`, { - replacement: "_", - maxLength: 255, - }); - const filePath = join(dir, filename); - if (!existsSync(dir)) { mkdirSync(dir); } @@ -311,10 +316,13 @@ async function downloadPlaylist(givenUrl) { const folder = getFolder(options.downloadFolder); const playlistFolder = join(folder, safePlaylistTitle); if (existsSync(playlistFolder)) { - sendError(new Error(`The folder ${playlistFolder} already exists`)); - return; + if (!options.skipExisting) { + sendError(new Error(`The folder ${playlistFolder} already exists`)); + return; + } + } else { + mkdirSync(playlistFolder, { recursive: true }); } - mkdirSync(playlistFolder, { recursive: true }); dialog.showMessageBox({ type: "info", diff --git a/plugins/downloader/menu.js b/plugins/downloader/menu.js index 439a5b6e..e7b72586 100644 --- a/plugins/downloader/menu.js +++ b/plugins/downloader/menu.js @@ -37,5 +37,14 @@ module.exports = (win, options) => { checked: options.preset === preset, })), }, + { + label: "Skip existing files", + type: "checkbox", + checked: options.skipExisting, + click: () => { + options.skipExisting = !options.skipExisting; + setMenuOptions("downloader", options); + } + } ]; }; From ad484ab7456697b5b830ac42155fcedef111dbbc Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sat, 4 Mar 2023 15:48:15 +0200 Subject: [PATCH 12/33] use centralized config --- plugins/downloader/back.js | 24 ++++++++++++------------ plugins/downloader/config.js | 23 +++++++++++++++++++++++ plugins/downloader/front.js | 5 +---- plugins/downloader/menu.js | 23 ++++++++++------------- 4 files changed, 46 insertions(+), 29 deletions(-) create mode 100644 plugins/downloader/config.js diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index b6a16f48..cd389401 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -25,7 +25,7 @@ const ffmpegMutex = new Mutex(); /** @type {Innertube} */ let yt; -let options; +const config = require("./config"); let win; let playingUrl = undefined; @@ -45,9 +45,9 @@ const sendError = (error) => { }); }; -module.exports = async (win_, options_) => { - options = options_; +module.exports = async (win_, options) => { win = win_; + config.init(options); injectCSS(win.webContents, join(__dirname, "style.css")); yt = await Innertube.create({ cache: new UniversalCache(false), generate_session_locally: true }); @@ -55,7 +55,7 @@ module.exports = async (win_, options_) => { ipcMain.on("video-src-changed", async (_, data) => { playingUrl = JSON.parse(data)?.microformat?.microformatDataRenderer?.urlCanonical; }); - ipcMain.on("download-playlist-request", async (_event, url) => downloadPlaylist(url, win, options)); + ipcMain.on("download-playlist-request", async (_event, url) => downloadPlaylist(url)); }; async function downloadSong(url, playlistFolder = undefined, trackId = undefined, increasePlaylistProgress = () => { }) { @@ -77,10 +77,10 @@ async function downloadSong(url, playlistFolder = undefined, trackId = undefined if (metadata.album === 'N/A') metadata.album = ''; metadata.trackId = trackId; - const dir = playlistFolder || options.downloadFolder || app.getPath("downloads"); + const dir = playlistFolder || config.get('downloadFolder') || app.getPath("downloads"); const name = `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`; - const extension = presets[options.preset]?.extension || 'mp3'; + const extension = presets[config.get('preset')]?.extension || 'mp3'; const filename = filenamify(`${name}.${extension}`, { replacement: "_", @@ -88,7 +88,7 @@ async function downloadSong(url, playlistFolder = undefined, trackId = undefined }); const filePath = join(dir, filename); - if (options.skipExisting && existsSync(filePath)) { + if (config.get('skipExisting') && existsSync(filePath)) { sendFeedback(null, -1); return; } @@ -110,7 +110,7 @@ async function downloadSong(url, playlistFolder = undefined, trackId = undefined mkdirSync(dir); } - if (!presets[options.preset]) { + if (!presets[config.get('preset')]) { const fileBuffer = await toMP3(iterableStream, metadata, format.content_length, sendFeedback, increasePlaylistProgress); writeFileSync(filePath, await writeID3(fileBuffer, metadata, sendFeedback)); } else { @@ -126,7 +126,7 @@ async function downloadSong(url, playlistFolder = undefined, trackId = undefined increasePlaylistProgress(ratio); file.write(chunk); } - await ffmpegWriteTags(filePath, metadata, presets[options.preset]?.ffmpegArgs); + await ffmpegWriteTags(filePath, metadata, presets[config.get('preset')]?.ffmpegArgs); sendFeedback(null, -1); } @@ -301,7 +301,7 @@ async function downloadPlaylist(givenUrl) { let playlist; try { playlist = await ytpl(playlistId, { - limit: options.playlistMaxItems || Infinity, + limit: config.get('playlistMaxItems') || Infinity, }); } catch (e) { sendError("Error getting playlist info: make sure it isn't a private or \"Mixed for you\" playlist\n\n" + e); @@ -313,10 +313,10 @@ async function downloadPlaylist(givenUrl) { } const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' }); - const folder = getFolder(options.downloadFolder); + const folder = getFolder(config.get('downloadFolder')); const playlistFolder = join(folder, safePlaylistTitle); if (existsSync(playlistFolder)) { - if (!options.skipExisting) { + if (!config.get('skipExisting')) { sendError(new Error(`The folder ${playlistFolder} already exists`)); return; } diff --git a/plugins/downloader/config.js b/plugins/downloader/config.js new file mode 100644 index 00000000..24e745e9 --- /dev/null +++ b/plugins/downloader/config.js @@ -0,0 +1,23 @@ +const { setOptions, setMenuOptions } = require("../../config/plugins"); +const defaultConfig = require("../../config/defaults"); + +let config = defaultConfig.plugins["downloader"]; + +module.exports.init = (options) => { + config = { ...config, ...options }; +}; + +module.exports.setAndMaybeRestart = (option, value) => { + config[option] = value; + setMenuOptions("downloader", config); +}; + +module.exports.set = (option, value) => { + config[option] = value; + setOptions("downloader", config); +}; + +module.exports.get = (option) => { + let res = config[option]; + return res; +}; diff --git a/plugins/downloader/front.js b/plugins/downloader/front.js index cb9b167f..37cfae8c 100644 --- a/plugins/downloader/front.js +++ b/plugins/downloader/front.js @@ -9,7 +9,6 @@ let progress = null; const downloadButton = ElementFromFile( templatePath(__dirname, "download.html") ); -let pluginOptions = {}; let doneFirstLoad = false; @@ -57,9 +56,7 @@ global.download = () => { ipcRenderer.send('download-song', videoUrl); }; -function observeMenu(options) { - pluginOptions = { ...pluginOptions, ...options }; - +function observeMenu() { document.addEventListener('apiLoaded', () => { observer.observe(document.querySelector('ytmusic-popup-container'), { childList: true, diff --git a/plugins/downloader/menu.js b/plugins/downloader/menu.js index e7b72586..86e4136c 100644 --- a/plugins/downloader/menu.js +++ b/plugins/downloader/menu.js @@ -1,27 +1,26 @@ const { dialog } = require("electron"); -const { setMenuOptions } = require("../../config/plugins"); const { downloadPlaylist } = require("./back"); const { defaultMenuDownloadLabel, getFolder, presets } = require("./utils"); +const config = require("./config"); let downloadLabel = defaultMenuDownloadLabel; -module.exports = (win, options) => { +module.exports = () => { return [ { label: downloadLabel, - click: () => downloadPlaylist(undefined, win, options), + click: () => downloadPlaylist(), }, { label: "Choose download folder", click: () => { let result = dialog.showOpenDialogSync({ properties: ["openDirectory", "createDirectory"], - defaultPath: getFolder(options.downloadFolder), + defaultPath: getFolder(config.get('downloadFolder')), }); if (result) { - options.downloadFolder = result[0]; - setMenuOptions("downloader", options); + config.set("downloadFolder", result[0]); } // else = user pressed cancel }, }, @@ -30,20 +29,18 @@ module.exports = (win, options) => { submenu: Object.keys(presets).map((preset) => ({ label: preset, type: "radio", + checked: config.get('preset') === preset, click: () => { - options.preset = preset; - setMenuOptions("downloader", options); + config.set("preset", preset); }, - checked: options.preset === preset, })), }, { label: "Skip existing files", type: "checkbox", - checked: options.skipExisting, - click: () => { - options.skipExisting = !options.skipExisting; - setMenuOptions("downloader", options); + checked: config.get('skipExisting'), + click: (item) => { + config.set("skipExisting", item.checked); } } ]; From 6ca64d68ca97115ccf69b12cd3649b82f452f2ee Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sat, 4 Mar 2023 16:16:01 +0200 Subject: [PATCH 13/33] show ffmpeg progress --- plugins/downloader/back.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index cd389401..a31d06b2 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -111,7 +111,7 @@ async function downloadSong(url, playlistFolder = undefined, trackId = undefined } if (!presets[config.get('preset')]) { - const fileBuffer = await toMP3(iterableStream, metadata, format.content_length, sendFeedback, increasePlaylistProgress); + const fileBuffer = await bufferToMP3(iterableStream, metadata, format.content_length, sendFeedback, increasePlaylistProgress); writeFileSync(filePath, await writeID3(fileBuffer, metadata, sendFeedback)); } else { const file = createWriteStream(filePath); @@ -186,7 +186,7 @@ async function writeID3(buffer, metadata, sendFeedback) { } } -async function toMP3(stream, metadata, content_length, sendFeedback, increasePlaylistProgress = () => { }, extension = "mp3") { +async function bufferToMP3(stream, metadata, content_length, sendFeedback, increasePlaylistProgress = () => { }, extension = "mp3") { const chunks = []; let downloaded = 0; let total = content_length; @@ -196,7 +196,9 @@ async function toMP3(stream, metadata, content_length, sendFeedback, increasePla const ratio = downloaded / total; const progress = Math.floor(ratio * 100); sendFeedback("Download: " + progress + "%", ratio); - increasePlaylistProgress(ratio); + // 15% for download, 85% for conversion + // This is a very rough estimate, trying to make the progress bar look nice + increasePlaylistProgress(ratio * 0.15); } sendFeedback("Loading…", 2); // indefinite progress bar after download @@ -214,6 +216,11 @@ async function toMP3(stream, metadata, content_length, sendFeedback, increasePla sendFeedback("Converting…"); + ffmpeg.setProgress(({ ratio }) => { + sendFeedback("Converting: " + Math.floor(ratio * 100) + "%", ratio); + increasePlaylistProgress(0.15 + ratio * 0.85); + }); + await ffmpeg.run( "-i", safeVideoName, @@ -307,6 +314,12 @@ async function downloadPlaylist(givenUrl) { sendError("Error getting playlist info: make sure it isn't a private or \"Mixed for you\" playlist\n\n" + e); return; } + if (playlist.items.length === 0) sendError(new Error("Playlist is empty")); + if (playlist.items.length === 1) { + sendFeedback("Playlist has only one item, downloading it directly"); + await downloadSong(playlist.items[0].url); + return; + } let isAlbum = playlist.title.startsWith('Album - '); if (isAlbum) { playlist.title = playlist.title.slice(8); From c5781962f404684c74fde5b72090b9576fd2f0c6 Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sun, 5 Mar 2023 18:56:04 +0200 Subject: [PATCH 14/33] lint --- plugins/downloader/back.js | 227 +++++++++++++++++++----------------- plugins/downloader/front.js | 19 +-- 2 files changed, 124 insertions(+), 122 deletions(-) diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index a31d06b2..150a06c7 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -23,9 +23,10 @@ const ffmpeg = require("@ffmpeg/ffmpeg").createFFmpeg({ }); const ffmpegMutex = new Mutex(); +const config = require("./config"); + /** @type {Innertube} */ let yt; -const config = require("./config"); let win; let playingUrl = undefined; @@ -58,6 +59,8 @@ module.exports = async (win_, options) => { ipcMain.on("download-playlist-request", async (_event, url) => downloadPlaylist(url)); }; +module.exports.downloadSong = downloadSong; + async function downloadSong(url, playlistFolder = undefined, trackId = undefined, increasePlaylistProgress = () => { }) { const sendFeedback = (message, progress) => { if (!playlistFolder) { @@ -70,7 +73,7 @@ async function downloadSong(url, playlistFolder = undefined, trackId = undefined sendFeedback(`Downloading...`, 2); - const id = url.match(/v=([^&]+)/)?.[1]; + const id = getVideoId(url); const info = await yt.music.getInfo(id); const metadata = getMetadata(info); @@ -111,7 +114,7 @@ async function downloadSong(url, playlistFolder = undefined, trackId = undefined } if (!presets[config.get('preset')]) { - const fileBuffer = await bufferToMP3(iterableStream, metadata, format.content_length, sendFeedback, increasePlaylistProgress); + const fileBuffer = await iterableStreamToMP3(iterableStream, metadata, format.content_length, sendFeedback, increasePlaylistProgress); writeFileSync(filePath, await writeID3(fileBuffer, metadata, sendFeedback)); } else { const file = createWriteStream(filePath); @@ -133,15 +136,58 @@ async function downloadSong(url, playlistFolder = undefined, trackId = undefined sendFeedback(null, -1); console.info(`Done: "${filePath}"`); } -module.exports.downloadSong = downloadSong; -const getMetadata = (info) => ({ - id: info.basic_info.id, - title: info.basic_info.title, - artist: info.basic_info.author, - album: info.player_overlays?.browser_media_session?.album?.text, - image: info.basic_info.thumbnail[0].url, -}); +async function iterableStreamToMP3(stream, metadata, content_length, sendFeedback, increasePlaylistProgress = () => { }) { + const chunks = []; + let downloaded = 0; + let total = content_length; + for await (const chunk of stream) { + downloaded += chunk.length; + chunks.push(chunk); + const ratio = downloaded / total; + const progress = Math.floor(ratio * 100); + sendFeedback("Download: " + progress + "%", ratio); + // 15% for download, 85% for conversion + // This is a very rough estimate, trying to make the progress bar look nice + increasePlaylistProgress(ratio * 0.15); + } + sendFeedback("Loading…", 2); // indefinite progress bar after download + + const buffer = Buffer.concat(chunks); + const safeVideoName = randomBytes(32).toString("hex"); + const releaseFFmpegMutex = await ffmpegMutex.acquire(); + + try { + if (!ffmpeg.isLoaded()) { + await ffmpeg.load(); + } + + sendFeedback("Preparing file…"); + ffmpeg.FS("writeFile", safeVideoName, buffer); + + sendFeedback("Converting…"); + + ffmpeg.setProgress(({ ratio }) => { + sendFeedback("Converting: " + Math.floor(ratio * 100) + "%", ratio); + increasePlaylistProgress(0.15 + ratio * 0.85); + }); + + await ffmpeg.run( + "-i", + safeVideoName, + ...getFFmpegMetadataArgs(metadata), + safeVideoName + ".mp3" + ); + + sendFeedback("Saving…"); + + return ffmpeg.FS("readFile", safeVideoName + ".mp3"); + } catch (e) { + sendError(e); + } finally { + releaseFFmpegMutex(); + } +} async function writeID3(buffer, metadata, sendFeedback) { try { @@ -186,104 +232,6 @@ async function writeID3(buffer, metadata, sendFeedback) { } } -async function bufferToMP3(stream, metadata, content_length, sendFeedback, increasePlaylistProgress = () => { }, extension = "mp3") { - const chunks = []; - let downloaded = 0; - let total = content_length; - for await (const chunk of stream) { - downloaded += chunk.length; - chunks.push(chunk); - const ratio = downloaded / total; - const progress = Math.floor(ratio * 100); - sendFeedback("Download: " + progress + "%", ratio); - // 15% for download, 85% for conversion - // This is a very rough estimate, trying to make the progress bar look nice - increasePlaylistProgress(ratio * 0.15); - } - sendFeedback("Loading…", 2); // indefinite progress bar after download - - const buffer = Buffer.concat(chunks); - const safeVideoName = randomBytes(32).toString("hex"); - const releaseFFmpegMutex = await ffmpegMutex.acquire(); - - try { - if (!ffmpeg.isLoaded()) { - await ffmpeg.load(); - } - - sendFeedback("Preparing file…"); - ffmpeg.FS("writeFile", safeVideoName, buffer); - - sendFeedback("Converting…"); - - ffmpeg.setProgress(({ ratio }) => { - sendFeedback("Converting: " + Math.floor(ratio * 100) + "%", ratio); - increasePlaylistProgress(0.15 + ratio * 0.85); - }); - - await ffmpeg.run( - "-i", - safeVideoName, - ...getFFmpegMetadataArgs(metadata), - safeVideoName + "." + extension - ); - - sendFeedback("Saving…"); - - return ffmpeg.FS("readFile", safeVideoName + "." + extension); - } catch (e) { - sendError(e); - } finally { - releaseFFmpegMutex(); - } -} - -async function ffmpegWriteTags(filePath, metadata, ffmpegArgs = []) { - const releaseFFmpegMutex = await ffmpegMutex.acquire(); - - try { - if (!ffmpeg.isLoaded()) { - await ffmpeg.load(); - } - - await ffmpeg.run( - "-i", - filePath, - ...getFFmpegMetadataArgs(metadata), - ...ffmpegArgs, - filePath - ); - } catch (e) { - sendError(e); - } finally { - releaseFFmpegMutex(); - } -} - -function getFFmpegMetadataArgs(metadata) { - if (!metadata) { - return; - } - - return [ - ...(metadata.title ? ["-metadata", `title=${metadata.title}`] : []), - ...(metadata.artist ? ["-metadata", `artist=${metadata.artist}`] : []), - ...(metadata.album ? ["-metadata", `album=${metadata.album}`] : []), - ...(metadata.trackId ? ["-metadata", `track=${metadata.trackId}`] : []), - ]; -}; - -// Playlist radio modifier needs to be cut from playlist ID -const INVALID_PLAYLIST_MODIFIER = 'RDAMPL'; - -const getPlaylistID = aURL => { - const result = aURL?.searchParams.get("list") || aURL?.searchParams.get("playlist"); - if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) { - return result.slice(6) - } - return result; -}; - async function downloadPlaylist(givenUrl) { if (givenUrl) { try { @@ -384,3 +332,64 @@ async function downloadPlaylist(givenUrl) { sendFeedback(); // clear feedback } } + +async function ffmpegWriteTags(filePath, metadata, ffmpegArgs = []) { + const releaseFFmpegMutex = await ffmpegMutex.acquire(); + + try { + if (!ffmpeg.isLoaded()) { + await ffmpeg.load(); + } + + await ffmpeg.run( + "-i", + filePath, + ...getFFmpegMetadataArgs(metadata), + ...ffmpegArgs, + filePath + ); + } catch (e) { + sendError(e); + } finally { + releaseFFmpegMutex(); + } +} + +function getFFmpegMetadataArgs(metadata) { + if (!metadata) { + return; + } + + return [ + ...(metadata.title ? ["-metadata", `title=${metadata.title}`] : []), + ...(metadata.artist ? ["-metadata", `artist=${metadata.artist}`] : []), + ...(metadata.album ? ["-metadata", `album=${metadata.album}`] : []), + ...(metadata.trackId ? ["-metadata", `track=${metadata.trackId}`] : []), + ]; +}; + +// Playlist radio modifier needs to be cut from playlist ID +const INVALID_PLAYLIST_MODIFIER = 'RDAMPL'; + +const getPlaylistID = aURL => { + const result = aURL?.searchParams.get("list") || aURL?.searchParams.get("playlist"); + if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) { + return result.slice(6) + } + return result; +}; + +const getVideoId = url => { + if (typeof url === "string") { + url = new URL(url); + } + return url.searchParams.get("v"); +} + +const getMetadata = (info) => ({ + id: info.basic_info.id, + title: info.basic_info.title, + artist: info.basic_info.author, + album: info.player_overlays?.browser_media_session?.album?.text, + image: info.basic_info.thumbnail[0].url, +}); diff --git a/plugins/downloader/front.js b/plugins/downloader/front.js index 37cfae8c..e0ab119f 100644 --- a/plugins/downloader/front.js +++ b/plugins/downloader/front.js @@ -12,7 +12,7 @@ const downloadButton = ElementFromFile( let doneFirstLoad = false; -const observer = new MutationObserver(() => { +const menuObserver = new MutationObserver(() => { if (!menu) { menu = getSongMenu(); if (!menu) return; @@ -28,37 +28,32 @@ const observer = new MutationObserver(() => { setTimeout(() => doneFirstLoad ||= true, 500); }); -const baseUrl = defaultConfig.url; - // TODO: re-enable once contextIsolation is set to true // contextBridge.exposeInMainWorld("downloader", { // download: () => { global.download = () => { - let metadata; let videoUrl = getSongMenu() // selector of first button which is always "Start Radio" ?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint') ?.getAttribute("href"); if (videoUrl) { if (videoUrl.startsWith('watch?')) { - videoUrl = baseUrl + "/" + videoUrl; + videoUrl = defaultConfig.url + "/" + videoUrl; } if (videoUrl.includes('?playlist=')) { ipcRenderer.send('download-playlist-request', videoUrl); return; } - metadata = null; } else { - metadata = global.songInfo; - videoUrl = metadata.url || window.location.href; + videoUrl = global.songInfo.url || window.location.href; } ipcRenderer.send('download-song', videoUrl); }; -function observeMenu() { +module.exports = () => { document.addEventListener('apiLoaded', () => { - observer.observe(document.querySelector('ytmusic-popup-container'), { + menuObserver.observe(document.querySelector('ytmusic-popup-container'), { childList: true, subtree: true, }); @@ -71,6 +66,4 @@ function observeMenu() { progress.innerHTML = feedback || "Download"; } }); -} - -module.exports = observeMenu; +}; From 96e6b5d01802ad389b83f6159791a6a2e735f41d Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sun, 12 Mar 2023 00:23:37 +0200 Subject: [PATCH 15/33] Add dynamic synced plugin config provider --- config/dynamic.js | 112 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 config/dynamic.js diff --git a/config/dynamic.js b/config/dynamic.js new file mode 100644 index 00000000..2e995f21 --- /dev/null +++ b/config/dynamic.js @@ -0,0 +1,112 @@ +const { ipcRenderer, ipcMain } = require("electron"); + +const defaultConfig = require("./defaults"); +const { getOptions, setOptions, setMenuOptions } = require("./plugins"); + +/** + * This class is used to create a dynamic synced config for plugins. + * + * [!IMPORTANT!] + * The methods are **sync** in the main process and **async** in the renderer process. + * + * @param {string} name - The name of the plugin. + * @param {boolean} [options.enableFront] - Whether the config should be available in front.js. Default: false. + * @param {object} [options.initialOptions] - The initial options for the plugin. Default: loaded from store. + * + * @example + * const { PluginConfig } = require("../../config/dynamic"); + * const config = new PluginConfig("plugin-name", { enableFront: true }); + * module.exports = { ...config }; + * + * // or + * + * module.exports = (win, options) => { + * const config = new PluginConfig("plugin-name", { + * enableFront: true, + * initialOptions: options, + * }); + * setupMyPlugin(win, config); + * }; + */ +module.exports.PluginConfig = class PluginConfig { + #name; + #config; + #defaultConfig; + #enableFront; + + constructor(name, { enableFront = false, initialOptions = undefined } = {}) { + const pluginDefaultConfig = defaultConfig.plugins[name] || {}; + const pluginConfig = initialOptions || getOptions(name) || {}; + + this.#name = name; + this.#enableFront = enableFront; + this.#defaultConfig = pluginDefaultConfig; + this.#config = { ...pluginDefaultConfig, ...pluginConfig }; + + if (this.#enableFront) { + this.#setupFront(); + } + } + + get = (option) => { + return this.#config[option]; + }; + + set = (option, value) => { + this.#config[option] = value; + this.#save(); + }; + + toggle = (option) => { + this.#config[option] = !this.#config[option]; + this.#save(); + }; + + getAll = () => { + return { ...this.#config }; + }; + + setAll = (options) => { + this.#config = { ...options }; + this.#save(); + }; + + getDefaultConfig = () => { + return this.#defaultConfig; + }; + + /** + * Use this method to set an option and restart the app if `appConfig.restartOnConfigChange === true` + * + * Used for options that require a restart to take effect. + */ + setAndMaybeRestart = (option, value) => { + this.#config[option] = value; + setMenuOptions(this.#name, this.#config); + }; + + #save() { + setOptions(this.#name, this.#config); + } + + #setupFront() { + if (process.type === "renderer") { + for (const [fnName, fn] of Object.entries(this)) { + if (typeof fn !== "function") return; + this[fnName] = async (...args) => { + return await ipcRenderer.invoke( + `${this.name}-config-${fnName}`, + ...args, + ); + }; + } + } else if (process.type === "browser") { + for (const [fnName, fn] of Object.entries(this)) { + if (typeof fn !== "function") return; + ipcMain.handle(`${this.name}-config-${fnName}`, (_, ...args) => { + return fn(...args); + }); + } + } + } +}; From bdfdf83c246ae2f15c8895690542f0743e0b35be Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sun, 12 Mar 2023 00:24:41 +0200 Subject: [PATCH 16/33] [notifications] use dynamic config --- plugins/notifications/back.js | 1 - plugins/notifications/config.js | 24 +++--------------------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/plugins/notifications/back.js b/plugins/notifications/back.js index 419e27d3..385ecca7 100644 --- a/plugins/notifications/back.js +++ b/plugins/notifications/back.js @@ -39,7 +39,6 @@ const setup = () => { /** @param {Electron.BrowserWindow} win */ module.exports = (win, options) => { - config.init(options); // Register the callback for new song information is.windows() && options.interactive ? require("./interactive")(win) : diff --git a/plugins/notifications/config.js b/plugins/notifications/config.js index 38d8100e..d0898dc3 100644 --- a/plugins/notifications/config.js +++ b/plugins/notifications/config.js @@ -1,23 +1,5 @@ -const { setOptions, setMenuOptions } = require("../../config/plugins"); -const defaultConfig = require("../../config/defaults"); +const { PluginConfig } = require("../../config/dynamic"); -let config = defaultConfig.plugins["notifications"]; +const config = new PluginConfig("notifications"); -module.exports.init = (options) => { - config = { ...config, ...options }; -}; - -module.exports.setAndMaybeRestart = (option, value) => { - config[option] = value; - setMenuOptions("notifications", config); -}; - -module.exports.set = (option, value) => { - config[option] = value; - setOptions("notifications", config); -}; - -module.exports.get = (option) => { - let res = config[option]; - return res; -}; +module.exports = { ...config }; From 7d93e9f031407be72ffcf76e57c112c9f5f92073 Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sun, 12 Mar 2023 02:04:29 +0200 Subject: [PATCH 17/33] fix `config.setAll()` --- config/dynamic.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/dynamic.js b/config/dynamic.js index 2e995f21..713379e6 100644 --- a/config/dynamic.js +++ b/config/dynamic.js @@ -67,7 +67,7 @@ module.exports.PluginConfig = class PluginConfig { }; setAll = (options) => { - this.#config = { ...options }; + this.#config = { ...this.#config, ...options }; this.#save(); }; From af2b6782e8262329f6e02ef586d8546faeea870a Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sat, 11 Mar 2023 14:52:09 +0200 Subject: [PATCH 18/33] bump custom-electron-prompt --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index e989a1dd..fef4f42f 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "butterchurn": "^2.6.7", "butterchurn-presets": "^2.4.7", "chokidar": "^3.5.3", - "custom-electron-prompt": "^1.5.1", + "custom-electron-prompt": "^1.5.4", "custom-electron-titlebar": "^4.1.6", "electron-better-web-request": "^1.0.1", "electron-debug": "^3.2.0", diff --git a/yarn.lock b/yarn.lock index da6c5954..05338cf0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2425,12 +2425,12 @@ __metadata: languageName: node linkType: hard -"custom-electron-prompt@npm:^1.5.1": - version: 1.5.1 - resolution: "custom-electron-prompt@npm:1.5.1" +"custom-electron-prompt@npm:^1.5.4": + version: 1.5.4 + resolution: "custom-electron-prompt@npm:1.5.4" peerDependencies: electron: ">=10.0.0" - checksum: 43a0d72a7a3471135822cb210d580285f70080d9d3a7b03f82cd4be403059fe20ea05ebdd1f9534928c386ab25a353e678f2cfb3f4ca016b41f3366bff700767 + checksum: 93995b5f0e9d14401a8c4fdd358af32d8b7585b59b111667cfa55f9505109c08914f3140953125b854e5d09e811de8c76c7fec718934c13e8a1ad09fe1b85270 languageName: node linkType: hard @@ -8981,7 +8981,7 @@ __metadata: butterchurn: ^2.6.7 butterchurn-presets: ^2.4.7 chokidar: ^3.5.3 - custom-electron-prompt: ^1.5.1 + custom-electron-prompt: ^1.5.4 custom-electron-titlebar: ^4.1.6 del-cli: ^5.0.0 electron: ^22.0.2 From 108c778f6da37e75d332ad9b54b83392625f668c Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sat, 11 Mar 2023 15:07:41 +0200 Subject: [PATCH 19/33] fix caption selector showing when unavailable --- plugins/captions-selector/front.js | 36 +++++++++++++++++------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/plugins/captions-selector/front.js b/plugins/captions-selector/front.js index 6ed83d90..bd5203bb 100644 --- a/plugins/captions-selector/front.js +++ b/plugins/captions-selector/front.js @@ -11,28 +11,32 @@ module.exports = (options) => { document.addEventListener('apiLoaded', (event) => setup(event, options), { once: true, passive: true }); } -/** - * If captions are disabled by default, - * unload "captions" module when video changes. - */ -const videoChanged = (api, options) => { - if (options.disableCaptions) { - setTimeout(() => api.unloadModule("captions"), 100); - } -} - function setup(event, options) { const api = event.detail; - $("video").addEventListener("srcChanged", () => videoChanged(api, options)); - $(".right-controls-buttons").append(captionsSettingsButton); + let captionTrackList = api.getOption("captions", "tracklist"); + + $("video").addEventListener("srcChanged", () => { + if (options.disableCaptions) { + setTimeout(() => api.unloadModule("captions"), 100); + captionsSettingsButton.style.display = "none"; + return; + } + + api.loadModule("captions"); + + setTimeout(() => { + captionTrackList = api.getOption("captions", "tracklist"); + + captionsSettingsButton.style.display = captionTrackList?.length + ? "inline-block" + : "none"; + }, 250); + }); + captionsSettingsButton.onclick = async () => { - api.loadModule("captions"); - - const captionTrackList = api.getOption("captions", "tracklist"); - if (captionTrackList?.length) { const currentCaptionTrack = api.getOption("captions", "track"); let currentIndex = !currentCaptionTrack ? From 83abbdb25a7d8192e92bdb9dbcbc9f4db66ca21a Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sun, 12 Mar 2023 19:11:54 +0200 Subject: [PATCH 20/33] add caching to getCoverBuffer when downloading an album, will no longer re-download an encode identical cover images --- plugins/downloader/back.js | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index 150a06c7..044ba3f9 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -23,6 +23,13 @@ const ffmpeg = require("@ffmpeg/ffmpeg").createFFmpeg({ }); const ffmpegMutex = new Mutex(); +const cache = { + getCoverBuffer: { + buffer: null, + url: null, + } +} + const config = require("./config"); /** @type {Innertube} */ @@ -189,13 +196,25 @@ async function iterableStreamToMP3(stream, metadata, content_length, sendFeedbac } } +async function getCoverBuffer(url) { + const store = cache.getCoverBuffer; + if (store.url === url) { + return store.buffer; + } + store.url = url; + + const nativeImage = cropMaxWidth(await getImage(url)); + store.buffer = nativeImage && !nativeImage.isEmpty() ? + nativeImage.toPNG() : null; + + return store.buffer; +} + async function writeID3(buffer, metadata, sendFeedback) { try { sendFeedback("Writing ID3 tags..."); - const nativeImage = cropMaxWidth(await getImage(metadata.image)); - const coverBuffer = nativeImage && !nativeImage.isEmpty() ? - nativeImage.toPNG() : null; + const coverBuffer = await getCoverBuffer(metadata.image); const writer = new ID3Writer(buffer); From b652a011a5a08978db6660aeca6908c47a7cf07a Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sun, 12 Mar 2023 20:00:10 +0200 Subject: [PATCH 21/33] lint --- plugins/downloader/back.js | 674 ++++++++++++++++++++----------------- plugins/downloader/menu.js | 16 +- 2 files changed, 370 insertions(+), 320 deletions(-) diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index 044ba3f9..94920e85 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -1,15 +1,26 @@ -const { existsSync, mkdirSync, createWriteStream, writeFileSync } = require('fs'); +const { + existsSync, + mkdirSync, + createWriteStream, + writeFileSync, +} = require("fs"); const { join } = require("path"); const { fetchFromGenius } = require("../lyrics-genius/back"); const { isEnabled } = require("../../config/plugins"); const { getImage } = require("../../providers/song-info"); const { injectCSS } = require("../utils"); -const { presets, cropMaxWidth, getFolder, setBadge, sendFeedback: sendFeedback_ } = require('./utils'); +const { + presets, + cropMaxWidth, + getFolder, + setBadge, + sendFeedback: sendFeedback_, +} = require("./utils"); const { ipcMain, app, dialog } = require("electron"); const is = require("electron-is"); -const { Innertube, UniversalCache, Utils } = require('youtubei.js'); +const { Innertube, UniversalCache, Utils } = require("youtubei.js"); const ytpl = require("ytpl"); // REPLACE with youtubei getplaylist https://github.com/LuanRT/YouTube.js#getplaylistid const filenamify = require("filenamify"); @@ -17,18 +28,18 @@ const ID3Writer = require("browser-id3-writer"); const { randomBytes } = require("crypto"); const Mutex = require("async-mutex").Mutex; const ffmpeg = require("@ffmpeg/ffmpeg").createFFmpeg({ - log: false, - logger: () => { }, // console.log, - progress: () => { }, // console.log, + log: false, + logger: () => {}, // console.log, + progress: () => {}, // console.log, }); const ffmpegMutex = new Mutex(); const cache = { - getCoverBuffer: { - buffer: null, - url: null, - } -} + getCoverBuffer: { + buffer: null, + url: null, + }, +}; const config = require("./config"); @@ -38,377 +49,418 @@ let win; let playingUrl = undefined; const sendError = (error) => { - win.setProgressBar(-1); // close progress bar - setBadge(0); // close badge - sendFeedback_(win); // reset feedback + win.setProgressBar(-1); // close progress bar + setBadge(0); // close badge + sendFeedback_(win); // reset feedback - - console.error(error); - dialog.showMessageBox({ - type: "info", - buttons: ["OK"], - title: "Error in download!", - message: "Argh! Apologies, download failed…", - detail: error.toString(), - }); + console.error(error); + dialog.showMessageBox({ + type: "info", + buttons: ["OK"], + title: "Error in download!", + message: "Argh! Apologies, download failed…", + detail: error.toString(), + }); }; module.exports = async (win_, options) => { - win = win_; - config.init(options); - injectCSS(win.webContents, join(__dirname, "style.css")); + win = win_; + config.init(options); + injectCSS(win.webContents, join(__dirname, "style.css")); - yt = await Innertube.create({ cache: new UniversalCache(false), generate_session_locally: true }); - ipcMain.on("download-song", (_, url) => downloadSong(url)); - ipcMain.on("video-src-changed", async (_, data) => { - playingUrl = JSON.parse(data)?.microformat?.microformatDataRenderer?.urlCanonical; - }); - ipcMain.on("download-playlist-request", async (_event, url) => downloadPlaylist(url)); + yt = await Innertube.create({ + cache: new UniversalCache(false), + generate_session_locally: true, + }); + ipcMain.on("download-song", (_, url) => downloadSong(url)); + ipcMain.on("video-src-changed", async (_, data) => { + playingUrl = + JSON.parse(data)?.microformat?.microformatDataRenderer?.urlCanonical; + }); + ipcMain.on("download-playlist-request", async (_event, url) => + downloadPlaylist(url), + ); }; module.exports.downloadSong = downloadSong; -async function downloadSong(url, playlistFolder = undefined, trackId = undefined, increasePlaylistProgress = () => { }) { - const sendFeedback = (message, progress) => { - if (!playlistFolder) { - sendFeedback_(win, message); - if (!isNaN(progress)) { - win.setProgressBar(progress); - } - } - }; +async function downloadSong( + url, + playlistFolder = undefined, + trackId = undefined, + increasePlaylistProgress = () => {}, +) { + const sendFeedback = (message, progress) => { + if (!playlistFolder) { + sendFeedback_(win, message); + if (!isNaN(progress)) { + win.setProgressBar(progress); + } + } + }; - sendFeedback(`Downloading...`, 2); + sendFeedback("Downloading...", 2); - const id = getVideoId(url); - const info = await yt.music.getInfo(id); + const id = getVideoId(url); + const info = await yt.music.getInfo(id); - const metadata = getMetadata(info); - if (metadata.album === 'N/A') metadata.album = ''; - metadata.trackId = trackId; + const metadata = getMetadata(info); + if (metadata.album === "N/A") metadata.album = ""; + metadata.trackId = trackId; - const dir = playlistFolder || config.get('downloadFolder') || app.getPath("downloads"); - const name = `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`; + const dir = + playlistFolder || config.get("downloadFolder") || app.getPath("downloads"); + const name = `${metadata.artist ? `${metadata.artist} - ` : ""}${ + metadata.title + }`; - const extension = presets[config.get('preset')]?.extension || 'mp3'; + const extension = presets[config.get("preset")]?.extension || "mp3"; - const filename = filenamify(`${name}.${extension}`, { - replacement: "_", - maxLength: 255, - }); - const filePath = join(dir, filename); + const filename = filenamify(`${name}.${extension}`, { + replacement: "_", + maxLength: 255, + }); + const filePath = join(dir, filename); - if (config.get('skipExisting') && existsSync(filePath)) { - sendFeedback(null, -1); - return; - } + if (config.get("skipExisting") && existsSync(filePath)) { + sendFeedback(null, -1); + return; + } - const download_options = { - type: 'audio', // audio, video or video+audio - quality: 'best', // best, bestefficiency, 144p, 240p, 480p, 720p and so on. - format: 'any' // media container format - }; + const download_options = { + type: "audio", // audio, video or video+audio + quality: "best", // best, bestefficiency, 144p, 240p, 480p, 720p and so on. + format: "any", // media container format + }; - const format = info.chooseFormat(download_options); - const stream = await info.download(download_options); + const format = info.chooseFormat(download_options); + const stream = await info.download(download_options); - console.info(`Downloading ${metadata.artist} - ${metadata.title} [${metadata.id}]`); + console.info( + `Downloading ${metadata.artist} - ${metadata.title} [${metadata.id}]`, + ); - const iterableStream = Utils.streamToIterable(stream); + const iterableStream = Utils.streamToIterable(stream); - if (!existsSync(dir)) { - mkdirSync(dir); - } + if (!existsSync(dir)) { + mkdirSync(dir); + } - if (!presets[config.get('preset')]) { - const fileBuffer = await iterableStreamToMP3(iterableStream, metadata, format.content_length, sendFeedback, increasePlaylistProgress); - writeFileSync(filePath, await writeID3(fileBuffer, metadata, sendFeedback)); - } else { - const file = createWriteStream(filePath); - let downloaded = 0; - let total = format.content_length; + if (!presets[config.get("preset")]) { + const fileBuffer = await iterableStreamToMP3( + iterableStream, + metadata, + format.content_length, + sendFeedback, + increasePlaylistProgress, + ); + writeFileSync(filePath, await writeID3(fileBuffer, metadata, sendFeedback)); + } else { + const file = createWriteStream(filePath); + let downloaded = 0; + const total = format.content_length; - for await (const chunk of iterableStream) { - downloaded += chunk.length; - const ratio = downloaded / total; - const progress = Math.floor(ratio * 100); - sendFeedback("Download: " + progress + "%", ratio); - increasePlaylistProgress(ratio); - file.write(chunk); - } - await ffmpegWriteTags(filePath, metadata, presets[config.get('preset')]?.ffmpegArgs); - sendFeedback(null, -1); - } + for await (const chunk of iterableStream) { + downloaded += chunk.length; + const ratio = downloaded / total; + const progress = Math.floor(ratio * 100); + sendFeedback(`Download: ${progress}%`, ratio); + increasePlaylistProgress(ratio); + file.write(chunk); + } + await ffmpegWriteTags( + filePath, + metadata, + presets[config.get("preset")]?.ffmpegArgs, + ); + sendFeedback(null, -1); + } - sendFeedback(null, -1); - console.info(`Done: "${filePath}"`); + sendFeedback(null, -1); + console.info(`Done: "${filePath}"`); } -async function iterableStreamToMP3(stream, metadata, content_length, sendFeedback, increasePlaylistProgress = () => { }) { - const chunks = []; - let downloaded = 0; - let total = content_length; - for await (const chunk of stream) { - downloaded += chunk.length; - chunks.push(chunk); - const ratio = downloaded / total; - const progress = Math.floor(ratio * 100); - sendFeedback("Download: " + progress + "%", ratio); - // 15% for download, 85% for conversion - // This is a very rough estimate, trying to make the progress bar look nice - increasePlaylistProgress(ratio * 0.15); - } - sendFeedback("Loading…", 2); // indefinite progress bar after download +async function iterableStreamToMP3( + stream, + metadata, + content_length, + sendFeedback, + increasePlaylistProgress = () => {}, +) { + const chunks = []; + let downloaded = 0; + const total = content_length; + for await (const chunk of stream) { + downloaded += chunk.length; + chunks.push(chunk); + const ratio = downloaded / total; + const progress = Math.floor(ratio * 100); + sendFeedback(`Download: ${progress}%`, ratio); + // 15% for download, 85% for conversion + // This is a very rough estimate, trying to make the progress bar look nice + increasePlaylistProgress(ratio * 0.15); + } + sendFeedback("Loading…", 2); // indefinite progress bar after download - const buffer = Buffer.concat(chunks); - const safeVideoName = randomBytes(32).toString("hex"); - const releaseFFmpegMutex = await ffmpegMutex.acquire(); + const buffer = Buffer.concat(chunks); + const safeVideoName = randomBytes(32).toString("hex"); + const releaseFFmpegMutex = await ffmpegMutex.acquire(); - try { - if (!ffmpeg.isLoaded()) { - await ffmpeg.load(); - } + try { + if (!ffmpeg.isLoaded()) { + await ffmpeg.load(); + } - sendFeedback("Preparing file…"); - ffmpeg.FS("writeFile", safeVideoName, buffer); + sendFeedback("Preparing file…"); + ffmpeg.FS("writeFile", safeVideoName, buffer); - sendFeedback("Converting…"); + sendFeedback("Converting…"); - ffmpeg.setProgress(({ ratio }) => { - sendFeedback("Converting: " + Math.floor(ratio * 100) + "%", ratio); - increasePlaylistProgress(0.15 + ratio * 0.85); - }); + ffmpeg.setProgress(({ ratio }) => { + sendFeedback(`Converting: ${Math.floor(ratio * 100)}%`, ratio); + increasePlaylistProgress(0.15 + ratio * 0.85); + }); - await ffmpeg.run( - "-i", - safeVideoName, - ...getFFmpegMetadataArgs(metadata), - safeVideoName + ".mp3" - ); + await ffmpeg.run( + "-i", + safeVideoName, + ...getFFmpegMetadataArgs(metadata), + `${safeVideoName}.mp3`, + ); - sendFeedback("Saving…"); + sendFeedback("Saving…"); - return ffmpeg.FS("readFile", safeVideoName + ".mp3"); - } catch (e) { - sendError(e); - } finally { - releaseFFmpegMutex(); - } + return ffmpeg.FS("readFile", `${safeVideoName}.mp3`); + } catch (e) { + sendError(e); + } finally { + releaseFFmpegMutex(); + } } async function getCoverBuffer(url) { - const store = cache.getCoverBuffer; - if (store.url === url) { - return store.buffer; - } - store.url = url; + const store = cache.getCoverBuffer; + if (store.url === url) { + return store.buffer; + } + store.url = url; - const nativeImage = cropMaxWidth(await getImage(url)); - store.buffer = nativeImage && !nativeImage.isEmpty() ? - nativeImage.toPNG() : null; + const nativeImage = cropMaxWidth(await getImage(url)); + store.buffer = + nativeImage && !nativeImage.isEmpty() ? nativeImage.toPNG() : null; - return store.buffer; + return store.buffer; } async function writeID3(buffer, metadata, sendFeedback) { - try { - sendFeedback("Writing ID3 tags..."); + try { + sendFeedback("Writing ID3 tags..."); - const coverBuffer = await getCoverBuffer(metadata.image); + const coverBuffer = await getCoverBuffer(metadata.image); - const writer = new ID3Writer(buffer); + const writer = new ID3Writer(buffer); - // Create the metadata tags - writer - .setFrame("TIT2", metadata.title) - .setFrame("TPE1", [metadata.artist]); - if (metadata.album) { - writer.setFrame("TALB", metadata.album); - } - if (coverBuffer) { - writer.setFrame("APIC", { - type: 3, - data: coverBuffer, - description: "", - }); - } - if (isEnabled("lyrics-genius")) { - const lyrics = await fetchFromGenius(metadata); - if (lyrics) { - writer.setFrame("USLT", { - description: '', - lyrics: lyrics, - }); - } - } - if (metadata.trackId) { - writer.setFrame("TRCK", metadata.trackId); - } - writer.addTag(); - return Buffer.from(writer.arrayBuffer); - } catch (e) { - sendError(e); - } + // Create the metadata tags + writer.setFrame("TIT2", metadata.title).setFrame("TPE1", [metadata.artist]); + if (metadata.album) { + writer.setFrame("TALB", metadata.album); + } + if (coverBuffer) { + writer.setFrame("APIC", { + type: 3, + data: coverBuffer, + description: "", + }); + } + if (isEnabled("lyrics-genius")) { + const lyrics = await fetchFromGenius(metadata); + if (lyrics) { + writer.setFrame("USLT", { + description: "", + lyrics: lyrics, + }); + } + } + if (metadata.trackId) { + writer.setFrame("TRCK", metadata.trackId); + } + writer.addTag(); + return Buffer.from(writer.arrayBuffer); + } catch (e) { + sendError(e); + } } async function downloadPlaylist(givenUrl) { - if (givenUrl) { - try { - givenUrl = new URL(givenUrl); - } catch { - givenUrl = undefined; - }; - } - const playlistId = getPlaylistID(givenUrl) - || getPlaylistID(new URL(win.webContents.getURL())) - || getPlaylistID(new URL(playingUrl)); + if (givenUrl) { + try { + givenUrl = new URL(givenUrl); + } catch { + givenUrl = undefined; + } + } + const playlistId = + getPlaylistID(givenUrl) || + getPlaylistID(new URL(win.webContents.getURL())) || + getPlaylistID(new URL(playingUrl)); - if (!playlistId) { - sendError(new Error("No playlist ID found")); - return; - } + if (!playlistId) { + sendError(new Error("No playlist ID found")); + return; + } - const sendFeedback = message => sendFeedback_(win, message); + const sendFeedback = (message) => sendFeedback_(win, message); - console.log(`trying to get playlist ID: '${playlistId}'`); - sendFeedback("Getting playlist info…"); - let playlist; - try { - playlist = await ytpl(playlistId, { - limit: config.get('playlistMaxItems') || Infinity, - }); - } catch (e) { - sendError("Error getting playlist info: make sure it isn't a private or \"Mixed for you\" playlist\n\n" + e); - return; - } - if (playlist.items.length === 0) sendError(new Error("Playlist is empty")); - if (playlist.items.length === 1) { - sendFeedback("Playlist has only one item, downloading it directly"); - await downloadSong(playlist.items[0].url); - return; - } - let isAlbum = playlist.title.startsWith('Album - '); - if (isAlbum) { - playlist.title = playlist.title.slice(8); - } - const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' }); + console.log(`trying to get playlist ID: '${playlistId}'`); + sendFeedback("Getting playlist info…"); + let playlist; + try { + playlist = await ytpl(playlistId, { + limit: config.get("playlistMaxItems") || Infinity, + }); + } catch (e) { + sendError( + `Error getting playlist info: make sure it isn\'t a private or "Mixed for you" playlist\n\n${e}`, + ); + return; + } + if (playlist.items.length === 0) sendError(new Error("Playlist is empty")); + if (playlist.items.length === 1) { + sendFeedback("Playlist has only one item, downloading it directly"); + await downloadSong(playlist.items[0].url); + return; + } + const isAlbum = playlist.title.startsWith("Album - "); + if (isAlbum) { + playlist.title = playlist.title.slice(8); + } + const safePlaylistTitle = filenamify(playlist.title, { replacement: " " }); - const folder = getFolder(config.get('downloadFolder')); - const playlistFolder = join(folder, safePlaylistTitle); - if (existsSync(playlistFolder)) { - if (!config.get('skipExisting')) { - sendError(new Error(`The folder ${playlistFolder} already exists`)); - return; - } - } else { - mkdirSync(playlistFolder, { recursive: true }); - } + const folder = getFolder(config.get("downloadFolder")); + const playlistFolder = join(folder, safePlaylistTitle); + if (existsSync(playlistFolder)) { + if (!config.get("skipExisting")) { + sendError(new Error(`The folder ${playlistFolder} already exists`)); + return; + } + } else { + mkdirSync(playlistFolder, { recursive: true }); + } - dialog.showMessageBox({ - type: "info", - buttons: ["OK"], - title: "Started Download", - message: `Downloading Playlist "${playlist.title}"`, - detail: `(${playlist.items.length} songs)`, - }); + dialog.showMessageBox({ + type: "info", + buttons: ["OK"], + title: "Started Download", + message: `Downloading Playlist "${playlist.title}"`, + detail: `(${playlist.items.length} songs)`, + }); - if (is.dev()) { - console.log( - `Downloading playlist "${playlist.title}" - ${playlist.items.length} songs (${playlistId})` - ); - } + if (is.dev()) { + console.log( + `Downloading playlist "${playlist.title}" - ${playlist.items.length} songs (${playlistId})`, + ); + } - win.setProgressBar(2); // starts with indefinite bar + win.setProgressBar(2); // starts with indefinite bar - setBadge(playlist.items.length); + setBadge(playlist.items.length); - let counter = 1; + let counter = 1; - const progressStep = 1 / playlist.items.length; + const progressStep = 1 / playlist.items.length; - const increaseProgress = (itemPercentage) => { - const currentProgress = (counter - 1) / playlist.items.length; - const newProgress = currentProgress + (progressStep * itemPercentage); - win.setProgressBar(newProgress); - }; + const increaseProgress = (itemPercentage) => { + const currentProgress = (counter - 1) / playlist.items.length; + const newProgress = currentProgress + progressStep * itemPercentage; + win.setProgressBar(newProgress); + }; - try { - for (const song of playlist.items) { - sendFeedback(`Downloading ${counter}/${playlist.items.length}...`); - const trackId = isAlbum ? counter : undefined; - await downloadSong(song.url, playlistFolder, trackId, increaseProgress) - .catch((e) => sendError(`Error downloading "${song.author.name} - ${song.title}":\n ${e}`)); + try { + for (const song of playlist.items) { + sendFeedback(`Downloading ${counter}/${playlist.items.length}...`); + const trackId = isAlbum ? counter : undefined; + await downloadSong( + song.url, + playlistFolder, + trackId, + increaseProgress, + ).catch((e) => + sendError( + `Error downloading "${song.author.name} - ${song.title}":\n ${e}`, + ), + ); - win.setProgressBar(counter / playlist.items.length); - setBadge(playlist.items.length - counter); - counter++; - } - } catch (e) { - sendError(e); - } finally { - win.setProgressBar(-1); // close progress bar - setBadge(0); // close badge counter - sendFeedback(); // clear feedback - } + win.setProgressBar(counter / playlist.items.length); + setBadge(playlist.items.length - counter); + counter++; + } + } catch (e) { + sendError(e); + } finally { + win.setProgressBar(-1); // close progress bar + setBadge(0); // close badge counter + sendFeedback(); // clear feedback + } } async function ffmpegWriteTags(filePath, metadata, ffmpegArgs = []) { - const releaseFFmpegMutex = await ffmpegMutex.acquire(); + const releaseFFmpegMutex = await ffmpegMutex.acquire(); - try { - if (!ffmpeg.isLoaded()) { - await ffmpeg.load(); - } + try { + if (!ffmpeg.isLoaded()) { + await ffmpeg.load(); + } - await ffmpeg.run( - "-i", - filePath, - ...getFFmpegMetadataArgs(metadata), - ...ffmpegArgs, - filePath - ); - } catch (e) { - sendError(e); - } finally { - releaseFFmpegMutex(); - } + await ffmpeg.run( + "-i", + filePath, + ...getFFmpegMetadataArgs(metadata), + ...ffmpegArgs, + filePath, + ); + } catch (e) { + sendError(e); + } finally { + releaseFFmpegMutex(); + } } function getFFmpegMetadataArgs(metadata) { - if (!metadata) { - return; - } + if (!metadata) { + return; + } - return [ - ...(metadata.title ? ["-metadata", `title=${metadata.title}`] : []), - ...(metadata.artist ? ["-metadata", `artist=${metadata.artist}`] : []), - ...(metadata.album ? ["-metadata", `album=${metadata.album}`] : []), - ...(metadata.trackId ? ["-metadata", `track=${metadata.trackId}`] : []), - ]; -}; - -// Playlist radio modifier needs to be cut from playlist ID -const INVALID_PLAYLIST_MODIFIER = 'RDAMPL'; - -const getPlaylistID = aURL => { - const result = aURL?.searchParams.get("list") || aURL?.searchParams.get("playlist"); - if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) { - return result.slice(6) - } - return result; -}; - -const getVideoId = url => { - if (typeof url === "string") { - url = new URL(url); - } - return url.searchParams.get("v"); + return [ + ...(metadata.title ? ["-metadata", `title=${metadata.title}`] : []), + ...(metadata.artist ? ["-metadata", `artist=${metadata.artist}`] : []), + ...(metadata.album ? ["-metadata", `album=${metadata.album}`] : []), + ...(metadata.trackId ? ["-metadata", `track=${metadata.trackId}`] : []), + ]; } +// Playlist radio modifier needs to be cut from playlist ID +const INVALID_PLAYLIST_MODIFIER = "RDAMPL"; + +const getPlaylistID = (aURL) => { + const result = + aURL?.searchParams.get("list") || aURL?.searchParams.get("playlist"); + if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) { + return result.slice(6); + } + return result; +}; + +const getVideoId = (url) => { + if (typeof url === "string") { + url = new URL(url); + } + return url.searchParams.get("v"); +}; + const getMetadata = (info) => ({ - id: info.basic_info.id, - title: info.basic_info.title, - artist: info.basic_info.author, - album: info.player_overlays?.browser_media_session?.album?.text, - image: info.basic_info.thumbnail[0].url, + id: info.basic_info.id, + title: info.basic_info.title, + artist: info.basic_info.author, + album: info.player_overlays?.browser_media_session?.album?.text, + image: info.basic_info.thumbnail[0].url, }); diff --git a/plugins/downloader/menu.js b/plugins/downloader/menu.js index 86e4136c..6d58bbf2 100644 --- a/plugins/downloader/menu.js +++ b/plugins/downloader/menu.js @@ -4,20 +4,18 @@ const { downloadPlaylist } = require("./back"); const { defaultMenuDownloadLabel, getFolder, presets } = require("./utils"); const config = require("./config"); -let downloadLabel = defaultMenuDownloadLabel; - module.exports = () => { return [ { - label: downloadLabel, + label: defaultMenuDownloadLabel, click: () => downloadPlaylist(), }, { label: "Choose download folder", click: () => { - let result = dialog.showOpenDialogSync({ + const result = dialog.showOpenDialogSync({ properties: ["openDirectory", "createDirectory"], - defaultPath: getFolder(config.get('downloadFolder')), + defaultPath: getFolder(config.get("downloadFolder")), }); if (result) { config.set("downloadFolder", result[0]); @@ -29,7 +27,7 @@ module.exports = () => { submenu: Object.keys(presets).map((preset) => ({ label: preset, type: "radio", - checked: config.get('preset') === preset, + checked: config.get("preset") === preset, click: () => { config.set("preset", preset); }, @@ -38,10 +36,10 @@ module.exports = () => { { label: "Skip existing files", type: "checkbox", - checked: config.get('skipExisting'), + checked: config.get("skipExisting"), click: (item) => { config.set("skipExisting", item.checked); - } - } + }, + }, ]; }; From 04ca4e85379774396fae78fc31525907657c521f Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Wed, 15 Mar 2023 15:15:48 +0000 Subject: [PATCH 22/33] fix: upgrade @cliqz/adblocker-electron from 1.25.2 to 1.26.0 Snyk has created this PR to upgrade @cliqz/adblocker-electron from 1.25.2 to 1.26.0. See this package in npm: See this project in Snyk: https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=referral&page=upgrade-pr --- package.json | 2 +- yarn.lock | 82 ++++++++++++++++++++++++++-------------------------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 75057a3f..72ec5fdf 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "npm": "Please use yarn instead" }, "dependencies": { - "@cliqz/adblocker-electron": "^1.25.2", + "@cliqz/adblocker-electron": "^1.26.0", "@ffmpeg/core": "^0.11.0", "@ffmpeg/ffmpeg": "^0.11.6", "@foobar404/wave": "^2.0.4", diff --git a/yarn.lock b/yarn.lock index da6c5954..d47fd404 100644 --- a/yarn.lock +++ b/yarn.lock @@ -48,59 +48,59 @@ __metadata: languageName: node linkType: hard -"@cliqz/adblocker-content@npm:^1.25.2": - version: 1.25.2 - resolution: "@cliqz/adblocker-content@npm:1.25.2" +"@cliqz/adblocker-content@npm:^1.26.2": + version: 1.26.2 + resolution: "@cliqz/adblocker-content@npm:1.26.2" dependencies: - "@cliqz/adblocker-extended-selectors": ^1.25.2 - checksum: c7781a9e07d6b685c10604abc72cdf79c48a7211f10901e316d506b4a296bf644c9292a256fe988c7074ebeedfd00075849ae74b648ec3672bf3da61723bb0c4 + "@cliqz/adblocker-extended-selectors": ^1.26.2 + checksum: 00ddbf157ca5ea97e3d8517f1d9397ca53eecfc95b6f09a327667ad388cafae84de655af8bac6d0c1f12b004b0c7eb8fc898f442f05e6b9d2294709a3bfb6a81 languageName: node linkType: hard -"@cliqz/adblocker-electron-preload@npm:^1.25.2": - version: 1.25.2 - resolution: "@cliqz/adblocker-electron-preload@npm:1.25.2" +"@cliqz/adblocker-electron-preload@npm:^1.26.2": + version: 1.26.2 + resolution: "@cliqz/adblocker-electron-preload@npm:1.26.2" dependencies: - "@cliqz/adblocker-content": ^1.25.2 + "@cliqz/adblocker-content": ^1.26.2 peerDependencies: electron: ">11" - checksum: 196324d67768bbd1a9ec1023c1a8be0ebd25605621690762641ecc40272f822c179d9081b6c78603b5ba70ec1d49bb6ba3f9f4d48c6f92794b21a06bdd277300 + checksum: e130911f00f9fbaa8f3edb3f36501a6f39b0f798a2dbeb8d8416219f3381edd7c34036452229472bd51fad13fa880ea24dc4b56419df0dffaa49f63f15a01451 languageName: node linkType: hard -"@cliqz/adblocker-electron@npm:^1.25.2": - version: 1.25.2 - resolution: "@cliqz/adblocker-electron@npm:1.25.2" +"@cliqz/adblocker-electron@npm:^1.26.0": + version: 1.26.2 + resolution: "@cliqz/adblocker-electron@npm:1.26.2" dependencies: - "@cliqz/adblocker": ^1.25.2 - "@cliqz/adblocker-electron-preload": ^1.25.2 + "@cliqz/adblocker": ^1.26.2 + "@cliqz/adblocker-electron-preload": ^1.26.2 tldts-experimental: ^5.6.21 peerDependencies: electron: ">11" - checksum: 8d18e85490bc60b95e880fd3315e2111e05f123bf820e87963f4fa0bf65b78d3043541b02588950b152b827414baa11cdf74159042ccc8e144afea9eef84fe62 + checksum: 308e7e6273bdf055ba5a637111d301c6c7cc3f46c7b31e6e658addab7bcb735f162e77b50f123e2841a769b38b810f85e959dd7da382298c0c6bbedf9da58d44 languageName: node linkType: hard -"@cliqz/adblocker-extended-selectors@npm:^1.25.2": - version: 1.25.2 - resolution: "@cliqz/adblocker-extended-selectors@npm:1.25.2" - checksum: 67541181fb7718d25e62184ea2b5236f0b659b82221163086179498c068f6e6c01911ce47f2ffe0ce2244c0728afa92add8e03e881bc8d6e64f0b9a0e2ff8091 +"@cliqz/adblocker-extended-selectors@npm:^1.26.2": + version: 1.26.2 + resolution: "@cliqz/adblocker-extended-selectors@npm:1.26.2" + checksum: 54736a3328e5175c6c0350d9f18faf83ed16a4f41bc1ff6fa550319ebf728fb5536015384ee4a5d005d7225c41a31eaa251d7a18486db26d41b251eeee8eaaae languageName: node linkType: hard -"@cliqz/adblocker@npm:^1.25.2": - version: 1.25.2 - resolution: "@cliqz/adblocker@npm:1.25.2" +"@cliqz/adblocker@npm:^1.26.2": + version: 1.26.2 + resolution: "@cliqz/adblocker@npm:1.26.2" dependencies: - "@cliqz/adblocker-content": ^1.25.2 - "@cliqz/adblocker-extended-selectors": ^1.25.2 + "@cliqz/adblocker-content": ^1.26.2 + "@cliqz/adblocker-extended-selectors": ^1.26.2 "@remusao/guess-url-type": ^1.1.2 "@remusao/small": ^1.1.2 "@remusao/smaz": ^1.7.1 - "@types/chrome": ^0.0.206 - "@types/firefox-webext-browser": ^94.0.0 + "@types/chrome": ^0.0.218 + "@types/firefox-webext-browser": ^109.0.0 tldts-experimental: ^5.6.21 - checksum: be97b20420ccfafd2fc887538a255db4c22bddc1a6944bbea9e9a8df3e2912e39f18101f99b37599653e2a8f2053513a7ec0908a7af49ba9794573ee6d186094 + checksum: e4fcfe15109f15ce95096501f00f4109743385158b3a746c30f4f8ff93bd73821e2424b67c9e58ba10a1c97da79826d26fb0b8eea77f7b3af3783f50b22d8009 languageName: node linkType: hard @@ -840,13 +840,13 @@ __metadata: languageName: node linkType: hard -"@types/chrome@npm:^0.0.206": - version: 0.0.206 - resolution: "@types/chrome@npm:0.0.206" +"@types/chrome@npm:^0.0.218": + version: 0.0.218 + resolution: "@types/chrome@npm:0.0.218" dependencies: "@types/filesystem": "*" "@types/har-format": "*" - checksum: 002af40b339ca0f665bf6e52af1fd2ecfdd4cffb6d10d891b0b4394c2a94d758e94e0d5497d38e9b45033eb8620ffa1a63e42294f88612507d9e74ddf3f6ce40 + checksum: 08f00e7c2ec8a8e869b0c7a93bd187c19471baa56f3c072c85b25467f2e0eeccfaa5a02f245bf1832fd2f7d4bb0ce3f4617a7e04f52ab37562efe1b868b62aca languageName: node linkType: hard @@ -892,10 +892,10 @@ __metadata: languageName: node linkType: hard -"@types/firefox-webext-browser@npm:^94.0.0": - version: 94.0.1 - resolution: "@types/firefox-webext-browser@npm:94.0.1" - checksum: 43f7e34857f5750c6dc6833f3c4735e75782dcc71800dd29eadc8211a58de0e36611cec79ca7a238da855d5ed7badea46992baec4420b067c4dfa4b052e91465 +"@types/firefox-webext-browser@npm:^109.0.0": + version: 109.0.0 + resolution: "@types/firefox-webext-browser@npm:109.0.0" + checksum: 6c3d8d98ca07329e88adda50911276864902d6b31f6f94712c4149327d01a06e7167eb4f6b34b8c7438a06792d0118a83e23afc21a7941ecd67ecaf40123e636 languageName: node linkType: hard @@ -4121,7 +4121,7 @@ __metadata: "fsevents@patch:fsevents@~2.3.2#~builtin": version: 2.3.2 - resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin::version=2.3.2&hash=df0bf1" + resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin::version=2.3.2&hash=18f3a7" dependencies: node-gyp: latest conditions: os=darwin @@ -7426,7 +7426,7 @@ __metadata: "resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin": version: 1.22.1 - resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin::version=1.22.1&hash=c3c19d" + resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin::version=1.22.1&hash=07638b" dependencies: is-core-module: ^2.9.0 path-parse: ^1.0.7 @@ -8412,11 +8412,11 @@ __metadata: "typescript@patch:typescript@^4.9.3#~builtin": version: 4.9.5 - resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=23ec76" + resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=701156" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: ab417a2f398380c90a6cf5a5f74badd17866adf57f1165617d6a551f059c3ba0a3e4da0d147b3ac5681db9ac76a303c5876394b13b3de75fdd5b1eaa06181c9d + checksum: 2eee5c37cad4390385db5db5a8e81470e42e8f1401b0358d7390095d6f681b410f2c4a0c496c6ff9ebd775423c7785cdace7bcdad76c7bee283df3d9718c0f20 languageName: node linkType: hard @@ -8969,7 +8969,7 @@ __metadata: version: 0.0.0-use.local resolution: "youtube-music@workspace:." dependencies: - "@cliqz/adblocker-electron": ^1.25.2 + "@cliqz/adblocker-electron": ^1.26.0 "@ffmpeg/core": ^0.11.0 "@ffmpeg/ffmpeg": ^0.11.6 "@foobar404/wave": ^2.0.4 From 51871a3fecd87bee6f44e862af565b17d091bff9 Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Wed, 15 Mar 2023 20:29:12 +0200 Subject: [PATCH 23/33] catch errors in `downloadSong()` --- plugins/downloader/back.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index 94920e85..22b9666e 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -59,7 +59,7 @@ const sendError = (error) => { buttons: ["OK"], title: "Error in download!", message: "Argh! Apologies, download failed…", - detail: error.toString(), + detail: `${error.toString()} ${error.cause ? `\n\n${error.cause.toString()}` : ''}`, }); }; @@ -89,6 +89,19 @@ async function downloadSong( playlistFolder = undefined, trackId = undefined, increasePlaylistProgress = () => {}, +) { + try { + await downloadSongUnsafe(url, playlistFolder, trackId, increasePlaylistProgress); + } catch (error) { + sendError(error); + } +} + +async function downloadSongUnsafe( + url, + playlistFolder = undefined, + trackId = undefined, + increasePlaylistProgress = () => {}, ) { const sendFeedback = (message, progress) => { if (!playlistFolder) { From 89c664b4d2bb5effeaccc7b0553447259c1fa3f6 Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Wed, 15 Mar 2023 21:52:25 +0200 Subject: [PATCH 24/33] fix uploaded library style follows up on [in-app-menu] fix items hidden by navbar in library #1067 --- plugins/in-app-menu/style.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/in-app-menu/style.css b/plugins/in-app-menu/style.css index 0148b38a..f0f31ed8 100644 --- a/plugins/in-app-menu/style.css +++ b/plugins/in-app-menu/style.css @@ -18,7 +18,8 @@ } /* fix navbar hiding library items */ -ytmusic-section-list-renderer[page-type="MUSIC_PAGE_TYPE_LIBRARY_CONTENT_LANDING_PAGE"] { +ytmusic-section-list-renderer[page-type="MUSIC_PAGE_TYPE_LIBRARY_CONTENT_LANDING_PAGE"], +ytmusic-section-list-renderer[page-type="MUSIC_PAGE_TYPE_PRIVATELY_OWNED_CONTENT_LANDING_PAGE"] { top: 50px; position: relative; } From 640ba26d55df81b5f9b7eba1761f5887511a0624 Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Thu, 16 Mar 2023 18:58:52 +0200 Subject: [PATCH 25/33] add option to hide the like buttons fix #1075 --- config/store.js | 5 +++++ menu.js | 33 +++++++++++++++++++++++++++------ preload.js | 14 ++++++++++---- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/config/store.js b/config/store.js index 07984820..1ba8ce26 100644 --- a/config/store.js +++ b/config/store.js @@ -19,6 +19,11 @@ const migrations = { ...pluginOptions, }); } + + if (store.get("options.ForceShowLikeButtons")) { + store.delete("options.ForceShowLikeButtons"); + store.set("options.likeButtons", 'force'); + } }, ">=1.17.0": (store) => { setDefaultPluginOptions(store, "picture-in-picture"); diff --git a/menu.js b/menu.js index a741cd5d..d35d8252 100644 --- a/menu.js +++ b/menu.js @@ -93,12 +93,33 @@ const mainMenuTemplate = (win) => { }, }, { - label: "Force show like buttons", - type: "checkbox", - checked: config.get("options.ForceShowLikeButtons"), - click: (item) => { - config.set("options.ForceShowLikeButtons", item.checked); - }, + label: "Like buttons", + submenu: [ + { + label: "Default", + type: "radio", + checked: !config.get("options.likeButtons"), + click: () => { + config.set("options.likeButtons", ''); + }, + }, + { + label: "Force show", + type: "radio", + checked: config.get("options.likeButtons") === 'force', + click: () => { + config.set("options.likeButtons", 'force'); + } + }, + { + label: "Hide", + type: "radio", + checked: config.get("options.likeButtons") === 'hide', + click: () => { + config.set("options.likeButtons", 'hide'); + } + }, + ], }, { label: "Theme", diff --git a/preload.js b/preload.js index 492f3192..e956effa 100644 --- a/preload.js +++ b/preload.js @@ -135,11 +135,17 @@ function onApiLoaded() { } } - // Force show like buttons - if (config.get("options.ForceShowLikeButtons")) { - const likeButtons = document.querySelector('ytmusic-like-button-renderer') + + // Hide / Force show like buttons + const likeButtonsOptions = config.get("options.likeButtons"); + if (likeButtonsOptions) { + const likeButtons = document.querySelector("ytmusic-like-button-renderer"); if (likeButtons) { - likeButtons.style.display = 'inherit'; + likeButtons.style.display = + { + hide: "none", + force: "inherit", + }[likeButtonsOptions] || ""; } } } From 3abef7cb8af859b98923cc62b2d46c274a5f9ec3 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Fri, 17 Mar 2023 11:49:22 -0500 Subject: [PATCH 26/33] Nitpick: Fix name casing in tray icon tooltip --- tray.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tray.js b/tray.js index d8712d7e..46b76f83 100644 --- a/tray.js +++ b/tray.js @@ -40,7 +40,7 @@ module.exports.setUpTray = (app, win) => { tray = new Tray(trayIcon); - tray.setToolTip("Youtube Music"); + tray.setToolTip("YouTube Music"); // macOS only tray.setIgnoreDoubleClickEvents(true); From d5c2ad2115131798845edaa1267e65813407a828 Mon Sep 17 00:00:00 2001 From: Derek Chen Date: Fri, 17 Mar 2023 18:02:48 -0500 Subject: [PATCH 27/33] Romanization update --- plugins/lyrics-genius/back.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lyrics-genius/back.js b/plugins/lyrics-genius/back.js index ad626d14..134f66d8 100644 --- a/plugins/lyrics-genius/back.js +++ b/plugins/lyrics-genius/back.js @@ -7,7 +7,7 @@ const fetch = require("node-fetch"); const { cleanupName } = require("../../providers/song-info"); const { injectCSS } = require("../utils"); -let eastAsianChars = new RegExp("[\u{3040}-\u{30ff}\u{3400}-\u{4dbf}\u{4e00}-\u{9fff}\u{f900}-\u{faff}\u{ff66}-\u{ff9f}]"); +let eastAsianChars = new RegExp("[\uac00-\ud7af]|[\u1100-\u11ff]|[\u3130-\u318f]|[\ua960-\ua97f]|[\ud7b0-\ud7ff]|[\u3040-\u309F]|[\u3000-\u303F\u3400-\u4DBF\u4E00-\u9FFF]"); let revRomanized = false; module.exports = async (win, options) => { From bc2a1f7f71ced5f271d271f9a6bb12012fb1babc Mon Sep 17 00:00:00 2001 From: Derek Chen Date: Sat, 18 Mar 2023 13:15:25 -0500 Subject: [PATCH 28/33] Updated Regex to be cleaner --- plugins/lyrics-genius/back.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lyrics-genius/back.js b/plugins/lyrics-genius/back.js index 134f66d8..cb781ca8 100644 --- a/plugins/lyrics-genius/back.js +++ b/plugins/lyrics-genius/back.js @@ -7,7 +7,7 @@ const fetch = require("node-fetch"); const { cleanupName } = require("../../providers/song-info"); const { injectCSS } = require("../utils"); -let eastAsianChars = new RegExp("[\uac00-\ud7af]|[\u1100-\u11ff]|[\u3130-\u318f]|[\ua960-\ua97f]|[\ud7b0-\ud7ff]|[\u3040-\u309F]|[\u3000-\u303F\u3400-\u4DBF\u4E00-\u9FFF]"); +let eastAsianChars = /\p{Script=Han}|\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}|\p{Script=Han}/u; let revRomanized = false; module.exports = async (win, options) => { From a6242d13aecca8b9b6148a0f4bf8b1a504d2b66a Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sun, 19 Mar 2023 03:00:17 +0200 Subject: [PATCH 29/33] add `getActivePlugins` and `isActive` --- config/dynamic.js | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/config/dynamic.js b/config/dynamic.js index 713379e6..34cb0ac3 100644 --- a/config/dynamic.js +++ b/config/dynamic.js @@ -3,6 +3,30 @@ const { ipcRenderer, ipcMain } = require("electron"); const defaultConfig = require("./defaults"); const { getOptions, setOptions, setMenuOptions } = require("./plugins"); +const activePlugins = {}; +/** + * [!IMPORTANT!] + * The method is **sync** in the main process and **async** in the renderer process. + */ +module.exports.getActivePlugins = + process.type === "renderer" + ? async () => ipcRenderer.invoke("get-active-plugins") + : () => activePlugins; + +if (process.type === "browser") { + ipcMain.handle("get-active-plugins", this.getActivePlugins); +} + +/** + * [!IMPORTANT!] + * The method is **sync** in the main process and **async** in the renderer process. + */ +module.exports.isActive = + process.type === "renderer" + ? async (plugin) => + plugin in (await ipcRenderer.invoke("get-active-plugins")) + : (plugin) => plugin in activePlugins; + /** * This class is used to create a dynamic synced config for plugins. * @@ -17,9 +41,9 @@ const { getOptions, setOptions, setMenuOptions } = require("./plugins"); * const { PluginConfig } = require("../../config/dynamic"); * const config = new PluginConfig("plugin-name", { enableFront: true }); * module.exports = { ...config }; - * + * * // or - * + * * module.exports = (win, options) => { * const config = new PluginConfig("plugin-name", { * enableFront: true, @@ -46,6 +70,8 @@ module.exports.PluginConfig = class PluginConfig { if (this.#enableFront) { this.#setupFront(); } + + activePlugins[name] = this; } get = (option) => { @@ -85,6 +111,7 @@ module.exports.PluginConfig = class PluginConfig { setMenuOptions(this.#name, this.#config); }; + /** Called only from back */ #save() { setOptions(this.#name, this.#config); } From 325026e3eae3daed33a6d66d1ef9f898d6805b28 Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sun, 19 Mar 2023 20:15:15 +0200 Subject: [PATCH 30/33] rome lint --- plugins/downloader/back.js | 749 ++++++++++++++++++----------------- plugins/downloader/config.js | 20 +- 2 files changed, 388 insertions(+), 381 deletions(-) diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index 22b9666e..7dd16d08 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -1,47 +1,47 @@ const { - existsSync, - mkdirSync, - createWriteStream, - writeFileSync, -} = require("fs"); -const { join } = require("path"); + existsSync, + mkdirSync, + createWriteStream, + writeFileSync, +} = require('fs'); +const { join } = require('path'); -const { fetchFromGenius } = require("../lyrics-genius/back"); -const { isEnabled } = require("../../config/plugins"); -const { getImage } = require("../../providers/song-info"); -const { injectCSS } = require("../utils"); +const { fetchFromGenius } = require('../lyrics-genius/back'); +const { isEnabled } = require('../../config/plugins'); +const { getImage } = require('../../providers/song-info'); +const { injectCSS } = require('../utils'); const { - presets, - cropMaxWidth, - getFolder, - setBadge, - sendFeedback: sendFeedback_, -} = require("./utils"); + presets, + cropMaxWidth, + getFolder, + setBadge, + sendFeedback: sendFeedback_, +} = require('./utils'); -const { ipcMain, app, dialog } = require("electron"); -const is = require("electron-is"); -const { Innertube, UniversalCache, Utils } = require("youtubei.js"); -const ytpl = require("ytpl"); // REPLACE with youtubei getplaylist https://github.com/LuanRT/YouTube.js#getplaylistid +const { ipcMain, app, dialog } = require('electron'); +const is = require('electron-is'); +const { Innertube, UniversalCache, Utils } = require('youtubei.js'); +const ytpl = require('ytpl'); // REPLACE with youtubei getplaylist https://github.com/LuanRT/YouTube.js#getplaylistid -const filenamify = require("filenamify"); -const ID3Writer = require("browser-id3-writer"); -const { randomBytes } = require("crypto"); -const Mutex = require("async-mutex").Mutex; -const ffmpeg = require("@ffmpeg/ffmpeg").createFFmpeg({ - log: false, - logger: () => {}, // console.log, - progress: () => {}, // console.log, +const filenamify = require('filenamify'); +const ID3Writer = require('browser-id3-writer'); +const { randomBytes } = require('crypto'); +const Mutex = require('async-mutex').Mutex; +const ffmpeg = require('@ffmpeg/ffmpeg').createFFmpeg({ + log: false, + logger: () => {}, // console.log, + progress: () => {}, // console.log, }); const ffmpegMutex = new Mutex(); const cache = { - getCoverBuffer: { - buffer: null, - url: null, - }, + getCoverBuffer: { + buffer: null, + url: null, + }, }; -const config = require("./config"); +const config = require('./config'); /** @type {Innertube} */ let yt; @@ -49,431 +49,438 @@ let win; let playingUrl = undefined; const sendError = (error) => { - win.setProgressBar(-1); // close progress bar - setBadge(0); // close badge - sendFeedback_(win); // reset feedback + win.setProgressBar(-1); // close progress bar + setBadge(0); // close badge + sendFeedback_(win); // reset feedback - console.error(error); - dialog.showMessageBox({ - type: "info", - buttons: ["OK"], - title: "Error in download!", - message: "Argh! Apologies, download failed…", - detail: `${error.toString()} ${error.cause ? `\n\n${error.cause.toString()}` : ''}`, - }); + console.error(error); + dialog.showMessageBox({ + type: 'info', + buttons: ['OK'], + title: 'Error in download!', + message: 'Argh! Apologies, download failed…', + detail: `${error.toString()} ${ + error.cause ? `\n\n${error.cause.toString()}` : '' + }`, + }); }; module.exports = async (win_, options) => { - win = win_; - config.init(options); - injectCSS(win.webContents, join(__dirname, "style.css")); + win = win_; + config.init(options); + injectCSS(win.webContents, join(__dirname, 'style.css')); - yt = await Innertube.create({ - cache: new UniversalCache(false), - generate_session_locally: true, - }); - ipcMain.on("download-song", (_, url) => downloadSong(url)); - ipcMain.on("video-src-changed", async (_, data) => { - playingUrl = - JSON.parse(data)?.microformat?.microformatDataRenderer?.urlCanonical; - }); - ipcMain.on("download-playlist-request", async (_event, url) => - downloadPlaylist(url), - ); + yt = await Innertube.create({ + cache: new UniversalCache(false), + generate_session_locally: true, + }); + ipcMain.on('download-song', (_, url) => downloadSong(url)); + ipcMain.on('video-src-changed', async (_, data) => { + playingUrl = + JSON.parse(data)?.microformat?.microformatDataRenderer?.urlCanonical; + }); + ipcMain.on('download-playlist-request', async (_event, url) => + downloadPlaylist(url), + ); }; module.exports.downloadSong = downloadSong; async function downloadSong( - url, - playlistFolder = undefined, - trackId = undefined, - increasePlaylistProgress = () => {}, + url, + playlistFolder = undefined, + trackId = undefined, + increasePlaylistProgress = () => {}, ) { - try { - await downloadSongUnsafe(url, playlistFolder, trackId, increasePlaylistProgress); - } catch (error) { - sendError(error); - } + try { + await downloadSongUnsafe( + url, + playlistFolder, + trackId, + increasePlaylistProgress, + ); + } catch (error) { + sendError(error); + } } async function downloadSongUnsafe( - url, - playlistFolder = undefined, - trackId = undefined, - increasePlaylistProgress = () => {}, + url, + playlistFolder = undefined, + trackId = undefined, + increasePlaylistProgress = () => {}, ) { - const sendFeedback = (message, progress) => { - if (!playlistFolder) { - sendFeedback_(win, message); - if (!isNaN(progress)) { - win.setProgressBar(progress); - } - } - }; + const sendFeedback = (message, progress) => { + if (!playlistFolder) { + sendFeedback_(win, message); + if (!isNaN(progress)) { + win.setProgressBar(progress); + } + } + }; - sendFeedback("Downloading...", 2); + sendFeedback('Downloading...', 2); - const id = getVideoId(url); - const info = await yt.music.getInfo(id); + const id = getVideoId(url); + const info = await yt.music.getInfo(id); - const metadata = getMetadata(info); - if (metadata.album === "N/A") metadata.album = ""; - metadata.trackId = trackId; + const metadata = getMetadata(info); + if (metadata.album === 'N/A') metadata.album = ''; + metadata.trackId = trackId; - const dir = - playlistFolder || config.get("downloadFolder") || app.getPath("downloads"); - const name = `${metadata.artist ? `${metadata.artist} - ` : ""}${ - metadata.title - }`; + const dir = + playlistFolder || config.get('downloadFolder') || app.getPath('downloads'); + const name = `${metadata.artist ? `${metadata.artist} - ` : ''}${ + metadata.title + }`; - const extension = presets[config.get("preset")]?.extension || "mp3"; + const extension = presets[config.get('preset')]?.extension || 'mp3'; - const filename = filenamify(`${name}.${extension}`, { - replacement: "_", - maxLength: 255, - }); - const filePath = join(dir, filename); + const filename = filenamify(`${name}.${extension}`, { + replacement: '_', + maxLength: 255, + }); + const filePath = join(dir, filename); - if (config.get("skipExisting") && existsSync(filePath)) { - sendFeedback(null, -1); - return; - } + if (config.get('skipExisting') && existsSync(filePath)) { + sendFeedback(null, -1); + return; + } - const download_options = { - type: "audio", // audio, video or video+audio - quality: "best", // best, bestefficiency, 144p, 240p, 480p, 720p and so on. - format: "any", // media container format - }; + const download_options = { + type: 'audio', // audio, video or video+audio + quality: 'best', // best, bestefficiency, 144p, 240p, 480p, 720p and so on. + format: 'any', // media container format + }; - const format = info.chooseFormat(download_options); - const stream = await info.download(download_options); + const format = info.chooseFormat(download_options); + const stream = await info.download(download_options); - console.info( - `Downloading ${metadata.artist} - ${metadata.title} [${metadata.id}]`, - ); + console.info( + `Downloading ${metadata.artist} - ${metadata.title} [${metadata.id}]`, + ); - const iterableStream = Utils.streamToIterable(stream); + const iterableStream = Utils.streamToIterable(stream); - if (!existsSync(dir)) { - mkdirSync(dir); - } + if (!existsSync(dir)) { + mkdirSync(dir); + } - if (!presets[config.get("preset")]) { - const fileBuffer = await iterableStreamToMP3( - iterableStream, - metadata, - format.content_length, - sendFeedback, - increasePlaylistProgress, - ); - writeFileSync(filePath, await writeID3(fileBuffer, metadata, sendFeedback)); - } else { - const file = createWriteStream(filePath); - let downloaded = 0; - const total = format.content_length; + if (!presets[config.get('preset')]) { + const fileBuffer = await iterableStreamToMP3( + iterableStream, + metadata, + format.content_length, + sendFeedback, + increasePlaylistProgress, + ); + writeFileSync(filePath, await writeID3(fileBuffer, metadata, sendFeedback)); + } else { + const file = createWriteStream(filePath); + let downloaded = 0; + const total = format.content_length; - for await (const chunk of iterableStream) { - downloaded += chunk.length; - const ratio = downloaded / total; - const progress = Math.floor(ratio * 100); - sendFeedback(`Download: ${progress}%`, ratio); - increasePlaylistProgress(ratio); - file.write(chunk); - } - await ffmpegWriteTags( - filePath, - metadata, - presets[config.get("preset")]?.ffmpegArgs, - ); - sendFeedback(null, -1); - } + for await (const chunk of iterableStream) { + downloaded += chunk.length; + const ratio = downloaded / total; + const progress = Math.floor(ratio * 100); + sendFeedback(`Download: ${progress}%`, ratio); + increasePlaylistProgress(ratio); + file.write(chunk); + } + await ffmpegWriteTags( + filePath, + metadata, + presets[config.get('preset')]?.ffmpegArgs, + ); + sendFeedback(null, -1); + } - sendFeedback(null, -1); - console.info(`Done: "${filePath}"`); + sendFeedback(null, -1); + console.info(`Done: "${filePath}"`); } async function iterableStreamToMP3( - stream, - metadata, - content_length, - sendFeedback, - increasePlaylistProgress = () => {}, + stream, + metadata, + content_length, + sendFeedback, + increasePlaylistProgress = () => {}, ) { - const chunks = []; - let downloaded = 0; - const total = content_length; - for await (const chunk of stream) { - downloaded += chunk.length; - chunks.push(chunk); - const ratio = downloaded / total; - const progress = Math.floor(ratio * 100); - sendFeedback(`Download: ${progress}%`, ratio); - // 15% for download, 85% for conversion - // This is a very rough estimate, trying to make the progress bar look nice - increasePlaylistProgress(ratio * 0.15); - } - sendFeedback("Loading…", 2); // indefinite progress bar after download + const chunks = []; + let downloaded = 0; + const total = content_length; + for await (const chunk of stream) { + downloaded += chunk.length; + chunks.push(chunk); + const ratio = downloaded / total; + const progress = Math.floor(ratio * 100); + sendFeedback(`Download: ${progress}%`, ratio); + // 15% for download, 85% for conversion + // This is a very rough estimate, trying to make the progress bar look nice + increasePlaylistProgress(ratio * 0.15); + } + sendFeedback('Loading…', 2); // indefinite progress bar after download - const buffer = Buffer.concat(chunks); - const safeVideoName = randomBytes(32).toString("hex"); - const releaseFFmpegMutex = await ffmpegMutex.acquire(); + const buffer = Buffer.concat(chunks); + const safeVideoName = randomBytes(32).toString('hex'); + const releaseFFmpegMutex = await ffmpegMutex.acquire(); - try { - if (!ffmpeg.isLoaded()) { - await ffmpeg.load(); - } + try { + if (!ffmpeg.isLoaded()) { + await ffmpeg.load(); + } - sendFeedback("Preparing file…"); - ffmpeg.FS("writeFile", safeVideoName, buffer); + sendFeedback('Preparing file…'); + ffmpeg.FS('writeFile', safeVideoName, buffer); - sendFeedback("Converting…"); + sendFeedback('Converting…'); - ffmpeg.setProgress(({ ratio }) => { - sendFeedback(`Converting: ${Math.floor(ratio * 100)}%`, ratio); - increasePlaylistProgress(0.15 + ratio * 0.85); - }); + ffmpeg.setProgress(({ ratio }) => { + sendFeedback(`Converting: ${Math.floor(ratio * 100)}%`, ratio); + increasePlaylistProgress(0.15 + ratio * 0.85); + }); - await ffmpeg.run( - "-i", - safeVideoName, - ...getFFmpegMetadataArgs(metadata), - `${safeVideoName}.mp3`, - ); + await ffmpeg.run( + '-i', + safeVideoName, + ...getFFmpegMetadataArgs(metadata), + `${safeVideoName}.mp3`, + ); - sendFeedback("Saving…"); + sendFeedback('Saving…'); - return ffmpeg.FS("readFile", `${safeVideoName}.mp3`); - } catch (e) { - sendError(e); - } finally { - releaseFFmpegMutex(); - } + return ffmpeg.FS('readFile', `${safeVideoName}.mp3`); + } catch (e) { + sendError(e); + } finally { + releaseFFmpegMutex(); + } } async function getCoverBuffer(url) { - const store = cache.getCoverBuffer; - if (store.url === url) { - return store.buffer; - } - store.url = url; + const store = cache.getCoverBuffer; + if (store.url === url) { + return store.buffer; + } + store.url = url; - const nativeImage = cropMaxWidth(await getImage(url)); - store.buffer = - nativeImage && !nativeImage.isEmpty() ? nativeImage.toPNG() : null; + const nativeImage = cropMaxWidth(await getImage(url)); + store.buffer = + nativeImage && !nativeImage.isEmpty() ? nativeImage.toPNG() : null; - return store.buffer; + return store.buffer; } async function writeID3(buffer, metadata, sendFeedback) { - try { - sendFeedback("Writing ID3 tags..."); + try { + sendFeedback('Writing ID3 tags...'); - const coverBuffer = await getCoverBuffer(metadata.image); + const coverBuffer = await getCoverBuffer(metadata.image); - const writer = new ID3Writer(buffer); + const writer = new ID3Writer(buffer); - // Create the metadata tags - writer.setFrame("TIT2", metadata.title).setFrame("TPE1", [metadata.artist]); - if (metadata.album) { - writer.setFrame("TALB", metadata.album); - } - if (coverBuffer) { - writer.setFrame("APIC", { - type: 3, - data: coverBuffer, - description: "", - }); - } - if (isEnabled("lyrics-genius")) { - const lyrics = await fetchFromGenius(metadata); - if (lyrics) { - writer.setFrame("USLT", { - description: "", - lyrics: lyrics, - }); - } - } - if (metadata.trackId) { - writer.setFrame("TRCK", metadata.trackId); - } - writer.addTag(); - return Buffer.from(writer.arrayBuffer); - } catch (e) { - sendError(e); - } + // Create the metadata tags + writer.setFrame('TIT2', metadata.title).setFrame('TPE1', [metadata.artist]); + if (metadata.album) { + writer.setFrame('TALB', metadata.album); + } + if (coverBuffer) { + writer.setFrame('APIC', { + type: 3, + data: coverBuffer, + description: '', + }); + } + if (isEnabled('lyrics-genius')) { + const lyrics = await fetchFromGenius(metadata); + if (lyrics) { + writer.setFrame('USLT', { + description: '', + lyrics: lyrics, + }); + } + } + if (metadata.trackId) { + writer.setFrame('TRCK', metadata.trackId); + } + writer.addTag(); + return Buffer.from(writer.arrayBuffer); + } catch (e) { + sendError(e); + } } async function downloadPlaylist(givenUrl) { - if (givenUrl) { - try { - givenUrl = new URL(givenUrl); - } catch { - givenUrl = undefined; - } - } - const playlistId = - getPlaylistID(givenUrl) || - getPlaylistID(new URL(win.webContents.getURL())) || - getPlaylistID(new URL(playingUrl)); + if (givenUrl) { + try { + givenUrl = new URL(givenUrl); + } catch { + givenUrl = undefined; + } + } + const playlistId = + getPlaylistID(givenUrl) || + getPlaylistID(new URL(win.webContents.getURL())) || + getPlaylistID(new URL(playingUrl)); - if (!playlistId) { - sendError(new Error("No playlist ID found")); - return; - } + if (!playlistId) { + sendError(new Error('No playlist ID found')); + return; + } - const sendFeedback = (message) => sendFeedback_(win, message); + const sendFeedback = (message) => sendFeedback_(win, message); - console.log(`trying to get playlist ID: '${playlistId}'`); - sendFeedback("Getting playlist info…"); - let playlist; - try { - playlist = await ytpl(playlistId, { - limit: config.get("playlistMaxItems") || Infinity, - }); - } catch (e) { - sendError( - `Error getting playlist info: make sure it isn\'t a private or "Mixed for you" playlist\n\n${e}`, - ); - return; - } - if (playlist.items.length === 0) sendError(new Error("Playlist is empty")); - if (playlist.items.length === 1) { - sendFeedback("Playlist has only one item, downloading it directly"); - await downloadSong(playlist.items[0].url); - return; - } - const isAlbum = playlist.title.startsWith("Album - "); - if (isAlbum) { - playlist.title = playlist.title.slice(8); - } - const safePlaylistTitle = filenamify(playlist.title, { replacement: " " }); + console.log(`trying to get playlist ID: '${playlistId}'`); + sendFeedback('Getting playlist info…'); + let playlist; + try { + playlist = await ytpl(playlistId, { + limit: config.get('playlistMaxItems') || Infinity, + }); + } catch (e) { + sendError( + `Error getting playlist info: make sure it isn\'t a private or "Mixed for you" playlist\n\n${e}`, + ); + return; + } + if (playlist.items.length === 0) sendError(new Error('Playlist is empty')); + if (playlist.items.length === 1) { + sendFeedback('Playlist has only one item, downloading it directly'); + await downloadSong(playlist.items[0].url); + return; + } + const isAlbum = playlist.title.startsWith('Album - '); + if (isAlbum) { + playlist.title = playlist.title.slice(8); + } + const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' }); - const folder = getFolder(config.get("downloadFolder")); - const playlistFolder = join(folder, safePlaylistTitle); - if (existsSync(playlistFolder)) { - if (!config.get("skipExisting")) { - sendError(new Error(`The folder ${playlistFolder} already exists`)); - return; - } - } else { - mkdirSync(playlistFolder, { recursive: true }); - } + const folder = getFolder(config.get('downloadFolder')); + const playlistFolder = join(folder, safePlaylistTitle); + if (existsSync(playlistFolder)) { + if (!config.get('skipExisting')) { + sendError(new Error(`The folder ${playlistFolder} already exists`)); + return; + } + } else { + mkdirSync(playlistFolder, { recursive: true }); + } - dialog.showMessageBox({ - type: "info", - buttons: ["OK"], - title: "Started Download", - message: `Downloading Playlist "${playlist.title}"`, - detail: `(${playlist.items.length} songs)`, - }); + dialog.showMessageBox({ + type: 'info', + buttons: ['OK'], + title: 'Started Download', + message: `Downloading Playlist "${playlist.title}"`, + detail: `(${playlist.items.length} songs)`, + }); - if (is.dev()) { - console.log( - `Downloading playlist "${playlist.title}" - ${playlist.items.length} songs (${playlistId})`, - ); - } + if (is.dev()) { + console.log( + `Downloading playlist "${playlist.title}" - ${playlist.items.length} songs (${playlistId})`, + ); + } - win.setProgressBar(2); // starts with indefinite bar + win.setProgressBar(2); // starts with indefinite bar - setBadge(playlist.items.length); + setBadge(playlist.items.length); - let counter = 1; + let counter = 1; - const progressStep = 1 / playlist.items.length; + const progressStep = 1 / playlist.items.length; - const increaseProgress = (itemPercentage) => { - const currentProgress = (counter - 1) / playlist.items.length; - const newProgress = currentProgress + progressStep * itemPercentage; - win.setProgressBar(newProgress); - }; + const increaseProgress = (itemPercentage) => { + const currentProgress = (counter - 1) / playlist.items.length; + const newProgress = currentProgress + progressStep * itemPercentage; + win.setProgressBar(newProgress); + }; - try { - for (const song of playlist.items) { - sendFeedback(`Downloading ${counter}/${playlist.items.length}...`); - const trackId = isAlbum ? counter : undefined; - await downloadSong( - song.url, - playlistFolder, - trackId, - increaseProgress, - ).catch((e) => - sendError( - `Error downloading "${song.author.name} - ${song.title}":\n ${e}`, - ), - ); + try { + for (const song of playlist.items) { + sendFeedback(`Downloading ${counter}/${playlist.items.length}...`); + const trackId = isAlbum ? counter : undefined; + await downloadSong( + song.url, + playlistFolder, + trackId, + increaseProgress, + ).catch((e) => + sendError( + `Error downloading "${song.author.name} - ${song.title}":\n ${e}`, + ), + ); - win.setProgressBar(counter / playlist.items.length); - setBadge(playlist.items.length - counter); - counter++; - } - } catch (e) { - sendError(e); - } finally { - win.setProgressBar(-1); // close progress bar - setBadge(0); // close badge counter - sendFeedback(); // clear feedback - } + win.setProgressBar(counter / playlist.items.length); + setBadge(playlist.items.length - counter); + counter++; + } + } catch (e) { + sendError(e); + } finally { + win.setProgressBar(-1); // close progress bar + setBadge(0); // close badge counter + sendFeedback(); // clear feedback + } } async function ffmpegWriteTags(filePath, metadata, ffmpegArgs = []) { - const releaseFFmpegMutex = await ffmpegMutex.acquire(); + const releaseFFmpegMutex = await ffmpegMutex.acquire(); - try { - if (!ffmpeg.isLoaded()) { - await ffmpeg.load(); - } + try { + if (!ffmpeg.isLoaded()) { + await ffmpeg.load(); + } - await ffmpeg.run( - "-i", - filePath, - ...getFFmpegMetadataArgs(metadata), - ...ffmpegArgs, - filePath, - ); - } catch (e) { - sendError(e); - } finally { - releaseFFmpegMutex(); - } + await ffmpeg.run( + '-i', + filePath, + ...getFFmpegMetadataArgs(metadata), + ...ffmpegArgs, + filePath, + ); + } catch (e) { + sendError(e); + } finally { + releaseFFmpegMutex(); + } } function getFFmpegMetadataArgs(metadata) { - if (!metadata) { - return; - } + if (!metadata) { + return; + } - return [ - ...(metadata.title ? ["-metadata", `title=${metadata.title}`] : []), - ...(metadata.artist ? ["-metadata", `artist=${metadata.artist}`] : []), - ...(metadata.album ? ["-metadata", `album=${metadata.album}`] : []), - ...(metadata.trackId ? ["-metadata", `track=${metadata.trackId}`] : []), - ]; + return [ + ...(metadata.title ? ['-metadata', `title=${metadata.title}`] : []), + ...(metadata.artist ? ['-metadata', `artist=${metadata.artist}`] : []), + ...(metadata.album ? ['-metadata', `album=${metadata.album}`] : []), + ...(metadata.trackId ? ['-metadata', `track=${metadata.trackId}`] : []), + ]; } // Playlist radio modifier needs to be cut from playlist ID -const INVALID_PLAYLIST_MODIFIER = "RDAMPL"; +const INVALID_PLAYLIST_MODIFIER = 'RDAMPL'; const getPlaylistID = (aURL) => { - const result = - aURL?.searchParams.get("list") || aURL?.searchParams.get("playlist"); - if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) { - return result.slice(6); - } - return result; + const result = + aURL?.searchParams.get('list') || aURL?.searchParams.get('playlist'); + if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) { + return result.slice(6); + } + return result; }; const getVideoId = (url) => { - if (typeof url === "string") { - url = new URL(url); - } - return url.searchParams.get("v"); + if (typeof url === 'string') { + url = new URL(url); + } + return url.searchParams.get('v'); }; const getMetadata = (info) => ({ - id: info.basic_info.id, - title: info.basic_info.title, - artist: info.basic_info.author, - album: info.player_overlays?.browser_media_session?.album?.text, - image: info.basic_info.thumbnail[0].url, + id: info.basic_info.id, + title: info.basic_info.title, + artist: info.basic_info.author, + album: info.player_overlays?.browser_media_session?.album?.text, + image: info.basic_info.thumbnail[0].url, }); diff --git a/plugins/downloader/config.js b/plugins/downloader/config.js index 24e745e9..bd2380e6 100644 --- a/plugins/downloader/config.js +++ b/plugins/downloader/config.js @@ -1,23 +1,23 @@ -const { setOptions, setMenuOptions } = require("../../config/plugins"); -const defaultConfig = require("../../config/defaults"); +const { setOptions, setMenuOptions } = require('../../config/plugins'); +const defaultConfig = require('../../config/defaults'); -let config = defaultConfig.plugins["downloader"]; +let config = defaultConfig.plugins['downloader']; module.exports.init = (options) => { - config = { ...config, ...options }; + config = { ...config, ...options }; }; module.exports.setAndMaybeRestart = (option, value) => { - config[option] = value; - setMenuOptions("downloader", config); + config[option] = value; + setMenuOptions('downloader', config); }; module.exports.set = (option, value) => { - config[option] = value; - setOptions("downloader", config); + config[option] = value; + setOptions('downloader', config); }; module.exports.get = (option) => { - let res = config[option]; - return res; + const res = config[option]; + return res; }; From 4f4372b65a1a9783a93d7c3f9afd7904082822cc Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sun, 19 Mar 2023 20:23:09 +0200 Subject: [PATCH 31/33] fix PR review comments --- plugins/downloader/back.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index 7dd16d08..c5743cf2 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -85,6 +85,7 @@ module.exports = async (win_, options) => { }; module.exports.downloadSong = downloadSong; +module.exports.downloadPlaylist = downloadPlaylist; async function downloadSong( url, @@ -312,12 +313,10 @@ async function writeID3(buffer, metadata, sendFeedback) { } async function downloadPlaylist(givenUrl) { - if (givenUrl) { - try { - givenUrl = new URL(givenUrl); - } catch { - givenUrl = undefined; - } + try { + givenUrl = new URL(givenUrl); + } catch { + givenUrl = undefined; } const playlistId = getPlaylistID(givenUrl) || @@ -465,7 +464,7 @@ const getPlaylistID = (aURL) => { const result = aURL?.searchParams.get('list') || aURL?.searchParams.get('playlist'); if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) { - return result.slice(6); + return result.slice(INVALID_PLAYLIST_MODIFIER.length); } return result; }; From e8c3716106924c2bc5afcaca5fd70d9d066c5e98 Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sun, 19 Mar 2023 21:02:23 +0200 Subject: [PATCH 32/33] use new dynamic config --- plugins/downloader/back.js | 3 +-- plugins/downloader/config.js | 26 +++----------------------- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index c5743cf2..35bedfc1 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -65,9 +65,8 @@ const sendError = (error) => { }); }; -module.exports = async (win_, options) => { +module.exports = async (win_) => { win = win_; - config.init(options); injectCSS(win.webContents, join(__dirname, 'style.css')); yt = await Innertube.create({ diff --git a/plugins/downloader/config.js b/plugins/downloader/config.js index bd2380e6..12c0384e 100644 --- a/plugins/downloader/config.js +++ b/plugins/downloader/config.js @@ -1,23 +1,3 @@ -const { setOptions, setMenuOptions } = require('../../config/plugins'); -const defaultConfig = require('../../config/defaults'); - -let config = defaultConfig.plugins['downloader']; - -module.exports.init = (options) => { - config = { ...config, ...options }; -}; - -module.exports.setAndMaybeRestart = (option, value) => { - config[option] = value; - setMenuOptions('downloader', config); -}; - -module.exports.set = (option, value) => { - config[option] = value; - setOptions('downloader', config); -}; - -module.exports.get = (option) => { - const res = config[option]; - return res; -}; +const { PluginConfig } = require('../../config/dynamic'); +const config = new PluginConfig('downloader'); +module.exports = { ...config }; From 4364d3be71a1636b2b81def8c4cd31ea2a317a13 Mon Sep 17 00:00:00 2001 From: TC Date: Sun, 19 Mar 2023 21:33:47 +0100 Subject: [PATCH 33/33] Update yarn.lock --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2ce5b9df..f641fc4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7329,7 +7329,7 @@ __metadata: "resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin": version: 1.22.1 - resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin::version=1.22.1&hash=07638b" + resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin::version=1.22.1&hash=c3c19d" dependencies: is-core-module: ^2.9.0 path-parse: ^1.0.7 @@ -8315,11 +8315,11 @@ __metadata: "typescript@patch:typescript@^4.9.3#~builtin": version: 4.9.5 - resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=701156" + resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=23ec76" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 2eee5c37cad4390385db5db5a8e81470e42e8f1401b0358d7390095d6f681b410f2c4a0c496c6ff9ebd775423c7785cdace7bcdad76c7bee283df3d9718c0f20 + checksum: ab417a2f398380c90a6cf5a5f74badd17866adf57f1165617d6a551f059c3ba0a3e4da0d147b3ac5681db9ac76a303c5876394b13b3de75fdd5b1eaa06181c9d languageName: node linkType: hard