mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-16 20:52:06 +00:00
lint
This commit is contained in:
@ -23,9 +23,10 @@ const ffmpeg = require("@ffmpeg/ffmpeg").createFFmpeg({
|
|||||||
});
|
});
|
||||||
const ffmpegMutex = new Mutex();
|
const ffmpegMutex = new Mutex();
|
||||||
|
|
||||||
|
const config = require("./config");
|
||||||
|
|
||||||
/** @type {Innertube} */
|
/** @type {Innertube} */
|
||||||
let yt;
|
let yt;
|
||||||
const config = require("./config");
|
|
||||||
let win;
|
let win;
|
||||||
let playingUrl = undefined;
|
let playingUrl = undefined;
|
||||||
|
|
||||||
@ -58,6 +59,8 @@ module.exports = async (win_, options) => {
|
|||||||
ipcMain.on("download-playlist-request", async (_event, url) => downloadPlaylist(url));
|
ipcMain.on("download-playlist-request", async (_event, url) => downloadPlaylist(url));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports.downloadSong = downloadSong;
|
||||||
|
|
||||||
async function downloadSong(url, playlistFolder = undefined, trackId = undefined, increasePlaylistProgress = () => { }) {
|
async function downloadSong(url, playlistFolder = undefined, trackId = undefined, increasePlaylistProgress = () => { }) {
|
||||||
const sendFeedback = (message, progress) => {
|
const sendFeedback = (message, progress) => {
|
||||||
if (!playlistFolder) {
|
if (!playlistFolder) {
|
||||||
@ -70,7 +73,7 @@ async function downloadSong(url, playlistFolder = undefined, trackId = undefined
|
|||||||
|
|
||||||
sendFeedback(`Downloading...`, 2);
|
sendFeedback(`Downloading...`, 2);
|
||||||
|
|
||||||
const id = url.match(/v=([^&]+)/)?.[1];
|
const id = getVideoId(url);
|
||||||
const info = await yt.music.getInfo(id);
|
const info = await yt.music.getInfo(id);
|
||||||
|
|
||||||
const metadata = getMetadata(info);
|
const metadata = getMetadata(info);
|
||||||
@ -111,7 +114,7 @@ async function downloadSong(url, playlistFolder = undefined, trackId = undefined
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!presets[config.get('preset')]) {
|
if (!presets[config.get('preset')]) {
|
||||||
const fileBuffer = await bufferToMP3(iterableStream, metadata, format.content_length, sendFeedback, increasePlaylistProgress);
|
const fileBuffer = await iterableStreamToMP3(iterableStream, metadata, format.content_length, sendFeedback, increasePlaylistProgress);
|
||||||
writeFileSync(filePath, await writeID3(fileBuffer, metadata, sendFeedback));
|
writeFileSync(filePath, await writeID3(fileBuffer, metadata, sendFeedback));
|
||||||
} else {
|
} else {
|
||||||
const file = createWriteStream(filePath);
|
const file = createWriteStream(filePath);
|
||||||
@ -133,15 +136,58 @@ async function downloadSong(url, playlistFolder = undefined, trackId = undefined
|
|||||||
sendFeedback(null, -1);
|
sendFeedback(null, -1);
|
||||||
console.info(`Done: "${filePath}"`);
|
console.info(`Done: "${filePath}"`);
|
||||||
}
|
}
|
||||||
module.exports.downloadSong = downloadSong;
|
|
||||||
|
|
||||||
const getMetadata = (info) => ({
|
async function iterableStreamToMP3(stream, metadata, content_length, sendFeedback, increasePlaylistProgress = () => { }) {
|
||||||
id: info.basic_info.id,
|
const chunks = [];
|
||||||
title: info.basic_info.title,
|
let downloaded = 0;
|
||||||
artist: info.basic_info.author,
|
let total = content_length;
|
||||||
album: info.player_overlays?.browser_media_session?.album?.text,
|
for await (const chunk of stream) {
|
||||||
image: info.basic_info.thumbnail[0].url,
|
downloaded += chunk.length;
|
||||||
});
|
chunks.push(chunk);
|
||||||
|
const ratio = downloaded / total;
|
||||||
|
const progress = Math.floor(ratio * 100);
|
||||||
|
sendFeedback("Download: " + progress + "%", ratio);
|
||||||
|
// 15% for download, 85% for conversion
|
||||||
|
// This is a very rough estimate, trying to make the progress bar look nice
|
||||||
|
increasePlaylistProgress(ratio * 0.15);
|
||||||
|
}
|
||||||
|
sendFeedback("Loading…", 2); // indefinite progress bar after download
|
||||||
|
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
const safeVideoName = randomBytes(32).toString("hex");
|
||||||
|
const releaseFFmpegMutex = await ffmpegMutex.acquire();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!ffmpeg.isLoaded()) {
|
||||||
|
await ffmpeg.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
sendFeedback("Preparing file…");
|
||||||
|
ffmpeg.FS("writeFile", safeVideoName, buffer);
|
||||||
|
|
||||||
|
sendFeedback("Converting…");
|
||||||
|
|
||||||
|
ffmpeg.setProgress(({ ratio }) => {
|
||||||
|
sendFeedback("Converting: " + Math.floor(ratio * 100) + "%", ratio);
|
||||||
|
increasePlaylistProgress(0.15 + ratio * 0.85);
|
||||||
|
});
|
||||||
|
|
||||||
|
await ffmpeg.run(
|
||||||
|
"-i",
|
||||||
|
safeVideoName,
|
||||||
|
...getFFmpegMetadataArgs(metadata),
|
||||||
|
safeVideoName + ".mp3"
|
||||||
|
);
|
||||||
|
|
||||||
|
sendFeedback("Saving…");
|
||||||
|
|
||||||
|
return ffmpeg.FS("readFile", safeVideoName + ".mp3");
|
||||||
|
} catch (e) {
|
||||||
|
sendError(e);
|
||||||
|
} finally {
|
||||||
|
releaseFFmpegMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function writeID3(buffer, metadata, sendFeedback) {
|
async function writeID3(buffer, metadata, sendFeedback) {
|
||||||
try {
|
try {
|
||||||
@ -186,104 +232,6 @@ async function writeID3(buffer, metadata, sendFeedback) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bufferToMP3(stream, metadata, content_length, sendFeedback, increasePlaylistProgress = () => { }, extension = "mp3") {
|
|
||||||
const chunks = [];
|
|
||||||
let downloaded = 0;
|
|
||||||
let total = content_length;
|
|
||||||
for await (const chunk of stream) {
|
|
||||||
downloaded += chunk.length;
|
|
||||||
chunks.push(chunk);
|
|
||||||
const ratio = downloaded / total;
|
|
||||||
const progress = Math.floor(ratio * 100);
|
|
||||||
sendFeedback("Download: " + progress + "%", ratio);
|
|
||||||
// 15% for download, 85% for conversion
|
|
||||||
// This is a very rough estimate, trying to make the progress bar look nice
|
|
||||||
increasePlaylistProgress(ratio * 0.15);
|
|
||||||
}
|
|
||||||
sendFeedback("Loading…", 2); // indefinite progress bar after download
|
|
||||||
|
|
||||||
const buffer = Buffer.concat(chunks);
|
|
||||||
const safeVideoName = randomBytes(32).toString("hex");
|
|
||||||
const releaseFFmpegMutex = await ffmpegMutex.acquire();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!ffmpeg.isLoaded()) {
|
|
||||||
await ffmpeg.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
sendFeedback("Preparing file…");
|
|
||||||
ffmpeg.FS("writeFile", safeVideoName, buffer);
|
|
||||||
|
|
||||||
sendFeedback("Converting…");
|
|
||||||
|
|
||||||
ffmpeg.setProgress(({ ratio }) => {
|
|
||||||
sendFeedback("Converting: " + Math.floor(ratio * 100) + "%", ratio);
|
|
||||||
increasePlaylistProgress(0.15 + ratio * 0.85);
|
|
||||||
});
|
|
||||||
|
|
||||||
await ffmpeg.run(
|
|
||||||
"-i",
|
|
||||||
safeVideoName,
|
|
||||||
...getFFmpegMetadataArgs(metadata),
|
|
||||||
safeVideoName + "." + extension
|
|
||||||
);
|
|
||||||
|
|
||||||
sendFeedback("Saving…");
|
|
||||||
|
|
||||||
return ffmpeg.FS("readFile", safeVideoName + "." + extension);
|
|
||||||
} 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}`] : []),
|
|
||||||
...(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) {
|
async function downloadPlaylist(givenUrl) {
|
||||||
if (givenUrl) {
|
if (givenUrl) {
|
||||||
try {
|
try {
|
||||||
@ -384,3 +332,64 @@ async function downloadPlaylist(givenUrl) {
|
|||||||
sendFeedback(); // clear feedback
|
sendFeedback(); // clear feedback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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}`] : []),
|
||||||
|
...(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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVideoId = url => {
|
||||||
|
if (typeof url === "string") {
|
||||||
|
url = new URL(url);
|
||||||
|
}
|
||||||
|
return url.searchParams.get("v");
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMetadata = (info) => ({
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|||||||
@ -12,7 +12,7 @@ const downloadButton = ElementFromFile(
|
|||||||
|
|
||||||
let doneFirstLoad = false;
|
let doneFirstLoad = false;
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
const menuObserver = new MutationObserver(() => {
|
||||||
if (!menu) {
|
if (!menu) {
|
||||||
menu = getSongMenu();
|
menu = getSongMenu();
|
||||||
if (!menu) return;
|
if (!menu) return;
|
||||||
@ -28,37 +28,32 @@ const observer = new MutationObserver(() => {
|
|||||||
setTimeout(() => doneFirstLoad ||= true, 500);
|
setTimeout(() => doneFirstLoad ||= true, 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
const baseUrl = defaultConfig.url;
|
|
||||||
|
|
||||||
// TODO: re-enable once contextIsolation is set to true
|
// TODO: re-enable once contextIsolation is set to true
|
||||||
// contextBridge.exposeInMainWorld("downloader", {
|
// contextBridge.exposeInMainWorld("downloader", {
|
||||||
// download: () => {
|
// download: () => {
|
||||||
global.download = () => {
|
global.download = () => {
|
||||||
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"
|
||||||
?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint')
|
?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint')
|
||||||
?.getAttribute("href");
|
?.getAttribute("href");
|
||||||
if (videoUrl) {
|
if (videoUrl) {
|
||||||
if (videoUrl.startsWith('watch?')) {
|
if (videoUrl.startsWith('watch?')) {
|
||||||
videoUrl = baseUrl + "/" + videoUrl;
|
videoUrl = defaultConfig.url + "/" + videoUrl;
|
||||||
}
|
}
|
||||||
if (videoUrl.includes('?playlist=')) {
|
if (videoUrl.includes('?playlist=')) {
|
||||||
ipcRenderer.send('download-playlist-request', videoUrl);
|
ipcRenderer.send('download-playlist-request', videoUrl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
metadata = null;
|
|
||||||
} else {
|
} else {
|
||||||
metadata = global.songInfo;
|
videoUrl = global.songInfo.url || window.location.href;
|
||||||
videoUrl = metadata.url || window.location.href;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcRenderer.send('download-song', videoUrl);
|
ipcRenderer.send('download-song', videoUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
function observeMenu() {
|
module.exports = () => {
|
||||||
document.addEventListener('apiLoaded', () => {
|
document.addEventListener('apiLoaded', () => {
|
||||||
observer.observe(document.querySelector('ytmusic-popup-container'), {
|
menuObserver.observe(document.querySelector('ytmusic-popup-container'), {
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: true,
|
subtree: true,
|
||||||
});
|
});
|
||||||
@ -71,6 +66,4 @@ function observeMenu() {
|
|||||||
progress.innerHTML = feedback || "Download";
|
progress.innerHTML = feedback || "Download";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
module.exports = observeMenu;
|
|
||||||
|
|||||||
Reference in New Issue
Block a user