diff --git a/config/defaults.js b/config/defaults.js index 855b1964..25f8141f 100644 --- a/config/defaults.js +++ b/config/defaults.js @@ -39,6 +39,10 @@ const defaultConfig = { activityTimoutEnabled: true, // if enabled, the discord rich presence gets cleared when music paused after the time specified below activityTimoutTime: 10 * 60 * 1000 // 10 minutes }, + notifications: { + enabled: false, + urgency: "normal" + } }, }; diff --git a/plugins/discord/back.js b/plugins/discord/back.js index 0ebcc624..2fb8cc79 100644 --- a/plugins/discord/back.js +++ b/plugins/discord/back.js @@ -24,7 +24,10 @@ module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => { details: songInfo.title, state: songInfo.artist, largeImageKey: "logo", - largeImageText: songInfo.views + " - " + songInfo.likes, + largeImageText: [ + songInfo.uploadDate, + songInfo.views.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " views" + ].join(' || '), }; if (songInfo.isPaused) { @@ -50,8 +53,7 @@ module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => { }); // Startup the rpc client - rpc - .login({ + rpc.login({ clientId, }) .catch(console.error); diff --git a/plugins/downloader/menu.js b/plugins/downloader/menu.js index caed50bb..cf78001c 100644 --- a/plugins/downloader/menu.js +++ b/plugins/downloader/menu.js @@ -2,15 +2,14 @@ const { existsSync, mkdirSync } = require("fs"); const { join } = require("path"); const { URL } = require("url"); -const { ipcMain } = require("electron"); +const { dialog, ipcMain } = require("electron"); const is = require("electron-is"); const ytpl = require("ytpl"); +const { setOptions } = require("../../config/plugins"); const { sendError } = require("./back"); const { defaultMenuDownloadLabel, getFolder } = require("./utils"); -const { setOptions } = require('../../config/plugins') -const { dialog } = require('electron'); let downloadLabel = defaultMenuDownloadLabel; module.exports = (win, options, refreshMenu) => [ @@ -63,16 +62,17 @@ module.exports = (win, options, refreshMenu) => [ }, }, { - label: 'Choose download folder:', + label: "Choose download folder", click: () => { - let result = dialog.showOpenDialogSync({ - properties: ['openDirectory', 'createDirectory'], + + let result = dialog.showOpenDialogSync({ + properties: ["openDirectory", "createDirectory"], defaultPath: getFolder(options.downloadFolder), - }) - if(result) { - options.downloadFolder = result[0] - setOptions("downloader", options) - } //else = user pressed cancel - } + }); + if (result) { + options.downloadFolder = result[0]; + setOptions("downloader", options); + } // else = user pressed cancel + }, }, ]; diff --git a/plugins/downloader/youtube-dl.js b/plugins/downloader/youtube-dl.js index 65907e2b..9993f4c6 100644 --- a/plugins/downloader/youtube-dl.js +++ b/plugins/downloader/youtube-dl.js @@ -120,7 +120,7 @@ const toMP3 = async ( ); const folder = getFolder(options.downloadFolder); - const name = metadata + const name = metadata.title ? `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}` : videoName; const filename = filenamify(name + "." + extension, { diff --git a/plugins/notifications/back.js b/plugins/notifications/back.js index 4fc1d87d..65e4337b 100644 --- a/plugins/notifications/back.js +++ b/plugins/notifications/back.js @@ -1,7 +1,7 @@ const { Notification } = require("electron"); const getSongInfo = require("../../providers/song-info"); -const notify = info => { +const notify = (info, options) => { let notificationImage = "assets/youtube-music.png"; if (info.image) { @@ -14,27 +14,28 @@ const notify = info => { body: info.artist, icon: notificationImage, silent: true, + urgency: options.urgency, }; - + // Send the notification currentNotification = new Notification(notification); currentNotification.show() - + return currentNotification; }; -module.exports = (win) => { +module.exports = (win, options) => { const registerCallback = getSongInfo(win); let oldNotification; win.on("ready-to-show", () => { // Register the callback for new song information registerCallback(songInfo => { // If song is playing send notification - if (!songInfo.isPaused) { + if (!songInfo.isPaused) { // 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) }, 10); + setTimeout(()=>{ oldNotification = notify(songInfo, options) }, 10); } }); }); diff --git a/plugins/notifications/menu.js b/plugins/notifications/menu.js new file mode 100644 index 00000000..83a7e4f6 --- /dev/null +++ b/plugins/notifications/menu.js @@ -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) + })), + }, +]; diff --git a/plugins/notifications/utils.js b/plugins/notifications/utils.js new file mode 100644 index 00000000..f80dd342 --- /dev/null +++ b/plugins/notifications/utils.js @@ -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) +}; diff --git a/plugins/styled-bars/back.js b/plugins/styled-bars/back.js index 71f2ff9d..37fd6c8b 100644 --- a/plugins/styled-bars/back.js +++ b/plugins/styled-bars/back.js @@ -11,7 +11,7 @@ mainMenuTemplate = function (winHook) { //get template let template = originTemplate(winHook); //fix checkbox and roles - fixCheck(template); + fixMenu(template); //return as normal return template; } @@ -52,12 +52,12 @@ function switchMenuVisibility() { } //go over each item in menu -function fixCheck(ogTemplate) { - for (let position in ogTemplate) { - let item = ogTemplate[position]; +function fixMenu(template) { + for (let index in template) { + let item = template[index]; //apply function on submenu if (item.submenu != null) { - fixCheck(item.submenu); + fixMenu(item.submenu); } //change onClick of checkbox+radio else if (item.type === 'checkbox' || item.type === 'radio') { @@ -98,7 +98,7 @@ function fixRoles(MenuItem) { MenuItem.click = () => { win.webContents.setZoomLevel(0); } break; default: - console.log(MenuItem.role + ' was not expected'); + console.log(`Error fixing MenuRoles: "${MenuItem.role}" was not expected`); } delete MenuItem.role; } diff --git a/plugins/styled-bars/style.css b/plugins/styled-bars/style.css index 108bb2ca..f0d0c88b 100644 --- a/plugins/styled-bars/style.css +++ b/plugins/styled-bars/style.css @@ -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 */ .menubar-menu-container{ overflow-y: visible !important; diff --git a/preload.js b/preload.js index c3d51d16..94f2a72a 100644 --- a/preload.js +++ b/preload.js @@ -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 global.reload = () => remote.getCurrentWindow().webContents.loadURL(config.get("url")); diff --git a/providers/song-info-front.js b/providers/song-info-front.js new file mode 100644 index 00000000..42c3afb2 --- /dev/null +++ b/providers/song-info-front.js @@ -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; diff --git a/providers/song-info.js b/providers/song-info.js index 4ddf9cab..b2764508 100644 --- a/providers/song-info.js +++ b/providers/song-info.js @@ -1,73 +1,18 @@ -const { nativeImage } = require("electron"); +const { ipcMain, 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 +// This selects the progress bar, used for 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 }; + return elapsedSeconds; }; // Grab the native image using the src @@ -77,6 +22,7 @@ const getImage = async (src) => { return nativeImage.createFromBuffer(buffer); }; +// To find the paused status, we check if the title contains `-` const getPausedStatus = async (win) => { const title = await win.webContents.executeJavaScript("document.title"); return !title.includes("-"); @@ -86,13 +32,26 @@ const getPausedStatus = async (win) => { const songInfo = { title: "", artist: "", - views: "", - likes: "", + views: 0, + uploadDate: "", imageSrc: "", image: null, isPaused: true, songDuration: 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) => { @@ -105,32 +64,21 @@ const registerProvider = (win) => { }; 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; + const elapsedSeconds = await getProgress(win); 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); }); }); + // This will be called when the song-info-front finds a new request with song data + ipcMain.on("song-info-request", handleData); + return registerCallback; }; diff --git a/tray.js b/tray.js index 5f5ea35d..e8eb22d3 100644 --- a/tray.js +++ b/tray.js @@ -66,11 +66,18 @@ module.exports.setUpTray = (app, win) => { }, ]; - // 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 template[template.findIndex(item => item.label==='View')]; + delete template[getIndex(template, 'View')]; + const trayMenu = Menu.buildFromTemplate(template); tray.setContextMenu(trayMenu); }; + +function getIndex(arr,label) { + return arr.findIndex(item => item.label === label) +}