mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
add download feedback and progress
This commit is contained in:
@ -115,7 +115,6 @@
|
|||||||
"browser-id3-writer": "^4.4.0",
|
"browser-id3-writer": "^4.4.0",
|
||||||
"butterchurn": "^2.6.7",
|
"butterchurn": "^2.6.7",
|
||||||
"butterchurn-presets": "^2.4.7",
|
"butterchurn-presets": "^2.4.7",
|
||||||
"chokidar": "^3.5.3",
|
|
||||||
"custom-electron-prompt": "^1.5.1",
|
"custom-electron-prompt": "^1.5.1",
|
||||||
"custom-electron-titlebar": "^4.1.5",
|
"custom-electron-titlebar": "^4.1.5",
|
||||||
"electron-better-web-request": "^1.0.1",
|
"electron-better-web-request": "^1.0.1",
|
||||||
|
|||||||
@ -1,38 +1,67 @@
|
|||||||
const { existsSync, mkdirSync, createWriteStream, writeFileSync } = require('fs');
|
const { existsSync, mkdirSync, createWriteStream, writeFileSync } = require('fs');
|
||||||
const { ipcMain, app } = require("electron");
|
|
||||||
const { join } = require("path");
|
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 { fetchFromGenius } = require("../lyrics-genius/back");
|
||||||
const { isEnabled } = require("../../config/plugins");
|
const { isEnabled } = require("../../config/plugins");
|
||||||
const { getImage } = require("../../providers/song-info");
|
const { getImage } = require("../../providers/song-info");
|
||||||
const { cropMaxWidth } = require("./utils");
|
|
||||||
|
|
||||||
const { sendError } = require("./back");
|
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} */
|
/** @type {Innertube} */
|
||||||
let yt;
|
let yt;
|
||||||
let options;
|
let options;
|
||||||
|
let win;
|
||||||
|
let playingUrl = undefined;
|
||||||
|
|
||||||
module.exports = async (options_) => {
|
module.exports = async (win_, options_) => {
|
||||||
options = options_;
|
options = options_;
|
||||||
|
win = win_;
|
||||||
yt = await Innertube.create({ cache: new UniversalCache(false), generate_session_locally: true });
|
yt = await Innertube.create({ cache: new UniversalCache(false), generate_session_locally: true });
|
||||||
ipcMain.handle("download-song", (_, url) => downloadSong(url));
|
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);
|
const metadata = await getMetadata(url);
|
||||||
metadata.trackId = trackId;
|
metadata.trackId = trackId;
|
||||||
|
|
||||||
const stream = await yt.download(metadata.id, {
|
const download_options = {
|
||||||
type: 'audio', // audio, video or video+audio
|
type: 'audio', // audio, video or video+audio
|
||||||
quality: 'best', // best, bestefficiency, 144p, 240p, 480p, 720p and so on.
|
quality: 'best', // best, bestefficiency, 144p, 240p, 480p, 720p and so on.
|
||||||
format: 'any' // media container format
|
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}}...`);
|
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]) {
|
if (!presets[options.preset]) {
|
||||||
const fileBuffer = await toMP3(iterableStream, metadata);
|
const fileBuffer = await toMP3(iterableStream, metadata, format.content_length, sendFeedback, increasePlaylistProgress);
|
||||||
console.info('writing id3 tags...'); // DELETE
|
writeFileSync(filePath, await writeID3(fileBuffer, metadata, sendFeedback));
|
||||||
writeFileSync(filePath, await writeID3(fileBuffer, metadata));
|
|
||||||
console.info('done writing id3 tags!'); // DELETE
|
|
||||||
} else {
|
} else {
|
||||||
const file = createWriteStream(filePath);
|
const file = createWriteStream(filePath);
|
||||||
//stream.pipeTo(file);
|
let downloaded = 0;
|
||||||
|
let total = format.content_length;
|
||||||
|
|
||||||
for await (const chunk of iterableStream) {
|
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);
|
file.write(chunk);
|
||||||
}
|
}
|
||||||
await ffmpegWriteTags(filePath, metadata, presets[options.preset]?.ffmpegArgs);
|
await ffmpegWriteTags(filePath, metadata, presets[options.preset]?.ffmpegArgs);
|
||||||
|
sendFeedback(null, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendFeedback(null, -1);
|
||||||
console.info(`${filePath} - Done!`, '\n');
|
console.info(`${filePath} - Done!`, '\n');
|
||||||
}
|
}
|
||||||
module.exports.downloadSong = downloadSong;
|
module.exports.downloadSong = downloadSong;
|
||||||
|
|
||||||
function getIdFromUrl(url) {
|
|
||||||
const match = url.match(/v=([^&]+)/);
|
|
||||||
return match ? match[1] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMetadata(url) {
|
async function getMetadata(url) {
|
||||||
const id = getIdFromUrl(url);
|
const id = url.match(/v=([^&]+)/)?.[1];
|
||||||
const info = await yt.music.getInfo(id);
|
const info = await yt.music.getInfo(id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -86,11 +117,14 @@ async function getMetadata(url) {
|
|||||||
artist: info.basic_info.author,
|
artist: info.basic_info.author,
|
||||||
album: info.player_overlays?.browser_media_session?.album?.text,
|
album: info.player_overlays?.browser_media_session?.album?.text,
|
||||||
image: info.basic_info.thumbnail[0].url,
|
image: info.basic_info.thumbnail[0].url,
|
||||||
|
info
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeID3(buffer, metadata) {
|
async function writeID3(buffer, metadata, sendFeedback) {
|
||||||
try {
|
try {
|
||||||
|
sendFeedback("Writing ID3 tags...");
|
||||||
|
|
||||||
const nativeImage = cropMaxWidth(await getImage(metadata.image));
|
const nativeImage = cropMaxWidth(await getImage(metadata.image));
|
||||||
const coverBuffer = nativeImage && !nativeImage.isEmpty() ?
|
const coverBuffer = nativeImage && !nativeImage.isEmpty() ?
|
||||||
nativeImage.toPNG() : null;
|
nativeImage.toPNG() : null;
|
||||||
@ -130,36 +164,33 @@ async function writeID3(buffer, metadata) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toMP3(stream, metadata, content_length, sendFeedback, increasePlaylistProgress = () => { }, extension = "mp3") {
|
||||||
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") {
|
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
|
let downloaded = 0;
|
||||||
|
let total = content_length;
|
||||||
for await (const chunk of stream) {
|
for await (const chunk of stream) {
|
||||||
|
downloaded += chunk.length;
|
||||||
chunks.push(chunk);
|
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 buffer = Buffer.concat(chunks);
|
||||||
const safeVideoName = randomBytes(32).toString("hex");
|
const safeVideoName = randomBytes(32).toString("hex");
|
||||||
const releaseFFmpegMutex = await ffmpegMutex.acquire();
|
const releaseFFmpegMutex = await ffmpegMutex.acquire();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!ffmpeg.isLoaded()) {
|
if (!ffmpeg.isLoaded()) {
|
||||||
// sendFeedback("Loading…", 2); // indefinite progress bar after download
|
|
||||||
await ffmpeg.load();
|
await ffmpeg.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendFeedback("Preparing file…");
|
sendFeedback("Preparing file…");
|
||||||
ffmpeg.FS("writeFile", safeVideoName, buffer);
|
ffmpeg.FS("writeFile", safeVideoName, buffer);
|
||||||
|
|
||||||
// sendFeedback("Converting…");
|
sendFeedback("Converting…");
|
||||||
|
|
||||||
await ffmpeg.run(
|
await ffmpeg.run(
|
||||||
"-i",
|
"-i",
|
||||||
@ -168,9 +199,9 @@ async function toMP3(stream, metadata, extension = "mp3") {
|
|||||||
safeVideoName + "." + extension
|
safeVideoName + "." + extension
|
||||||
);
|
);
|
||||||
|
|
||||||
// sendFeedback("Saving…");
|
sendFeedback("Saving…");
|
||||||
|
|
||||||
return ffmpeg.FS("readFile", safeVideoName + "." + extension);
|
return ffmpeg.FS("readFile", safeVideoName + "." + extension);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
sendError(e);
|
sendError(e);
|
||||||
} finally {
|
} finally {
|
||||||
@ -212,3 +243,104 @@ function getFFmpegMetadataArgs(metadata) {
|
|||||||
...(metadata.trackId ? ["-metadata", `track=${metadata.trackId}`] : []),
|
...(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -4,12 +4,16 @@ const { dialog } = require("electron");
|
|||||||
|
|
||||||
const registerCallback = require("../../providers/song-info");
|
const registerCallback = require("../../providers/song-info");
|
||||||
const { injectCSS, listenAction } = require("../utils");
|
const { injectCSS, listenAction } = require("../utils");
|
||||||
|
const { setBadge, sendFeedback } = require("./utils");
|
||||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||||
|
|
||||||
let win = {};
|
let win = {};
|
||||||
|
|
||||||
const sendError = (error) => {
|
const sendError = (error) => {
|
||||||
win.setProgressBar(-1); // close progress bar
|
win.setProgressBar(-1); // close progress bar
|
||||||
|
setBadge(0); // close badge
|
||||||
|
sendFeedback(); // reset feedback
|
||||||
|
|
||||||
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
dialog.showMessageBox({
|
dialog.showMessageBox({
|
||||||
@ -28,7 +32,7 @@ function handle(win_, options) {
|
|||||||
win = win_;
|
win = win_;
|
||||||
injectCSS(win.webContents, join(__dirname, "style.css"));
|
injectCSS(win.webContents, join(__dirname, "style.css"));
|
||||||
|
|
||||||
require("./back-downloader")(options);
|
require("./back-downloader")(win, options);
|
||||||
|
|
||||||
registerCallback((info) => {
|
registerCallback((info) => {
|
||||||
nowPlayingMetadata = info;
|
nowPlayingMetadata = info;
|
||||||
|
|||||||
@ -40,7 +40,7 @@ const baseUrl = defaultConfig.url;
|
|||||||
// contextBridge.exposeInMainWorld("downloader", {
|
// contextBridge.exposeInMainWorld("downloader", {
|
||||||
// download: () => {
|
// download: () => {
|
||||||
global.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 metadata;
|
||||||
let videoUrl = getSongMenu()
|
let videoUrl = getSongMenu()
|
||||||
// selector of first button which is always "Start Radio"
|
// selector of first button which is always "Start Radio"
|
||||||
@ -60,7 +60,7 @@ global.download = () => {
|
|||||||
videoUrl = metadata.url || window.location.href;
|
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;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -73,6 +73,14 @@ function observeMenu(options) {
|
|||||||
subtree: true,
|
subtree: true,
|
||||||
});
|
});
|
||||||
}, { once: true, passive: 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;
|
module.exports = observeMenu;
|
||||||
|
|||||||
@ -1,41 +1,12 @@
|
|||||||
const { existsSync, mkdirSync } = require("fs");
|
const { dialog } = require("electron");
|
||||||
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 { setMenuOptions } = require("../../config/plugins");
|
const { setMenuOptions } = require("../../config/plugins");
|
||||||
const { sendError } = require("./back");
|
const { downloadPlaylist } = require("./back-downloader");
|
||||||
const { downloadSong } = require("./back-downloader");
|
const { defaultMenuDownloadLabel, getFolder, presets } = require("./utils");
|
||||||
const { defaultMenuDownloadLabel, getFolder, presets, setBadge } = require("./utils");
|
|
||||||
|
|
||||||
let downloadLabel = defaultMenuDownloadLabel;
|
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) => {
|
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 [
|
return [
|
||||||
{
|
{
|
||||||
label: downloadLabel,
|
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -4,6 +4,10 @@ const is = require('electron-is');
|
|||||||
module.exports.getFolder = customFolder => customFolder || app.getPath("downloads");
|
module.exports.getFolder = customFolder => customFolder || app.getPath("downloads");
|
||||||
module.exports.defaultMenuDownloadLabel = "Download playlist";
|
module.exports.defaultMenuDownloadLabel = "Download playlist";
|
||||||
|
|
||||||
|
module.exports.sendFeedback = (win, message) => {
|
||||||
|
win.webContents.send("downloader-feedback", message);
|
||||||
|
};
|
||||||
|
|
||||||
const orderedQualityList = ["maxresdefault", "hqdefault", "mqdefault", "sdddefault"];
|
const orderedQualityList = ["maxresdefault", "hqdefault", "mqdefault", "sdddefault"];
|
||||||
module.exports.urlToJPG = (imgUrl, videoId) => {
|
module.exports.urlToJPG = (imgUrl, videoId) => {
|
||||||
if (!imgUrl || imgUrl.includes(".jpg")) return imgUrl;
|
if (!imgUrl || imgUrl.includes(".jpg")) return imgUrl;
|
||||||
|
|||||||
89
yarn.lock
89
yarn.lock
@ -1365,16 +1365,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"app-builder-bin@npm:4.0.0":
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
resolution: "app-builder-bin@npm:4.0.0"
|
resolution: "app-builder-bin@npm:4.0.0"
|
||||||
@ -1682,13 +1672,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"bindings@npm:^1.2.1":
|
||||||
version: 1.5.0
|
version: 1.5.0
|
||||||
resolution: "bindings@npm:1.5.0"
|
resolution: "bindings@npm:1.5.0"
|
||||||
@ -1754,7 +1737,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"braces@npm:^3.0.2, braces@npm:~3.0.2":
|
"braces@npm:^3.0.2":
|
||||||
version: 3.0.2
|
version: 3.0.2
|
||||||
resolution: "braces@npm:3.0.2"
|
resolution: "braces@npm:3.0.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -2065,25 +2048,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"chownr@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "chownr@npm:2.0.0"
|
resolution: "chownr@npm:2.0.0"
|
||||||
@ -4102,25 +4066,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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<compat/fsevents>":
|
|
||||||
version: 2.3.2
|
|
||||||
resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin<compat/fsevents>::version=2.3.2&hash=df0bf1"
|
|
||||||
dependencies:
|
|
||||||
node-gyp: latest
|
|
||||||
conditions: os=darwin
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"function-bind@npm:^1.1.1":
|
"function-bind@npm:^1.1.1":
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
resolution: "function-bind@npm:1.1.1"
|
resolution: "function-bind@npm:1.1.1"
|
||||||
@ -4247,7 +4192,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2":
|
"glob-parent@npm:^5.1.2":
|
||||||
version: 5.1.2
|
version: 5.1.2
|
||||||
resolution: "glob-parent@npm:5.1.2"
|
resolution: "glob-parent@npm:5.1.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -4896,15 +4841,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"is-boolean-object@npm:^1.1.0":
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
resolution: "is-boolean-object@npm:1.1.2"
|
resolution: "is-boolean-object@npm:1.1.2"
|
||||||
@ -5016,7 +4952,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 4.0.3
|
||||||
resolution: "is-glob@npm:4.0.3"
|
resolution: "is-glob@npm:4.0.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -6363,13 +6299,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"normalize-url@npm:^6.0.1":
|
||||||
version: 6.1.0
|
version: 6.1.0
|
||||||
resolution: "normalize-url@npm:6.1.0"
|
resolution: "normalize-url@npm:6.1.0"
|
||||||
@ -6860,7 +6789,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 2.3.1
|
||||||
resolution: "picomatch@npm:2.3.1"
|
resolution: "picomatch@npm:2.3.1"
|
||||||
checksum: 050c865ce81119c4822c45d3c84f1ced46f93a0126febae20737bd05ca20589c564d6e9226977df859ed5e03dc73f02584a2b0faad36e896936238238b0446cf
|
checksum: 050c865ce81119c4822c45d3c84f1ced46f93a0126febae20737bd05ca20589c564d6e9226977df859ed5e03dc73f02584a2b0faad36e896936238238b0446cf
|
||||||
@ -7269,15 +7198,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"redent@npm:^4.0.0":
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
resolution: "redent@npm:4.0.0"
|
resolution: "redent@npm:4.0.0"
|
||||||
@ -8991,7 +8911,6 @@ __metadata:
|
|||||||
browser-id3-writer: ^4.4.0
|
browser-id3-writer: ^4.4.0
|
||||||
butterchurn: ^2.6.7
|
butterchurn: ^2.6.7
|
||||||
butterchurn-presets: ^2.4.7
|
butterchurn-presets: ^2.4.7
|
||||||
chokidar: ^3.5.3
|
|
||||||
custom-electron-prompt: ^1.5.1
|
custom-electron-prompt: ^1.5.1
|
||||||
custom-electron-titlebar: ^4.1.5
|
custom-electron-titlebar: ^4.1.5
|
||||||
del-cli: ^5.0.0
|
del-cli: ^5.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user