mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 18:41:47 +00:00
merge source (#2)
* Added Discord timeout
* Add getOptions in plugin util
* Mutex in ffmpeg conversion (only supports one command at a time)
* Add menu customization in plugin system
* Add ytpl package (playlist info)
* Handle ffmpeg metadata flags when metadata is not present
* Only use artist in file name if present
* Export sendError method
* Handle image not present in metadata util
* Add downloader utils (getFolder and default menu label)
* Pass (optional) existing metadata and subfolder in mp3 converter
* Add listener to download playlist
* Add custom menu in downloader plugin ("download playlist" item)
* nit: fix main CSS style
* Only set the "enable" item in menu if plugin not enabled
* Navigation plugin: inject HTML once CSS is loaded
Co-authored-by: Sem Visscher <semvisscher10@gmail.com>
Co-authored-by: TC <th-ch@users.noreply.github.com>
This commit is contained in:
@ -9,7 +9,9 @@ const rpc = new Discord.Client({
|
||||
// Application ID registered by @semvis123
|
||||
const clientId = "790655993809338398";
|
||||
|
||||
module.exports = (win) => {
|
||||
let clearActivity;
|
||||
|
||||
module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => {
|
||||
const registerCallback = getSongInfo(win);
|
||||
|
||||
// If the page is ready, register the callback
|
||||
@ -29,7 +31,13 @@ module.exports = (win) => {
|
||||
// Add an idle icon to show that the song is paused
|
||||
activityInfo.smallImageKey = "idle";
|
||||
activityInfo.smallImageText = "idle/paused";
|
||||
// Set start the timer so the activity gets cleared after a while if enabled
|
||||
if (activityTimoutEnabled)
|
||||
clearActivity = setTimeout(()=>rpc.clearActivity(), activityTimoutTime||10,000);
|
||||
|
||||
} else {
|
||||
// stop the clear activity timout
|
||||
clearTimeout(clearActivity);
|
||||
// Add the start and end time of the song
|
||||
const songStartTime = Date.now() - songInfo.elapsedSeconds * 1000;
|
||||
activityInfo.startTimestamp = songStartTime;
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@ -4,7 +4,10 @@ const { injectCSS, listenAction } = require("../utils");
|
||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||
|
||||
function handle(win) {
|
||||
injectCSS(win.webContents, path.join(__dirname, "style.css"));
|
||||
injectCSS(win.webContents, path.join(__dirname, "style.css"), () => {
|
||||
win.webContents.send("navigation-css-ready");
|
||||
});
|
||||
|
||||
listenAction(CHANNEL, (event, action) => {
|
||||
switch (action) {
|
||||
case ACTIONS.NEXT:
|
||||
|
||||
@ -1,15 +1,19 @@
|
||||
const { ipcRenderer } = require("electron");
|
||||
|
||||
const { ElementFromFile, templatePath } = require("../utils");
|
||||
|
||||
function run() {
|
||||
const forwardButton = ElementFromFile(
|
||||
templatePath(__dirname, "forward.html")
|
||||
);
|
||||
const backButton = ElementFromFile(templatePath(__dirname, "back.html"));
|
||||
const menu = document.querySelector("ytmusic-pivot-bar-renderer");
|
||||
ipcRenderer.on("navigation-css-ready", () => {
|
||||
const forwardButton = ElementFromFile(
|
||||
templatePath(__dirname, "forward.html")
|
||||
);
|
||||
const backButton = ElementFromFile(templatePath(__dirname, "back.html"));
|
||||
const menu = document.querySelector("ytmusic-pivot-bar-renderer");
|
||||
|
||||
if (menu) {
|
||||
menu.prepend(backButton, forwardButton);
|
||||
}
|
||||
if (menu) {
|
||||
menu.prepend(backButton, forwardButton);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = run;
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
focusable="false"
|
||||
class="style-scope iron-icon"
|
||||
style="pointer-events: none; display: block; width: 100%; height: 100%;"
|
||||
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
||||
>
|
||||
<g class="style-scope iron-icon">
|
||||
<path
|
||||
|
||||
@ -42,9 +42,12 @@ module.exports.fileExists = (path, callbackIfExists) => {
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.injectCSS = (webContents, filepath) => {
|
||||
webContents.on("did-finish-load", () => {
|
||||
webContents.insertCSS(fs.readFileSync(filepath, "utf8"));
|
||||
module.exports.injectCSS = (webContents, filepath, cb = undefined) => {
|
||||
webContents.on("did-finish-load", async () => {
|
||||
await webContents.insertCSS(fs.readFileSync(filepath, "utf8"));
|
||||
if (cb) {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user