diff --git a/index.js b/index.js index cbf3650e29..0c2396bb9b 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 cb287519f0..2d0f8272b5 100644 --- a/package.json +++ b/package.json @@ -113,16 +113,18 @@ "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", "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 0000000000..0aac6cec2d --- /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 daea62b212..c10a812f21 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 095d4968f9..97d733978f 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 622370a2c7..b133394ee6 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 7016018890..0569a6d165 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 443059fc90..0000000000 --- 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.replace("|", "").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 2d7a6028e6..c397229d28 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" @@ -3845,6 +3838,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" @@ -4675,6 +4679,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" @@ -5305,12 +5318,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 @@ -6285,6 +6298,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" @@ -6803,6 +6825,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" @@ -7236,7 +7265,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: @@ -8020,6 +8049,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" @@ -8207,6 +8246,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" @@ -8452,12 +8501,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 @@ -8976,6 +9025,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 @@ -8983,26 +9033,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