mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-14 20:01:47 +00:00
Merge remote-tracking branch 'upstream/master' into styled-bars-download-chooser
This commit is contained in:
@ -39,6 +39,10 @@ const defaultConfig = {
|
|||||||
activityTimoutEnabled: true, // if enabled, the discord rich presence gets cleared when music paused after the time specified below
|
activityTimoutEnabled: true, // if enabled, the discord rich presence gets cleared when music paused after the time specified below
|
||||||
activityTimoutTime: 10 * 60 * 1000 // 10 minutes
|
activityTimoutTime: 10 * 60 * 1000 // 10 minutes
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
enabled: false,
|
||||||
|
urgency: "normal"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,10 @@ module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => {
|
|||||||
details: songInfo.title,
|
details: songInfo.title,
|
||||||
state: songInfo.artist,
|
state: songInfo.artist,
|
||||||
largeImageKey: "logo",
|
largeImageKey: "logo",
|
||||||
largeImageText: songInfo.views + " - " + songInfo.likes,
|
largeImageText: [
|
||||||
|
songInfo.uploadDate,
|
||||||
|
songInfo.views.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " views"
|
||||||
|
].join(' || '),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (songInfo.isPaused) {
|
if (songInfo.isPaused) {
|
||||||
@ -50,8 +53,7 @@ module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Startup the rpc client
|
// Startup the rpc client
|
||||||
rpc
|
rpc.login({
|
||||||
.login({
|
|
||||||
clientId,
|
clientId,
|
||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
|
|||||||
@ -2,15 +2,14 @@ const { existsSync, mkdirSync } = require("fs");
|
|||||||
const { join } = require("path");
|
const { join } = require("path");
|
||||||
const { URL } = require("url");
|
const { URL } = require("url");
|
||||||
|
|
||||||
const { ipcMain } = require("electron");
|
const { dialog, ipcMain } = require("electron");
|
||||||
const is = require("electron-is");
|
const is = require("electron-is");
|
||||||
const ytpl = require("ytpl");
|
const ytpl = require("ytpl");
|
||||||
|
|
||||||
|
const { setOptions } = require("../../config/plugins");
|
||||||
const { sendError } = require("./back");
|
const { sendError } = require("./back");
|
||||||
const { defaultMenuDownloadLabel, getFolder } = require("./utils");
|
const { defaultMenuDownloadLabel, getFolder } = require("./utils");
|
||||||
|
|
||||||
const { setOptions } = require('../../config/plugins')
|
|
||||||
const { dialog } = require('electron');
|
|
||||||
let downloadLabel = defaultMenuDownloadLabel;
|
let downloadLabel = defaultMenuDownloadLabel;
|
||||||
|
|
||||||
module.exports = (win, options, refreshMenu) => [
|
module.exports = (win, options, refreshMenu) => [
|
||||||
@ -63,16 +62,17 @@ module.exports = (win, options, refreshMenu) => [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Choose download folder:',
|
label: "Choose download folder",
|
||||||
click: () => {
|
click: () => {
|
||||||
let result = dialog.showOpenDialogSync({
|
|
||||||
properties: ['openDirectory', 'createDirectory'],
|
let result = dialog.showOpenDialogSync({
|
||||||
|
properties: ["openDirectory", "createDirectory"],
|
||||||
defaultPath: getFolder(options.downloadFolder),
|
defaultPath: getFolder(options.downloadFolder),
|
||||||
})
|
});
|
||||||
if(result) {
|
if (result) {
|
||||||
options.downloadFolder = result[0]
|
options.downloadFolder = result[0];
|
||||||
setOptions("downloader", options)
|
setOptions("downloader", options);
|
||||||
} //else = user pressed cancel
|
} // else = user pressed cancel
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -120,7 +120,7 @@ const toMP3 = async (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const folder = getFolder(options.downloadFolder);
|
const folder = getFolder(options.downloadFolder);
|
||||||
const name = metadata
|
const name = metadata.title
|
||||||
? `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`
|
? `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`
|
||||||
: videoName;
|
: videoName;
|
||||||
const filename = filenamify(name + "." + extension, {
|
const filename = filenamify(name + "." + extension, {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
const { Notification } = require("electron");
|
const { Notification } = require("electron");
|
||||||
const getSongInfo = require("../../providers/song-info");
|
const getSongInfo = require("../../providers/song-info");
|
||||||
|
|
||||||
const notify = info => {
|
const notify = (info, options) => {
|
||||||
let notificationImage = "assets/youtube-music.png";
|
let notificationImage = "assets/youtube-music.png";
|
||||||
|
|
||||||
if (info.image) {
|
if (info.image) {
|
||||||
@ -14,27 +14,28 @@ const notify = info => {
|
|||||||
body: info.artist,
|
body: info.artist,
|
||||||
icon: notificationImage,
|
icon: notificationImage,
|
||||||
silent: true,
|
silent: true,
|
||||||
|
urgency: options.urgency,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send the notification
|
// Send the notification
|
||||||
currentNotification = new Notification(notification);
|
currentNotification = new Notification(notification);
|
||||||
currentNotification.show()
|
currentNotification.show()
|
||||||
|
|
||||||
return currentNotification;
|
return currentNotification;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = (win) => {
|
module.exports = (win, options) => {
|
||||||
const registerCallback = getSongInfo(win);
|
const registerCallback = getSongInfo(win);
|
||||||
let oldNotification;
|
let oldNotification;
|
||||||
win.on("ready-to-show", () => {
|
win.on("ready-to-show", () => {
|
||||||
// Register the callback for new song information
|
// Register the callback for new song information
|
||||||
registerCallback(songInfo => {
|
registerCallback(songInfo => {
|
||||||
// If song is playing send notification
|
// If song is playing send notification
|
||||||
if (!songInfo.isPaused) {
|
if (!songInfo.isPaused) {
|
||||||
// Close the old notification
|
// Close the old notification
|
||||||
oldNotification?.close();
|
oldNotification?.close();
|
||||||
// This fixes a weird bug that would cause the notification to be updated instead of showing
|
// This fixes a weird bug that would cause the notification to be updated instead of showing
|
||||||
setTimeout(()=>{ oldNotification = notify(songInfo) }, 10);
|
setTimeout(()=>{ oldNotification = notify(songInfo, options) }, 10);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
13
plugins/notifications/menu.js
Normal file
13
plugins/notifications/menu.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const {urgencyLevels, setUrgency} = require("./utils");
|
||||||
|
|
||||||
|
module.exports = (win, options) => [
|
||||||
|
{
|
||||||
|
label: "Notification Priority",
|
||||||
|
submenu: urgencyLevels.map(level => ({
|
||||||
|
label: level.name,
|
||||||
|
type: "radio",
|
||||||
|
checked: options.urgency === level.value,
|
||||||
|
click: () => setUrgency(options, level.value)
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
];
|
||||||
11
plugins/notifications/utils.js
Normal file
11
plugins/notifications/utils.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
const {setOptions} = require("../../config/plugins");
|
||||||
|
|
||||||
|
module.exports.urgencyLevels = [
|
||||||
|
{name: "Low", value: "low"},
|
||||||
|
{name: "Normal", value: "normal"},
|
||||||
|
{name: "High", value: "critical"},
|
||||||
|
];
|
||||||
|
module.exports.setUrgency = (options, level) => {
|
||||||
|
options.urgency = level
|
||||||
|
setOptions("notifications", options)
|
||||||
|
};
|
||||||
@ -11,7 +11,7 @@ mainMenuTemplate = function (winHook) {
|
|||||||
//get template
|
//get template
|
||||||
let template = originTemplate(winHook);
|
let template = originTemplate(winHook);
|
||||||
//fix checkbox and roles
|
//fix checkbox and roles
|
||||||
fixCheck(template);
|
fixMenu(template);
|
||||||
//return as normal
|
//return as normal
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
@ -52,12 +52,12 @@ function switchMenuVisibility() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//go over each item in menu
|
//go over each item in menu
|
||||||
function fixCheck(ogTemplate) {
|
function fixMenu(template) {
|
||||||
for (let position in ogTemplate) {
|
for (let index in template) {
|
||||||
let item = ogTemplate[position];
|
let item = template[index];
|
||||||
//apply function on submenu
|
//apply function on submenu
|
||||||
if (item.submenu != null) {
|
if (item.submenu != null) {
|
||||||
fixCheck(item.submenu);
|
fixMenu(item.submenu);
|
||||||
}
|
}
|
||||||
//change onClick of checkbox+radio
|
//change onClick of checkbox+radio
|
||||||
else if (item.type === 'checkbox' || item.type === 'radio') {
|
else if (item.type === 'checkbox' || item.type === 'radio') {
|
||||||
@ -98,7 +98,7 @@ function fixRoles(MenuItem) {
|
|||||||
MenuItem.click = () => { win.webContents.setZoomLevel(0); }
|
MenuItem.click = () => { win.webContents.setZoomLevel(0); }
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log(MenuItem.role + ' was not expected');
|
console.log(`Error fixing MenuRoles: "${MenuItem.role}" was not expected`);
|
||||||
}
|
}
|
||||||
delete MenuItem.role;
|
delete MenuItem.role;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
|
/* increase font size for menu and menuItems */
|
||||||
|
.titlebar, .menubar-menu-container .action-label{
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* allow submenu's to show correctly */
|
/* allow submenu's to show correctly */
|
||||||
.menubar-menu-container{
|
.menubar-menu-container{
|
||||||
overflow-y: visible !important;
|
overflow-y: visible !important;
|
||||||
|
|||||||
@ -29,6 +29,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// inject song-info provider
|
||||||
|
const songInfoProviderPath = path.join(__dirname, "providers", "song-info-front.js")
|
||||||
|
fileExists(songInfoProviderPath, require(songInfoProviderPath));
|
||||||
|
|
||||||
// Add action for reloading
|
// Add action for reloading
|
||||||
global.reload = () =>
|
global.reload = () =>
|
||||||
remote.getCurrentWindow().webContents.loadURL(config.get("url"));
|
remote.getCurrentWindow().webContents.loadURL(config.get("url"));
|
||||||
|
|||||||
22
providers/song-info-front.js
Normal file
22
providers/song-info-front.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
const { ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
const injectListener = () => {
|
||||||
|
var oldXHR = window.XMLHttpRequest;
|
||||||
|
function newXHR() {
|
||||||
|
var realXHR = new oldXHR();
|
||||||
|
realXHR.addEventListener("readystatechange", () => {
|
||||||
|
if(realXHR.readyState==4 && realXHR.status==200){
|
||||||
|
if (realXHR.responseURL.includes('/player')){
|
||||||
|
// if the request is the contains the song info send the response to ipcMain
|
||||||
|
ipcRenderer.send(
|
||||||
|
"song-info-request",
|
||||||
|
realXHR.responseText
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
return realXHR;
|
||||||
|
}
|
||||||
|
window.XMLHttpRequest = newXHR;
|
||||||
|
}
|
||||||
|
module.exports = injectListener;
|
||||||
@ -1,73 +1,18 @@
|
|||||||
const { nativeImage } = require("electron");
|
const { ipcMain, nativeImage } = require("electron");
|
||||||
|
|
||||||
const fetch = require("node-fetch");
|
const fetch = require("node-fetch");
|
||||||
|
|
||||||
// This selects the song title
|
// This selects the progress bar, used for current progress
|
||||||
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";
|
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
|
// Grab the progress using the selector
|
||||||
const getProgress = async (win) => {
|
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
|
// Get current value of the progressbar element
|
||||||
const elapsedSeconds = await win.webContents.executeJavaScript(
|
const elapsedSeconds = await win.webContents.executeJavaScript(
|
||||||
'document.querySelector("' + progressSelector + '").value'
|
'document.querySelector("' + progressSelector + '").value'
|
||||||
);
|
);
|
||||||
|
|
||||||
return { songDuration, elapsedSeconds };
|
return elapsedSeconds;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Grab the native image using the src
|
// Grab the native image using the src
|
||||||
@ -77,6 +22,7 @@ const getImage = async (src) => {
|
|||||||
return nativeImage.createFromBuffer(buffer);
|
return nativeImage.createFromBuffer(buffer);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// To find the paused status, we check if the title contains `-`
|
||||||
const getPausedStatus = async (win) => {
|
const getPausedStatus = async (win) => {
|
||||||
const title = await win.webContents.executeJavaScript("document.title");
|
const title = await win.webContents.executeJavaScript("document.title");
|
||||||
return !title.includes("-");
|
return !title.includes("-");
|
||||||
@ -86,13 +32,26 @@ const getPausedStatus = async (win) => {
|
|||||||
const songInfo = {
|
const songInfo = {
|
||||||
title: "",
|
title: "",
|
||||||
artist: "",
|
artist: "",
|
||||||
views: "",
|
views: 0,
|
||||||
likes: "",
|
uploadDate: "",
|
||||||
imageSrc: "",
|
imageSrc: "",
|
||||||
image: null,
|
image: null,
|
||||||
isPaused: true,
|
isPaused: true,
|
||||||
songDuration: 0,
|
songDuration: 0,
|
||||||
elapsedSeconds: 0,
|
elapsedSeconds: 0,
|
||||||
|
url: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleData = async (_event, responseText) => {
|
||||||
|
let data = JSON.parse(responseText);
|
||||||
|
songInfo.title = data?.videoDetails?.title;
|
||||||
|
songInfo.artist = data?.videoDetails?.author;
|
||||||
|
songInfo.views = data?.videoDetails?.viewCount;
|
||||||
|
songInfo.imageSrc = data?.videoDetails?.thumbnail?.thumbnails?.pop()?.url;
|
||||||
|
songInfo.songDuration = data?.videoDetails?.lengthSeconds;
|
||||||
|
songInfo.image = await getImage(songInfo.imageSrc);
|
||||||
|
songInfo.uploadDate = data?.microformat?.microformatDataRenderer?.uploadDate;
|
||||||
|
songInfo.url = data?.microformat?.microformatDataRenderer?.urlCanonical;
|
||||||
};
|
};
|
||||||
|
|
||||||
const registerProvider = (win) => {
|
const registerProvider = (win) => {
|
||||||
@ -105,32 +64,21 @@ const registerProvider = (win) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
win.on("page-title-updated", async () => {
|
win.on("page-title-updated", async () => {
|
||||||
// Save the old title temporarily
|
|
||||||
const oldTitle = songInfo.title;
|
|
||||||
// Get and set the new data
|
// Get and set the new data
|
||||||
songInfo.title = await getTitle(win);
|
|
||||||
songInfo.isPaused = await getPausedStatus(win);
|
songInfo.isPaused = await getPausedStatus(win);
|
||||||
|
|
||||||
const { songDuration, elapsedSeconds } = await getProgress(win);
|
const elapsedSeconds = await getProgress(win);
|
||||||
songInfo.songDuration = songDuration;
|
|
||||||
songInfo.elapsedSeconds = elapsedSeconds;
|
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
|
// Trigger the callbacks
|
||||||
callbacks.forEach((c) => {
|
callbacks.forEach((c) => {
|
||||||
c(songInfo);
|
c(songInfo);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// This will be called when the song-info-front finds a new request with song data
|
||||||
|
ipcMain.on("song-info-request", handleData);
|
||||||
|
|
||||||
return registerCallback;
|
return registerCallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
13
tray.js
13
tray.js
@ -66,11 +66,18 @@ module.exports.setUpTray = (app, win) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
// delete quit button from navigation submenu
|
// delete quit button from navigation submenu
|
||||||
delete template[template.findIndex(item => item.label==='Navigation')].submenu[3];
|
let navigation = getIndex(template,'Navigation');
|
||||||
|
let quit = getIndex(template[navigation].submenu,'Quit App');
|
||||||
|
delete template[navigation].submenu[quit];
|
||||||
|
|
||||||
// delete View submenu (all buttons are useless in tray)
|
// delete View submenu (all buttons are useless in tray)
|
||||||
delete template[template.findIndex(item => item.label==='View')];
|
delete template[getIndex(template, 'View')];
|
||||||
|
|
||||||
const trayMenu = Menu.buildFromTemplate(template);
|
const trayMenu = Menu.buildFromTemplate(template);
|
||||||
tray.setContextMenu(trayMenu);
|
tray.setContextMenu(trayMenu);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getIndex(arr,label) {
|
||||||
|
return arr.findIndex(item => item.label === label)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user