mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-14 03:41:46 +00:00
Merge pull request #203 from th-ch/downloader-plugin-playlist
Add playlist feature in downloader plugin + custom menus in plugin system
This commit is contained in:
@ -45,19 +45,21 @@ function handle(win) {
|
||||
let fileBuffer = songBuffer;
|
||||
|
||||
try {
|
||||
const coverBuffer = metadata.image.toPNG();
|
||||
const writer = new ID3Writer(songBuffer);
|
||||
if (metadata.image) {
|
||||
const coverBuffer = metadata.image.toPNG();
|
||||
|
||||
// Create the metadata tags
|
||||
writer
|
||||
.setFrame("TIT2", metadata.title)
|
||||
.setFrame("TPE1", [metadata.artist])
|
||||
.setFrame("APIC", {
|
||||
type: 3,
|
||||
data: coverBuffer,
|
||||
description: "",
|
||||
});
|
||||
writer.addTag();
|
||||
// Create the metadata tags
|
||||
writer
|
||||
.setFrame("TIT2", metadata.title)
|
||||
.setFrame("TPE1", [metadata.artist])
|
||||
.setFrame("APIC", {
|
||||
type: 3,
|
||||
data: coverBuffer,
|
||||
description: "",
|
||||
});
|
||||
writer.addTag();
|
||||
}
|
||||
fileBuffer = Buffer.from(writer.arrayBuffer);
|
||||
} catch (error) {
|
||||
sendError(win, error);
|
||||
@ -70,3 +72,4 @@ function handle(win) {
|
||||
}
|
||||
|
||||
module.exports = handle;
|
||||
module.exports.sendError = sendError;
|
||||
|
||||
63
plugins/downloader/menu.js
Normal file
63
plugins/downloader/menu.js
Normal file
@ -0,0 +1,63 @@
|
||||
const { existsSync, mkdirSync } = require("fs");
|
||||
const { join } = require("path");
|
||||
const { URL } = require("url");
|
||||
|
||||
const { ipcMain } = require("electron");
|
||||
const is = require("electron-is");
|
||||
const ytpl = require("ytpl");
|
||||
|
||||
const { sendError } = require("./back");
|
||||
const { defaultMenuDownloadLabel, getFolder } = require("./utils");
|
||||
|
||||
let downloadLabel = defaultMenuDownloadLabel;
|
||||
|
||||
module.exports = (win, options, refreshMenu) => [
|
||||
{
|
||||
label: downloadLabel,
|
||||
click: async () => {
|
||||
const currentURL = win.webContents.getURL();
|
||||
const playlistID = new URL(currentURL).searchParams.get("list");
|
||||
if (!playlistID) {
|
||||
sendError(win, new Error("No playlist ID found"));
|
||||
return;
|
||||
}
|
||||
|
||||
const playlist = await ytpl(playlistID);
|
||||
const playlistTitle = playlist.title;
|
||||
|
||||
const folder = getFolder(options.downloadFolder);
|
||||
const playlistFolder = join(folder, playlistTitle);
|
||||
if (existsSync(playlistFolder)) {
|
||||
sendError(
|
||||
win,
|
||||
new Error(`The folder ${playlistFolder} already exists`)
|
||||
);
|
||||
return;
|
||||
}
|
||||
mkdirSync(playlistFolder, { recursive: true });
|
||||
|
||||
ipcMain.on("downloader-feedback", (_, feedback) => {
|
||||
downloadLabel = feedback;
|
||||
refreshMenu();
|
||||
});
|
||||
|
||||
downloadLabel = `Downloading "${playlistTitle}"`;
|
||||
refreshMenu();
|
||||
|
||||
if (is.dev()) {
|
||||
console.log(
|
||||
`Downloading playlist "${playlistTitle}" (${playlist.items.length} songs)`
|
||||
);
|
||||
}
|
||||
|
||||
playlist.items.slice(0, options.playlistMaxItems).forEach((song) => {
|
||||
win.webContents.send(
|
||||
"downloader-download-playlist",
|
||||
song,
|
||||
playlistTitle,
|
||||
options
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
4
plugins/downloader/utils.js
Normal file
4
plugins/downloader/utils.js
Normal file
@ -0,0 +1,4 @@
|
||||
const downloadsFolder = require("downloads-folder");
|
||||
|
||||
module.exports.getFolder = (customFolder) => customFolder || downloadsFolder();
|
||||
module.exports.defaultMenuDownloadLabel = "Download playlist";
|
||||
@ -1,7 +1,8 @@
|
||||
const { randomBytes } = require("crypto");
|
||||
const { writeFileSync } = require("fs");
|
||||
const { join } = require("path");
|
||||
|
||||
const downloadsFolder = require("downloads-folder");
|
||||
const Mutex = require("async-mutex").Mutex;
|
||||
const { ipcRenderer } = require("electron");
|
||||
const is = require("electron-is");
|
||||
const filenamify = require("filenamify");
|
||||
@ -12,8 +13,9 @@ const filenamify = require("filenamify");
|
||||
const FFmpeg = require("@ffmpeg/ffmpeg/dist/ffmpeg.min");
|
||||
const ytdl = require("ytdl-core");
|
||||
|
||||
const { triggerActionSync } = require("../utils");
|
||||
const { triggerAction, triggerActionSync } = require("../utils");
|
||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||
const { defaultMenuDownloadLabel, getFolder } = require("./utils");
|
||||
|
||||
const { createFFmpeg } = FFmpeg;
|
||||
const ffmpeg = createFFmpeg({
|
||||
@ -21,13 +23,16 @@ const ffmpeg = createFFmpeg({
|
||||
logger: () => {}, // console.log,
|
||||
progress: () => {}, // console.log,
|
||||
});
|
||||
const ffmpegMutex = new Mutex();
|
||||
|
||||
const downloadVideoToMP3 = (
|
||||
videoUrl,
|
||||
sendFeedback,
|
||||
sendError,
|
||||
reinit,
|
||||
options
|
||||
options,
|
||||
metadata = undefined,
|
||||
subfolder = ""
|
||||
) => {
|
||||
sendFeedback("Downloading…");
|
||||
|
||||
@ -66,9 +71,18 @@ const downloadVideoToMP3 = (
|
||||
}
|
||||
})
|
||||
.on("error", sendError)
|
||||
.on("end", () => {
|
||||
.on("end", async () => {
|
||||
const buffer = Buffer.concat(chunks);
|
||||
toMP3(videoName, buffer, sendFeedback, sendError, reinit, options);
|
||||
await toMP3(
|
||||
videoName,
|
||||
buffer,
|
||||
sendFeedback,
|
||||
sendError,
|
||||
reinit,
|
||||
options,
|
||||
metadata,
|
||||
subfolder
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@ -78,10 +92,13 @@ const toMP3 = async (
|
||||
sendFeedback,
|
||||
sendError,
|
||||
reinit,
|
||||
options
|
||||
options,
|
||||
existingMetadata = undefined,
|
||||
subfolder = ""
|
||||
) => {
|
||||
const safeVideoName = randomBytes(32).toString("hex");
|
||||
const extension = options.extension || "mp3";
|
||||
const releaseFFmpegMutex = await ffmpegMutex.acquire();
|
||||
|
||||
try {
|
||||
if (!ffmpeg.isLoaded()) {
|
||||
@ -93,7 +110,7 @@ const toMP3 = async (
|
||||
ffmpeg.FS("writeFile", safeVideoName, buffer);
|
||||
|
||||
sendFeedback("Converting…");
|
||||
const metadata = getMetadata();
|
||||
const metadata = existingMetadata || getMetadata();
|
||||
await ffmpeg.run(
|
||||
"-i",
|
||||
safeVideoName,
|
||||
@ -102,24 +119,31 @@ const toMP3 = async (
|
||||
safeVideoName + "." + extension
|
||||
);
|
||||
|
||||
const folder = options.downloadFolder || downloadsFolder();
|
||||
const folder = getFolder(options.downloadFolder);
|
||||
const name = metadata
|
||||
? `${metadata.artist} - ${metadata.title}`
|
||||
? `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`
|
||||
: videoName;
|
||||
const filename = filenamify(name + "." + extension, {
|
||||
replacement: "_",
|
||||
});
|
||||
|
||||
// Add the metadata
|
||||
sendFeedback("Adding metadata…");
|
||||
ipcRenderer.send(
|
||||
"add-metadata",
|
||||
join(folder, filename),
|
||||
ffmpeg.FS("readFile", safeVideoName + "." + extension)
|
||||
);
|
||||
ipcRenderer.once("add-metadata-done", reinit);
|
||||
const filePath = join(folder, subfolder, filename);
|
||||
const fileBuffer = ffmpeg.FS("readFile", safeVideoName + "." + extension);
|
||||
|
||||
if (existingMetadata) {
|
||||
writeFileSync(filePath, fileBuffer);
|
||||
reinit();
|
||||
} else {
|
||||
// Add the metadata
|
||||
sendFeedback("Adding metadata…");
|
||||
ipcRenderer.send("add-metadata", filePath, fileBuffer);
|
||||
ipcRenderer.once("add-metadata-done", reinit);
|
||||
sendFeedback("Finished converting", metadata);
|
||||
}
|
||||
} catch (e) {
|
||||
sendError(e);
|
||||
} finally {
|
||||
releaseFFmpegMutex();
|
||||
}
|
||||
};
|
||||
|
||||
@ -133,13 +157,34 @@ const getFFmpegMetadataArgs = (metadata) => {
|
||||
}
|
||||
|
||||
return [
|
||||
"-metadata",
|
||||
`title=${metadata.title}`,
|
||||
"-metadata",
|
||||
`artist=${metadata.artist}`,
|
||||
...(metadata.title ? ["-metadata", `title=${metadata.title}`] : []),
|
||||
...(metadata.artist ? ["-metadata", `artist=${metadata.artist}`] : []),
|
||||
];
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
downloadVideoToMP3,
|
||||
};
|
||||
|
||||
ipcRenderer.on(
|
||||
"downloader-download-playlist",
|
||||
(_, songMetadata, playlistFolder, options) => {
|
||||
const reinit = () =>
|
||||
ipcRenderer.send("downloader-feedback", defaultMenuDownloadLabel);
|
||||
|
||||
downloadVideoToMP3(
|
||||
songMetadata.url,
|
||||
(feedback) => {
|
||||
ipcRenderer.send("downloader-feedback", feedback);
|
||||
},
|
||||
(error) => {
|
||||
triggerAction(CHANNEL, ACTIONS.ERROR, error);
|
||||
reinit();
|
||||
},
|
||||
reinit,
|
||||
options,
|
||||
songMetadata,
|
||||
playlistFolder
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user