mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-14 03:41:46 +00:00
download using youtubei,js instead of ytdl-core
This commit is contained in:
5
index.js
5
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.commandLine.appendSwitch("enable-features", "SharedArrayBuffer"); // Required for downloader
|
||||||
app.allowRendererProcessReuse = true; // https://github.com/electron/electron/issues/18397
|
app.allowRendererProcessReuse = true; // https://github.com/electron/electron/issues/18397
|
||||||
if (config.get("options.disableHardwareAcceleration")) {
|
if (config.get("options.disableHardwareAcceleration")) {
|
||||||
|
|||||||
@ -125,6 +125,7 @@
|
|||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
"electron-unhandled": "^4.0.1",
|
"electron-unhandled": "^4.0.1",
|
||||||
"electron-updater": "^5.3.0",
|
"electron-updater": "^5.3.0",
|
||||||
|
"file-type": "^18.2.1",
|
||||||
"filenamify": "^4.3.0",
|
"filenamify": "^4.3.0",
|
||||||
"howler": "^2.2.3",
|
"howler": "^2.2.3",
|
||||||
"html-to-text": "^9.0.3",
|
"html-to-text": "^9.0.3",
|
||||||
@ -133,10 +134,11 @@
|
|||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"mpris-service": "^2.1.2",
|
"mpris-service": "^2.1.2",
|
||||||
"node-fetch": "^2.6.8",
|
"node-fetch": "^2.6.8",
|
||||||
|
"node-id3": "^0.2.6",
|
||||||
"node-notifier": "^10.0.1",
|
"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",
|
"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",
|
"vudio": "^2.1.1",
|
||||||
"youtubei.js": "^2.9.0",
|
"youtubei.js": "^3.1.1",
|
||||||
"ytdl-core": "^4.11.1",
|
"ytdl-core": "^4.11.1",
|
||||||
"ytpl": "^2.3.0"
|
"ytpl": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
|||||||
193
plugins/downloader/back-downloader.js
Normal file
193
plugins/downloader/back-downloader.js
Normal file
@ -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}`] : []),
|
||||||
|
];
|
||||||
|
};
|
||||||
@ -12,7 +12,9 @@ const { isEnabled } = require("../../config/plugins");
|
|||||||
const { getImage } = require("../../providers/song-info");
|
const { getImage } = require("../../providers/song-info");
|
||||||
const { fetchFromGenius } = require("../lyrics-genius/back");
|
const { fetchFromGenius } = require("../lyrics-genius/back");
|
||||||
|
|
||||||
const sendError = (win, error) => {
|
let win = {};
|
||||||
|
|
||||||
|
const sendError = (error) => {
|
||||||
win.setProgressBar(-1); // close progress bar
|
win.setProgressBar(-1); // close progress bar
|
||||||
dialog.showMessageBox({
|
dialog.showMessageBox({
|
||||||
type: "info",
|
type: "info",
|
||||||
@ -25,8 +27,13 @@ const sendError = (win, error) => {
|
|||||||
|
|
||||||
let nowPlayingMetadata = {};
|
let nowPlayingMetadata = {};
|
||||||
|
|
||||||
function handle(win) {
|
|
||||||
|
function handle(win_, options) {
|
||||||
|
win = win_;
|
||||||
injectCSS(win.webContents, join(__dirname, "style.css"));
|
injectCSS(win.webContents, join(__dirname, "style.css"));
|
||||||
|
|
||||||
|
require("./back-downloader")(options);
|
||||||
|
|
||||||
registerCallback((info) => {
|
registerCallback((info) => {
|
||||||
nowPlayingMetadata = info;
|
nowPlayingMetadata = info;
|
||||||
});
|
});
|
||||||
@ -34,7 +41,7 @@ function handle(win) {
|
|||||||
listenAction(CHANNEL, (event, action, arg) => {
|
listenAction(CHANNEL, (event, action, arg) => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case ACTIONS.ERROR: // arg = error
|
case ACTIONS.ERROR: // arg = error
|
||||||
sendError(win, arg);
|
sendError(arg);
|
||||||
break;
|
break;
|
||||||
case ACTIONS.METADATA:
|
case ACTIONS.METADATA:
|
||||||
event.returnValue = JSON.stringify(nowPlayingMetadata);
|
event.returnValue = JSON.stringify(nowPlayingMetadata);
|
||||||
@ -46,52 +53,6 @@ function handle(win) {
|
|||||||
console.log("Unknown action: " + action);
|
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;
|
module.exports = handle;
|
||||||
|
|||||||
@ -4,7 +4,6 @@ const { defaultConfig } = require("../../config");
|
|||||||
const { getSongMenu } = require("../../providers/dom-elements");
|
const { getSongMenu } = require("../../providers/dom-elements");
|
||||||
const { ElementFromFile, templatePath, triggerAction } = require("../utils");
|
const { ElementFromFile, templatePath, triggerAction } = require("../utils");
|
||||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||||
const { downloadVideoToMP3 } = require("./youtube-dl");
|
|
||||||
|
|
||||||
let menu = null;
|
let menu = null;
|
||||||
let progress = null;
|
let progress = null;
|
||||||
@ -61,28 +60,9 @@ global.download = () => {
|
|||||||
videoUrl = metadata.url || window.location.href;
|
videoUrl = metadata.url || window.location.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadVideoToMP3(
|
ipcRenderer.invoke('download-song', videoUrl).finally(() => triggerAction(CHANNEL, ACTIONS.PROGRESS, -1));
|
||||||
videoUrl,
|
return;
|
||||||
(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
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
// });
|
|
||||||
|
|
||||||
function observeMenu(options) {
|
function observeMenu(options) {
|
||||||
pluginOptions = { ...pluginOptions, ...options };
|
pluginOptions = { ...pluginOptions, ...options };
|
||||||
|
|||||||
@ -9,6 +9,7 @@ const filenamify = require('filenamify');
|
|||||||
|
|
||||||
const { setMenuOptions } = require("../../config/plugins");
|
const { setMenuOptions } = require("../../config/plugins");
|
||||||
const { sendError } = require("./back");
|
const { sendError } = require("./back");
|
||||||
|
const { downloadSong } = require("./back-downloader");
|
||||||
const { defaultMenuDownloadLabel, getFolder, presets, setBadge } = require("./utils");
|
const { defaultMenuDownloadLabel, getFolder, presets, setBadge } = require("./utils");
|
||||||
|
|
||||||
let downloadLabel = defaultMenuDownloadLabel;
|
let downloadLabel = defaultMenuDownloadLabel;
|
||||||
@ -62,7 +63,7 @@ module.exports = (win, options) => {
|
|||||||
options.preset = preset;
|
options.preset = preset;
|
||||||
setMenuOptions("downloader", options);
|
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));
|
|| getPlaylistID(new URL(playingUrl));
|
||||||
|
|
||||||
if (!playlistId) {
|
if (!playlistId) {
|
||||||
sendError(win, new Error("No playlist ID found"));
|
sendError(new Error("No playlist ID found"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,18 +93,15 @@ async function downloadPlaylist(givenUrl, win, options) {
|
|||||||
limit: options.playlistMaxItems || Infinity,
|
limit: options.playlistMaxItems || Infinity,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
sendError(win, e);
|
sendError(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const safePlaylistTitle = filenamify(playlist.title, {replacement: ' '});
|
const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' });
|
||||||
|
|
||||||
const folder = getFolder(options.downloadFolder);
|
const folder = getFolder(options.downloadFolder);
|
||||||
const playlistFolder = join(folder, safePlaylistTitle);
|
const playlistFolder = join(folder, safePlaylistTitle);
|
||||||
if (existsSync(playlistFolder)) {
|
if (existsSync(playlistFolder)) {
|
||||||
sendError(
|
sendError(new Error(`The folder ${playlistFolder} already exists`));
|
||||||
win,
|
|
||||||
new Error(`The folder ${playlistFolder} already exists`)
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mkdirSync(playlistFolder, { recursive: true });
|
mkdirSync(playlistFolder, { recursive: true });
|
||||||
@ -128,24 +126,30 @@ async function downloadPlaylist(givenUrl, win, options) {
|
|||||||
setBadge(playlist.items.length);
|
setBadge(playlist.items.length);
|
||||||
|
|
||||||
let dirWatcher = chokidar.watch(playlistFolder);
|
let dirWatcher = chokidar.watch(playlistFolder);
|
||||||
dirWatcher.on('add', () => {
|
const closeDirWatcher = () => {
|
||||||
downloadCount += 1;
|
if (dirWatcher) {
|
||||||
if (downloadCount >= playlist.items.length) {
|
|
||||||
win.setProgressBar(-1); // close progress bar
|
win.setProgressBar(-1); // close progress bar
|
||||||
setBadge(0); // close badge counter
|
setBadge(0); // close badge counter
|
||||||
dirWatcher.close().then(() => (dirWatcher = null));
|
dirWatcher.close().then(() => (dirWatcher = null));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
dirWatcher.on('add', () => {
|
||||||
|
downloadCount += 1;
|
||||||
|
if (downloadCount >= playlist.items.length) {
|
||||||
|
closeDirWatcher();
|
||||||
} else {
|
} else {
|
||||||
win.setProgressBar(downloadCount / playlist.items.length);
|
win.setProgressBar(downloadCount / playlist.items.length);
|
||||||
setBadge(playlist.items.length - downloadCount);
|
setBadge(playlist.items.length - downloadCount);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
playlist.items.forEach((song) => {
|
try {
|
||||||
win.webContents.send(
|
for (const song of playlist.items) {
|
||||||
"downloader-download-playlist",
|
await downloadSong(song.url, playlistFolder).catch((e) => sendError(e));
|
||||||
song.url,
|
}
|
||||||
safePlaylistTitle,
|
} catch (e) {
|
||||||
options
|
sendError(e);
|
||||||
);
|
} finally {
|
||||||
});
|
closeDirWatcher();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
const electron = require("electron");
|
const { app } = require("electron");
|
||||||
const is = require('electron-is');
|
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";
|
module.exports.defaultMenuDownloadLabel = "Download playlist";
|
||||||
|
|
||||||
const orderedQualityList = ["maxresdefault", "hqdefault", "mqdefault", "sdddefault"];
|
const orderedQualityList = ["maxresdefault", "hqdefault", "mqdefault", "sdddefault"];
|
||||||
@ -41,6 +41,6 @@ module.exports.presets = {
|
|||||||
|
|
||||||
module.exports.setBadge = n => {
|
module.exports.setBadge = n => {
|
||||||
if (is.linux() || is.macOS()) {
|
if (is.linux() || is.macOS()) {
|
||||||
electron.app.setBadgeCount(n);
|
app.setBadgeCount(n);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
98
yarn.lock
98
yarn.lock
@ -734,13 +734,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@remusao/guess-url-type@npm:^1.1.2":
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
resolution: "@remusao/guess-url-type@npm:1.2.1"
|
resolution: "@remusao/guess-url-type@npm:1.2.1"
|
||||||
@ -3860,6 +3853,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"file-uri-to-path@npm:1.0.0":
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
resolution: "file-uri-to-path@npm:1.0.0"
|
resolution: "file-uri-to-path@npm:1.0.0"
|
||||||
@ -4709,6 +4713,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"iconv-lite@npm:^0.6.2":
|
||||||
version: 0.6.3
|
version: 0.6.3
|
||||||
resolution: "iconv-lite@npm:0.6.3"
|
resolution: "iconv-lite@npm:0.6.3"
|
||||||
@ -5339,12 +5352,12 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"jintr@npm:^0.3.1":
|
"jintr@npm:^0.4.1":
|
||||||
version: 0.3.1
|
version: 0.4.1
|
||||||
resolution: "jintr@npm:0.3.1"
|
resolution: "jintr@npm:0.4.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: ^8.8.0
|
acorn: ^8.8.0
|
||||||
checksum: 1fb2454904461c3bbe6b55251dce4ac352fb3b94803773e3d8925ede4a907b5d52a2f30f3f76757c770e1785f34a3665d5cffd710c3ae99837cd157762130a24
|
checksum: 9dd5932be611aa926dba90e3b1bf09afbdc8864a128dbba53f5ee8461f0ac27955fca780dfd4cbb1575e6873d0d74dd346127554a4b2cae01986fe12aad3ba09
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -6319,6 +6332,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"node-notifier@npm:^10.0.1":
|
||||||
version: 10.0.1
|
version: 10.0.1
|
||||||
resolution: "node-notifier@npm:10.0.1"
|
resolution: "node-notifier@npm:10.0.1"
|
||||||
@ -6837,6 +6859,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"pend@npm:~1.2.0":
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
resolution: "pend@npm:1.2.0"
|
resolution: "pend@npm:1.2.0"
|
||||||
@ -7277,7 +7306,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 3.0.2
|
||||||
resolution: "readable-web-to-node-stream@npm:3.0.2"
|
resolution: "readable-web-to-node-stream@npm:3.0.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -8051,6 +8080,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"sumchecker@npm:^3.0.1":
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
resolution: "sumchecker@npm:3.0.1"
|
resolution: "sumchecker@npm:3.0.1"
|
||||||
@ -8238,6 +8277,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"tough-cookie@npm:~2.5.0":
|
||||||
version: 2.5.0
|
version: 2.5.0
|
||||||
resolution: "tough-cookie@npm:2.5.0"
|
resolution: "tough-cookie@npm:2.5.0"
|
||||||
@ -8483,12 +8532,12 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"undici@npm:^5.7.0":
|
"undici@npm:^5.19.1":
|
||||||
version: 5.18.0
|
version: 5.20.0
|
||||||
resolution: "undici@npm:5.18.0"
|
resolution: "undici@npm:5.20.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
busboy: ^1.6.0
|
busboy: ^1.6.0
|
||||||
checksum: 74e0f357c376c745fcca612481a5742866ae36086ad387e626255f4c4a15fc5357d9d0fa4355271b6a633d50f5556c3e85720844680c44861c66e23afca7245f
|
checksum: 25412a785b2bd0b12f0bb0ec47ef00aa7a611ca0e570cb7af97cffe6a42e0d78e4b15190363a43771e9002defc3c6647c1b2d52201b3f64e2196819db4d150d3
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -9023,6 +9072,7 @@ __metadata:
|
|||||||
electron-store: ^8.1.0
|
electron-store: ^8.1.0
|
||||||
electron-unhandled: ^4.0.1
|
electron-unhandled: ^4.0.1
|
||||||
electron-updater: ^5.3.0
|
electron-updater: ^5.3.0
|
||||||
|
file-type: ^18.2.1
|
||||||
filenamify: ^4.3.0
|
filenamify: ^4.3.0
|
||||||
howler: ^2.2.3
|
howler: ^2.2.3
|
||||||
html-to-text: ^9.0.3
|
html-to-text: ^9.0.3
|
||||||
@ -9032,26 +9082,26 @@ __metadata:
|
|||||||
mpris-service: ^2.1.2
|
mpris-service: ^2.1.2
|
||||||
node-fetch: ^2.6.8
|
node-fetch: ^2.6.8
|
||||||
node-gyp: ^9.3.1
|
node-gyp: ^9.3.1
|
||||||
|
node-id3: ^0.2.6
|
||||||
node-notifier: ^10.0.1
|
node-notifier: ^10.0.1
|
||||||
playwright: ^1.29.2
|
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"
|
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
|
vudio: ^2.1.1
|
||||||
xo: ^0.53.1
|
xo: ^0.53.1
|
||||||
youtubei.js: ^2.9.0
|
youtubei.js: ^3.1.1
|
||||||
ytdl-core: ^4.11.1
|
ytdl-core: ^4.11.1
|
||||||
ytpl: ^2.3.0
|
ytpl: ^2.3.0
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
"youtubei.js@npm:^2.9.0":
|
"youtubei.js@npm:^3.1.1":
|
||||||
version: 2.9.0
|
version: 3.1.1
|
||||||
resolution: "youtubei.js@npm:2.9.0"
|
resolution: "youtubei.js@npm:3.1.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@protobuf-ts/runtime": ^2.7.0
|
jintr: ^0.4.1
|
||||||
jintr: ^0.3.1
|
|
||||||
linkedom: ^0.14.12
|
linkedom: ^0.14.12
|
||||||
undici: ^5.7.0
|
undici: ^5.19.1
|
||||||
checksum: 0b9d86c1ec7297ee41b9013d6cb951976d82b2775d9d9d5abf0447d7acb9f36b07ebb689710bf8ccfa256a6f56088f49b699fb1a3e5bac2b0ea7d2daa508c109
|
checksum: 1280e2ddacec3034ee8e1b398ba80662a6854e184416d3484119e7cf47b69ab2e58b4f1efdf468dcad3e50bdc7bd42b6ee66b95660ffb521efb5f0634ef60fb7
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user