Compare commits

...

42 Commits

Author SHA1 Message Date
TC
644950ccc0 Bump version 2021-01-15 09:31:34 +01:00
TC
d848937e6a Rename discord plugin 2021-01-14 23:16:59 +01:00
446aa8becc Merge pull request #121 from th-ch/snyk-upgrade-d29b51f488ba9deb228b72952a3f7b8e
[Snyk] Upgrade electron-debug from 3.1.0 to 3.2.0
2021-01-14 23:12:27 +01:00
TC
d52dbee13c Prettier utils 2021-01-14 23:09:16 +01:00
TC
ed09304ed7 Refactor tray to use provider + play/pause on click 2021-01-14 23:09:04 +01:00
TC
2861473097 Set title/artist metadata in downloader 2021-01-14 23:01:26 +01:00
25fd48697b Merge pull request #125 from th-ch/refactor-providers
Refactor providers
2021-01-14 21:03:26 +01:00
TC
aec542e95e Update discord plugin for new provider + wait for ready 2021-01-13 22:22:22 +01:00
TC
eae95befe1 Merge branch 'master' of github.com:th-ch/youtube-music into refactor-providers
# By TC (9) and semvis123 (2)
# Via GitHub (4) and semvis123 (1)
* 'master' of github.com:th-ch/youtube-music:
  renamed DiscordRPC to Discord
  Downloader plugin: log audio bitrate
  Disable context isolation (to load ffmpeg wasm)
  Use contextBridge in preload script + update navigation plugin
  Fix downloader plugin with context isolation
  Bump version
  Add portable target to windows builds
  Added Discord rich presence and added extra properties to songinfo provider
  Bump version
  Allow custom audio extensions in downloader
  Defensive: handle null/undefined ffmpeg args
2021-01-13 22:10:10 +01:00
f0200e7b38 Merge pull request #124 from semvis123/master
Added Discord rich presence and added extra properties to songInfo provider
2021-01-13 22:08:10 +01:00
70775f4988 renamed DiscordRPC to Discord 2021-01-13 21:56:53 +01:00
074840ef56 Merge pull request #127 from th-ch/fix-plugins-context-isolation
Fix plugins with context isolation
2021-01-13 21:44:37 +01:00
TC
b54c501eeb Downloader plugin: log audio bitrate 2021-01-13 21:36:16 +01:00
6d587cb432 Merge pull request #126 from th-ch/windows-portable
Windows portable exe
2021-01-13 21:32:53 +01:00
TC
39c8031cd7 Disable context isolation (to load ffmpeg wasm) 2021-01-13 21:28:42 +01:00
TC
9ad1dad6df Use contextBridge in preload script + update navigation plugin 2021-01-12 22:54:04 +01:00
TC
79e8fc2fac Fix downloader plugin with context isolation 2021-01-12 22:52:21 +01:00
TC
67c4422eb8 Bump version 2021-01-12 21:37:02 +01:00
TC
18df1223af Add portable target to windows builds 2021-01-12 21:36:38 +01:00
0fafed7c53 Merge pull request #118 from th-ch/dl-custom-audio-format
Downloader plugin - custom audio format
2021-01-12 21:35:55 +01:00
TC
f7cbf2c221 No autoloading of providers (loaded on demand in plugins) 2021-01-12 21:19:01 +01:00
TC
87d2693e2b Use refactored provider in shortcuts plugin 2021-01-12 21:18:32 +01:00
TC
de1e4196d9 Use refactored provider in notification plugin 2021-01-12 21:18:05 +01:00
TC
9110e79c16 Use refactored providers in touchbar plugin 2021-01-12 21:17:40 +01:00
TC
0743034de0 Split providers in 2 2021-01-12 21:17:08 +01:00
TC
f1ddb92886 nit: prettier 2021-01-12 21:16:29 +01:00
a8ce87f2cc Added Discord rich presence and added extra properties to songinfo provider 2021-01-12 20:16:49 +01:00
86a329a61b fix: upgrade electron-debug from 3.1.0 to 3.2.0
Snyk has created this PR to upgrade electron-debug from 3.1.0 to 3.2.0.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=upgrade-pr
2021-01-12 02:24:30 +00:00
77e24f41a5 Merge pull request #102 from semvis123/master
Globalized the song info and song controls, and updated Touch Bar for it.
2021-01-10 22:15:11 +01:00
3a5d9bd973 Loads providers before plugins 2021-01-10 21:37:50 +01:00
69f486d53f moved the song info file and removed the capital letters in folder name 2021-01-10 21:22:01 +01:00
5d89043884 fixed typo, plugins/songInfo/back.js
Co-authored-by: th-ch <th-ch@users.noreply.github.com>
2021-01-10 20:31:55 +01:00
2b297c245a Merge pull request #120 from th-ch/bump-electron
Bump electron to v11
2021-01-10 14:37:07 +01:00
TC
945a61fafd Remove warning by setting contextIsolation 2021-01-10 14:10:16 +01:00
TC
1ba166a172 Bump electron to v11 2021-01-10 12:29:12 +01:00
TC
a9a840b6c3 Bump version 2021-01-08 22:41:17 +01:00
TC
6a100c8cb1 Allow custom audio extensions in downloader 2021-01-08 22:29:25 +01:00
TC
b04e2ea130 Defensive: handle null/undefined ffmpeg args 2021-01-08 22:26:44 +01:00
588e0019d6 Changed function style in notifications 2020-12-21 22:11:23 +01:00
5bffdbd628 Simplifies the notification plugin to use the globalized song info 2020-12-21 21:35:02 +01:00
ee239da647 removed unnecessary await keyword 2020-12-21 21:11:00 +01:00
9be3e1afe9 Globalized the songinfo and song controls, and changed the pause/play button.
Globalized the songinfo and song controls, and changed the pause/play button. The songInfo file should eventually get another location, because it isn't really a plugin.
2020-12-21 18:18:34 +01:00
24 changed files with 556 additions and 406 deletions

View File

@ -12,6 +12,7 @@ const defaultConfig = {
startAtLogin: false,
disableHardwareAcceleration: false,
restartOnConfigChanges: false,
trayClickPlayPause: false,
},
plugins: {
// Enabled plugins

View File

@ -75,12 +75,21 @@ function createMainWindow() {
backgroundColor: "#000",
show: false,
webPreferences: {
nodeIntegration: isTesting(), // Only necessary when testing with Spectron
// TODO: re-enable contextIsolation once it can work with ffmepg.wasm
// Possible bundling? https://github.com/ffmpegwasm/ffmpeg.wasm/issues/126
contextIsolation: false,
preload: path.join(__dirname, "preload.js"),
nodeIntegrationInSubFrames: true,
nativeWindowOpen: true, // window.open return Window object(like in regular browsers), not BrowserWindowProxy
enableRemoteModule: true,
affinity: "main-window", // main window, and addition windows should work in one process
...(isTesting()
? {
// Only necessary when testing with Spectron
contextIsolation: false,
nodeIntegration: true,
}
: undefined),
},
frame: !is.macOS(),
titleBarStyle: is.macOS() ? "hiddenInset" : "default",

View File

@ -116,6 +116,15 @@ const mainMenuTemplate = (win) => [
config.set("options.appVisible", false);
},
},
{ type: "separator" },
{
label: "Play/Pause on click",
type: "checkbox",
checked: config.get("options.trayClickPlayPause"),
click: (item) => {
config.set("options.trayClickPlayPause", item.checked);
},
},
],
},
{ type: "separator" },

View File

@ -1,7 +1,7 @@
{
"name": "youtube-music",
"productName": "YouTube Music",
"version": "1.8.0",
"version": "1.9.0",
"description": "YouTube Music Desktop App - including custom plugins",
"license": "MIT",
"repository": "th-ch/youtube-music",
@ -18,12 +18,19 @@
"icon": "assets/generated/icons/mac/icon.icns"
},
"win": {
"icon": "assets/generated/icons/win/icon.ico"
"icon": "assets/generated/icons/win/icon.ico",
"target": ["nsis", "portable"]
},
"linux": {
"icon": "assets/generated/icons/png",
"category": "AudioVideo",
"target": ["AppImage", "snap", "freebsd", "deb", "rpm"]
"target": [
"AppImage",
"snap",
"freebsd",
"deb",
"rpm"
]
}
},
"scripts": {
@ -53,8 +60,9 @@
"@ffmpeg/core": "^0.8.5",
"@ffmpeg/ffmpeg": "^0.9.6",
"YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.8.0",
"discord-rpc": "^3.1.4",
"downloads-folder": "^3.0.1",
"electron-debug": "^3.1.0",
"electron-debug": "^3.2.0",
"electron-is": "^3.0.0",
"electron-localshortcut": "^3.2.1",
"electron-store": "^6.0.1",
@ -64,14 +72,14 @@
"ytdl-core": "^4.1.2"
},
"devDependencies": {
"electron": "^10.1.3",
"electron": "^11.1.1",
"electron-builder": "^22.8.1",
"electron-devtools-installer": "^3.1.1",
"electron-icon-maker": "0.0.5",
"get-port": "^5.1.1",
"jest": "^26.4.2",
"rimraf": "^3.0.2",
"spectron": "^12.0.0",
"spectron": "^13.0.0",
"xo": "^0.33.1"
},
"resolutions": {

51
plugins/discord/back.js Normal file
View File

@ -0,0 +1,51 @@
const Discord = require("discord-rpc");
const getSongInfo = require("../../providers/song-info");
const rpc = new Discord.Client({
transport: "ipc",
});
// Application ID registered by @semvis123
const clientId = "790655993809338398";
module.exports = (win) => {
const registerCallback = getSongInfo(win);
// If the page is ready, register the callback
win.on("ready-to-show", () => {
rpc.on("ready", () => {
// Register the callback
registerCallback((songInfo) => {
// Song information changed, so lets update the rich presence
const activityInfo = {
details: songInfo.title,
state: songInfo.artist,
largeImageKey: "logo",
largeImageText: songInfo.views + " - " + songInfo.likes,
};
if (songInfo.isPaused) {
// Add an idle icon to show that the song is paused
activityInfo.smallImageKey = "idle";
activityInfo.smallImageText = "idle/paused";
} else {
// Add the start and end time of the song
const songStartTime = Date.now() - songInfo.elapsedSeconds * 1000;
activityInfo.startTimestamp = songStartTime;
activityInfo.endTimestamp =
songStartTime + songInfo.songDuration * 1000;
}
rpc.setActivity(activityInfo);
});
});
// Startup the rpc client
rpc
.login({
clientId,
})
.catch(console.error);
});
};

View File

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

View File

@ -2,6 +2,7 @@ const { join } = require("path");
const { dialog } = require("electron");
const getSongInfo = require("../../providers/song-info");
const { injectCSS, listenAction } = require("../utils");
const { ACTIONS, CHANNEL } = require("./actions.js");
@ -16,14 +17,26 @@ const sendError = (win, err) => {
dialog.showMessageBox(dialogOpts);
};
let metadata = {};
function handle(win) {
injectCSS(win.webContents, join(__dirname, "style.css"));
const registerCallback = getSongInfo(win);
registerCallback((info) => {
metadata = {
...info,
image: info.image ? info.image.toDataURL() : undefined,
};
});
listenAction(CHANNEL, (event, action, error) => {
switch (action) {
case ACTIONS.ERROR:
sendError(win, error);
break;
case ACTIONS.METADATA:
event.returnValue = JSON.stringify(metadata);
break;
default:
console.log("Unknown action: " + action);
}

View File

@ -1,3 +1,5 @@
const { contextBridge } = require("electron");
const { ElementFromFile, templatePath, triggerAction } = require("../utils");
const { ACTIONS, CHANNEL } = require("./actions.js");
const { downloadVideoToMP3 } = require("./youtube-dl");
@ -28,6 +30,9 @@ const reinit = () => {
}
};
// TODO: re-enable once contextIsolation is set to true
// contextBridge.exposeInMainWorld("downloader", {
// download: () => {
global.download = () => {
const videoUrl = window.location.href;
@ -48,6 +53,7 @@ global.download = () => {
pluginOptions
);
};
// });
function observeMenu(options) {
pluginOptions = { ...pluginOptions, ...options };

View File

@ -12,6 +12,9 @@ const filenamify = require("filenamify");
const FFmpeg = require("@ffmpeg/ffmpeg/dist/ffmpeg.min");
const ytdl = require("ytdl-core");
const { triggerActionSync } = require("../utils");
const { ACTIONS, CHANNEL } = require("./actions.js");
const { createFFmpeg } = FFmpeg;
const ffmpeg = createFFmpeg({
log: false,
@ -54,7 +57,12 @@ const downloadVideoToMP3 = (
.on("info", (info, format) => {
videoName = info.videoDetails.title.replace("|", "").toString("ascii");
if (is.dev()) {
console.log("Downloading video - name:", videoName);
console.log(
"Downloading video - name:",
videoName,
"- quality:",
format.audioBitrate + "kbits/s"
);
}
})
.on("error", sendError)
@ -73,6 +81,7 @@ const toMP3 = async (
options
) => {
const safeVideoName = randomBytes(32).toString("hex");
const extension = options.extension || "mp3";
try {
if (!ffmpeg.isLoaded()) {
@ -87,15 +96,18 @@ const toMP3 = async (
await ffmpeg.run(
"-i",
safeVideoName,
...options.ffmpegArgs,
safeVideoName + ".mp3"
...getFFmpegMetadataArgs(),
...(options.ffmpegArgs || []),
safeVideoName + "." + extension
);
const folder = options.downloadFolder || downloadsFolder();
const filename = filenamify(videoName + ".mp3", { replacement: "_" });
const filename = filenamify(videoName + "." + extension, {
replacement: "_",
});
writeFileSync(
join(folder, filename),
ffmpeg.FS("readFile", safeVideoName + ".mp3")
ffmpeg.FS("readFile", safeVideoName + "." + extension)
);
reinit();
@ -104,6 +116,20 @@ const toMP3 = async (
}
};
const getFFmpegMetadataArgs = () => {
const metadata = JSON.parse(triggerActionSync(CHANNEL, ACTIONS.METADATA));
if (!metadata) {
return;
}
return [
"-metadata",
`title=${metadata.title}`,
"-metadata",
`artist=${metadata.artist}`,
];
};
module.exports = {
downloadVideoToMP3,
};

View File

@ -1,24 +1,24 @@
const { triggerAction } = require('../utils');
const { triggerAction } = require("../utils");
const CHANNEL = "navigation";
const ACTIONS = {
NEXT: "next",
BACK: 'back',
}
NEXT: "next",
BACK: "back",
};
function goToNextPage() {
triggerAction(CHANNEL, ACTIONS.NEXT);
triggerAction(CHANNEL, ACTIONS.NEXT);
}
function goToPreviousPage() {
triggerAction(CHANNEL, ACTIONS.BACK);
triggerAction(CHANNEL, ACTIONS.BACK);
}
module.exports = {
CHANNEL: CHANNEL,
ACTIONS: ACTIONS,
global: {
goToNextPage: goToNextPage,
goToPreviousPage: goToPreviousPage,
}
CHANNEL: CHANNEL,
ACTIONS: ACTIONS,
actions: {
goToNextPage: goToNextPage,
goToPreviousPage: goToPreviousPage,
},
};

View File

@ -1,23 +1,23 @@
const path = require("path");
const { injectCSS, listenAction } = require("../utils");
const { ACTIONS, CHANNEL } = require("./actions.js");
const { ACTIONS, CHANNEL } = require("./actions.js");
function handle(win) {
injectCSS(win.webContents, path.join(__dirname, "style.css"));
listenAction(CHANNEL, (event, action) => {
switch (action) {
case ACTIONS.NEXT:
case ACTIONS.NEXT:
if (win.webContents.canGoForward()) {
win.webContents.goForward();
}
break;
case ACTIONS.BACK:
case ACTIONS.BACK:
if (win.webContents.canGoBack()) {
win.webContents.goBack();
}
break;
default:
default:
console.log("Unknown action: " + action);
}
});

View File

@ -1,18 +0,0 @@
const { triggerAction } = require("../utils");
const CHANNEL = "notification";
const ACTIONS = {
NOTIFICATION: "notification",
};
function notify(info) {
triggerAction(CHANNEL, ACTIONS.NOTIFICATION, info);
}
module.exports = {
CHANNEL,
ACTIONS,
global: {
notify,
},
};

View File

@ -1,33 +1,35 @@
const { nativeImage, Notification } = require("electron");
const { Notification } = require("electron");
const { listenAction } = require("../utils");
const { ACTIONS, CHANNEL } = require("./actions.js");
const getSongInfo = require("../../providers/song-info");
function notify(info) {
const notify = (info) => {
let notificationImage = "assets/youtube-music.png";
if (info.image) {
notificationImage = nativeImage.createFromDataURL(info.image);
notificationImage = info.image.resize({ height: 256, width: 256 });
}
// Fill the notification with content
const notification = {
title: info.title || "Playing",
body: info.artist,
icon: notificationImage,
silent: true,
};
// Send the notification
new Notification(notification).show();
}
};
function listenAndNotify() {
listenAction(CHANNEL, (event, action, imageSrc) => {
switch (action) {
case ACTIONS.NOTIFICATION:
notify(imageSrc);
break;
default:
console.log("Unknown action: " + action);
}
module.exports = (win) => {
const registerCallback = getSongInfo(win);
win.on("ready-to-show", () => {
// Register the callback for new song information
registerCallback((songInfo) => {
// If song is playing send notification
if (!songInfo.isPaused) {
notify(songInfo);
}
});
});
}
module.exports = listenAndNotify;
};

View File

@ -1,86 +0,0 @@
let videoElement = null;
let image = null;
const observer = new MutationObserver((mutations, observer) => {
if (!videoElement) {
videoElement = document.querySelector("video");
}
if (!image) {
image = document.querySelector(".ytmusic-player-bar.image");
}
if (videoElement !== null && image !== null) {
observer.disconnect();
let notificationImage = null;
videoElement.addEventListener("play", () => {
notify({
title: getTitle(),
artist: getArtist(),
image: notificationImage,
});
});
image.addEventListener("load", () => {
notificationImage = null;
const imageInBase64 = convertImageToBase64(image);
if (image && image.complete && image.naturalHeight !== 0) {
notificationImage = imageInBase64;
}
});
}
});
// Convert an image (DOM element) to base64 string
const convertImageToBase64 = (image, size = 256) => {
image.setAttribute("crossorigin", "anonymous");
const c = document.createElement("canvas");
c.height = size;
c.width = size;
const ctx = c.getContext("2d");
ctx.drawImage(
image,
0,
0,
image.naturalWidth,
image.naturalHeight,
0,
0,
c.width,
c.height
);
const imageInBase64 = c.toDataURL();
return imageInBase64;
};
const getTitle = () => {
const title = document.querySelector(".title.ytmusic-player-bar").textContent;
return title;
};
const getArtist = () => {
const bar = document.querySelectorAll(".subtitle.ytmusic-player-bar")[0];
let artist;
if (bar.querySelectorAll(".yt-simple-endpoint.yt-formatted-string")[0]) {
artist = bar.querySelectorAll(".yt-simple-endpoint.yt-formatted-string")[0]
.textContent;
} else if (bar.querySelectorAll(".byline.ytmusic-player-bar")[0]) {
artist = bar.querySelectorAll(".byline.ytmusic-player-bar")[0].textContent;
}
return artist;
};
const observeVideoAndThumbnail = () => {
observer.observe(document, {
childList: true,
subtree: true,
});
};
module.exports = observeVideoAndThumbnail;

View File

@ -1,12 +1,7 @@
const { globalShortcut } = require("electron");
const electronLocalshortcut = require("electron-localshortcut");
const {
playPause,
nextTrack,
previousTrack,
startSearch
} = require("./youtube.js");
const getSongControls = require("../../providers/song-controls");
function _registerGlobalShortcut(webContents, shortcut, action) {
globalShortcut.register(shortcut, () => {
@ -21,11 +16,13 @@ function _registerLocalShortcut(win, shortcut, action) {
}
function registerShortcuts(win) {
const { playPause, next, previous, search } = getSongControls(win);
_registerGlobalShortcut(win.webContents, "MediaPlayPause", playPause);
_registerGlobalShortcut(win.webContents, "MediaNextTrack", nextTrack);
_registerGlobalShortcut(win.webContents, "MediaPreviousTrack", previousTrack);
_registerLocalShortcut(win, "CommandOrControl+F", startSearch);
_registerLocalShortcut(win, "CommandOrControl+L", startSearch);
_registerGlobalShortcut(win.webContents, "MediaNextTrack", next);
_registerGlobalShortcut(win.webContents, "MediaPreviousTrack", previous);
_registerLocalShortcut(win, "CommandOrControl+F", search);
_registerLocalShortcut(win, "CommandOrControl+L", search);
}
module.exports = registerShortcuts;

View File

@ -1,29 +0,0 @@
function _keyboardInput(webContents, key) {
return webContents.sendInputEvent({
type : "keydown",
keyCode: key
});
}
function playPause(webContents) {
return _keyboardInput(webContents, "Space");
}
function nextTrack(webContents) {
return _keyboardInput(webContents, "j");
}
function previousTrack(webContents) {
return _keyboardInput(webContents, "k");
}
function startSearch(webContents) {
return _keyboardInput(webContents, "/");
}
module.exports = {
playPause : playPause,
nextTrack : nextTrack,
previousTrack: previousTrack,
startSearch : startSearch
};

View File

@ -1,110 +1,87 @@
const {
TouchBar, nativeImage
} = require('electron');
const { TouchBar } = require("electron");
const {
TouchBarButton,
TouchBarLabel,
TouchBarSpacer,
TouchBarSegmentedControl,
TouchBarScrubber
TouchBarScrubber,
} = TouchBar;
const fetch = require('node-fetch');
// This selects the song title
const titleSelector = '.title.style-scope.ytmusic-player-bar';
const getSongInfo = require("../../providers/song-info");
const getSongControls = require("../../providers/song-controls");
// This selects the song image
const imageSelector = '#layout > ytmusic-player-bar > div.middle-controls.style-scope.ytmusic-player-bar > img';
// Songtitle label
const songTitle = new TouchBarLabel({
label: "",
});
// This will store the song controls once available
let controls = [];
// These keys will be used to go backwards, pause, skip songs, like songs, dislike songs
const keys = ['k', 'space', 'j', '_', '+'];
// This will store the song image once available
const songImage = {};
const presskey = (window, key) => {
window.webContents.sendInputEvent({
type: 'keydown',
keyCode: key
});
};
// Grab the title using the selector
const getTitle = win => {
return win.webContents.executeJavaScript(
'document.querySelector(\'' + titleSelector + '\').innerText'
).catch(error => {
console.log(error);
});
};
// Grab the image src using the selector
const getImage = win => {
return win.webContents.executeJavaScript(
'document.querySelector(\'' + imageSelector + '\').src'
).catch(error => {
console.log(error);
});
};
module.exports = win => {
// Songtitle label
const songTitle = new TouchBarLabel({
label: ''
});
// This will store the song image once available
const songImage = {};
// The song control buttons (keys to press are in the same order)
const buttons = new TouchBarSegmentedControl({
mode: 'buttons',
segments: [
new TouchBarButton({
label: '⏮'
}),
new TouchBarButton({
label: '⏯️'
}),
new TouchBarButton({
label: '⏭'
}),
new TouchBarButton({
label: '👎'
}),
new TouchBarButton({
label: '👍'
})
],
change: i => presskey(win, keys[i])
});
// This is the touchbar object, this combines everything with proper layout
const touchBar = new TouchBar({
items: [
new TouchBarScrubber({
items: [songImage, songTitle],
continuous: false
}),
new TouchBarSpacer({
size: 'flexible'
}),
buttons
]
});
// If the page title changes, update touchbar and song title
win.on('page-title-updated', async () => {
// Set the song title
songTitle.label = await getTitle(win);
// Get image source
const imageSrc = await getImage(win);
// Fetch and set song image
await fetch(imageSrc)
.then(response => response.buffer())
.then(data => {
songImage.icon = nativeImage.createFromBuffer(data).resize({height: 23});
});
win.setTouchBar(touchBar);
// Pause/play button
const pausePlayButton = new TouchBarButton();
// The song control buttons (control functions are in the same order)
const buttons = new TouchBarSegmentedControl({
mode: "buttons",
segments: [
new TouchBarButton({
label: "⏮",
}),
pausePlayButton,
new TouchBarButton({
label: "⏭",
}),
new TouchBarButton({
label: "👎",
}),
new TouchBarButton({
label: "👍",
}),
],
change: (i) => controls[i](),
});
// This is the touchbar object, this combines everything with proper layout
const touchBar = new TouchBar({
items: [
new TouchBarScrubber({
items: [songImage, songTitle],
continuous: false,
}),
new TouchBarSpacer({
size: "flexible",
}),
buttons,
],
});
module.exports = (win) => {
const registerCallback = getSongInfo(win);
const { playPause, next, previous, like, dislike } = getSongControls(win);
// If the page is ready, register the callback
win.on("ready-to-show", () => {
controls = [previous, playPause, next, like, dislike];
// Register the callback
registerCallback((songInfo) => {
// Song information changed, so lets update the touchBar
// Set the song title
songTitle.label = songInfo.title;
// Changes the pause button if paused
pausePlayButton.label = songInfo.isPaused ? "▶️" : "⏸";
// Get image source
songImage.icon = songInfo.image
? songInfo.image.resize({ height: 23 })
: null;
win.setTouchBar(touchBar);
});
});
};

View File

@ -4,15 +4,15 @@ const path = require("path");
const { ipcMain, ipcRenderer } = require("electron");
// Creates a DOM element from a HTML string
module.exports.ElementFromHtml = html => {
var template = document.createElement("template");
html = html.trim(); // Never return a text node of whitespace as the result
template.innerHTML = html;
module.exports.ElementFromHtml = (html) => {
var template = document.createElement("template");
html = html.trim(); // Never return a text node of whitespace as the result
template.innerHTML = html;
return template.content.firstChild;
};
// Creates a DOM element from a HTML file
module.exports.ElementFromFile = filepath => {
module.exports.ElementFromFile = (filepath) => {
return module.exports.ElementFromHtml(fs.readFileSync(filepath, "utf8"));
};
@ -24,12 +24,16 @@ module.exports.triggerAction = (channel, action, ...args) => {
return ipcRenderer.send(channel, action, ...args);
};
module.exports.triggerActionSync = (channel, action, ...args) => {
return ipcRenderer.sendSync(channel, action, ...args);
};
module.exports.listenAction = (channel, callback) => {
return ipcMain.on(channel, callback);
};
module.exports.fileExists = (path, callbackIfExists) => {
fs.access(path, fs.F_OK, err => {
fs.access(path, fs.F_OK, (err) => {
if (err) {
return;
}
@ -45,10 +49,10 @@ module.exports.injectCSS = (webContents, filepath) => {
};
module.exports.getAllPlugins = () => {
const isDirectory = source => fs.lstatSync(source).isDirectory();
const isDirectory = (source) => fs.lstatSync(source).isDirectory();
return fs
.readdirSync(__dirname)
.map(name => path.join(__dirname, name))
.map((name) => path.join(__dirname, name))
.filter(isDirectory)
.map(name => path.basename(name));
.map((name) => path.basename(name));
};

View File

@ -1,6 +1,6 @@
const path = require("path");
const { remote } = require("electron");
const { contextBridge, remote } = require("electron");
const config = require("./config");
const { fileExists } = require("./plugins/utils");
@ -10,7 +10,10 @@ const plugins = config.plugins.getEnabled();
plugins.forEach(([plugin, options]) => {
const pluginPath = path.join(__dirname, "plugins", plugin, "actions.js");
fileExists(pluginPath, () => {
const actions = require(pluginPath).global || {};
const actions = require(pluginPath).actions || {};
// TODO: re-enable once contextIsolation is set to true
// contextBridge.exposeInMainWorld(plugin + "Actions", actions);
Object.keys(actions).forEach((actionName) => {
global[actionName] = actions[actionName];
});

View File

@ -0,0 +1,18 @@
// This is used for to control the songs
const pressKey = (window, key) => {
window.webContents.sendInputEvent({
type: "keydown",
keyCode: key,
});
};
module.exports = (win) => {
return {
previous: () => pressKey(win, "k"),
next: () => pressKey(win, "j"),
playPause: () => pressKey(win, "space"),
like: () => pressKey(win, "_"),
dislike: () => pressKey(win, "+"),
search: () => pressKey(win, "/"),
};
};

137
providers/song-info.js Normal file
View File

@ -0,0 +1,137 @@
const { nativeImage } = require("electron");
const fetch = require("node-fetch");
// This selects the song title
const titleSelector = ".title.style-scope.ytmusic-player-bar";
// This selects the song image
const imageSelector =
"#layout > ytmusic-player-bar > div.middle-controls.style-scope.ytmusic-player-bar > img";
// This selects the song subinfo, this includes artist, views, likes
const subInfoSelector =
"#layout > ytmusic-player-bar > div.middle-controls.style-scope.ytmusic-player-bar > div.content-info-wrapper.style-scope.ytmusic-player-bar > span";
// This selects the progress bar, used for songlength and current progress
const progressSelector = "#progress-bar";
// Grab the title using the selector
const getTitle = (win) => {
return win.webContents
.executeJavaScript(
"document.querySelector('" + titleSelector + "').innerText"
)
.catch((error) => {
console.log(error);
});
};
// Grab the image src using the selector
const getImageSrc = (win) => {
return win.webContents
.executeJavaScript("document.querySelector('" + imageSelector + "').src")
.catch((error) => {
console.log(error);
});
};
// Grab the subinfo using the selector
const getSubInfo = async (win) => {
// Get innerText of subinfo element
const subInfoString = await win.webContents.executeJavaScript(
'document.querySelector("' + subInfoSelector + '").innerText'
);
// Split and clean the string
const splittedSubInfo = subInfoString.replaceAll("\n", "").split(" • ");
// Make sure we always return 3 elements in the aray
const subInfo = [];
for (let i = 0; i < 3; i++) {
// Fill array with empty string if not defined
subInfo.push(splittedSubInfo[i] || "");
}
return subInfo;
};
// Grab the progress using the selector
const getProgress = async (win) => {
// Get max value of the progressbar element
const songDuration = await win.webContents.executeJavaScript(
'document.querySelector("' + progressSelector + '").max'
);
// Get current value of the progressbar element
const elapsedSeconds = await win.webContents.executeJavaScript(
'document.querySelector("' + progressSelector + '").value'
);
return { songDuration, elapsedSeconds };
};
// Grab the native image using the src
const getImage = async (src) => {
const result = await fetch(src);
const buffer = await result.buffer();
return nativeImage.createFromBuffer(buffer);
};
const getPausedStatus = async (win) => {
const title = await win.webContents.executeJavaScript("document.title");
return !title.includes("-");
};
// Fill songInfo with empty values
const songInfo = {
title: "",
artist: "",
views: "",
likes: "",
imageSrc: "",
image: null,
isPaused: true,
songDuration: 0,
elapsedSeconds: 0,
};
const registerProvider = (win) => {
// This variable will be filled with the callbacks once they register
const callbacks = [];
// This function will allow plugins to register callback that will be triggered when data changes
const registerCallback = (callback) => {
callbacks.push(callback);
};
win.on("page-title-updated", async () => {
// Save the old title temporarily
const oldTitle = songInfo.title;
// Get and set the new data
songInfo.title = await getTitle(win);
songInfo.isPaused = await getPausedStatus(win);
const { songDuration, elapsedSeconds } = await getProgress(win);
songInfo.songDuration = songDuration;
songInfo.elapsedSeconds = elapsedSeconds;
// If title changed then we do need to update other info
if (oldTitle !== songInfo.title) {
const subInfo = await getSubInfo(win);
songInfo.artist = subInfo[0];
songInfo.views = subInfo[1];
songInfo.likes = subInfo[2];
songInfo.imageSrc = await getImageSrc(win);
songInfo.image = await getImage(songInfo.imageSrc);
}
// Trigger the callbacks
callbacks.forEach((c) => {
c(songInfo);
});
});
return registerCallback;
};
module.exports = registerProvider;

24
tray.js
View File

@ -4,7 +4,7 @@ const { Menu, nativeImage, Tray } = require("electron");
const config = require("./config");
const { mainMenuTemplate } = require("./menu");
const { clickInYoutubeMusic } = require("./utils/youtube-music");
const getSongControls = require("./providers/song-controls");
// Prevent tray being garbage collected
let tray;
@ -15,6 +15,7 @@ module.exports.setUpTray = (app, win) => {
return;
}
const { playPause, next, previous } = getSongControls(win);
const iconPath = path.join(__dirname, "assets", "youtube-music-tray.png");
let trayIcon = nativeImage.createFromPath(iconPath).resize({
width: 16,
@ -24,35 +25,30 @@ module.exports.setUpTray = (app, win) => {
tray.setToolTip("Youtube Music");
tray.setIgnoreDoubleClickEvents(true);
tray.on("click", () => {
win.isVisible() ? win.hide() : win.show();
if (config.get("options.trayClickPlayPause")) {
playPause();
} else {
win.isVisible() ? win.hide() : win.show();
}
});
const trayMenu = Menu.buildFromTemplate([
{
label: "Play/Pause",
click: () => {
clickInYoutubeMusic(
win,
"#left-controls > div > paper-icon-button.play-pause-button.style-scope.ytmusic-player-bar"
);
playPause();
},
},
{
label: "Next",
click: () => {
clickInYoutubeMusic(
win,
"#left-controls > div > paper-icon-button.next-button.style-scope.ytmusic-player-bar"
);
next();
},
},
{
label: "Previous",
click: () => {
clickInYoutubeMusic(
win,
"#left-controls > div > paper-icon-button.previous-button.style-scope.ytmusic-player-bar"
);
previous();
},
},
{

View File

@ -1,8 +0,0 @@
const clickInYoutubeMusic = (win, selector) => {
win.webContents.executeJavaScript(
`document.querySelector("${selector}").click();`,
true
);
};
module.exports = { clickInYoutubeMusic };

205
yarn.lock
View File

@ -1244,10 +1244,17 @@
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.1.tgz#be148756d5480a84cde100324c03a86ae5739fb5"
integrity sha512-2zs+O+UkDsJ1Vcp667pd3f8xearMdopz/z54i99wtRDI5KLmngk7vlrYZD0ZjKHaROR03EznlBbVY9PfAEyJIQ==
"@types/puppeteer@^3.0.1":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-3.0.2.tgz#20085220593b560c7332b6d46aecaf81ae263540"
integrity sha512-JRuHPSbHZBadOxxFwpyZPeRlpPTTeMbQneMdpFd8LXdyNfFSiX950CGewdm69g/ipzEAXAmMyFF1WOWJOL/nKw==
"@types/puppeteer-core@^5.4.0":
version "5.4.0"
resolved "https://registry.yarnpkg.com/@types/puppeteer-core/-/puppeteer-core-5.4.0.tgz#880a7917b4ede95cbfe2d5e81a558cfcb072c0fb"
integrity sha512-yqRPuv4EFcSkTyin6Yy17pN6Qz2vwVwTCJIDYMXbE3j8vTPhv0nCQlZOl5xfi0WHUkqvQsjAR8hAfjeMCoetwg==
dependencies:
"@types/puppeteer" "*"
"@types/puppeteer@*":
version "5.4.2"
resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-5.4.2.tgz#80f3a1f54dedbbf750779716de81401549062072"
integrity sha512-yjbHoKjZFOGqA6bIEI2dfBE5UPqU0YGWzP+ipDVP1iGzmlhksVKTBVZfT3Aj3wnvmcJ2PQ9zcncwOwyavmafBw==
dependencies:
"@types/node" "*"
@ -1354,43 +1361,43 @@
dependencies:
eslint-visitor-keys "^1.1.0"
"@wdio/config@6.4.7":
version "6.4.7"
resolved "https://registry.yarnpkg.com/@wdio/config/-/config-6.4.7.tgz#f2402a6067b15fdc61e9701458f60adc388ae75c"
integrity sha512-wtcj9yKm5+SivwhsgpusBrFR7a3rpDsN/WH6ekoqlZFs7oCpJeTLwawWnoX6MJQy2no5o00lGxDDJnqjaBdiiQ==
"@wdio/config@6.11.0":
version "6.11.0"
resolved "https://registry.yarnpkg.com/@wdio/config/-/config-6.11.0.tgz#ba6b0ef72d9f40bd577da16602de84390c3dcdb4"
integrity sha512-aNkH5sPEybOf7ND1JrAlCsKZow6KMAaY3Wyf9yHralQ3xmclmswFKU/DseP7go17Ivc2KHLl7MMkNaqVY90siw==
dependencies:
"@wdio/logger" "6.4.7"
"@wdio/logger" "6.10.10"
deepmerge "^4.0.0"
glob "^7.1.2"
"@wdio/logger@6.4.7":
version "6.4.7"
resolved "https://registry.yarnpkg.com/@wdio/logger/-/logger-6.4.7.tgz#6d83033a0d7ef20f3b2cc2420956105903a6a4e7"
integrity sha512-Mm/rsRa/1u/l8/IrNKM2c9tkvLE90i83d3KZ0Ujh4cicYJv+lNi9whsCi+p3QNFCo64nJ6bfC+0Ho5VgD3MiKw==
"@wdio/logger@6.10.10":
version "6.10.10"
resolved "https://registry.yarnpkg.com/@wdio/logger/-/logger-6.10.10.tgz#1e07cf32a69606ddb94fa9fd4b0171cb839a5980"
integrity sha512-2nh0hJz9HeZE0VIEMI+oPgjr/Q37ohrR9iqsl7f7GW5ik+PnKYCT9Eab5mR1GNMG60askwbskgGC1S9ygtvrSw==
dependencies:
chalk "^4.0.0"
loglevel "^1.6.0"
loglevel-plugin-prefix "^0.8.4"
strip-ansi "^6.0.0"
"@wdio/protocols@6.3.6":
version "6.3.6"
resolved "https://registry.yarnpkg.com/@wdio/protocols/-/protocols-6.3.6.tgz#fc408b4441d9701bdd370b0981cf243862ce7e19"
integrity sha512-cocBRkv5sYUBxXResuxskQhIkKgDgE/yAtgMGR5wXLrtG/sMpZ2HVy6LOcOeARidAaRwbav80M2ZHjTCjPn53w==
"@wdio/protocols@6.10.6":
version "6.10.6"
resolved "https://registry.yarnpkg.com/@wdio/protocols/-/protocols-6.10.6.tgz#8d1deed6651a5ca0a185ea334fc1a371dc4c700c"
integrity sha512-CLLVdc82S+Zij7f9djL90JC1bE5gtaOn+EF2pY4n8XdypqPUa1orQip8stQtX/wXEX0Ak45MEcSU9nCY+CzNnQ==
"@wdio/repl@6.5.0":
version "6.5.0"
resolved "https://registry.yarnpkg.com/@wdio/repl/-/repl-6.5.0.tgz#1e3ef2dfbc1ead1280b21bb053ad509816722466"
integrity sha512-qKm2j0qY7mrZQipHv4PhKpAL7pkyxCzW1XDoEjp09OHLvmGvvCwY6aEBuLziD9BaiR30BXVNLIKPZfM4Xl2Zfg==
"@wdio/repl@6.11.0":
version "6.11.0"
resolved "https://registry.yarnpkg.com/@wdio/repl/-/repl-6.11.0.tgz#5b1eab574b6b89f7f7c383e7295c06af23c3818e"
integrity sha512-FxrFKiTkFyELNGGVEH1uijyvNY7lUpmff6x+FGskFGZB4uSRs0rxkOMaEjxnxw7QP1zgQKr2xC7GyO03gIGRGg==
dependencies:
"@wdio/utils" "6.5.0"
"@wdio/utils" "6.11.0"
"@wdio/utils@6.5.0":
version "6.5.0"
resolved "https://registry.yarnpkg.com/@wdio/utils/-/utils-6.5.0.tgz#46398b08e6741eb97a57089b2c909622c480ea22"
integrity sha512-k5RxRj/re/BbK76SjWSmyhJFHWnXD74vl/doCAQNuOaKFBd2dqMCs3GiFjYCyLcU37XGMAnRvI3tKHflyLGJYw==
"@wdio/utils@6.11.0":
version "6.11.0"
resolved "https://registry.yarnpkg.com/@wdio/utils/-/utils-6.11.0.tgz#878c2500efb1a325bf5a66d2ff3d08162f976e8c"
integrity sha512-vf0sOQzd28WbI26d6/ORrQ4XKWTzSlWLm9W/K/eJO0NASKPEzR+E+Q2kaa+MJ4FKXUpjbt+Lxfo+C26TzBk7tg==
dependencies:
"@wdio/logger" "6.4.7"
"@wdio/logger" "6.10.10"
"YoutubeNonStop@git://github.com/lawfx/YoutubeNonStop.git#v0.8.0":
version "0.0.0"
@ -2630,6 +2637,11 @@ crypto-random-string@^2.0.0:
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
css-shorthand-properties@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/css-shorthand-properties/-/css-shorthand-properties-1.1.1.tgz#1c808e63553c283f289f2dd56fcee8f3337bd935"
integrity sha512-Md+Juc7M3uOdbAFwOYlTrccIZ7oCFuzrhKYQjdeUEW/sE1hv17Jp/Bws+ReOPpGVBTYCBoYo+G17V5Qo8QQ75A==
css-value@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/css-value/-/css-value-0.0.1.tgz#5efd6c2eea5ea1fd6b6ac57ec0427b18452424ea"
@ -2863,16 +2875,17 @@ devtools-protocol@0.0.799653:
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.799653.tgz#86fc95ce5bf4fdf4b77a58047ba9d2301078f119"
integrity sha512-t1CcaZbvm8pOlikqrsIM9GOa7Ipp07+4h/q9u0JXBWjPCjHdBl9KkddX87Vv9vBHoBGtwV79sYQNGnQM6iS5gg==
devtools@6.5.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/devtools/-/devtools-6.5.0.tgz#48f9b72cb8f347c8658b46db940da75288f721c6"
integrity sha512-P/9+jSK+Jq4gWO5a79OLtDsZPcrNZN9JDCqWdCmKcbCCikV3fYic+0wmRzAPff8iYLCdmNXf/no4XMLwXR5LXQ==
devtools@6.11.0:
version "6.11.0"
resolved "https://registry.yarnpkg.com/devtools/-/devtools-6.11.0.tgz#d20f1d1bb42357d3f2e424d7103fdd3e0d339879"
integrity sha512-fqiRz77IIGkHfHdIkVYIpBNKz7qllJJvHP92NKDo5e41J+sSVKsjzHsPqvYLugRhSGsZKPrJ19Sj8kmPyFi18w==
dependencies:
"@wdio/config" "6.4.7"
"@wdio/logger" "6.4.7"
"@wdio/protocols" "6.3.6"
"@wdio/utils" "6.5.0"
"@wdio/config" "6.11.0"
"@wdio/logger" "6.10.10"
"@wdio/protocols" "6.10.6"
"@wdio/utils" "6.11.0"
chrome-launcher "^0.13.1"
edge-paths "^2.1.0"
puppeteer-core "^5.1.0"
ua-parser-js "^0.7.21"
uuid "^8.0.0"
@ -2898,6 +2911,14 @@ dir-glob@^2.2.2:
dependencies:
path-type "^3.0.0"
discord-rpc@^3.1.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/discord-rpc/-/discord-rpc-3.1.4.tgz#6d449a682e6a0dec4f0444d5f36f9ebfabaccf91"
integrity sha512-QaBu+gHica2SzgRAmTpuJ4J8DX9+fDwAqhvaie3hcbkU9WPqewEPh21pWdd/7vTI/JNuapU7PFm2ZKg3BTkbGg==
dependencies:
node-fetch "^2.6.1"
ws "^7.3.1"
dmg-builder@22.8.1:
version "22.8.1"
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-22.8.1.tgz#9b3bcbbc43e5fed232525d61a5567ea4b66085c3"
@ -2979,6 +3000,11 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
edge-paths@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/edge-paths/-/edge-paths-2.1.0.tgz#f273f3a0fe022422048bb78f83eb61aca29977ef"
integrity sha512-ZpIN1Vm5hlo9dkkST/1s8QqPNne2uwk3Plf6HcVUhnpfal0WnDRLdNj/wdQo3xRc+wnN3C25wPpPlV2E6aOunQ==
ejs@^3.1.3:
version "3.1.5"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.5.tgz#aed723844dc20acb4b170cd9ab1017e476a0d93b"
@ -3006,18 +3032,18 @@ electron-builder@^22.8.1:
update-notifier "^4.1.0"
yargs "^15.4.1"
electron-chromedriver@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/electron-chromedriver/-/electron-chromedriver-10.0.0.tgz#cea6ee9f2d67f794b7439074fd70060109a7ff56"
integrity sha512-6jvMnQNHsIFTnvSn8kYQk8dRXFjqtp7E4QVIP4Cc6xR7SM8QI0/EmAqfuysd8CGJOpa8wFkEYxCT2dbHGp3bDw==
electron-chromedriver@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/electron-chromedriver/-/electron-chromedriver-11.0.0.tgz#49b034ed0ad12c12e3522862c7bb46875a0d85e1"
integrity sha512-ayMJPBbB4puU0SqYbcD9XvF3/7GWIhqKE1n5lG2/GQPRnrZkNoPIilsrS0rQcD50Xhl69KowatDqLhUznZWtbA==
dependencies:
"@electron/get" "^1.12.2"
extract-zip "^2.0.0"
electron-debug@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/electron-debug/-/electron-debug-3.1.0.tgz#0df17297487fa3c82344d810812853bf67f0bd69"
integrity sha512-SWEqLj4MgfV3tGuO5eBLQ5/Nr6M+KPxsnE0bUJZvQebGJus6RAcdmvd7L+l0Ji31h2mmrN23l2tHFtCa2FvurA==
electron-debug@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/electron-debug/-/electron-debug-3.2.0.tgz#46a15b555c3b11872218c65ea01d058aa0814920"
integrity sha512-7xZh+LfUvJ52M9rn6N+tPuDw6oRAjxUj9SoxAZfJ0hVCXhZCsdkrSt7TgXOiWiEOBgEV8qwUIO/ScxllsPS7ow==
dependencies:
electron-is-dev "^1.1.0"
electron-localshortcut "^3.1.0"
@ -3118,10 +3144,10 @@ electron-updater@^4.3.6:
lodash.isequal "^4.5.0"
semver "^7.3.4"
electron@^10.1.3:
version "10.1.3"
resolved "https://registry.yarnpkg.com/electron/-/electron-10.1.3.tgz#7e276e373bf30078bd4cb1184850a91268dc0e6c"
integrity sha512-CR8LrlG47MdAp317SQ3vGYa2o2cIMdMSMPYH46OVitFLk35dwE9fn3VqvhUIXhCHYcNWIAPzMhkVHpkoFdKWuw==
electron@^11.1.1:
version "11.1.1"
resolved "https://registry.yarnpkg.com/electron/-/electron-11.1.1.tgz#188f036f8282798398dca9513e9bb3b10213e3aa"
integrity sha512-tlbex3xosJgfileN6BAQRotevPRXB/wQIq48QeQ08tUJJrXwE72c8smsM/hbHx5eDgnbfJ2G3a60PmRjHU2NhA==
dependencies:
"@electron/get" "^1.0.1"
"@types/node" "^12.0.12"
@ -7287,7 +7313,7 @@ request-promise-native@^1.0.8:
stealthy-require "^1.1.1"
tough-cookie "^2.3.3"
request@^2.81.0, request@^2.87.0, request@^2.88.2:
request@^2.81.0, request@^2.88.2:
version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
@ -7388,10 +7414,10 @@ responselike@^2.0.0:
dependencies:
lowercase-keys "^2.0.0"
resq@^1.6.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/resq/-/resq-1.8.0.tgz#06c738e122bd024395cd729fb1e07248a1d2c340"
integrity sha512-VObcnfPcE6/EKfHqsi5qoJ0+BF9qfl5181CytP1su3HgzilqF03DrQ+Y7kZQrd+5myfmantl9W3/5uUcpwvKeg==
resq@^1.9.1:
version "1.10.0"
resolved "https://registry.yarnpkg.com/resq/-/resq-1.10.0.tgz#40b5e3515ff984668e6b6b7c2401f282b08042ea"
integrity sha512-hCUd0xMalqtPDz4jXIqs0M5Wnv/LZXN8h7unFOo4/nvExT9dDPbhwd3udRxLlp0HgBnHcV009UlduE9NZi7A6w==
dependencies:
fast-deep-equal "^2.0.1"
@ -7400,10 +7426,10 @@ ret@~0.1.10:
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
rgb2hex@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/rgb2hex/-/rgb2hex-0.2.0.tgz#801b4887127181d1e691f610df2cecdb77330265"
integrity sha512-cHdNTwmTMPu/TpP1bJfdApd6MbD+Kzi4GNnM6h35mdFChhQPSi9cAI8J7DMn5kQDKX8NuBaQXAyo360Oa7tOEA==
rgb2hex@0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/rgb2hex/-/rgb2hex-0.2.3.tgz#8aa464c517b8a26c7a79d767dabaec2b49ee78ec"
integrity sha512-clEe0m1xv+Tva1B/TOepuIcvLAxP0U+sCDfgt1SX1HmI2Ahr5/Cd/nzJM1e78NKVtWdoo0s33YehpFA8UfIShQ==
rimraf@2.6.3, rimraf@^2.2.8:
version "2.6.3"
@ -7750,16 +7776,16 @@ spdx-license-ids@^3.0.0:
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654"
integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==
spectron@^12.0.0:
version "12.0.0"
resolved "https://registry.yarnpkg.com/spectron/-/spectron-12.0.0.tgz#8a3454a1366cdb82fbb6be75be0104915b607340"
integrity sha512-ZyDFS7I+4dWa/YXSQ/trbC4s1Rd0Ks5oi4MQ6XSJHULPasJhx5q2bM93Ae7BNUvwrGrbhjk7O6f14JwqJimLyA==
spectron@^13.0.0:
version "13.0.0"
resolved "https://registry.yarnpkg.com/spectron/-/spectron-13.0.0.tgz#16bdfcf9a2b26cb5ee6c3e29b4f08101e339aa4d"
integrity sha512-7RPa6Fp8gqL4V0DubobnqIRFHIijkpjg6MFHcJlxoerWyvLJd+cQvOh756XpB1Z/U3DyA9jPcS+HE2PvYRP5+A==
dependencies:
dev-null "^0.1.1"
electron-chromedriver "^10.0.0"
request "^2.87.0"
split "^1.0.0"
webdriverio "^6.1.20"
electron-chromedriver "^11.0.0"
request "^2.88.2"
split "^1.0.1"
webdriverio "^6.9.1"
split-string@^3.0.1, split-string@^3.0.2:
version "3.1.0"
@ -7768,7 +7794,7 @@ split-string@^3.0.1, split-string@^3.0.2:
dependencies:
extend-shallow "^3.0.0"
split@^1.0.0:
split@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9"
integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==
@ -8591,32 +8617,34 @@ walker@^1.0.7, walker@~1.0.5:
dependencies:
makeerror "1.0.x"
webdriver@6.5.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/webdriver/-/webdriver-6.5.0.tgz#c2ab978570843c6fe8a778b274d9acf781cfcfd4"
integrity sha512-6iOll9TshD4+2J+em+bLshvM1uXtnotdZ+JaALqRLbkVswLRFU0pTVP1oug0e/IYwL7Me4Cafh9ugQ4PwPuOnA==
webdriver@6.11.0:
version "6.11.0"
resolved "https://registry.yarnpkg.com/webdriver/-/webdriver-6.11.0.tgz#2997bf4ff6c1dcb4e58b4ae9e2b83af8b938f643"
integrity sha512-31uD1Vi+9QAzDSpN3+0oFFRnzJP8IVp8wRjLVbIOaQGPXV1sKjAP7v6LJcxl1JjcmW8keAIh2eyAgbjZJbcyZw==
dependencies:
"@wdio/config" "6.4.7"
"@wdio/logger" "6.4.7"
"@wdio/protocols" "6.3.6"
"@wdio/utils" "6.5.0"
"@wdio/config" "6.11.0"
"@wdio/logger" "6.10.10"
"@wdio/protocols" "6.10.6"
"@wdio/utils" "6.11.0"
got "^11.0.2"
lodash.merge "^4.6.1"
webdriverio@^6.1.20:
version "6.5.2"
resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-6.5.2.tgz#0c733c293b88a0ee45082bfde9fc3d9a10308216"
integrity sha512-ChAV6RmF10mlyWnAL2y+PdnzAjpxL/UuyAHJsYSuirEeEAAqFWWePxniz67bUEVQPVClVj8Jh7oeoK6rhu4RAA==
webdriverio@^6.9.1:
version "6.11.3"
resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-6.11.3.tgz#043bc98514d7410264fbf14991bc0546fa119779"
integrity sha512-yHS01H0+oz59Y+JLj/u8piLzOhtTiQSeASwb3hHF1EDuIiiK6JgGYFAqCYr26BrlzqzUOQ/9VpJj11LtcKWF/A==
dependencies:
"@types/puppeteer" "^3.0.1"
"@wdio/config" "6.4.7"
"@wdio/logger" "6.4.7"
"@wdio/repl" "6.5.0"
"@wdio/utils" "6.5.0"
"@types/puppeteer-core" "^5.4.0"
"@wdio/config" "6.11.0"
"@wdio/logger" "6.10.10"
"@wdio/repl" "6.11.0"
"@wdio/utils" "6.11.0"
archiver "^5.0.0"
atob "^2.1.2"
css-shorthand-properties "^1.1.1"
css-value "^0.0.1"
devtools "6.5.0"
devtools "6.11.0"
fs-extra "^9.0.1"
get-port "^5.1.1"
grapheme-splitter "^1.0.2"
lodash.clonedeep "^4.5.0"
@ -8625,10 +8653,10 @@ webdriverio@^6.1.20:
lodash.zip "^4.2.0"
minimatch "^3.0.4"
puppeteer-core "^5.1.0"
resq "^1.6.0"
rgb2hex "^0.2.0"
resq "^1.9.1"
rgb2hex "0.2.3"
serialize-error "^7.0.0"
webdriver "6.5.0"
webdriver "6.11.0"
webidl-conversions@^5.0.0:
version "5.0.0"
@ -8753,6 +8781,11 @@ ws@^7.2.3:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==
ws@^7.3.1:
version "7.4.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.2.tgz#782100048e54eb36fe9843363ab1c68672b261dd"
integrity sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==
xdg-basedir@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"