Merge branch 'master' into custom-electron-prompt

This commit is contained in:
Araxeus
2021-06-25 12:21:40 +03:00
committed by GitHub
26 changed files with 596 additions and 323 deletions

View File

@ -8,7 +8,9 @@ const SOURCES = [
"https://raw.githubusercontent.com/kbinani/adblock-youtube-ads/master/signed.txt",
// uBlock Origin
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt",
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-2020.txt",
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-2021.txt",
// Fanboy Annoyances
"https://secure.fanboy.co.nz/fanboy-annoyance_ubo.txt",
];
const loadAdBlockerEngine = (

View File

@ -1,25 +1,10 @@
let videoElement = null;
const { ontimeupdate } = require("../../providers/video-element");
const observer = new MutationObserver((mutations, observer) => {
if (!videoElement) {
videoElement = document.querySelector("video");
}
if (videoElement) {
videoElement.ontimeupdate = () => {
if (videoElement.currentTime === 0 && videoElement.duration !== NaN) {
// auto-confirm-when-paused plugin can interfere here if not disabled!
videoElement.pause();
}
};
}
});
function observeVideoElement() {
observer.observe(document, {
childList: true,
subtree: true,
module.exports = () => {
ontimeupdate((videoElement) => {
if (videoElement.currentTime === 0 && videoElement.duration !== NaN) {
// auto-confirm-when-paused plugin can interfere here if not disabled!
videoElement.pause();
}
});
}
module.exports = observeVideoElement;
};

View File

@ -1,6 +1,6 @@
const Discord = require("discord-rpc");
const getSongInfo = require("../../providers/song-info");
const registerCallback = require("../../providers/song-info");
const rpc = new Discord.Client({
transport: "ipc",
@ -12,8 +12,6 @@ const clientId = "790655993809338398";
let clearActivity;
module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => {
const registerCallback = getSongInfo(win);
// If the page is ready, register the callback
win.once("ready-to-show", () => {
rpc.once("ready", () => {

View File

@ -2,6 +2,7 @@ const CHANNEL = "downloader";
const ACTIONS = {
ERROR: "error",
METADATA: "metadata",
PROGRESS: "progress",
};
module.exports = {

View File

@ -4,38 +4,41 @@ const { join } = require("path");
const ID3Writer = require("browser-id3-writer");
const { dialog, ipcMain } = require("electron");
const getSongInfo = require("../../providers/song-info");
const registerCallback = 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,14 +47,17 @@ 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;
const coverBuffer = songMetadata.image && !songMetadata.image.isEmpty() ?
songMetadata.image.toPNG() : null;
const writer = new ID3Writer(songBuffer);
// Create the metadata tags
@ -62,7 +68,7 @@ function handle(win) {
writer.setFrame("APIC", {
type: 3,
data: coverBuffer,
description: "",
description: ""
});
}
writer.addTag();

View File

@ -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);

View File

@ -2,13 +2,13 @@ const { existsSync, mkdirSync } = require("fs");
const { join } = require("path");
const { URL } = require("url");
const { dialog, ipcMain } = require("electron");
const { dialog } = require("electron");
const is = require("electron-is");
const ytpl = require("ytpl");
const chokidar = require('chokidar');
const { setOptions } = require("../../config/plugins");
const getSongInfo = require("../../providers/song-info");
const registerCallback = require("../../providers/song-info");
const { sendError } = require("./back");
const { defaultMenuDownloadLabel, getFolder } = require("./utils");
@ -18,7 +18,6 @@ let callbackIsRegistered = false;
module.exports = (win, options) => {
if (!callbackIsRegistered) {
const registerCallback = getSongInfo(win);
registerCallback((info) => {
metadataURL = info.url;
});
@ -36,10 +35,16 @@ module.exports = (win, options) => {
return;
}
console.log("trying to get playlist ID" +playlistID);
const playlist = await ytpl(playlistID,
{ limit: options.playlistMaxItems || Infinity }
);
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);

View File

@ -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;
}

View File

@ -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) {

View File

@ -1,8 +1,8 @@
const fetch = require('node-fetch');
const md5 = require('md5');
const open = require("open");
const { shell } = require('electron');
const { setOptions } = require('../../config/plugins');
const getSongInfo = require('../../providers/song-info');
const registerCallback = require('../../providers/song-info');
const defaultConfig = require('../../config/defaults');
const createFormData = params => {
@ -58,7 +58,7 @@ const authenticate = async config => {
// asks the user for authentication
config.token = await createToken(config);
setOptions('last-fm', config);
open(`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`);
shell.openExternal(`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`);
return config;
}
@ -128,10 +128,8 @@ const setNowPlaying = (songInfo, config) => {
// this will store the timeout that will trigger addScrobble
let scrobbleTimer = undefined;
const lastfm = async (win, config) => {
const registerCallback = getSongInfo(win);
if (!config.api_root || !config.suffixesToRemove) {
const lastfm = async (_win, config) => {
if (!config.api_root) {
// settings are not present, creating them with the default values
config = defaultConfig.plugins['last-fm'];
config.enabled = true;

View File

@ -1,9 +1,9 @@
const { Notification } = require("electron");
const is = require("electron-is");
const getSongInfo = require("../../providers/song-info");
const registerCallback = require("../../providers/song-info");
const { notificationImage } = require("./utils");
const { setupInteractive, notifyInteractive } = require("./interactive")
const setupInteractive = require("./interactive")
const notify = (info, options) => {
@ -23,38 +23,24 @@ const notify = (info, options) => {
return currentNotification;
};
module.exports = (win, options) => {
const isInteractive = is.windows() && options.interactive;
//setup interactive notifications for windows
if (isInteractive) {
setupInteractive(win, options.unpauseNotification);
}
const registerCallback = getSongInfo(win);
const setup = (options) => {
let oldNotification;
let oldURL = "";
win.once("ready-to-show", () => {
// Register the callback for new song information
registerCallback(songInfo => {
// on pause - reset url? and skip notification
if (songInfo.isPaused) {
//reset oldURL if unpause notification option is on
if (options.unpauseNotification) {
oldURL = "";
}
return;
}
// If url isn't the same as last one - send notification
if (songInfo.url !== oldURL) {
oldURL = songInfo.url;
if (isInteractive) {
notifyInteractive(songInfo);
} else {
// Close the old notification
oldNotification?.close();
// This fixes a weird bug that would cause the notification to be updated instead of showing
setTimeout(() => { oldNotification = notify(songInfo, options) }, 10);
}
}
});
let currentUrl;
registerCallback(songInfo => {
if (!songInfo.isPaused && (songInfo.url !== currentUrl || options.unpauseNotification)) {
// Close the old notification
oldNotification?.close();
currentUrl = songInfo.url;
// This fixes a weird bug that would cause the notification to be updated instead of showing
setTimeout(() => { oldNotification = notify(songInfo, options) }, 10);
}
});
}
module.exports = (win, options) => {
// Register the callback for new song information
is.windows() && options.interactive ?
setupInteractive(win, options.unpauseNotification) :
setup(options);
};

View File

@ -1,17 +1,27 @@
const { notificationImage, icons } = require("./utils");
const getSongControls = require('../../providers/song-controls');
const registerCallback = require("../../providers/song-info");
const notifier = require("node-notifier");
//store song controls reference on launch
let controls;
let notificationOnPause;
let notificationOnUnpause;
//Save controls and onPause option
module.exports.setupInteractive = (win, unpauseNotification) => {
module.exports = (win, unpauseNotification) => {
//Save controls and onPause option
const { playPause, next, previous } = getSongControls(win);
controls = { playPause, next, previous };
notificationOnUnpause = unpauseNotification;
notificationOnPause = unpauseNotification;
let currentUrl;
// Register songInfoCallback
registerCallback(songInfo => {
if (!songInfo.isPaused && (songInfo.url !== currentUrl || notificationOnUnpause)) {
currentUrl = songInfo.url;
sendToaster(songInfo);
}
});
win.webContents.once("closed", () => {
deleteNotification()
@ -33,7 +43,7 @@ function deleteNotification() {
}
//New notification
module.exports.notifyInteractive = function sendToaster(songInfo) {
function sendToaster(songInfo) {
deleteNotification();
//download image and get path
let imgSrc = notificationImage(songInfo, true);
@ -71,7 +81,7 @@ module.exports.notifyInteractive = function sendToaster(songInfo) {
// dont delete notification on play/pause
toDelete = undefined;
//manually send notification if not sending automatically
if (!notificationOnPause) {
if (!notificationOnUnpause) {
songInfo.isPaused = false;
sendToaster(songInfo);
}

View File

@ -0,0 +1,51 @@
const fetch = require("node-fetch");
const defaultConfig = require("../../config/defaults");
const registerCallback = require("../../providers/song-info");
const { sortSegments } = require("./segments");
let videoID;
module.exports = (win, options) => {
const { apiURL, categories } = {
...defaultConfig.plugins.sponsorblock,
...options,
};
registerCallback(async (info) => {
const newURL = info.url || win.webContents.getURL();
const newVideoID = new URL(newURL).searchParams.get("v");
if (videoID !== newVideoID) {
videoID = newVideoID;
const segments = await fetchSegments(apiURL, categories);
win.webContents.send("sponsorblock-skip", segments);
}
});
};
const fetchSegments = async (apiURL, categories) => {
const sponsorBlockURL = `${apiURL}/api/skipSegments?videoID=${videoID}&categories=${JSON.stringify(
categories
)}`;
try {
const resp = await fetch(sponsorBlockURL, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
redirect: "follow",
});
if (resp.status !== 200) {
return [];
}
const segments = await resp.json();
const sortedSegments = sortSegments(
segments.map((submission) => submission.segment)
);
return sortedSegments;
} catch {
return [];
}
};

View File

@ -0,0 +1,27 @@
const { ipcRenderer } = require("electron");
const is = require("electron-is");
const { ontimeupdate } = require("../../providers/video-element");
let currentSegments = [];
module.exports = () => {
ipcRenderer.on("sponsorblock-skip", (_, segments) => {
currentSegments = segments;
});
ontimeupdate((videoElement) => {
currentSegments.forEach((segment) => {
if (
videoElement.currentTime >= segment[0] &&
videoElement.currentTime <= segment[1]
) {
videoElement.currentTime = segment[1];
if (is.dev()) {
console.log("SponsorBlock: skipping segment", segment);
}
}
});
});
};

View File

@ -0,0 +1,29 @@
// Segments are an array [ [start, end], … ]
module.exports.sortSegments = (segments) => {
segments.sort((segment1, segment2) =>
segment1[0] === segment2[0]
? segment1[1] - segment2[1]
: segment1[0] - segment2[0]
);
const compiledSegments = [];
let currentSegment;
segments.forEach((segment) => {
if (!currentSegment) {
currentSegment = segment;
return;
}
if (currentSegment[1] < segment[0]) {
compiledSegments.push(currentSegment);
currentSegment = segment;
return;
}
currentSegment[1] = Math.max(currentSegment[1], segment[1]);
});
compiledSegments.push(currentSegment);
return compiledSegments;
};

View File

@ -0,0 +1,34 @@
const { sortSegments } = require("../segments");
test("Segment sorting", () => {
expect(
sortSegments([
[0, 3],
[7, 8],
[5, 6],
])
).toEqual([
[0, 3],
[5, 6],
[7, 8],
]);
expect(
sortSegments([
[0, 5],
[6, 8],
[4, 6],
])
).toEqual([[0, 8]]);
expect(
sortSegments([
[0, 6],
[7, 8],
[4, 6],
])
).toEqual([
[0, 6],
[7, 8],
]);
});

View File

@ -1,12 +1,11 @@
const getSongControls = require('../../providers/song-controls');
const getSongInfo = require('../../providers/song-info');
const registerCallback = require('../../providers/song-info');
const path = require('path');
let controls;
let currentSongInfo;
module.exports = win => {
const registerCallback = getSongInfo(win);
const { playPause, next, previous } = getSongControls(win);
controls = { playPause, next, previous };

View File

@ -7,7 +7,7 @@ const {
TouchBarScrubber,
} = TouchBar;
const getSongInfo = require("../../providers/song-info");
const registerCallback = require("../../providers/song-info");
const getSongControls = require("../../providers/song-controls");
// Songtitle label
@ -59,7 +59,6 @@ const touchBar = new TouchBar({
});
module.exports = (win) => {
const registerCallback = getSongInfo(win);
const { playPause, next, previous, like, dislike } = getSongControls(win);
// If the page is ready, register the callback