diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e5c7e8a0..962b4adb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,9 @@ name: Build YouTube Music on: - - push + push: + branches: [ master ] + pull_request: env: NODE_VERSION: "16.x" diff --git a/config/defaults.js b/config/defaults.js index d01d059a..97c88c0d 100644 --- a/config/defaults.js +++ b/config/defaults.js @@ -16,6 +16,7 @@ const defaultConfig = { autoResetAppCache: false, resumeOnStart: true, proxy: "", + startingPage: "", }, plugins: { // Enabled plugins diff --git a/config/dynamic.js b/config/dynamic.js new file mode 100644 index 00000000..34cb0ac3 --- /dev/null +++ b/config/dynamic.js @@ -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); + }); + } + } + } +}; diff --git a/config/store.js b/config/store.js index 07984820..1ba8ce26 100644 --- a/config/store.js +++ b/config/store.js @@ -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"); diff --git a/index.js b/index.js index 6de4c18b..dcc23eb7 100644 --- a/index.js +++ b/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")) { diff --git a/menu.js b/menu.js index 0f127124..acdf5666 100644 --- a/menu.js +++ b/menu.js @@ -7,6 +7,7 @@ const { restart } = require("./providers/app-controls"); const { getAllPlugins } = require("./plugins/utils"); const config = require("./config"); +const { startingPages } = require("./providers/extracted-data"); const prompt = require("custom-electron-prompt"); const promptOptions = require("./providers/prompt-options"); @@ -81,6 +82,17 @@ const mainMenuTemplate = (win) => { config.setMenuOption("options.resumeOnStart", item.checked); }, }, + { + label: 'Starting page', + submenu: Object.keys(startingPages).map((name) => ({ + label: name, + type: 'radio', + checked: config.get('options.startingPage') === name, + click: () => { + config.set('options.startingPage', name); + }, + })) + }, { label: "Visual Tweaks", submenu: [ @@ -93,12 +105,33 @@ 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", @@ -131,7 +164,7 @@ const mainMenuTemplate = (win) => { ], }, { - label: "Release single instance lock", + label: "Single instance lock", type: "checkbox", checked: false, click: (item) => { diff --git a/package.json b/package.json index e989a1dd..76faad75 100644 --- a/package.json +++ b/package.json @@ -87,11 +87,11 @@ "generate:package": "node utils/generate-package-json.js", "postinstall": "yarn run icon && yarn run plugins", "clean": "del-cli dist", - "build": "yarn run clean && electron-builder --win --mac --linux", - "build:linux": "yarn run clean && electron-builder --linux", - "build:mac": "yarn run clean && electron-builder --mac dmg:x64", - "build:mac:arm64": "yarn run clean && electron-builder --mac dmg:arm64", - "build:win": "yarn run clean && electron-builder --win", + "build": "yarn run clean && electron-builder --win --mac --linux -p never", + "build:linux": "yarn run clean && electron-builder --linux -p never", + "build:mac": "yarn run clean && electron-builder --mac dmg:x64 -p never", + "build:mac:arm64": "yarn run clean && electron-builder --mac dmg:arm64 -p never", + "build:win": "yarn run clean && electron-builder --win -p never", "lint": "xo", "changelog": "auto-changelog", "plugins": "yarn run plugin:adblocker && yarn run plugin:bypass-age-restrictions", @@ -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": "^4.1.0", "ytpl": "^2.3.0" }, "devDependencies": { diff --git a/plugins/captions-selector/front.js b/plugins/captions-selector/front.js index 6ed83d90..bd5203bb 100644 --- a/plugins/captions-selector/front.js +++ b/plugins/captions-selector/front.js @@ -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); + 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"); + + setTimeout(() => { + captionTrackList = api.getOption("captions", "tracklist"); + + captionsSettingsButton.style.display = captionTrackList?.length + ? "inline-block" + : "none"; + }, 250); + }); + captionsSettingsButton.onclick = async () => { - api.loadModule("captions"); - - const captionTrackList = api.getOption("captions", "tracklist"); - if (captionTrackList?.length) { const currentCaptionTrack = api.getOption("captions", "track"); let currentIndex = !currentCaptionTrack ? diff --git a/plugins/downloader/actions.js b/plugins/downloader/actions.js deleted file mode 100644 index 0d6c3426..00000000 --- a/plugins/downloader/actions.js +++ /dev/null @@ -1,11 +0,0 @@ -const CHANNEL = "downloader"; -const ACTIONS = { - ERROR: "error", - METADATA: "metadata", - PROGRESS: "progress", -}; - -module.exports = { - CHANNEL: CHANNEL, - ACTIONS: ACTIONS, -}; diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index daea62b2..6263930b 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -1,98 +1,534 @@ -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, ClientType } = require('youtubei.js'); +const ytpl = require('ytpl'); // REPLACE with youtubei getplaylist https://github.com/LuanRT/YouTube.js#getplaylistid -const sendError = (win, error) => { - win.setProgressBar(-1); // close progress bar - dialog.showMessageBox({ - type: "info", - buttons: ["OK"], - title: "Error in download!", - message: "Argh! Apologies, download failed…", - detail: error.toString(), - }); +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, + }, }; -let nowPlayingMetadata = {}; +const config = require('./config'); -function handle(win) { - injectCSS(win.webContents, join(__dirname, "style.css")); - registerCallback((info) => { - nowPlayingMetadata = info; - }); +/** @type {Innertube} */ +let yt; +let win; +let playingUrl = undefined; - 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); - } - }); +const sendError = (error, source) => { + win.setProgressBar(-1); // close progress bar + setBadge(0); // close badge + sendFeedback_(win); // reset feedback - 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 }; + const songNameMessage = source ? `\nin ${source}` : ''; + const cause = error.cause ? `\n\n${error.cause.toString()}` : ''; + const message = `${error.toString()}${songNameMessage}${cause}`; - try { - const coverBuffer = songMetadata.image && !songMetadata.image.isEmpty() ? - songMetadata.image.toPNG() : null; + console.error(message); + dialog.showMessageBox({ + type: 'info', + buttons: ['OK'], + title: 'Error in download!', + message: 'Argh! Apologies, download failed…', + detail: message, + }); +}; - const writer = new ID3Writer(songBuffer); +module.exports = async (win_) => { + win = win_; + injectCSS(win.webContents, join(__dirname, 'style.css')); - // Create the metadata tags - writer - .setFrame("TIT2", songMetadata.title) - .setFrame("TPE1", [songMetadata.artist]); - if (coverBuffer) { - writer.setFrame("APIC", { - type: 3, - data: coverBuffer, - description: "" - }); - } - if (isEnabled("lyrics-genius")) { - const lyrics = await fetchFromGenius(songMetadata); - if (lyrics) { - writer.setFrame("USLT", { - description: lyrics, - lyrics: lyrics, - }); - } - } - writer.addTag(); - fileBuffer = Buffer.from(writer.arrayBuffer); - } catch (error) { - sendError(win, error); - } + 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), + ); +}; - writeFileSync(filePath, fileBuffer); - // Notify the youtube-dl file - event.reply("add-metadata-done"); - }); +module.exports.downloadSong = downloadSong; +module.exports.downloadPlaylist = downloadPlaylist; + +async function downloadSong( + url, + playlistFolder = undefined, + trackId = undefined, + increasePlaylistProgress = () => {}, +) { + let resolvedName = undefined; + try { + await downloadSongUnsafe( + url, + name=>resolvedName=name, + playlistFolder, + trackId, + increasePlaylistProgress, + ); + } catch (error) { + sendError(error, resolvedName || url); + } } -module.exports = handle; -module.exports.sendError = sendError; +async function downloadSongUnsafe( + url, + setName, + 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); + let info = await yt.music.getInfo(id); + + if (!info) { + throw new Error('Video not found'); + } + + 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 + }`; + setName(name); + + let playabilityStatus = info.playability_status; + let bypassedResult = null; + if (playabilityStatus.status === "LOGIN_REQUIRED") { + // try to bypass the age restriction + bypassedResult = await getAndroidTvInfo(id); + playabilityStatus = bypassedResult.playability_status; + + if (playabilityStatus.status === "LOGIN_REQUIRED") { + throw new Error( + `[${playabilityStatus.status}] ${playabilityStatus.reason}`, + ); + } + + info = bypassedResult; + } + + if (playabilityStatus.status === "UNPLAYABLE") { + /** + * @typedef {import('youtubei.js/dist/src/parser/classes/PlayerErrorMessage').default} PlayerErrorMessage + * @type {PlayerErrorMessage} + */ + const errorScreen = playabilityStatus.error_screen; + throw new Error( + `[${playabilityStatus.status}] ${errorScreen.reason.text}: ${errorScreen.subreason.text}`, + ); + } + + 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 { + 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, safeVideoName); + } 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', metadata.title).setFrame('TPE1', [metadata.artist]); + if (metadata.album) { + writer.setFrame('TALB', metadata.album); + } + if (coverBuffer) { + writer.setFrame('APIC', { + type: 3, + data: coverBuffer, + description: '', + }); + } + if (isEnabled('lyrics-genius')) { + const lyrics = await fetchFromGenius(metadata); + if (lyrics) { + writer.setFrame('USLT', { + description: '', + lyrics: lyrics, + }); + } + } + if (metadata.trackId) { + writer.setFrame('TRCK', metadata.trackId); + } + writer.addTag(); + return Buffer.from(writer.arrayBuffer); + } catch (e) { + sendError(e, `${metadata.artist} - ${metadata.title}`); + } +} + +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, +}); + +// This is used to bypass age restrictions +const getAndroidTvInfo = async (id) => { + const innertube = await Innertube.create({ + clientType: ClientType.TV_EMBEDDED, + generate_session_locally: true, + retrieve_player: true, + }); + const info = await innertube.getBasicInfo(id, 'TV_EMBEDDED'); + // getInfo 404s with the bypass, so we use getBasicInfo instead + // that's fine as we only need the streaming data + return info; +} diff --git a/plugins/downloader/config.js b/plugins/downloader/config.js new file mode 100644 index 00000000..12c0384e --- /dev/null +++ b/plugins/downloader/config.js @@ -0,0 +1,3 @@ +const { PluginConfig } = require('../../config/dynamic'); +const config = new PluginConfig('downloader'); +module.exports = { ...config }; diff --git a/plugins/downloader/front.js b/plugins/downloader/front.js index 095d4968..e0ab119f 100644 --- a/plugins/downloader/front.js +++ b/plugins/downloader/front.js @@ -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"; + } + }); +}; diff --git a/plugins/downloader/menu.js b/plugins/downloader/menu.js index 622370a2..6d58bbf2 100644 --- a/plugins/downloader/menu.js +++ b/plugins/downloader/menu.js @@ -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 - ); - }); -} diff --git a/plugins/downloader/utils.js b/plugins/downloader/utils.js index 70160188..6b9c449b 100644 --- a/plugins/downloader/utils.js +++ b/plugins/downloader/utils.js @@ -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); } } diff --git a/plugins/downloader/youtube-dl.js b/plugins/downloader/youtube-dl.js deleted file mode 100644 index 8a7b0bf1..00000000 --- a/plugins/downloader/youtube-dl.js +++ /dev/null @@ -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 - ); - } -); diff --git a/plugins/in-app-menu/style.css b/plugins/in-app-menu/style.css index a5de9cf0..f0f31ed8 100644 --- a/plugins/in-app-menu/style.css +++ b/plugins/in-app-menu/style.css @@ -12,11 +12,18 @@ height: 75px !important; } -/* fixes top gap between nav-bar and browse-page */ +/* fix top gap between nav-bar and browse-page */ #browse-page { padding-top: 0 !important; } +/* 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_PRIVATELY_OWNED_CONTENT_LANDING_PAGE"] { + top: 50px; + position: relative; +} + /* remove window dragging for nav bar (conflict with titlebar drag) */ ytmusic-nav-bar, .tab-titleiron-icon, diff --git a/plugins/lyrics-genius/back.js b/plugins/lyrics-genius/back.js index 21e91811..cb781ca8 100644 --- a/plugins/lyrics-genius/back.js +++ b/plugins/lyrics-genius/back.js @@ -7,8 +7,13 @@ const fetch = require("node-fetch"); const { cleanupName } = require("../../providers/song-info"); const { injectCSS } = require("../utils"); +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) => { +module.exports = async (win, options) => { + if(options.romanizedLyrics) { + revRomanized = true; + } injectCSS(win.webContents, join(__dirname, "style.css")); ipcMain.on("search-genius-lyrics", async (event, extractedSongInfo) => { @@ -17,10 +22,41 @@ module.exports = async (win) => { }); }; +const toggleRomanized = () => { + revRomanized = !revRomanized; +}; + const fetchFromGenius = async (metadata) => { - const queryString = `${cleanupName(metadata.artist)} ${cleanupName( - metadata.title - )}`; + const songTitle = `${cleanupName(metadata.title)}`; + const songArtist = `${cleanupName(metadata.artist)}`; + let lyrics; + + /* Uses Regex to test the title and artist first for said characters if romanization is enabled. Otherwise normal + Genius Lyrics behavior is observed. + */ + let hasAsianChars = false; + if (revRomanized && (eastAsianChars.test(songTitle) || eastAsianChars.test(songArtist))) { + lyrics = await getLyricsList(`${songArtist} ${songTitle} Romanized`); + hasAsianChars = true; + } else { + lyrics = await getLyricsList(`${songArtist} ${songTitle}`); + } + + /* If the romanization toggle is on, and we did not detect any characters in the title or artist, we do a check + for characters in the lyrics themselves. If this check proves true, we search for Romanized lyrics. + */ + if(revRomanized && !hasAsianChars && eastAsianChars.test(lyrics)) { + lyrics = await getLyricsList(`${songArtist} ${songTitle} Romanized`); + } + return lyrics; +}; + +/** + * Fetches a JSON of songs which is then parsed and passed into getLyrics to get the lyrical content of the first song + * @param {*} queryString + * @returns The lyrics of the first song found using the Genius-Lyrics API + */ +const getLyricsList = async (queryString) => { let response = await fetch( `https://genius.com/api/search/multi?per_page=5&q=${encodeURIComponent(queryString)}` ); @@ -28,6 +64,9 @@ const fetchFromGenius = async (metadata) => { return null; } + /* Fetch the first URL with the api, giving a collection of song results. + Pick the first song, parsing the json given by the API. + */ const info = await response.json(); let url = ""; try { @@ -36,16 +75,23 @@ const fetchFromGenius = async (metadata) => { } catch { return null; } + let lyrics = await getLyrics(url); + return lyrics; +} - if (is.dev()) { - console.log("Fetching lyrics from Genius:", url); - } - +/** + * + * @param {*} url + * @returns The lyrics of the song URL provided, null if none + */ +const getLyrics = async (url) => { response = await fetch(url); if (!response.ok) { return null; } - + if (is.dev()) { + console.log("Fetching lyrics from Genius:", url); + } const html = await response.text(); const lyrics = convert(html, { baseElements: { @@ -64,8 +110,8 @@ const fetchFromGenius = async (metadata) => { }, }, }); - return lyrics; }; -module.exports.fetchFromGenius = fetchFromGenius; +module.exports.toggleRomanized = toggleRomanized; +module.exports.fetchFromGenius = fetchFromGenius; \ No newline at end of file diff --git a/plugins/lyrics-genius/menu.js b/plugins/lyrics-genius/menu.js new file mode 100644 index 00000000..5d8c390e --- /dev/null +++ b/plugins/lyrics-genius/menu.js @@ -0,0 +1,17 @@ +const { setOptions } = require("../../config/plugins"); +const { toggleRomanized } = require("./back"); + +module.exports = (win, options, refreshMenu) => { + return [ + { + label: "Romanized Lyrics", + type: "checkbox", + checked: options.romanizedLyrics, + click: (item) => { + options.romanizedLyrics = item.checked; + setOptions('lyrics-genius', options); + toggleRomanized(); + }, + }, + ]; +}; \ No newline at end of file diff --git a/plugins/notifications/back.js b/plugins/notifications/back.js index 419e27d3..385ecca7 100644 --- a/plugins/notifications/back.js +++ b/plugins/notifications/back.js @@ -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) : diff --git a/plugins/notifications/config.js b/plugins/notifications/config.js index 38d8100e..d0898dc3 100644 --- a/plugins/notifications/config.js +++ b/plugins/notifications/config.js @@ -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 }; diff --git a/preload.js b/preload.js index 492f3192..c91b0722 100644 --- a/preload.js +++ b/preload.js @@ -5,8 +5,12 @@ const { setupSongControls } = require("./providers/song-controls-front"); const { ipcRenderer } = require("electron"); const is = require("electron-is"); +const { startingPages } = require("./providers/extracted-data"); + const plugins = config.plugins.getEnabled(); +const $ = document.querySelector.bind(document); + let api; plugins.forEach(async ([plugin, options]) => { @@ -79,14 +83,14 @@ document.addEventListener("DOMContentLoaded", () => { }); function listenForApiLoad() { - api = document.querySelector('#movie_player'); + api = $('#movie_player'); if (api) { onApiLoaded(); return; } const observer = new MutationObserver(() => { - api = document.querySelector('#movie_player'); + api = $('#movie_player'); if (api) { observer.disconnect(); onApiLoaded(); @@ -97,7 +101,7 @@ function listenForApiLoad() { } function onApiLoaded() { - const video = document.querySelector("video"); + const video = $("video"); const audioContext = new AudioContext(); const audioSource = audioContext.createMediaElementSource(video); audioSource.connect(audioContext.destination); @@ -127,19 +131,31 @@ function onApiLoaded() { document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api })); ipcRenderer.send('apiLoaded'); + // Navigate to "Starting page" + const startingPage = config.get("options.startingPage"); + if (startingPage && startingPages[startingPage]) { + $('ytmusic-app')?.navigate_(startingPages[startingPage]); + } + // Remove upgrade button if (config.get("options.removeUpgradeButton")) { - const upgradeButton = document.querySelector('ytmusic-pivot-bar-item-renderer[tab-id="SPunlimited"]') + const upgradeButton = $('ytmusic-pivot-bar-item-renderer[tab-id="SPunlimited"]') if (upgradeButton) { upgradeButton.style.display = "none"; } } - // Force show like buttons - if (config.get("options.ForceShowLikeButtons")) { - const likeButtons = document.querySelector('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] || ""; } } } diff --git a/providers/extracted-data.js b/providers/extracted-data.js new file mode 100644 index 00000000..a7020a0d --- /dev/null +++ b/providers/extracted-data.js @@ -0,0 +1,23 @@ +const startingPages = { + Default: '', + Home: 'FEmusic_home', + Explore: 'FEmusic_explore', + 'New Releases': 'FEmusic_new_releases', + Charts: 'FEmusic_charts', + 'Moods & Genres': 'FEmusic_moods_and_genres', + Library: 'FEmusic_library_landing', + Playlists: 'FEmusic_liked_playlists', + Songs: 'FEmusic_liked_videos', + Albums: 'FEmusic_liked_albums', + Artists: 'FEmusic_library_corpus_track_artists', + 'Subscribed Artists': 'FEmusic_library_corpus_artists', + Uploads: 'FEmusic_library_privately_owned_landing', + 'Uploaded Playlists': 'FEmusic_liked_playlists', + 'Uploaded Songs': 'FEmusic_library_privately_owned_tracks', + 'Uploaded Albums': 'FEmusic_library_privately_owned_releases', + 'Uploaded Artists': 'FEmusic_library_privately_owned_artists', +}; + +module.exports = { + startingPages, +}; diff --git a/providers/song-info-front.js b/providers/song-info-front.js index 000928c6..69de3eae 100644 --- a/providers/song-info-front.js +++ b/providers/song-info-front.js @@ -68,7 +68,7 @@ module.exports = () => { apiEvent.detail.addEventListener('videodatachange', (name, _dataEvent) => { if (name !== 'dataloaded') return; video.dispatchEvent(srcChangedEvent); - sendSongInfo(); + setTimeout(sendSongInfo()); }) for (const status of ['playing', 'pause']) { @@ -87,7 +87,10 @@ module.exports = () => { data.videoDetails.album = $$( ".byline.ytmusic-player-bar > .yt-simple-endpoint" - ).find(e => e.href?.includes("browse"))?.textContent; + ).find(e => + e.href?.includes("browse/FEmusic_library_privately_owned_release") + || e.href?.includes("browse/MPREb") + )?.textContent; data.videoDetails.elapsedSeconds = Math.floor(video.currentTime); data.videoDetails.isPaused = false; diff --git a/tray.js b/tray.js index d8712d7e..46b76f83 100644 --- a/tray.js +++ b/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); diff --git a/yarn.lock b/yarn.lock index da6c5954..bfcc1a8a 100644 --- a/yarn.lock +++ b/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": - version: 2.3.2 - resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin::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: ^4.1.0 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:^4.1.0": + version: 4.1.0 + resolution: "youtubei.js@npm:4.1.0" 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: fa0090aa5b86c06a765757b0716ad9e5742c401b4fe662460db82495751e1fda3380b78f5fb916699f1707ab9b7c2783312dceac974afea3a5d101be62906bea languageName: node linkType: hard diff --git a/youtube-music.css b/youtube-music.css index ee48dc3a..c0d3cc4e 100644 --- a/youtube-music.css +++ b/youtube-music.css @@ -44,3 +44,9 @@ ytmusic-cast-button { .ytp-chrome-top-buttons { display: none !important; } + +/* Make youtube-music logo un-draggable */ +ytmusic-nav-bar>div.left-content>a, +ytmusic-nav-bar>div.left-content>a>picture>img { + -webkit-user-drag: none; +}