From 5e68d2487ff068ddf21b5562ea1dbaff0faf8f73 Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Tue, 11 Jan 2022 22:44:06 +0200 Subject: [PATCH 1/4] allow downloading playlists from popup menu --- plugins/downloader/front.js | 33 ++++--- plugins/downloader/menu.js | 158 +++++++++++++++++-------------- plugins/downloader/youtube-dl.js | 2 +- 3 files changed, 111 insertions(+), 82 deletions(-) diff --git a/plugins/downloader/front.js b/plugins/downloader/front.js index 879ba222..c8731935 100644 --- a/plugins/downloader/front.js +++ b/plugins/downloader/front.js @@ -1,4 +1,4 @@ -const { contextBridge } = require("electron"); +const { ipcRenderer } = require("electron"); const { defaultConfig } = require("../../config"); const { getSongMenu } = require("../../providers/dom-elements"); @@ -13,15 +13,17 @@ const downloadButton = ElementFromFile( ); let pluginOptions = {}; -const observer = new MutationObserver((mutations, observer) => { +const observer = 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 (menu && !menu.contains(downloadButton)) { - menu.prepend(downloadButton); - progress = document.querySelector("#ytmcustom-download"); - } + menu.prepend(downloadButton); + progress = document.querySelector("#ytmcustom-download"); }); const reinit = () => { @@ -46,7 +48,13 @@ global.download = () => { ?.querySelector('ytmusic-menu-navigation-item-renderer.iron-selected[tabindex="0"] #navigation-endpoint') ?.getAttribute("href"); if (videoUrl) { - videoUrl = baseUrl + "/" + videoUrl; + if (videoUrl.startsWith('watch?')) { + videoUrl = baseUrl + "/" + videoUrl; + } + if (videoUrl.includes('?playlist=')) { + ipcRenderer.send('download-playlist-request', videoUrl); + return; + } metadata = null; } else { metadata = global.songInfo; @@ -78,10 +86,13 @@ global.download = () => { function observeMenu(options) { pluginOptions = { ...pluginOptions, ...options }; - observer.observe(document, { - childList: true, - subtree: true, - }); + + document.addEventListener('apiLoaded', () => { + observer.observe(document.querySelector('ytmusic-popup-container'), { + childList: true, + subtree: true, + }); + }, { once: true, passive: true }) } module.exports = observeMenu; diff --git a/plugins/downloader/menu.js b/plugins/downloader/menu.js index a1ae503a..449fa2c3 100644 --- a/plugins/downloader/menu.js +++ b/plugins/downloader/menu.js @@ -14,87 +14,21 @@ let downloadLabel = defaultMenuDownloadLabel; let playingPlaylistId = undefined; let callbackIsRegistered = false; +const INVALID_PLAYLIST_MODIFIER = 'RDAMPLPL'; + module.exports = (win, options) => { if (!callbackIsRegistered) { ipcMain.on("video-src-changed", async (_, data) => { playingPlaylistId = JSON.parse(data)?.videoDetails?.playlistId; }); + ipcMain.on("download-playlist-request", async (_event, url) => downloadPlaylist(url, win, options)); callbackIsRegistered = true; } return [ { label: downloadLabel, - click: async () => { - const currentPagePlaylistId = new URL(win.webContents.getURL()).searchParams.get("list"); - const playlistId = currentPagePlaylistId || playingPlaylistId; - 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 playlistTitle = playlist.title; - - const folder = getFolder(options.downloadFolder); - const playlistFolder = join(folder, playlistTitle); - if (existsSync(playlistFolder)) { - sendError( - win, - new Error(`The folder ${playlistFolder} already exists`) - ); - return; - } - mkdirSync(playlistFolder, { recursive: true }); - - dialog.showMessageBox({ - type: "info", - buttons: ["OK"], - title: "Started Download", - message: `Downloading Playlist "${playlistTitle}"`, - detail: `(${playlist.items.length} songs)`, - }); - - if (is.dev()) { - console.log( - `Downloading playlist "${playlistTitle}" (${playlist.items.length} songs)` - ); - } - - const steps = 1 / playlist.items.length; - let progress = 0; - - win.setProgressBar(2); // starts with indefinite bar - - let dirWatcher = chokidar.watch(playlistFolder); - dirWatcher.on('add', () => { - progress += steps; - if (progress >= 0.9999) { - win.setProgressBar(-1); // close progress bar - dirWatcher.close().then(() => dirWatcher = null); - } else { - win.setProgressBar(progress); - } - }); - - playlist.items.forEach((song) => { - win.webContents.send( - "downloader-download-playlist", - song.url, - playlistTitle, - options - ); - }); - }, + click: () => downloadPlaylist(undefined, win, options) }, { label: "Choose download folder", @@ -123,3 +57,87 @@ module.exports = (win, options) => { }, ]; }; + +async function downloadPlaylist(url, win, options) { + const getPlaylistID = aURL => { + const result = aURL?.searchParams.get("list") || aURL?.searchParams.get("playlist"); + return (!result || result.startsWith(INVALID_PLAYLIST_MODIFIER)) ? undefined : result; + }; + if (url) { + try { + url = new URL(url); + } catch { + url = undefined; + }; + } + const playlistId = getPlaylistID(url) + || getPlaylistID(new URL(win.webContents.getURL())) + || playingPlaylistId; + + 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 playlistTitle = playlist.title; + + const folder = getFolder(options.downloadFolder); + const playlistFolder = join(folder, playlistTitle); + if (existsSync(playlistFolder)) { + sendError( + win, + new Error(`The folder ${playlistFolder} already exists`) + ); + return; + } + mkdirSync(playlistFolder, { recursive: true }); + + dialog.showMessageBox({ + type: "info", + buttons: ["OK"], + title: "Started Download", + message: `Downloading Playlist "${playlistTitle}"`, + detail: `(${playlist.items.length} songs)`, + }); + + if (is.dev()) { + console.log( + `Downloading playlist "${playlistTitle}" - ${playlist.items.length} songs (${playlistId})` + ); + } + + const steps = 1 / playlist.items.length; + let progress = 0; + + win.setProgressBar(2); // starts with indefinite bar + + let dirWatcher = chokidar.watch(playlistFolder); + dirWatcher.on('add', () => { + progress += steps; + if (progress >= 0.9999) { + win.setProgressBar(-1); // close progress bar + dirWatcher.close().then(() => dirWatcher = null); + } else { + win.setProgressBar(progress); + } + }); + + playlist.items.forEach((song) => { + win.webContents.send( + "downloader-download-playlist", + song.url, + playlistTitle, + options + ); + }); +} diff --git a/plugins/downloader/youtube-dl.js b/plugins/downloader/youtube-dl.js index a8194b0b..d7f34ff4 100644 --- a/plugins/downloader/youtube-dl.js +++ b/plugins/downloader/youtube-dl.js @@ -46,7 +46,7 @@ const downloadVideoToMP3 = async ( cleanupName(videoDetails?.author?.name) || "", title: videoDetails?.media?.song || videoDetails?.title || "", - imageSrcYTPL: thumbnails ? + imageSrcYTPL: thumbnails ? urlToJPG(thumbnails[thumbnails.length - 1].url, videoDetails?.videoId) : "" } From 21c149efc7b4432e111f503e225d92ca88d29262 Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Tue, 11 Jan 2022 22:54:41 +0200 Subject: [PATCH 2/4] update ytdl-core --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a278f29a..6e5691be 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "mpris-service": "^2.1.2", "node-fetch": "^2.6.6", "node-notifier": "^9.0.1", - "ytdl-core": "^4.9.2", + "ytdl-core": "^4.10.0", "ytpl": "^2.2.3" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 9c24957f..16cad532 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8361,10 +8361,10 @@ yocto-queue@^1.0.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== -ytdl-core@^4.9.2: - version "4.9.2" - resolved "https://registry.yarnpkg.com/ytdl-core/-/ytdl-core-4.9.2.tgz#c2d1ec44ee3cabff35e5843c6831755e69ffacf0" - integrity sha512-aTlsvsN++03MuOtyVD4DRF9Z/9UAeeuiNbjs+LjQBAiw4Hrdp48T3U9vAmRPyvREzupraY8pqRoBfKGqpq+eHA== +ytdl-core@^4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/ytdl-core/-/ytdl-core-4.10.0.tgz#0835cb411677684539fac2bcc10553f6f58db3e1" + integrity sha512-RCCoSVTmMeBPH5NFR1fh3nkDU9okvWM0ZdN6plw6I5+vBBZVUEpOt8vjbSgprLRMmGUsmrQZJhvG1CHOat4mLA== dependencies: m3u8stream "^0.8.4" miniget "^4.0.0" From c7e793b66ec734bd08b6012ac67b1435381927ec Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Tue, 11 Jan 2022 23:41:17 +0200 Subject: [PATCH 3/4] fix playback speed slider showing up where it shouldn't --- plugins/playback-speed/front.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/playback-speed/front.js b/plugins/playback-speed/front.js index c6cd1316..5755ad19 100644 --- a/plugins/playback-speed/front.js +++ b/plugins/playback-speed/front.js @@ -30,7 +30,7 @@ const observePopupContainer = () => { menu = getSongMenu(); } - if (menu && !menu.contains(slider)) { + if (menu && menu.lastElementChild.lastElementChild.innerText.startsWith('Stats') && !menu.contains(slider)) { menu.prepend(slider); if (!observingSlider) { setupSliderListener(); From 74f61a532d6481683d07c16e739bb3271716e1f4 Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Wed, 12 Jan 2022 09:49:27 +0200 Subject: [PATCH 4/4] downloader lint&refactor --- plugins/downloader/front.js | 2 +- plugins/downloader/menu.js | 31 ++++++++++++++++++------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/plugins/downloader/front.js b/plugins/downloader/front.js index c8731935..095d4968 100644 --- a/plugins/downloader/front.js +++ b/plugins/downloader/front.js @@ -45,7 +45,7 @@ global.download = () => { let metadata; let videoUrl = getSongMenu() // selector of first button which is always "Start Radio" - ?.querySelector('ytmusic-menu-navigation-item-renderer.iron-selected[tabindex="0"] #navigation-endpoint') + ?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint') ?.getAttribute("href"); if (videoUrl) { if (videoUrl.startsWith('watch?')) { diff --git a/plugins/downloader/menu.js b/plugins/downloader/menu.js index 449fa2c3..e519055c 100644 --- a/plugins/downloader/menu.js +++ b/plugins/downloader/menu.js @@ -11,15 +11,24 @@ const { sendError } = require("./back"); const { defaultMenuDownloadLabel, getFolder, presets } = require("./utils"); let downloadLabel = defaultMenuDownloadLabel; -let playingPlaylistId = undefined; +let playingUrl = undefined; let callbackIsRegistered = false; -const INVALID_PLAYLIST_MODIFIER = 'RDAMPLPL'; +// 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) => { - playingPlaylistId = JSON.parse(data)?.videoDetails?.playlistId; + playingUrl = JSON.parse(data)?.microformat?.microformatDataRenderer?.urlCanonical; }); ipcMain.on("download-playlist-request", async (_event, url) => downloadPlaylist(url, win, options)); callbackIsRegistered = true; @@ -58,21 +67,17 @@ module.exports = (win, options) => { ]; }; -async function downloadPlaylist(url, win, options) { - const getPlaylistID = aURL => { - const result = aURL?.searchParams.get("list") || aURL?.searchParams.get("playlist"); - return (!result || result.startsWith(INVALID_PLAYLIST_MODIFIER)) ? undefined : result; - }; - if (url) { +async function downloadPlaylist(givenUrl, win, options) { + if (givenUrl) { try { - url = new URL(url); + givenUrl = new URL(givenUrl); } catch { - url = undefined; + givenUrl = undefined; }; } - const playlistId = getPlaylistID(url) + const playlistId = getPlaylistID(givenUrl) || getPlaylistID(new URL(win.webContents.getURL())) - || playingPlaylistId; + || getPlaylistID(new URL(playingUrl)); if (!playlistId) { sendError(win, new Error("No playlist ID found"));