diff --git a/plugins/downloader/actions.js b/plugins/downloader/actions.js index da75f181..0d6c3426 100644 --- a/plugins/downloader/actions.js +++ b/plugins/downloader/actions.js @@ -2,6 +2,7 @@ const CHANNEL = "downloader"; const ACTIONS = { ERROR: "error", METADATA: "metadata", + PROGRESS: "progress", }; module.exports = { diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index ac32623d..76a77b92 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -6,36 +6,40 @@ const { dialog, ipcMain } = require("electron"); const getSongInfo = require("../../providers/song-info"); const { injectCSS, listenAction } = require("../utils"); +const { cropMaxWidth } = require("./utils"); const { ACTIONS, CHANNEL } = require("./actions.js"); const { getImage } = require("../../providers/song-info"); -const sendError = (win, err) => { - const dialogOpts = { +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: err.toString(), - }; - dialog.showMessageBox(dialogOpts); + detail: error.toString(), + }); }; -let metadata = {}; +let nowPlayingMetadata = {}; function handle(win) { injectCSS(win.webContents, join(__dirname, "style.css")); const registerCallback = getSongInfo(win); registerCallback((info) => { - metadata = info; + nowPlayingMetadata = info; }); - listenAction(CHANNEL, (event, action, error) => { + listenAction(CHANNEL, (event, action, arg) => { switch (action) { - case ACTIONS.ERROR: - sendError(win, error); + case ACTIONS.ERROR: // arg = error + sendError(win, arg); break; case ACTIONS.METADATA: - event.returnValue = JSON.stringify(metadata); + event.returnValue = JSON.stringify(nowPlayingMetadata); + break; + case ACTIONS.PROGRESS: // arg = progress + win.setProgressBar(arg); break; default: console.log("Unknown action: " + action); @@ -44,11 +48,12 @@ function handle(win) { ipcMain.on("add-metadata", async (event, filePath, songBuffer, currentMetadata) => { let fileBuffer = songBuffer; - const songMetadata = { ...metadata, ...currentMetadata }; - - if (!songMetadata.image && songMetadata.imageSrc) { - songMetadata.image = await getImage(songMetadata.imageSrc); - } + const songMetadata = currentMetadata.imageSrcYTPL ? // This means metadata come from ytpl.getInfo(); + { + ...currentMetadata, + image: cropMaxWidth(await getImage(currentMetadata.imageSrcYTPL)) + } : + { ...nowPlayingMetadata, ...currentMetadata }; try { const coverBuffer = songMetadata.image ? songMetadata.image.toPNG() : null; @@ -62,7 +67,7 @@ function handle(win) { writer.setFrame("APIC", { type: 3, data: coverBuffer, - description: "", + description: "" }); } writer.addTag(); diff --git a/plugins/downloader/front.js b/plugins/downloader/front.js index 200a7e15..879ba222 100644 --- a/plugins/downloader/front.js +++ b/plugins/downloader/front.js @@ -25,6 +25,7 @@ const observer = new MutationObserver((mutations, observer) => { }); const reinit = () => { + triggerAction(CHANNEL, ACTIONS.PROGRESS, -1); // closes progress bar if (!progress) { console.warn("Cannot update progress"); } else { @@ -38,11 +39,12 @@ const baseUrl = defaultConfig.url; // contextBridge.exposeInMainWorld("downloader", { // download: () => { global.download = () => { + triggerAction(CHANNEL, ACTIONS.PROGRESS, 2); // starts with indefinite progress bar let metadata; let videoUrl = getSongMenu() - .querySelector("ytmusic-menu-navigation-item-renderer") - .querySelector("#navigation-endpoint") - .getAttribute("href"); + // selector of first button which is always "Start Radio" + ?.querySelector('ytmusic-menu-navigation-item-renderer.iron-selected[tabindex="0"] #navigation-endpoint') + ?.getAttribute("href"); if (videoUrl) { videoUrl = baseUrl + "/" + videoUrl; metadata = null; @@ -53,12 +55,15 @@ global.download = () => { downloadVideoToMP3( videoUrl, - (feedback) => { + (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); diff --git a/plugins/downloader/utils.js b/plugins/downloader/utils.js index e2763541..8408456d 100644 --- a/plugins/downloader/utils.js +++ b/plugins/downloader/utils.js @@ -3,3 +3,29 @@ const electron = require("electron"); module.exports.getFolder = (customFolder) => customFolder || (electron.app || electron.remote.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.cropMaxWidth = (image) => { + const imageSize = image.getSize(); + // standart youtube artwork width with margins from both sides is 280 + 720 + 280 + if (imageSize.width === 1280 && imageSize.height === 720) { + return image.crop({ + x: 280, + y: 0, + width: 720, + height: 720 + }); + } + return image; +} diff --git a/plugins/downloader/youtube-dl.js b/plugins/downloader/youtube-dl.js index 814d9924..7e54195a 100644 --- a/plugins/downloader/youtube-dl.js +++ b/plugins/downloader/youtube-dl.js @@ -14,7 +14,7 @@ const ytdl = require("ytdl-core"); const { triggerAction, triggerActionSync } = require("../utils"); const { ACTIONS, CHANNEL } = require("./actions.js"); -const { getFolder } = require("./utils"); +const { getFolder, UrlToJPG } = require("./utils"); const { cleanupArtistName } = require("../../providers/song-info"); const { createFFmpeg } = FFmpeg; @@ -37,12 +37,14 @@ const downloadVideoToMP3 = async ( sendFeedback("Downloading…"); if (metadata === null) { - const info = await ytdl.getInfo(videoUrl); - const thumbnails = info.videoDetails?.author?.thumbnails; + const { videoDetails } = await ytdl.getInfo(videoUrl); + const thumbnails = videoDetails?.thumbnails; metadata = { - artist: info.videoDetails?.media?.artist || cleanupArtistName(info.videoDetails?.author?.name) || "", - title: info.videoDetails?.media?.song || info.videoDetails?.title || "", - imageSrc: thumbnails ? thumbnails[thumbnails.length - 1].url : "" + artist: videoDetails?.media?.artist || cleanupArtistName(videoDetails?.author?.name) || "", + title: videoDetails?.media?.song || videoDetails?.title || "", + imageSrcYTPL: thumbnails ? + UrlToJPG(thumbnails[thumbnails.length - 1].url, videoDetails?.videoId) + : "" } } @@ -65,9 +67,10 @@ const downloadVideoToMP3 = async ( .on("data", (chunk) => { chunks.push(chunk); }) - .on("progress", (chunkLength, downloaded, total) => { - const progress = Math.floor((downloaded / total) * 100); - sendFeedback("Download: " + progress + "%"); + .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.replace("|", "").toString("ascii"); @@ -112,7 +115,7 @@ const toMP3 = async ( try { if (!ffmpeg.isLoaded()) { - sendFeedback("Loading…"); + sendFeedback("Loading…", 2); // indefinite progress bar after download await ffmpeg.load(); } @@ -146,7 +149,7 @@ const toMP3 = async ( ipcRenderer.send("add-metadata", filePath, fileBuffer, { artist: metadata.artist, title: metadata.title, - imageSrc: metadata.imageSrc + imageSrcYTPL: metadata.imageSrcYTPL }); ipcRenderer.once("add-metadata-done", reinit); } catch (e) { diff --git a/providers/song-info.js b/providers/song-info.js index 15b51459..5701d1f3 100644 --- a/providers/song-info.js +++ b/providers/song-info.js @@ -30,15 +30,10 @@ const getPausedStatus = async (win) => { }; const getArtist = async (win) => { - return await win.webContents.executeJavaScript( - ` - var bar = document.getElementsByClassName('subtitle ytmusic-player-bar')[0]; - var artistName = (bar.getElementsByClassName('yt-formatted-string')[0]) || (bar.getElementsByClassName('byline ytmusic-player-bar')[0]); - if (artistName) { - artistName.textContent; - } - ` - ); + return await win.webContents.executeJavaScript(` + document.querySelector(".subtitle.ytmusic-player-bar .yt-formatted-string") + ?.textContent + `); } // Fill songInfo with empty values @@ -57,8 +52,8 @@ const songInfo = { const handleData = async (responseText, win) => { let data = JSON.parse(responseText); - songInfo.title = data.videoDetails?.media?.song || data?.videoDetails?.title; - songInfo.artist = data.videoDetails?.media?.artist || await getArtist(win) || cleanupArtistName(data?.videoDetails?.author); + songInfo.title = data?.videoDetails?.title; + songInfo.artist = await getArtist(win) || cleanupArtistName(data?.videoDetails?.author); songInfo.views = data?.videoDetails?.viewCount; songInfo.imageSrc = data?.videoDetails?.thumbnail?.thumbnails?.pop()?.url; songInfo.songDuration = data?.videoDetails?.lengthSeconds;