mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-10 10:11:46 +00:00
Merge branch 'local-upstream/master' into add-starting-page-option
This commit is contained in:
139
config/dynamic.js
Normal file
139
config/dynamic.js
Normal file
@ -0,0 +1,139 @@
|
||||
const { ipcRenderer, ipcMain } = require("electron");
|
||||
|
||||
const defaultConfig = require("./defaults");
|
||||
const { getOptions, setOptions, setMenuOptions } = require("./plugins");
|
||||
|
||||
const activePlugins = {};
|
||||
/**
|
||||
* [!IMPORTANT!]
|
||||
* The method is **sync** in the main process and **async** in the renderer process.
|
||||
*/
|
||||
module.exports.getActivePlugins =
|
||||
process.type === "renderer"
|
||||
? async () => ipcRenderer.invoke("get-active-plugins")
|
||||
: () => activePlugins;
|
||||
|
||||
if (process.type === "browser") {
|
||||
ipcMain.handle("get-active-plugins", this.getActivePlugins);
|
||||
}
|
||||
|
||||
/**
|
||||
* [!IMPORTANT!]
|
||||
* The method is **sync** in the main process and **async** in the renderer process.
|
||||
*/
|
||||
module.exports.isActive =
|
||||
process.type === "renderer"
|
||||
? async (plugin) =>
|
||||
plugin in (await ipcRenderer.invoke("get-active-plugins"))
|
||||
: (plugin) => plugin in activePlugins;
|
||||
|
||||
/**
|
||||
* This class is used to create a dynamic synced config for plugins.
|
||||
*
|
||||
* [!IMPORTANT!]
|
||||
* The methods are **sync** in the main process and **async** in the renderer process.
|
||||
*
|
||||
* @param {string} name - The name of the plugin.
|
||||
* @param {boolean} [options.enableFront] - Whether the config should be available in front.js. Default: false.
|
||||
* @param {object} [options.initialOptions] - The initial options for the plugin. Default: loaded from store.
|
||||
*
|
||||
* @example
|
||||
* const { PluginConfig } = require("../../config/dynamic");
|
||||
* const config = new PluginConfig("plugin-name", { enableFront: true });
|
||||
* module.exports = { ...config };
|
||||
*
|
||||
* // or
|
||||
*
|
||||
* module.exports = (win, options) => {
|
||||
* const config = new PluginConfig("plugin-name", {
|
||||
* enableFront: true,
|
||||
* initialOptions: options,
|
||||
* });
|
||||
* setupMyPlugin(win, config);
|
||||
* };
|
||||
*/
|
||||
module.exports.PluginConfig = class PluginConfig {
|
||||
#name;
|
||||
#config;
|
||||
#defaultConfig;
|
||||
#enableFront;
|
||||
|
||||
constructor(name, { enableFront = false, initialOptions = undefined } = {}) {
|
||||
const pluginDefaultConfig = defaultConfig.plugins[name] || {};
|
||||
const pluginConfig = initialOptions || getOptions(name) || {};
|
||||
|
||||
this.#name = name;
|
||||
this.#enableFront = enableFront;
|
||||
this.#defaultConfig = pluginDefaultConfig;
|
||||
this.#config = { ...pluginDefaultConfig, ...pluginConfig };
|
||||
|
||||
if (this.#enableFront) {
|
||||
this.#setupFront();
|
||||
}
|
||||
|
||||
activePlugins[name] = this;
|
||||
}
|
||||
|
||||
get = (option) => {
|
||||
return this.#config[option];
|
||||
};
|
||||
|
||||
set = (option, value) => {
|
||||
this.#config[option] = value;
|
||||
this.#save();
|
||||
};
|
||||
|
||||
toggle = (option) => {
|
||||
this.#config[option] = !this.#config[option];
|
||||
this.#save();
|
||||
};
|
||||
|
||||
getAll = () => {
|
||||
return { ...this.#config };
|
||||
};
|
||||
|
||||
setAll = (options) => {
|
||||
this.#config = { ...this.#config, ...options };
|
||||
this.#save();
|
||||
};
|
||||
|
||||
getDefaultConfig = () => {
|
||||
return this.#defaultConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
* Use this method to set an option and restart the app if `appConfig.restartOnConfigChange === true`
|
||||
*
|
||||
* Used for options that require a restart to take effect.
|
||||
*/
|
||||
setAndMaybeRestart = (option, value) => {
|
||||
this.#config[option] = value;
|
||||
setMenuOptions(this.#name, this.#config);
|
||||
};
|
||||
|
||||
/** Called only from back */
|
||||
#save() {
|
||||
setOptions(this.#name, this.#config);
|
||||
}
|
||||
|
||||
#setupFront() {
|
||||
if (process.type === "renderer") {
|
||||
for (const [fnName, fn] of Object.entries(this)) {
|
||||
if (typeof fn !== "function") return;
|
||||
this[fnName] = async (...args) => {
|
||||
return await ipcRenderer.invoke(
|
||||
`${this.name}-config-${fnName}`,
|
||||
...args,
|
||||
);
|
||||
};
|
||||
}
|
||||
} else if (process.type === "browser") {
|
||||
for (const [fnName, fn] of Object.entries(this)) {
|
||||
if (typeof fn !== "function") return;
|
||||
ipcMain.handle(`${this.name}-config-${fnName}`, (_, ...args) => {
|
||||
return fn(...args);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -19,6 +19,11 @@ const migrations = {
|
||||
...pluginOptions,
|
||||
});
|
||||
}
|
||||
|
||||
if (store.get("options.ForceShowLikeButtons")) {
|
||||
store.delete("options.ForceShowLikeButtons");
|
||||
store.set("options.likeButtons", 'force');
|
||||
}
|
||||
},
|
||||
">=1.17.0": (store) => {
|
||||
setDefaultPluginOptions(store, "picture-in-picture");
|
||||
|
||||
5
index.js
5
index.js
@ -34,11 +34,6 @@ autoUpdater.autoDownload = false;
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) app.exit();
|
||||
|
||||
app.commandLine.appendSwitch(
|
||||
"js-flags",
|
||||
// WebAssembly flags
|
||||
"--experimental-wasm-threads"
|
||||
);
|
||||
app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer"); // Required for downloader
|
||||
app.allowRendererProcessReuse = true; // https://github.com/electron/electron/issues/18397
|
||||
if (config.get("options.disableHardwareAcceleration")) {
|
||||
|
||||
31
menu.js
31
menu.js
@ -105,13 +105,34 @@ const mainMenuTemplate = (win) => {
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Force show like buttons",
|
||||
type: "checkbox",
|
||||
checked: config.get("options.ForceShowLikeButtons"),
|
||||
click: (item) => {
|
||||
config.set("options.ForceShowLikeButtons", item.checked);
|
||||
label: "Like buttons",
|
||||
submenu: [
|
||||
{
|
||||
label: "Default",
|
||||
type: "radio",
|
||||
checked: !config.get("options.likeButtons"),
|
||||
click: () => {
|
||||
config.set("options.likeButtons", '');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Force show",
|
||||
type: "radio",
|
||||
checked: config.get("options.likeButtons") === 'force',
|
||||
click: () => {
|
||||
config.set("options.likeButtons", 'force');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Hide",
|
||||
type: "radio",
|
||||
checked: config.get("options.likeButtons") === 'hide',
|
||||
click: () => {
|
||||
config.set("options.likeButtons", 'hide');
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Theme",
|
||||
submenu: [
|
||||
|
||||
@ -106,7 +106,7 @@
|
||||
"npm": "Please use yarn instead"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cliqz/adblocker-electron": "^1.25.2",
|
||||
"@cliqz/adblocker-electron": "^1.26.0",
|
||||
"@ffmpeg/core": "^0.11.0",
|
||||
"@ffmpeg/ffmpeg": "^0.11.6",
|
||||
"@foobar404/wave": "^2.0.4",
|
||||
@ -115,8 +115,7 @@
|
||||
"browser-id3-writer": "^4.4.0",
|
||||
"butterchurn": "^2.6.7",
|
||||
"butterchurn-presets": "^2.4.7",
|
||||
"chokidar": "^3.5.3",
|
||||
"custom-electron-prompt": "^1.5.1",
|
||||
"custom-electron-prompt": "^1.5.4",
|
||||
"custom-electron-titlebar": "^4.1.6",
|
||||
"electron-better-web-request": "^1.0.1",
|
||||
"electron-debug": "^3.2.0",
|
||||
@ -135,8 +134,7 @@
|
||||
"node-fetch": "^2.6.8",
|
||||
"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",
|
||||
"youtubei.js": "^2.9.0",
|
||||
"ytdl-core": "^4.11.1",
|
||||
"youtubei.js": "^3.1.1",
|
||||
"ytpl": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -11,28 +11,32 @@ module.exports = (options) => {
|
||||
document.addEventListener('apiLoaded', (event) => setup(event, options), { once: true, passive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* If captions are disabled by default,
|
||||
* unload "captions" module when video changes.
|
||||
*/
|
||||
const videoChanged = (api, options) => {
|
||||
if (options.disableCaptions) {
|
||||
setTimeout(() => api.unloadModule("captions"), 100);
|
||||
}
|
||||
}
|
||||
|
||||
function setup(event, options) {
|
||||
const api = event.detail;
|
||||
|
||||
$("video").addEventListener("srcChanged", () => videoChanged(api, options));
|
||||
|
||||
$(".right-controls-buttons").append(captionsSettingsButton);
|
||||
|
||||
captionsSettingsButton.onclick = async () => {
|
||||
let captionTrackList = api.getOption("captions", "tracklist");
|
||||
|
||||
$("video").addEventListener("srcChanged", () => {
|
||||
if (options.disableCaptions) {
|
||||
setTimeout(() => api.unloadModule("captions"), 100);
|
||||
captionsSettingsButton.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
api.loadModule("captions");
|
||||
|
||||
const captionTrackList = api.getOption("captions", "tracklist");
|
||||
setTimeout(() => {
|
||||
captionTrackList = api.getOption("captions", "tracklist");
|
||||
|
||||
captionsSettingsButton.style.display = captionTrackList?.length
|
||||
? "inline-block"
|
||||
: "none";
|
||||
}, 250);
|
||||
});
|
||||
|
||||
captionsSettingsButton.onclick = async () => {
|
||||
if (captionTrackList?.length) {
|
||||
const currentCaptionTrack = api.getOption("captions", "track");
|
||||
let currentIndex = !currentCaptionTrack ?
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
const CHANNEL = "downloader";
|
||||
const ACTIONS = {
|
||||
ERROR: "error",
|
||||
METADATA: "metadata",
|
||||
PROGRESS: "progress",
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
CHANNEL: CHANNEL,
|
||||
ACTIONS: ACTIONS,
|
||||
};
|
||||
@ -1,98 +1,484 @@
|
||||
const { writeFileSync } = require("fs");
|
||||
const { join } = require("path");
|
||||
const {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
createWriteStream,
|
||||
writeFileSync,
|
||||
} = require('fs');
|
||||
const { join } = require('path');
|
||||
|
||||
const ID3Writer = require("browser-id3-writer");
|
||||
const { dialog, ipcMain } = require("electron");
|
||||
const { fetchFromGenius } = require('../lyrics-genius/back');
|
||||
const { isEnabled } = require('../../config/plugins');
|
||||
const { getImage } = require('../../providers/song-info');
|
||||
const { injectCSS } = require('../utils');
|
||||
const {
|
||||
presets,
|
||||
cropMaxWidth,
|
||||
getFolder,
|
||||
setBadge,
|
||||
sendFeedback: sendFeedback_,
|
||||
} = require('./utils');
|
||||
|
||||
const registerCallback = require("../../providers/song-info");
|
||||
const { injectCSS, listenAction } = require("../utils");
|
||||
const { cropMaxWidth } = require("./utils");
|
||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||
const { isEnabled } = require("../../config/plugins");
|
||||
const { getImage } = require("../../providers/song-info");
|
||||
const { fetchFromGenius } = require("../lyrics-genius/back");
|
||||
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 sendError = (win, error) => {
|
||||
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();
|
||||
|
||||
const cache = {
|
||||
getCoverBuffer: {
|
||||
buffer: null,
|
||||
url: null,
|
||||
},
|
||||
};
|
||||
|
||||
const config = require('./config');
|
||||
|
||||
/** @type {Innertube} */
|
||||
let yt;
|
||||
let win;
|
||||
let playingUrl = undefined;
|
||||
|
||||
const sendError = (error) => {
|
||||
win.setProgressBar(-1); // close progress bar
|
||||
setBadge(0); // close badge
|
||||
sendFeedback_(win); // reset feedback
|
||||
|
||||
console.error(error);
|
||||
dialog.showMessageBox({
|
||||
type: "info",
|
||||
buttons: ["OK"],
|
||||
title: "Error in download!",
|
||||
message: "Argh! Apologies, download failed…",
|
||||
detail: error.toString(),
|
||||
type: 'info',
|
||||
buttons: ['OK'],
|
||||
title: 'Error in download!',
|
||||
message: 'Argh! Apologies, download failed…',
|
||||
detail: `${error.toString()} ${
|
||||
error.cause ? `\n\n${error.cause.toString()}` : ''
|
||||
}`,
|
||||
});
|
||||
};
|
||||
|
||||
let nowPlayingMetadata = {};
|
||||
module.exports = async (win_) => {
|
||||
win = win_;
|
||||
injectCSS(win.webContents, join(__dirname, 'style.css'));
|
||||
|
||||
function handle(win) {
|
||||
injectCSS(win.webContents, join(__dirname, "style.css"));
|
||||
registerCallback((info) => {
|
||||
nowPlayingMetadata = info;
|
||||
yt = await Innertube.create({
|
||||
cache: new UniversalCache(false),
|
||||
generate_session_locally: true,
|
||||
});
|
||||
ipcMain.on('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),
|
||||
);
|
||||
};
|
||||
|
||||
listenAction(CHANNEL, (event, action, arg) => {
|
||||
switch (action) {
|
||||
case ACTIONS.ERROR: // arg = error
|
||||
sendError(win, arg);
|
||||
break;
|
||||
case ACTIONS.METADATA:
|
||||
event.returnValue = JSON.stringify(nowPlayingMetadata);
|
||||
break;
|
||||
case ACTIONS.PROGRESS: // arg = progress
|
||||
win.setProgressBar(arg);
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown action: " + action);
|
||||
module.exports.downloadSong = downloadSong;
|
||||
module.exports.downloadPlaylist = downloadPlaylist;
|
||||
|
||||
async function downloadSong(
|
||||
url,
|
||||
playlistFolder = undefined,
|
||||
trackId = undefined,
|
||||
increasePlaylistProgress = () => {},
|
||||
) {
|
||||
try {
|
||||
await downloadSongUnsafe(
|
||||
url,
|
||||
playlistFolder,
|
||||
trackId,
|
||||
increasePlaylistProgress,
|
||||
);
|
||||
} catch (error) {
|
||||
sendError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 };
|
||||
async function downloadSongUnsafe(
|
||||
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 id = getVideoId(url);
|
||||
const info = await yt.music.getInfo(id);
|
||||
|
||||
const metadata = getMetadata(info);
|
||||
if (metadata.album === 'N/A') metadata.album = '';
|
||||
metadata.trackId = trackId;
|
||||
|
||||
const dir =
|
||||
playlistFolder || config.get('downloadFolder') || app.getPath('downloads');
|
||||
const name = `${metadata.artist ? `${metadata.artist} - ` : ''}${
|
||||
metadata.title
|
||||
}`;
|
||||
|
||||
const extension = presets[config.get('preset')]?.extension || 'mp3';
|
||||
|
||||
const filename = filenamify(`${name}.${extension}`, {
|
||||
replacement: '_',
|
||||
maxLength: 255,
|
||||
});
|
||||
const filePath = join(dir, filename);
|
||||
|
||||
if (config.get('skipExisting') && existsSync(filePath)) {
|
||||
sendFeedback(null, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
const download_options = {
|
||||
type: 'audio', // audio, video or video+audio
|
||||
quality: 'best', // best, bestefficiency, 144p, 240p, 480p, 720p and so on.
|
||||
format: 'any', // media container format
|
||||
};
|
||||
|
||||
const format = info.chooseFormat(download_options);
|
||||
const stream = await info.download(download_options);
|
||||
|
||||
console.info(
|
||||
`Downloading ${metadata.artist} - ${metadata.title} [${metadata.id}]`,
|
||||
);
|
||||
|
||||
const iterableStream = Utils.streamToIterable(stream);
|
||||
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir);
|
||||
}
|
||||
|
||||
if (!presets[config.get('preset')]) {
|
||||
const fileBuffer = await iterableStreamToMP3(
|
||||
iterableStream,
|
||||
metadata,
|
||||
format.content_length,
|
||||
sendFeedback,
|
||||
increasePlaylistProgress,
|
||||
);
|
||||
writeFileSync(filePath, await writeID3(fileBuffer, metadata, sendFeedback));
|
||||
} else {
|
||||
const file = createWriteStream(filePath);
|
||||
let downloaded = 0;
|
||||
const total = format.content_length;
|
||||
|
||||
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);
|
||||
}
|
||||
await ffmpegWriteTags(
|
||||
filePath,
|
||||
metadata,
|
||||
presets[config.get('preset')]?.ffmpegArgs,
|
||||
);
|
||||
sendFeedback(null, -1);
|
||||
}
|
||||
|
||||
sendFeedback(null, -1);
|
||||
console.info(`Done: "${filePath}"`);
|
||||
}
|
||||
|
||||
async function iterableStreamToMP3(
|
||||
stream,
|
||||
metadata,
|
||||
content_length,
|
||||
sendFeedback,
|
||||
increasePlaylistProgress = () => {},
|
||||
) {
|
||||
const chunks = [];
|
||||
let downloaded = 0;
|
||||
const 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 {
|
||||
const coverBuffer = songMetadata.image && !songMetadata.image.isEmpty() ?
|
||||
songMetadata.image.toPNG() : null;
|
||||
if (!ffmpeg.isLoaded()) {
|
||||
await ffmpeg.load();
|
||||
}
|
||||
|
||||
const writer = new ID3Writer(songBuffer);
|
||||
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 getCoverBuffer(url) {
|
||||
const store = cache.getCoverBuffer;
|
||||
if (store.url === url) {
|
||||
return store.buffer;
|
||||
}
|
||||
store.url = url;
|
||||
|
||||
const nativeImage = cropMaxWidth(await getImage(url));
|
||||
store.buffer =
|
||||
nativeImage && !nativeImage.isEmpty() ? nativeImage.toPNG() : null;
|
||||
|
||||
return store.buffer;
|
||||
}
|
||||
|
||||
async function writeID3(buffer, metadata, sendFeedback) {
|
||||
try {
|
||||
sendFeedback('Writing ID3 tags...');
|
||||
|
||||
const coverBuffer = await getCoverBuffer(metadata.image);
|
||||
|
||||
const writer = new ID3Writer(buffer);
|
||||
|
||||
// Create the metadata tags
|
||||
writer
|
||||
.setFrame("TIT2", songMetadata.title)
|
||||
.setFrame("TPE1", [songMetadata.artist]);
|
||||
writer.setFrame('TIT2', metadata.title).setFrame('TPE1', [metadata.artist]);
|
||||
if (metadata.album) {
|
||||
writer.setFrame('TALB', metadata.album);
|
||||
}
|
||||
if (coverBuffer) {
|
||||
writer.setFrame("APIC", {
|
||||
writer.setFrame('APIC', {
|
||||
type: 3,
|
||||
data: coverBuffer,
|
||||
description: ""
|
||||
description: '',
|
||||
});
|
||||
}
|
||||
if (isEnabled("lyrics-genius")) {
|
||||
const lyrics = await fetchFromGenius(songMetadata);
|
||||
if (isEnabled('lyrics-genius')) {
|
||||
const lyrics = await fetchFromGenius(metadata);
|
||||
if (lyrics) {
|
||||
writer.setFrame("USLT", {
|
||||
description: lyrics,
|
||||
writer.setFrame('USLT', {
|
||||
description: '',
|
||||
lyrics: lyrics,
|
||||
});
|
||||
}
|
||||
}
|
||||
writer.addTag();
|
||||
fileBuffer = Buffer.from(writer.arrayBuffer);
|
||||
} catch (error) {
|
||||
sendError(win, error);
|
||||
if (metadata.trackId) {
|
||||
writer.setFrame('TRCK', metadata.trackId);
|
||||
}
|
||||
writer.addTag();
|
||||
return Buffer.from(writer.arrayBuffer);
|
||||
} catch (e) {
|
||||
sendError(e);
|
||||
}
|
||||
|
||||
writeFileSync(filePath, fileBuffer);
|
||||
// Notify the youtube-dl file
|
||||
event.reply("add-metadata-done");
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = handle;
|
||||
module.exports.sendError = sendError;
|
||||
async function downloadPlaylist(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: config.get('playlistMaxItems') || Infinity,
|
||||
});
|
||||
} catch (e) {
|
||||
sendError(
|
||||
`Error getting playlist info: make sure it isn\'t a private or "Mixed for you" playlist\n\n${e}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (playlist.items.length === 0) sendError(new Error('Playlist is empty'));
|
||||
if (playlist.items.length === 1) {
|
||||
sendFeedback('Playlist has only one item, downloading it directly');
|
||||
await downloadSong(playlist.items[0].url);
|
||||
return;
|
||||
}
|
||||
const isAlbum = playlist.title.startsWith('Album - ');
|
||||
if (isAlbum) {
|
||||
playlist.title = playlist.title.slice(8);
|
||||
}
|
||||
const safePlaylistTitle = filenamify(playlist.title, { replacement: ' ' });
|
||||
|
||||
const folder = getFolder(config.get('downloadFolder'));
|
||||
const playlistFolder = join(folder, safePlaylistTitle);
|
||||
if (existsSync(playlistFolder)) {
|
||||
if (!config.get('skipExisting')) {
|
||||
sendError(new Error(`The folder ${playlistFolder} already exists`));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
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(
|
||||
`Error downloading "${song.author.name} - ${song.title}":\n ${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
|
||||
}
|
||||
}
|
||||
|
||||
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(INVALID_PLAYLIST_MODIFIER.length);
|
||||
}
|
||||
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,
|
||||
});
|
||||
|
||||
3
plugins/downloader/config.js
Normal file
3
plugins/downloader/config.js
Normal file
@ -0,0 +1,3 @@
|
||||
const { PluginConfig } = require('../../config/dynamic');
|
||||
const config = new PluginConfig('downloader');
|
||||
module.exports = { ...config };
|
||||
@ -2,97 +2,68 @@ const { ipcRenderer } = require("electron");
|
||||
|
||||
const { defaultConfig } = require("../../config");
|
||||
const { getSongMenu } = require("../../providers/dom-elements");
|
||||
const { ElementFromFile, templatePath, triggerAction } = require("../utils");
|
||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||
const { downloadVideoToMP3 } = require("./youtube-dl");
|
||||
const { ElementFromFile, templatePath } = require("../utils");
|
||||
|
||||
let menu = null;
|
||||
let progress = null;
|
||||
const downloadButton = ElementFromFile(
|
||||
templatePath(__dirname, "download.html")
|
||||
);
|
||||
let pluginOptions = {};
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
let doneFirstLoad = false;
|
||||
|
||||
const menuObserver = new MutationObserver(() => {
|
||||
if (!menu) {
|
||||
menu = getSongMenu();
|
||||
if (!menu) return;
|
||||
}
|
||||
if (menu.contains(downloadButton)) return;
|
||||
const menuUrl = document.querySelector('tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint')?.href;
|
||||
if (menuUrl && !menuUrl.includes('watch?')) return;
|
||||
if (!menuUrl?.includes('watch?') && doneFirstLoad) return;
|
||||
|
||||
menu.prepend(downloadButton);
|
||||
progress = document.querySelector("#ytmcustom-download");
|
||||
|
||||
if (doneFirstLoad) return;
|
||||
setTimeout(() => doneFirstLoad ||= true, 500);
|
||||
});
|
||||
|
||||
const reinit = () => {
|
||||
triggerAction(CHANNEL, ACTIONS.PROGRESS, -1); // closes progress bar
|
||||
if (!progress) {
|
||||
console.warn("Cannot update progress");
|
||||
} else {
|
||||
progress.innerHTML = "Download";
|
||||
}
|
||||
};
|
||||
|
||||
const baseUrl = defaultConfig.url;
|
||||
|
||||
// TODO: re-enable once contextIsolation is set to true
|
||||
// contextBridge.exposeInMainWorld("downloader", {
|
||||
// download: () => {
|
||||
global.download = () => {
|
||||
triggerAction(CHANNEL, ACTIONS.PROGRESS, 2); // starts with indefinite progress bar
|
||||
let metadata;
|
||||
let videoUrl = getSongMenu()
|
||||
// selector of first button which is always "Start Radio"
|
||||
?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint')
|
||||
?.getAttribute("href");
|
||||
if (videoUrl) {
|
||||
if (videoUrl.startsWith('watch?')) {
|
||||
videoUrl = baseUrl + "/" + videoUrl;
|
||||
videoUrl = defaultConfig.url + "/" + videoUrl;
|
||||
}
|
||||
if (videoUrl.includes('?playlist=')) {
|
||||
ipcRenderer.send('download-playlist-request', videoUrl);
|
||||
return;
|
||||
}
|
||||
metadata = null;
|
||||
} else {
|
||||
metadata = global.songInfo;
|
||||
videoUrl = metadata.url || window.location.href;
|
||||
videoUrl = global.songInfo.url || window.location.href;
|
||||
}
|
||||
|
||||
downloadVideoToMP3(
|
||||
videoUrl,
|
||||
(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
|
||||
);
|
||||
ipcRenderer.send('download-song', videoUrl);
|
||||
};
|
||||
// });
|
||||
|
||||
function observeMenu(options) {
|
||||
pluginOptions = { ...pluginOptions, ...options };
|
||||
|
||||
module.exports = () => {
|
||||
document.addEventListener('apiLoaded', () => {
|
||||
observer.observe(document.querySelector('ytmusic-popup-container'), {
|
||||
menuObserver.observe(document.querySelector('ytmusic-popup-container'), {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}, { once: true, passive: true })
|
||||
}
|
||||
|
||||
module.exports = observeMenu;
|
||||
ipcRenderer.on('downloader-feedback', (_, feedback) => {
|
||||
if (!progress) {
|
||||
console.warn("Cannot update progress");
|
||||
} else {
|
||||
progress.innerHTML = feedback || "Download";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,55 +1,24 @@
|
||||
const { existsSync, mkdirSync } = require("fs");
|
||||
const { join } = require("path");
|
||||
const { dialog } = require("electron");
|
||||
|
||||
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 { sendError } = require("./back");
|
||||
const { defaultMenuDownloadLabel, getFolder, presets, setBadge } = require("./utils");
|
||||
|
||||
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) => {
|
||||
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;
|
||||
}
|
||||
const { downloadPlaylist } = require("./back");
|
||||
const { defaultMenuDownloadLabel, getFolder, presets } = require("./utils");
|
||||
const config = require("./config");
|
||||
|
||||
module.exports = () => {
|
||||
return [
|
||||
{
|
||||
label: downloadLabel,
|
||||
click: () => downloadPlaylist(undefined, win, options),
|
||||
label: defaultMenuDownloadLabel,
|
||||
click: () => downloadPlaylist(),
|
||||
},
|
||||
{
|
||||
label: "Choose download folder",
|
||||
click: () => {
|
||||
let result = dialog.showOpenDialogSync({
|
||||
const result = dialog.showOpenDialogSync({
|
||||
properties: ["openDirectory", "createDirectory"],
|
||||
defaultPath: getFolder(options.downloadFolder),
|
||||
defaultPath: getFolder(config.get("downloadFolder")),
|
||||
});
|
||||
if (result) {
|
||||
options.downloadFolder = result[0];
|
||||
setMenuOptions("downloader", options);
|
||||
config.set("downloadFolder", result[0]);
|
||||
} // else = user pressed cancel
|
||||
},
|
||||
},
|
||||
@ -58,94 +27,19 @@ module.exports = (win, options) => {
|
||||
submenu: Object.keys(presets).map((preset) => ({
|
||||
label: preset,
|
||||
type: "radio",
|
||||
checked: config.get("preset") === preset,
|
||||
click: () => {
|
||||
options.preset = preset;
|
||||
setMenuOptions("downloader", options);
|
||||
config.set("preset", preset);
|
||||
},
|
||||
checked: options.preset === preset || presets[preset] === undefined,
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: "Skip existing files",
|
||||
type: "checkbox",
|
||||
checked: config.get("skipExisting"),
|
||||
click: (item) => {
|
||||
config.set("skipExisting", item.checked);
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
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(win, 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(win, e);
|
||||
return;
|
||||
}
|
||||
const safePlaylistTitle = filenamify(playlist.title, {replacement: ' '});
|
||||
|
||||
const folder = getFolder(options.downloadFolder);
|
||||
const playlistFolder = join(folder, safePlaylistTitle);
|
||||
if (existsSync(playlistFolder)) {
|
||||
sendError(
|
||||
win,
|
||||
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);
|
||||
dirWatcher.on('add', () => {
|
||||
downloadCount += 1;
|
||||
if (downloadCount >= playlist.items.length) {
|
||||
win.setProgressBar(-1); // close progress bar
|
||||
setBadge(0); // close badge counter
|
||||
dirWatcher.close().then(() => (dirWatcher = null));
|
||||
} else {
|
||||
win.setProgressBar(downloadCount / playlist.items.length);
|
||||
setBadge(playlist.items.length - downloadCount);
|
||||
}
|
||||
});
|
||||
|
||||
playlist.items.forEach((song) => {
|
||||
win.webContents.send(
|
||||
"downloader-download-playlist",
|
||||
song.url,
|
||||
safePlaylistTitle,
|
||||
options
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,20 +1,12 @@
|
||||
const electron = require("electron");
|
||||
const { app } = require("electron");
|
||||
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";
|
||||
|
||||
const orderedQualityList = ["maxresdefault", "hqdefault", "mqdefault", "sdddefault"];
|
||||
module.exports.urlToJPG = (imgUrl, videoId) => {
|
||||
if (!imgUrl || imgUrl.includes(".jpg")) return imgUrl;
|
||||
//it will almost never get further than hqdefault
|
||||
for (const quality of orderedQualityList) {
|
||||
if (imgUrl.includes(quality)) {
|
||||
return `https://img.youtube.com/vi/${videoId}/${quality}.jpg`;
|
||||
}
|
||||
}
|
||||
return `https://img.youtube.com/vi/${videoId}/default.jpg`;
|
||||
}
|
||||
module.exports.sendFeedback = (win, message) => {
|
||||
win.webContents.send("downloader-feedback", message);
|
||||
};
|
||||
|
||||
module.exports.cropMaxWidth = (image) => {
|
||||
const imageSize = image.getSize();
|
||||
@ -41,6 +33,6 @@ module.exports.presets = {
|
||||
|
||||
module.exports.setBadge = n => {
|
||||
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
|
||||
);
|
||||
}
|
||||
);
|
||||
@ -18,7 +18,8 @@
|
||||
}
|
||||
|
||||
/* fix navbar hiding library items */
|
||||
ytmusic-section-list-renderer[page-type="MUSIC_PAGE_TYPE_LIBRARY_CONTENT_LANDING_PAGE"] {
|
||||
ytmusic-section-list-renderer[page-type="MUSIC_PAGE_TYPE_LIBRARY_CONTENT_LANDING_PAGE"],
|
||||
ytmusic-section-list-renderer[page-type="MUSIC_PAGE_TYPE_PRIVATELY_OWNED_CONTENT_LANDING_PAGE"] {
|
||||
top: 50px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ const fetch = require("node-fetch");
|
||||
|
||||
const { cleanupName } = require("../../providers/song-info");
|
||||
const { injectCSS } = require("../utils");
|
||||
let eastAsianChars = new RegExp("[\u{3040}-\u{30ff}\u{3400}-\u{4dbf}\u{4e00}-\u{9fff}\u{f900}-\u{faff}\u{ff66}-\u{ff9f}]");
|
||||
let eastAsianChars = /\p{Script=Han}|\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}|\p{Script=Han}/u;
|
||||
let revRomanized = false;
|
||||
|
||||
module.exports = async (win, options) => {
|
||||
|
||||
@ -39,7 +39,6 @@ const setup = () => {
|
||||
|
||||
/** @param {Electron.BrowserWindow} win */
|
||||
module.exports = (win, options) => {
|
||||
config.init(options);
|
||||
// Register the callback for new song information
|
||||
is.windows() && options.interactive ?
|
||||
require("./interactive")(win) :
|
||||
|
||||
@ -1,23 +1,5 @@
|
||||
const { setOptions, setMenuOptions } = require("../../config/plugins");
|
||||
const defaultConfig = require("../../config/defaults");
|
||||
const { PluginConfig } = require("../../config/dynamic");
|
||||
|
||||
let config = defaultConfig.plugins["notifications"];
|
||||
const config = new PluginConfig("notifications");
|
||||
|
||||
module.exports.init = (options) => {
|
||||
config = { ...config, ...options };
|
||||
};
|
||||
|
||||
module.exports.setAndMaybeRestart = (option, value) => {
|
||||
config[option] = value;
|
||||
setMenuOptions("notifications", config);
|
||||
};
|
||||
|
||||
module.exports.set = (option, value) => {
|
||||
config[option] = value;
|
||||
setOptions("notifications", config);
|
||||
};
|
||||
|
||||
module.exports.get = (option) => {
|
||||
let res = config[option];
|
||||
return res;
|
||||
};
|
||||
module.exports = { ...config };
|
||||
|
||||
14
preload.js
14
preload.js
@ -145,11 +145,17 @@ function onApiLoaded() {
|
||||
}
|
||||
}
|
||||
|
||||
// Force show like buttons
|
||||
if (config.get("options.ForceShowLikeButtons")) {
|
||||
const likeButtons = $('ytmusic-like-button-renderer')
|
||||
|
||||
// Hide / Force show like buttons
|
||||
const likeButtonsOptions = config.get("options.likeButtons");
|
||||
if (likeButtonsOptions) {
|
||||
const likeButtons = $("ytmusic-like-button-renderer");
|
||||
if (likeButtons) {
|
||||
likeButtons.style.display = 'inherit';
|
||||
likeButtons.style.display =
|
||||
{
|
||||
hide: "none",
|
||||
force: "inherit",
|
||||
}[likeButtonsOptions] || "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
tray.js
2
tray.js
@ -40,7 +40,7 @@ module.exports.setUpTray = (app, win) => {
|
||||
|
||||
tray = new Tray(trayIcon);
|
||||
|
||||
tray.setToolTip("Youtube Music");
|
||||
tray.setToolTip("YouTube Music");
|
||||
|
||||
// macOS only
|
||||
tray.setIgnoreDoubleClickEvents(true);
|
||||
|
||||
235
yarn.lock
235
yarn.lock
@ -48,59 +48,59 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cliqz/adblocker-content@npm:^1.25.2":
|
||||
version: 1.25.2
|
||||
resolution: "@cliqz/adblocker-content@npm:1.25.2"
|
||||
"@cliqz/adblocker-content@npm:^1.26.2":
|
||||
version: 1.26.2
|
||||
resolution: "@cliqz/adblocker-content@npm:1.26.2"
|
||||
dependencies:
|
||||
"@cliqz/adblocker-extended-selectors": ^1.25.2
|
||||
checksum: c7781a9e07d6b685c10604abc72cdf79c48a7211f10901e316d506b4a296bf644c9292a256fe988c7074ebeedfd00075849ae74b648ec3672bf3da61723bb0c4
|
||||
"@cliqz/adblocker-extended-selectors": ^1.26.2
|
||||
checksum: 00ddbf157ca5ea97e3d8517f1d9397ca53eecfc95b6f09a327667ad388cafae84de655af8bac6d0c1f12b004b0c7eb8fc898f442f05e6b9d2294709a3bfb6a81
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cliqz/adblocker-electron-preload@npm:^1.25.2":
|
||||
version: 1.25.2
|
||||
resolution: "@cliqz/adblocker-electron-preload@npm:1.25.2"
|
||||
"@cliqz/adblocker-electron-preload@npm:^1.26.2":
|
||||
version: 1.26.2
|
||||
resolution: "@cliqz/adblocker-electron-preload@npm:1.26.2"
|
||||
dependencies:
|
||||
"@cliqz/adblocker-content": ^1.25.2
|
||||
"@cliqz/adblocker-content": ^1.26.2
|
||||
peerDependencies:
|
||||
electron: ">11"
|
||||
checksum: 196324d67768bbd1a9ec1023c1a8be0ebd25605621690762641ecc40272f822c179d9081b6c78603b5ba70ec1d49bb6ba3f9f4d48c6f92794b21a06bdd277300
|
||||
checksum: e130911f00f9fbaa8f3edb3f36501a6f39b0f798a2dbeb8d8416219f3381edd7c34036452229472bd51fad13fa880ea24dc4b56419df0dffaa49f63f15a01451
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cliqz/adblocker-electron@npm:^1.25.2":
|
||||
version: 1.25.2
|
||||
resolution: "@cliqz/adblocker-electron@npm:1.25.2"
|
||||
"@cliqz/adblocker-electron@npm:^1.26.0":
|
||||
version: 1.26.2
|
||||
resolution: "@cliqz/adblocker-electron@npm:1.26.2"
|
||||
dependencies:
|
||||
"@cliqz/adblocker": ^1.25.2
|
||||
"@cliqz/adblocker-electron-preload": ^1.25.2
|
||||
"@cliqz/adblocker": ^1.26.2
|
||||
"@cliqz/adblocker-electron-preload": ^1.26.2
|
||||
tldts-experimental: ^5.6.21
|
||||
peerDependencies:
|
||||
electron: ">11"
|
||||
checksum: 8d18e85490bc60b95e880fd3315e2111e05f123bf820e87963f4fa0bf65b78d3043541b02588950b152b827414baa11cdf74159042ccc8e144afea9eef84fe62
|
||||
checksum: 308e7e6273bdf055ba5a637111d301c6c7cc3f46c7b31e6e658addab7bcb735f162e77b50f123e2841a769b38b810f85e959dd7da382298c0c6bbedf9da58d44
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cliqz/adblocker-extended-selectors@npm:^1.25.2":
|
||||
version: 1.25.2
|
||||
resolution: "@cliqz/adblocker-extended-selectors@npm:1.25.2"
|
||||
checksum: 67541181fb7718d25e62184ea2b5236f0b659b82221163086179498c068f6e6c01911ce47f2ffe0ce2244c0728afa92add8e03e881bc8d6e64f0b9a0e2ff8091
|
||||
"@cliqz/adblocker-extended-selectors@npm:^1.26.2":
|
||||
version: 1.26.2
|
||||
resolution: "@cliqz/adblocker-extended-selectors@npm:1.26.2"
|
||||
checksum: 54736a3328e5175c6c0350d9f18faf83ed16a4f41bc1ff6fa550319ebf728fb5536015384ee4a5d005d7225c41a31eaa251d7a18486db26d41b251eeee8eaaae
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@cliqz/adblocker@npm:^1.25.2":
|
||||
version: 1.25.2
|
||||
resolution: "@cliqz/adblocker@npm:1.25.2"
|
||||
"@cliqz/adblocker@npm:^1.26.2":
|
||||
version: 1.26.2
|
||||
resolution: "@cliqz/adblocker@npm:1.26.2"
|
||||
dependencies:
|
||||
"@cliqz/adblocker-content": ^1.25.2
|
||||
"@cliqz/adblocker-extended-selectors": ^1.25.2
|
||||
"@cliqz/adblocker-content": ^1.26.2
|
||||
"@cliqz/adblocker-extended-selectors": ^1.26.2
|
||||
"@remusao/guess-url-type": ^1.1.2
|
||||
"@remusao/small": ^1.1.2
|
||||
"@remusao/smaz": ^1.7.1
|
||||
"@types/chrome": ^0.0.206
|
||||
"@types/firefox-webext-browser": ^94.0.0
|
||||
"@types/chrome": ^0.0.218
|
||||
"@types/firefox-webext-browser": ^109.0.0
|
||||
tldts-experimental: ^5.6.21
|
||||
checksum: be97b20420ccfafd2fc887538a255db4c22bddc1a6944bbea9e9a8df3e2912e39f18101f99b37599653e2a8f2053513a7ec0908a7af49ba9794573ee6d186094
|
||||
checksum: e4fcfe15109f15ce95096501f00f4109743385158b3a746c30f4f8ff93bd73821e2424b67c9e58ba10a1c97da79826d26fb0b8eea77f7b3af3783f50b22d8009
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -734,13 +734,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.2.1
|
||||
resolution: "@remusao/guess-url-type@npm:1.2.1"
|
||||
@ -840,13 +833,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/chrome@npm:^0.0.206":
|
||||
version: 0.0.206
|
||||
resolution: "@types/chrome@npm:0.0.206"
|
||||
"@types/chrome@npm:^0.0.218":
|
||||
version: 0.0.218
|
||||
resolution: "@types/chrome@npm:0.0.218"
|
||||
dependencies:
|
||||
"@types/filesystem": "*"
|
||||
"@types/har-format": "*"
|
||||
checksum: 002af40b339ca0f665bf6e52af1fd2ecfdd4cffb6d10d891b0b4394c2a94d758e94e0d5497d38e9b45033eb8620ffa1a63e42294f88612507d9e74ddf3f6ce40
|
||||
checksum: 08f00e7c2ec8a8e869b0c7a93bd187c19471baa56f3c072c85b25467f2e0eeccfaa5a02f245bf1832fd2f7d4bb0ce3f4617a7e04f52ab37562efe1b868b62aca
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -892,10 +885,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/firefox-webext-browser@npm:^94.0.0":
|
||||
version: 94.0.1
|
||||
resolution: "@types/firefox-webext-browser@npm:94.0.1"
|
||||
checksum: 43f7e34857f5750c6dc6833f3c4735e75782dcc71800dd29eadc8211a58de0e36611cec79ca7a238da855d5ed7badea46992baec4420b067c4dfa4b052e91465
|
||||
"@types/firefox-webext-browser@npm:^109.0.0":
|
||||
version: 109.0.0
|
||||
resolution: "@types/firefox-webext-browser@npm:109.0.0"
|
||||
checksum: 6c3d8d98ca07329e88adda50911276864902d6b31f6f94712c4149327d01a06e7167eb4f6b34b8c7438a06792d0118a83e23afc21a7941ecd67ecaf40123e636
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -1372,16 +1365,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 4.0.0
|
||||
resolution: "app-builder-bin@npm:4.0.0"
|
||||
@ -1689,13 +1672,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.5.0
|
||||
resolution: "bindings@npm:1.5.0"
|
||||
@ -1761,7 +1737,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"braces@npm:^3.0.2, braces@npm:~3.0.2":
|
||||
"braces@npm:^3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "braces@npm:3.0.2"
|
||||
dependencies:
|
||||
@ -2072,25 +2048,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 2.0.0
|
||||
resolution: "chownr@npm:2.0.0"
|
||||
@ -2425,12 +2382,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"custom-electron-prompt@npm:^1.5.1":
|
||||
version: 1.5.1
|
||||
resolution: "custom-electron-prompt@npm:1.5.1"
|
||||
"custom-electron-prompt@npm:^1.5.4":
|
||||
version: 1.5.4
|
||||
resolution: "custom-electron-prompt@npm:1.5.4"
|
||||
peerDependencies:
|
||||
electron: ">=10.0.0"
|
||||
checksum: 43a0d72a7a3471135822cb210d580285f70080d9d3a7b03f82cd4be403059fe20ea05ebdd1f9534928c386ab25a353e678f2cfb3f4ca016b41f3366bff700767
|
||||
checksum: 93995b5f0e9d14401a8c4fdd358af32d8b7585b59b111667cfa55f9505109c08914f3140953125b854e5d09e811de8c76c7fec718934c13e8a1ad09fe1b85270
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -4109,25 +4066,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.1.1
|
||||
resolution: "function-bind@npm:1.1.1"
|
||||
@ -4254,7 +4192,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2":
|
||||
"glob-parent@npm:^5.1.2":
|
||||
version: 5.1.2
|
||||
resolution: "glob-parent@npm:5.1.2"
|
||||
dependencies:
|
||||
@ -4896,15 +4834,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.1.2
|
||||
resolution: "is-boolean-object@npm:1.1.2"
|
||||
@ -5016,7 +4945,7 @@ __metadata:
|
||||
languageName: node
|
||||
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
|
||||
resolution: "is-glob@npm:4.0.3"
|
||||
dependencies:
|
||||
@ -5332,12 +5261,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jintr@npm:^0.3.1":
|
||||
version: 0.3.1
|
||||
resolution: "jintr@npm:0.3.1"
|
||||
"jintr@npm:^0.4.1":
|
||||
version: 0.4.1
|
||||
resolution: "jintr@npm:0.4.1"
|
||||
dependencies:
|
||||
acorn: ^8.8.0
|
||||
checksum: 1fb2454904461c3bbe6b55251dce4ac352fb3b94803773e3d8925ede4a907b5d52a2f30f3f76757c770e1785f34a3665d5cffd710c3ae99837cd157762130a24
|
||||
checksum: 9dd5932be611aa926dba90e3b1bf09afbdc8864a128dbba53f5ee8461f0ac27955fca780dfd4cbb1575e6873d0d74dd346127554a4b2cae01986fe12aad3ba09
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -5813,16 +5742,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"m3u8stream@npm:^0.8.6":
|
||||
version: 0.8.6
|
||||
resolution: "m3u8stream@npm:0.8.6"
|
||||
dependencies:
|
||||
miniget: ^4.2.2
|
||||
sax: ^1.2.4
|
||||
checksum: b8f61c1101dd3ad873ff2f3d0e9e6a5139ad17e20990b89ae67f2585043bc2b727151ed85f3e58aabc8a1a95af28e1ee26f69af6ac9e8841ff68129eae2f5ac5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"make-fetch-happen@npm:^10.0.3":
|
||||
version: 10.2.1
|
||||
resolution: "make-fetch-happen@npm:10.2.1"
|
||||
@ -6359,13 +6278,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 6.1.0
|
||||
resolution: "normalize-url@npm:6.1.0"
|
||||
@ -6856,7 +6768,7 @@ __metadata:
|
||||
languageName: node
|
||||
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
|
||||
resolution: "picomatch@npm:2.3.1"
|
||||
checksum: 050c865ce81119c4822c45d3c84f1ced46f93a0126febae20737bd05ca20589c564d6e9226977df859ed5e03dc73f02584a2b0faad36e896936238238b0446cf
|
||||
@ -7265,15 +7177,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 4.0.0
|
||||
resolution: "redent@npm:4.0.0"
|
||||
@ -7553,7 +7456,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sax@npm:>=0.6.0, sax@npm:^1.1.3, sax@npm:^1.2.4":
|
||||
"sax@npm:>=0.6.0, sax@npm:^1.2.4":
|
||||
version: 1.2.4
|
||||
resolution: "sax@npm:1.2.4"
|
||||
checksum: d3df7d32b897a2c2f28e941f732c71ba90e27c24f62ee918bd4d9a8cfb3553f2f81e5493c7f0be94a11c1911b643a9108f231dd6f60df3fa9586b5d2e3e9e1fe
|
||||
@ -8455,12 +8358,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"undici@npm:^5.7.0":
|
||||
version: 5.18.0
|
||||
resolution: "undici@npm:5.18.0"
|
||||
"undici@npm:^5.19.1":
|
||||
version: 5.20.0
|
||||
resolution: "undici@npm:5.20.0"
|
||||
dependencies:
|
||||
busboy: ^1.6.0
|
||||
checksum: 74e0f357c376c745fcca612481a5742866ae36086ad387e626255f4c4a15fc5357d9d0fa4355271b6a633d50f5556c3e85720844680c44861c66e23afca7245f
|
||||
checksum: 25412a785b2bd0b12f0bb0ec47ef00aa7a611ca0e570cb7af97cffe6a42e0d78e4b15190363a43771e9002defc3c6647c1b2d52201b3f64e2196819db4d150d3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -8969,7 +8872,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "youtube-music@workspace:."
|
||||
dependencies:
|
||||
"@cliqz/adblocker-electron": ^1.25.2
|
||||
"@cliqz/adblocker-electron": ^1.26.0
|
||||
"@ffmpeg/core": ^0.11.0
|
||||
"@ffmpeg/ffmpeg": ^0.11.6
|
||||
"@foobar404/wave": ^2.0.4
|
||||
@ -8980,8 +8883,7 @@ __metadata:
|
||||
browser-id3-writer: ^4.4.0
|
||||
butterchurn: ^2.6.7
|
||||
butterchurn-presets: ^2.4.7
|
||||
chokidar: ^3.5.3
|
||||
custom-electron-prompt: ^1.5.1
|
||||
custom-electron-prompt: ^1.5.4
|
||||
custom-electron-titlebar: ^4.1.6
|
||||
del-cli: ^5.0.0
|
||||
electron: ^22.0.2
|
||||
@ -9008,32 +8910,19 @@ __metadata:
|
||||
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
|
||||
xo: ^0.53.1
|
||||
youtubei.js: ^2.9.0
|
||||
ytdl-core: ^4.11.1
|
||||
youtubei.js: ^3.1.1
|
||||
ytpl: ^2.3.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"youtubei.js@npm:^2.9.0":
|
||||
version: 2.9.0
|
||||
resolution: "youtubei.js@npm:2.9.0"
|
||||
"youtubei.js@npm:^3.1.1":
|
||||
version: 3.1.1
|
||||
resolution: "youtubei.js@npm:3.1.1"
|
||||
dependencies:
|
||||
"@protobuf-ts/runtime": ^2.7.0
|
||||
jintr: ^0.3.1
|
||||
jintr: ^0.4.1
|
||||
linkedom: ^0.14.12
|
||||
undici: ^5.7.0
|
||||
checksum: 0b9d86c1ec7297ee41b9013d6cb951976d82b2775d9d9d5abf0447d7acb9f36b07ebb689710bf8ccfa256a6f56088f49b699fb1a3e5bac2b0ea7d2daa508c109
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ytdl-core@npm:^4.11.1":
|
||||
version: 4.11.2
|
||||
resolution: "ytdl-core@npm:4.11.2"
|
||||
dependencies:
|
||||
m3u8stream: ^0.8.6
|
||||
miniget: ^4.2.2
|
||||
sax: ^1.1.3
|
||||
checksum: 57df38b5b1e4955db0e0c0be8d185f34de9eaee102ad1281d69de91628230cc84e8d46d278409eafa68c4aab4085a0f9fe8de30e9ea8644e011e20cae7f37c0e
|
||||
undici: ^5.19.1
|
||||
checksum: 1280e2ddacec3034ee8e1b398ba80662a6854e184416d3484119e7cf47b69ab2e58b4f1efdf468dcad3e50bdc7bd42b6ee66b95660ffb521efb5f0634ef60fb7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user