Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Downloader tweaks + taskbar progress bar #265

Merged
merged 14 commits into from
May 8, 2021
1 change: 1 addition & 0 deletions plugins/downloader/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const CHANNEL = "downloader";
const ACTIONS = {
ERROR: "error",
METADATA: "metadata",
PROGRESS: "progress",
};

module.exports = {
Expand Down
39 changes: 22 additions & 17 deletions plugins/downloader/back.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -62,7 +67,7 @@ function handle(win) {
writer.setFrame("APIC", {
type: 3,
data: coverBuffer,
description: "",
description: ""
});
}
writer.addTag();
Expand Down
13 changes: 9 additions & 4 deletions plugins/downloader/front.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand All @@ -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);
Expand Down
26 changes: 26 additions & 0 deletions plugins/downloader/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
module.exports.UrlToJPG = (imgUrl, videoId) => {
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`;
}
Comment on lines +7 to +17
Copy link
Collaborator Author

@Araxeus Araxeus May 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function will 99.9% of the time:
return a maxresdefault webp converted to jpg (quite frequent with newer videos)
or return the original url (is jpg)

0.1% chance (might even be 0%)
to do more than 1 iteration over qualityList (will only happen if original doesn't contains .jpg or maxresdefault)

This was mostly coded this way to 100% guarantee a jpg url output

note that this function is only applicable to thumbnails from ytpl.getInfo and not from XMLHttpRequest (song-info provider), since they are stored in a different source and in completely different formats


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;
}
25 changes: 14 additions & 11 deletions plugins/downloader/youtube-dl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
: ""
}
}

Expand All @@ -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");
Expand Down Expand Up @@ -112,7 +115,7 @@ const toMP3 = async (

try {
if (!ffmpeg.isLoaded()) {
sendFeedback("Loading…");
sendFeedback("Loading…", 2); // indefinite progress bar after download
await ffmpeg.load();
}

Expand Down Expand Up @@ -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) {
Expand Down
17 changes: 6 additions & 11 deletions providers/song-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Comment on lines -60 to -61
Copy link
Collaborator Author

@Araxeus Araxeus May 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

videoDetails.media can only appear in data parsed from ytpl.getInfo()
(never from XHR, I shouldn't have added it here in the first place)

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;
Expand Down