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/plugins/downloader/front.js b/plugins/downloader/front.js index 879ba222..095d4968 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 = () => { @@ -43,10 +45,16 @@ 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) { - 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..e519055c 100644 --- a/plugins/downloader/menu.js +++ b/plugins/downloader/menu.js @@ -11,90 +11,33 @@ const { sendError } = require("./back"); const { defaultMenuDownloadLabel, getFolder, presets } = require("./utils"); let downloadLabel = defaultMenuDownloadLabel; -let playingPlaylistId = undefined; +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) => { - 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; } 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 +66,83 @@ module.exports = (win, options) => { }, ]; }; + +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 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) : "" } 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(); 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"