mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 10:31:47 +00:00
fix: remove xo, migration to eslint
This commit is contained in:
@ -1,34 +1,35 @@
|
||||
const path = require("path");
|
||||
const path = require('node:path');
|
||||
|
||||
const { app, BrowserWindow, ipcMain, ipcRenderer } = require("electron");
|
||||
const config = require("../config");
|
||||
const { app, BrowserWindow, ipcMain, ipcRenderer } = require('electron');
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
module.exports.restart = () => {
|
||||
process.type === 'browser' ? restart() : ipcRenderer.send('restart');
|
||||
};
|
||||
|
||||
module.exports.setupAppControls = () => {
|
||||
ipcMain.on('restart', restart);
|
||||
ipcMain.handle('getDownloadsFolder', () => app.getPath("downloads"));
|
||||
ipcMain.on('reload', () => BrowserWindow.getFocusedWindow().webContents.loadURL(config.get("url")));
|
||||
ipcMain.handle('getPath', (_, ...args) => path.join(...args));
|
||||
}
|
||||
ipcMain.on('restart', restart);
|
||||
ipcMain.handle('getDownloadsFolder', () => app.getPath('downloads'));
|
||||
ipcMain.on('reload', () => BrowserWindow.getFocusedWindow().webContents.loadURL(config.get('url')));
|
||||
ipcMain.handle('getPath', (_, ...args) => path.join(...args));
|
||||
};
|
||||
|
||||
function restart() {
|
||||
app.relaunch({ execPath: process.env.PORTABLE_EXECUTABLE_FILE });
|
||||
// execPath will be undefined if not running portable app, resulting in default behavior
|
||||
app.quit();
|
||||
app.relaunch({ execPath: process.env.PORTABLE_EXECUTABLE_FILE });
|
||||
// ExecPath will be undefined if not running portable app, resulting in default behavior
|
||||
app.quit();
|
||||
}
|
||||
|
||||
function sendToFront(channel, ...args) {
|
||||
BrowserWindow.getAllWindows().forEach(win => {
|
||||
win.webContents.send(channel, ...args);
|
||||
});
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
win.webContents.send(channel, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.sendToFront =
|
||||
process.type === 'browser'
|
||||
? sendToFront
|
||||
: () => {
|
||||
console.error('sendToFront called from renderer');
|
||||
};
|
||||
module.exports.sendToFront
|
||||
= process.type === 'browser'
|
||||
? sendToFront
|
||||
: () => {
|
||||
console.error('sendToFront called from renderer');
|
||||
};
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
module.exports = {
|
||||
singleton,
|
||||
debounce,
|
||||
cache,
|
||||
throttle,
|
||||
memoize,
|
||||
retry,
|
||||
singleton,
|
||||
debounce,
|
||||
cache,
|
||||
throttle,
|
||||
memoize,
|
||||
retry,
|
||||
};
|
||||
|
||||
/**
|
||||
@ -13,12 +13,15 @@ module.exports = {
|
||||
* @returns {T}
|
||||
*/
|
||||
function singleton(fn) {
|
||||
let called = false;
|
||||
return (...args) => {
|
||||
if (called) return;
|
||||
called = true;
|
||||
return fn(...args);
|
||||
};
|
||||
let called = false;
|
||||
return (...args) => {
|
||||
if (called) {
|
||||
return;
|
||||
}
|
||||
|
||||
called = true;
|
||||
return fn(...args);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -28,11 +31,11 @@ function singleton(fn) {
|
||||
* @returns {T}
|
||||
*/
|
||||
function debounce(fn, delay) {
|
||||
let timeout;
|
||||
return (...args) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => fn(...args), delay);
|
||||
};
|
||||
let timeout;
|
||||
return (...args) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => fn(...args), delay);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -41,22 +44,23 @@ function debounce(fn, delay) {
|
||||
* @returns {T}
|
||||
*/
|
||||
function cache(fn) {
|
||||
let lastArgs;
|
||||
let lastResult;
|
||||
return (...args) => {
|
||||
if (
|
||||
args.length !== lastArgs?.length ||
|
||||
args.some((arg, i) => arg !== lastArgs[i])
|
||||
) {
|
||||
lastArgs = args;
|
||||
lastResult = fn(...args);
|
||||
}
|
||||
return lastResult;
|
||||
};
|
||||
let lastArgs;
|
||||
let lastResult;
|
||||
return (...args) => {
|
||||
if (
|
||||
args.length !== lastArgs?.length
|
||||
|| args.some((arg, i) => arg !== lastArgs[i])
|
||||
) {
|
||||
lastArgs = args;
|
||||
lastResult = fn(...args);
|
||||
}
|
||||
|
||||
return lastResult;
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
the following are currently unused, but potentially useful in the future
|
||||
The following are currently unused, but potentially useful in the future
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -66,14 +70,17 @@ function cache(fn) {
|
||||
* @returns {T}
|
||||
*/
|
||||
function throttle(fn, delay) {
|
||||
let timeout;
|
||||
return (...args) => {
|
||||
if (timeout) return;
|
||||
timeout = setTimeout(() => {
|
||||
timeout = undefined;
|
||||
fn(...args);
|
||||
}, delay);
|
||||
};
|
||||
let timeout;
|
||||
return (...args) => {
|
||||
if (timeout) {
|
||||
return;
|
||||
}
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
timeout = undefined;
|
||||
fn(...args);
|
||||
}, delay);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -82,14 +89,15 @@ function throttle(fn, delay) {
|
||||
* @returns {T}
|
||||
*/
|
||||
function memoize(fn) {
|
||||
const cache = new Map();
|
||||
return (...args) => {
|
||||
const key = JSON.stringify(args);
|
||||
if (!cache.has(key)) {
|
||||
cache.set(key, fn(...args));
|
||||
}
|
||||
return cache.get(key);
|
||||
};
|
||||
const cache = new Map();
|
||||
return (...args) => {
|
||||
const key = JSON.stringify(args);
|
||||
if (!cache.has(key)) {
|
||||
cache.set(key, fn(...args));
|
||||
}
|
||||
|
||||
return cache.get(key);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -98,16 +106,16 @@ function memoize(fn) {
|
||||
* @returns {T}
|
||||
*/
|
||||
function retry(fn, { retries = 3, delay = 1000 } = {}) {
|
||||
return (...args) => {
|
||||
try {
|
||||
return fn(...args);
|
||||
} catch (e) {
|
||||
if (retries > 0) {
|
||||
retries--;
|
||||
setTimeout(() => retry(fn, { retries, delay })(...args), delay);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
return (...args) => {
|
||||
try {
|
||||
return fn(...args);
|
||||
} catch (error) {
|
||||
if (retries > 0) {
|
||||
retries--;
|
||||
setTimeout(() => retry(fn, { retries, delay })(...args), delay);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const getSongMenu = () =>
|
||||
document.querySelector("ytmusic-menu-popup-renderer tp-yt-paper-listbox");
|
||||
document.querySelector('ytmusic-menu-popup-renderer tp-yt-paper-listbox');
|
||||
|
||||
module.exports = { getSongMenu };
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
const startingPages = {
|
||||
Default: '',
|
||||
Home: 'FEmusic_home',
|
||||
Explore: 'FEmusic_explore',
|
||||
'New Releases': 'FEmusic_new_releases',
|
||||
Charts: 'FEmusic_charts',
|
||||
'Moods & Genres': 'FEmusic_moods_and_genres',
|
||||
Library: 'FEmusic_library_landing',
|
||||
Playlists: 'FEmusic_liked_playlists',
|
||||
Songs: 'FEmusic_liked_videos',
|
||||
Albums: 'FEmusic_liked_albums',
|
||||
Artists: 'FEmusic_library_corpus_track_artists',
|
||||
'Subscribed Artists': 'FEmusic_library_corpus_artists',
|
||||
Uploads: 'FEmusic_library_privately_owned_landing',
|
||||
'Uploaded Playlists': 'FEmusic_liked_playlists',
|
||||
'Uploaded Songs': 'FEmusic_library_privately_owned_tracks',
|
||||
'Uploaded Albums': 'FEmusic_library_privately_owned_releases',
|
||||
'Uploaded Artists': 'FEmusic_library_privately_owned_artists',
|
||||
'Default': '',
|
||||
'Home': 'FEmusic_home',
|
||||
'Explore': 'FEmusic_explore',
|
||||
'New Releases': 'FEmusic_new_releases',
|
||||
'Charts': 'FEmusic_charts',
|
||||
'Moods & Genres': 'FEmusic_moods_and_genres',
|
||||
'Library': 'FEmusic_library_landing',
|
||||
'Playlists': 'FEmusic_liked_playlists',
|
||||
'Songs': 'FEmusic_liked_videos',
|
||||
'Albums': 'FEmusic_liked_albums',
|
||||
'Artists': 'FEmusic_library_corpus_track_artists',
|
||||
'Subscribed Artists': 'FEmusic_library_corpus_artists',
|
||||
'Uploads': 'FEmusic_library_privately_owned_landing',
|
||||
'Uploaded Playlists': 'FEmusic_liked_playlists',
|
||||
'Uploaded Songs': 'FEmusic_library_privately_owned_tracks',
|
||||
'Uploaded Albums': 'FEmusic_library_privately_owned_releases',
|
||||
'Uploaded Artists': 'FEmusic_library_privately_owned_artists',
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
startingPages,
|
||||
startingPages,
|
||||
};
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
const { Titlebar, Color } = require("custom-electron-titlebar");
|
||||
const { Titlebar, Color } = require('custom-electron-titlebar');
|
||||
|
||||
module.exports = () => {
|
||||
new Titlebar({
|
||||
backgroundColor: Color.fromHex("#050505"),
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
menu: null
|
||||
});
|
||||
const mainStyle = document.querySelector("#container").style;
|
||||
mainStyle.width = "100%";
|
||||
mainStyle.position = "fixed";
|
||||
mainStyle.border = "unset";
|
||||
new Titlebar({
|
||||
backgroundColor: Color.fromHex('#050505'),
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
menu: null,
|
||||
});
|
||||
const mainStyle = document.querySelector('#container').style;
|
||||
mainStyle.width = '100%';
|
||||
mainStyle.position = 'fixed';
|
||||
mainStyle.border = 'unset';
|
||||
};
|
||||
|
||||
@ -1,18 +1,20 @@
|
||||
const path = require("path");
|
||||
const is = require("electron-is");
|
||||
const { isEnabled } = require("../config/plugins");
|
||||
const path = require('node:path');
|
||||
|
||||
const iconPath = path.join(__dirname, "..", "assets", "youtube-music-tray.png");
|
||||
const customTitlebarPath = path.join(__dirname, "prompt-custom-titlebar.js");
|
||||
const is = require('electron-is');
|
||||
|
||||
const promptOptions = !is.macOS() && isEnabled("in-app-menu") ? {
|
||||
customStylesheet: "dark",
|
||||
// The following are used for custom titlebar
|
||||
frame: false,
|
||||
customScript: customTitlebarPath,
|
||||
const { isEnabled } = require('../config/plugins');
|
||||
|
||||
const iconPath = path.join(__dirname, '..', 'assets', 'youtube-music-tray.png');
|
||||
const customTitlebarPath = path.join(__dirname, 'prompt-custom-titlebar.js');
|
||||
|
||||
const promptOptions = !is.macOS() && isEnabled('in-app-menu') ? {
|
||||
customStylesheet: 'dark',
|
||||
// The following are used for custom titlebar
|
||||
frame: false,
|
||||
customScript: customTitlebarPath,
|
||||
} : {
|
||||
customStylesheet: "dark",
|
||||
icon: iconPath
|
||||
customStylesheet: 'dark',
|
||||
icon: iconPath,
|
||||
};
|
||||
|
||||
module.exports = () => promptOptions;
|
||||
|
||||
@ -1,44 +1,45 @@
|
||||
const { app } = require("electron");
|
||||
const path = require("path");
|
||||
const getSongControls = require("./song-controls");
|
||||
const path = require('node:path');
|
||||
|
||||
const APP_PROTOCOL = "youtubemusic";
|
||||
const { app } = require('electron');
|
||||
|
||||
const getSongControls = require('./song-controls');
|
||||
|
||||
const APP_PROTOCOL = 'youtubemusic';
|
||||
|
||||
let protocolHandler;
|
||||
|
||||
function setupProtocolHandler(win) {
|
||||
if (process.defaultApp && process.argv.length >= 2) {
|
||||
app.setAsDefaultProtocolClient(
|
||||
APP_PROTOCOL,
|
||||
process.execPath,
|
||||
[path.resolve(process.argv[1])]
|
||||
);
|
||||
} else {
|
||||
app.setAsDefaultProtocolClient(APP_PROTOCOL)
|
||||
}
|
||||
if (process.defaultApp && process.argv.length >= 2) {
|
||||
app.setAsDefaultProtocolClient(
|
||||
APP_PROTOCOL,
|
||||
process.execPath,
|
||||
[path.resolve(process.argv[1])],
|
||||
);
|
||||
} else {
|
||||
app.setAsDefaultProtocolClient(APP_PROTOCOL);
|
||||
}
|
||||
|
||||
const songControls = getSongControls(win);
|
||||
const songControls = getSongControls(win);
|
||||
|
||||
protocolHandler = (cmd) => {
|
||||
if (Object.keys(songControls).includes(cmd)) {
|
||||
songControls[cmd]();
|
||||
}
|
||||
}
|
||||
protocolHandler = (cmd) => {
|
||||
if (Object.keys(songControls).includes(cmd)) {
|
||||
songControls[cmd]();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function handleProtocol(cmd) {
|
||||
protocolHandler(cmd);
|
||||
protocolHandler(cmd);
|
||||
}
|
||||
|
||||
function changeProtocolHandler(f) {
|
||||
protocolHandler = f;
|
||||
protocolHandler = f;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
APP_PROTOCOL,
|
||||
setupProtocolHandler,
|
||||
handleProtocol,
|
||||
changeProtocolHandler,
|
||||
APP_PROTOCOL,
|
||||
setupProtocolHandler,
|
||||
handleProtocol,
|
||||
changeProtocolHandler,
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
const { ipcRenderer } = require("electron");
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
module.exports.setupSongControls = () => {
|
||||
document.addEventListener('apiLoaded', e => {
|
||||
ipcRenderer.on("seekTo", (_, t) => e.detail.seekTo(t));
|
||||
ipcRenderer.on("seekBy", (_, t) => e.detail.seekBy(t));
|
||||
}, { once: true, passive: true })
|
||||
document.addEventListener('apiLoaded', (e) => {
|
||||
ipcRenderer.on('seekTo', (_, t) => e.detail.seekTo(t));
|
||||
ipcRenderer.on('seekBy', (_, t) => e.detail.seekBy(t));
|
||||
}, { once: true, passive: true });
|
||||
};
|
||||
|
||||
@ -1,57 +1,59 @@
|
||||
// This is used for to control the songs
|
||||
const pressKey = (window, key, modifiers = []) => {
|
||||
window.webContents.sendInputEvent({
|
||||
type: "keydown",
|
||||
modifiers,
|
||||
keyCode: key,
|
||||
});
|
||||
window.webContents.sendInputEvent({
|
||||
type: 'keydown',
|
||||
modifiers,
|
||||
keyCode: key,
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = (win) => {
|
||||
const commands = {
|
||||
// Playback
|
||||
previous: () => pressKey(win, "k"),
|
||||
next: () => pressKey(win, "j"),
|
||||
playPause: () => pressKey(win, ";"),
|
||||
like: () => pressKey(win, "+"),
|
||||
dislike: () => pressKey(win, "_"),
|
||||
go10sBack: () => pressKey(win, "h"),
|
||||
go10sForward: () => pressKey(win, "l"),
|
||||
go1sBack: () => pressKey(win, "h", ["shift"]),
|
||||
go1sForward: () => pressKey(win, "l", ["shift"]),
|
||||
shuffle: () => pressKey(win, "s"),
|
||||
switchRepeat: (n = 1) => {
|
||||
for (let i = 0; i < n; i++) pressKey(win, "r");
|
||||
},
|
||||
// General
|
||||
volumeMinus10: () => pressKey(win, "-"),
|
||||
volumePlus10: () => pressKey(win, "="),
|
||||
fullscreen: () => pressKey(win, "f"),
|
||||
muteUnmute: () => pressKey(win, "m"),
|
||||
maximizeMinimisePlayer: () => pressKey(win, "q"),
|
||||
// Navigation
|
||||
goToHome: () => {
|
||||
pressKey(win, "g");
|
||||
pressKey(win, "h");
|
||||
},
|
||||
goToLibrary: () => {
|
||||
pressKey(win, "g");
|
||||
pressKey(win, "l");
|
||||
},
|
||||
goToSettings: () => {
|
||||
pressKey(win, "g");
|
||||
pressKey(win, ",");
|
||||
},
|
||||
goToExplore: () => {
|
||||
pressKey(win, "g");
|
||||
pressKey(win, "e");
|
||||
},
|
||||
search: () => pressKey(win, "/"),
|
||||
showShortcuts: () => pressKey(win, "/", ["shift"]),
|
||||
};
|
||||
return {
|
||||
...commands,
|
||||
play: commands.playPause,
|
||||
pause: commands.playPause
|
||||
};
|
||||
const commands = {
|
||||
// Playback
|
||||
previous: () => pressKey(win, 'k'),
|
||||
next: () => pressKey(win, 'j'),
|
||||
playPause: () => pressKey(win, ';'),
|
||||
like: () => pressKey(win, '+'),
|
||||
dislike: () => pressKey(win, '_'),
|
||||
go10sBack: () => pressKey(win, 'h'),
|
||||
go10sForward: () => pressKey(win, 'l'),
|
||||
go1sBack: () => pressKey(win, 'h', ['shift']),
|
||||
go1sForward: () => pressKey(win, 'l', ['shift']),
|
||||
shuffle: () => pressKey(win, 's'),
|
||||
switchRepeat(n = 1) {
|
||||
for (let i = 0; i < n; i++) {
|
||||
pressKey(win, 'r');
|
||||
}
|
||||
},
|
||||
// General
|
||||
volumeMinus10: () => pressKey(win, '-'),
|
||||
volumePlus10: () => pressKey(win, '='),
|
||||
fullscreen: () => pressKey(win, 'f'),
|
||||
muteUnmute: () => pressKey(win, 'm'),
|
||||
maximizeMinimisePlayer: () => pressKey(win, 'q'),
|
||||
// Navigation
|
||||
goToHome() {
|
||||
pressKey(win, 'g');
|
||||
pressKey(win, 'h');
|
||||
},
|
||||
goToLibrary() {
|
||||
pressKey(win, 'g');
|
||||
pressKey(win, 'l');
|
||||
},
|
||||
goToSettings() {
|
||||
pressKey(win, 'g');
|
||||
pressKey(win, ',');
|
||||
},
|
||||
goToExplore() {
|
||||
pressKey(win, 'g');
|
||||
pressKey(win, 'e');
|
||||
},
|
||||
search: () => pressKey(win, '/'),
|
||||
showShortcuts: () => pressKey(win, '/', ['shift']),
|
||||
};
|
||||
return {
|
||||
...commands,
|
||||
play: commands.playPause,
|
||||
pause: commands.playPause,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,100 +1,105 @@
|
||||
const { ipcRenderer } = require("electron");
|
||||
const { getImage } = require("./song-info");
|
||||
const { singleton } = require("../providers/decorators");
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
const { getImage } = require('./song-info');
|
||||
|
||||
const { singleton } = require('../providers/decorators');
|
||||
|
||||
global.songInfo = {};
|
||||
|
||||
const $ = s => document.querySelector(s);
|
||||
const $$ = s => Array.from(document.querySelectorAll(s));
|
||||
const $ = (s) => document.querySelector(s);
|
||||
const $$ = (s) => [...document.querySelectorAll(s)];
|
||||
|
||||
ipcRenderer.on("update-song-info", async (_, extractedSongInfo) => {
|
||||
global.songInfo = JSON.parse(extractedSongInfo);
|
||||
global.songInfo.image = await getImage(global.songInfo.imageSrc);
|
||||
ipcRenderer.on('update-song-info', async (_, extractedSongInfo) => {
|
||||
global.songInfo = JSON.parse(extractedSongInfo);
|
||||
global.songInfo.image = await getImage(global.songInfo.imageSrc);
|
||||
});
|
||||
|
||||
// used because 'loadeddata' or 'loadedmetadata' weren't firing on song start for some users (https://github.com/th-ch/youtube-music/issues/473)
|
||||
// Used because 'loadeddata' or 'loadedmetadata' weren't firing on song start for some users (https://github.com/th-ch/youtube-music/issues/473)
|
||||
const srcChangedEvent = new CustomEvent('srcChanged');
|
||||
|
||||
module.exports.setupSeekedListener = singleton(() => {
|
||||
$('video')?.addEventListener('seeked', v => ipcRenderer.send('seeked', v.target.currentTime));
|
||||
$('video')?.addEventListener('seeked', (v) => ipcRenderer.send('seeked', v.target.currentTime));
|
||||
});
|
||||
|
||||
module.exports.setupTimeChangedListener = singleton(() => {
|
||||
const progressObserver = new MutationObserver(mutations => {
|
||||
ipcRenderer.send('timeChanged', mutations[0].target.value);
|
||||
global.songInfo.elapsedSeconds = mutations[0].target.value;
|
||||
});
|
||||
progressObserver.observe($('#progress-bar'), { attributeFilter: ["value"] });
|
||||
const progressObserver = new MutationObserver((mutations) => {
|
||||
ipcRenderer.send('timeChanged', mutations[0].target.value);
|
||||
global.songInfo.elapsedSeconds = mutations[0].target.value;
|
||||
});
|
||||
progressObserver.observe($('#progress-bar'), { attributeFilter: ['value'] });
|
||||
});
|
||||
|
||||
module.exports.setupRepeatChangedListener = singleton(() => {
|
||||
const repeatObserver = new MutationObserver(mutations => {
|
||||
ipcRenderer.send('repeatChanged', mutations[0].target.__dataHost.getState().queue.repeatMode);
|
||||
});
|
||||
repeatObserver.observe($('#right-controls .repeat'), { attributeFilter: ["title"] });
|
||||
const repeatObserver = new MutationObserver((mutations) => {
|
||||
ipcRenderer.send('repeatChanged', mutations[0].target.__dataHost.getState().queue.repeatMode);
|
||||
});
|
||||
repeatObserver.observe($('#right-controls .repeat'), { attributeFilter: ['title'] });
|
||||
|
||||
// Emit the initial value as well; as it's persistent between launches.
|
||||
ipcRenderer.send('repeatChanged', $('ytmusic-player-bar').getState().queue.repeatMode);
|
||||
// Emit the initial value as well; as it's persistent between launches.
|
||||
ipcRenderer.send('repeatChanged', $('ytmusic-player-bar').getState().queue.repeatMode);
|
||||
});
|
||||
|
||||
module.exports.setupVolumeChangedListener = singleton((api) => {
|
||||
$('video').addEventListener('volumechange', (_) => {
|
||||
ipcRenderer.send('volumeChanged', api.getVolume());
|
||||
});
|
||||
// Emit the initial value as well; as it's persistent between launches.
|
||||
ipcRenderer.send('volumeChanged', api.getVolume());
|
||||
$('video').addEventListener('volumechange', (_) => {
|
||||
ipcRenderer.send('volumeChanged', api.getVolume());
|
||||
});
|
||||
// Emit the initial value as well; as it's persistent between launches.
|
||||
ipcRenderer.send('volumeChanged', api.getVolume());
|
||||
});
|
||||
|
||||
module.exports = () => {
|
||||
document.addEventListener('apiLoaded', apiEvent => {
|
||||
ipcRenderer.on("setupTimeChangedListener", async () => {
|
||||
this.setupTimeChangedListener();
|
||||
});
|
||||
document.addEventListener('apiLoaded', (apiEvent) => {
|
||||
ipcRenderer.on('setupTimeChangedListener', async () => {
|
||||
this.setupTimeChangedListener();
|
||||
});
|
||||
|
||||
ipcRenderer.on("setupRepeatChangedListener", async () => {
|
||||
this.setupRepeatChangedListener();
|
||||
});
|
||||
ipcRenderer.on('setupRepeatChangedListener', async () => {
|
||||
this.setupRepeatChangedListener();
|
||||
});
|
||||
|
||||
ipcRenderer.on("setupVolumeChangedListener", async () => {
|
||||
this.setupVolumeChangedListener(apiEvent.detail);
|
||||
});
|
||||
ipcRenderer.on('setupVolumeChangedListener', async () => {
|
||||
this.setupVolumeChangedListener(apiEvent.detail);
|
||||
});
|
||||
|
||||
ipcRenderer.on("setupSeekedListener", async () => {
|
||||
this.setupSeekedListener();
|
||||
});
|
||||
ipcRenderer.on('setupSeekedListener', async () => {
|
||||
this.setupSeekedListener();
|
||||
});
|
||||
|
||||
const video = $('video');
|
||||
// name = "dataloaded" and abit later "dataupdated"
|
||||
apiEvent.detail.addEventListener('videodatachange', (name, _dataEvent) => {
|
||||
if (name !== 'dataloaded') return;
|
||||
video.dispatchEvent(srcChangedEvent);
|
||||
setTimeout(sendSongInfo, 200);
|
||||
})
|
||||
const video = $('video');
|
||||
// Name = "dataloaded" and abit later "dataupdated"
|
||||
apiEvent.detail.addEventListener('videodatachange', (name, _dataEvent) => {
|
||||
if (name !== 'dataloaded') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const status of ['playing', 'pause']) {
|
||||
video.addEventListener(status, e => {
|
||||
if (Math.round(e.target.currentTime) > 0) {
|
||||
ipcRenderer.send("playPaused", {
|
||||
isPaused: status === 'pause',
|
||||
elapsedSeconds: Math.floor(e.target.currentTime)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
video.dispatchEvent(srcChangedEvent);
|
||||
setTimeout(sendSongInfo, 200);
|
||||
});
|
||||
|
||||
function sendSongInfo() {
|
||||
const data = apiEvent.detail.getPlayerResponse();
|
||||
for (const status of ['playing', 'pause']) {
|
||||
video.addEventListener(status, (e) => {
|
||||
if (Math.round(e.target.currentTime) > 0) {
|
||||
ipcRenderer.send('playPaused', {
|
||||
isPaused: status === 'pause',
|
||||
elapsedSeconds: Math.floor(e.target.currentTime),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
data.videoDetails.album = $$(
|
||||
".byline.ytmusic-player-bar > .yt-simple-endpoint"
|
||||
).find(e =>
|
||||
e.href?.includes("browse/FEmusic_library_privately_owned_release")
|
||||
|| e.href?.includes("browse/MPREb")
|
||||
)?.textContent;
|
||||
function sendSongInfo() {
|
||||
const data = apiEvent.detail.getPlayerResponse();
|
||||
|
||||
data.videoDetails.elapsedSeconds = 0;
|
||||
data.videoDetails.isPaused = false;
|
||||
ipcRenderer.send("video-src-changed", JSON.stringify(data));
|
||||
}
|
||||
}, { once: true, passive: true });
|
||||
data.videoDetails.album = $$(
|
||||
'.byline.ytmusic-player-bar > .yt-simple-endpoint',
|
||||
).find((e) =>
|
||||
e.href?.includes('browse/FEmusic_library_privately_owned_release')
|
||||
|| e.href?.includes('browse/MPREb'),
|
||||
)?.textContent;
|
||||
|
||||
data.videoDetails.elapsedSeconds = 0;
|
||||
data.videoDetails.isPaused = false;
|
||||
ipcRenderer.send('video-src-changed', JSON.stringify(data));
|
||||
}
|
||||
}, { once: true, passive: true });
|
||||
};
|
||||
|
||||
@ -1,78 +1,78 @@
|
||||
const { ipcMain, nativeImage } = require("electron");
|
||||
const { ipcMain, nativeImage } = require('electron');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
const fetch = require("node-fetch");
|
||||
|
||||
const config = require("../config");
|
||||
|
||||
const { cache } = require("../providers/decorators")
|
||||
const config = require('../config');
|
||||
const { cache } = require('../providers/decorators');
|
||||
|
||||
// Fill songInfo with empty values
|
||||
/**
|
||||
* @typedef {songInfo} SongInfo
|
||||
*/
|
||||
const songInfo = {
|
||||
title: "",
|
||||
artist: "",
|
||||
views: 0,
|
||||
uploadDate: "",
|
||||
imageSrc: "",
|
||||
image: null,
|
||||
isPaused: undefined,
|
||||
songDuration: 0,
|
||||
elapsedSeconds: 0,
|
||||
url: "",
|
||||
album: undefined,
|
||||
videoId: "",
|
||||
playlistId: "",
|
||||
title: '',
|
||||
artist: '',
|
||||
views: 0,
|
||||
uploadDate: '',
|
||||
imageSrc: '',
|
||||
image: null,
|
||||
isPaused: undefined,
|
||||
songDuration: 0,
|
||||
elapsedSeconds: 0,
|
||||
url: '',
|
||||
album: undefined,
|
||||
videoId: '',
|
||||
playlistId: '',
|
||||
};
|
||||
|
||||
// Grab the native image using the src
|
||||
const getImage = cache(
|
||||
/**
|
||||
* @returns {Promise<Electron.NativeImage>}
|
||||
*/
|
||||
async (src) => {
|
||||
const result = await fetch(src);
|
||||
const buffer = await result.buffer();
|
||||
const output = nativeImage.createFromBuffer(buffer);
|
||||
if (output.isEmpty() && !src.endsWith(".jpg") && src.includes(".jpg")) { // fix hidden webp files (https://github.com/th-ch/youtube-music/issues/315)
|
||||
return getImage(src.slice(0, src.lastIndexOf(".jpg") + 4));
|
||||
} else {
|
||||
return output;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @returns {Promise<Electron.NativeImage>}
|
||||
*/
|
||||
async (src) => {
|
||||
const result = await fetch(src);
|
||||
const buffer = await result.buffer();
|
||||
const output = nativeImage.createFromBuffer(buffer);
|
||||
if (output.isEmpty() && !src.endsWith('.jpg') && src.includes('.jpg')) { // Fix hidden webp files (https://github.com/th-ch/youtube-music/issues/315)
|
||||
return getImage(src.slice(0, src.lastIndexOf('.jpg') + 4));
|
||||
}
|
||||
|
||||
return output;
|
||||
},
|
||||
);
|
||||
|
||||
const handleData = async (responseText, win) => {
|
||||
const data = JSON.parse(responseText);
|
||||
if (!data) return;
|
||||
const data = JSON.parse(responseText);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const microformat = data.microformat?.microformatDataRenderer;
|
||||
if (microformat) {
|
||||
songInfo.uploadDate = microformat.uploadDate;
|
||||
songInfo.url = microformat.urlCanonical?.split("&")[0];
|
||||
songInfo.playlistId = new URL(microformat.urlCanonical).searchParams.get("list");
|
||||
// used for options.resumeOnStart
|
||||
config.set("url", microformat.urlCanonical);
|
||||
}
|
||||
const microformat = data.microformat?.microformatDataRenderer;
|
||||
if (microformat) {
|
||||
songInfo.uploadDate = microformat.uploadDate;
|
||||
songInfo.url = microformat.urlCanonical?.split('&')[0];
|
||||
songInfo.playlistId = new URL(microformat.urlCanonical).searchParams.get('list');
|
||||
// Used for options.resumeOnStart
|
||||
config.set('url', microformat.urlCanonical);
|
||||
}
|
||||
|
||||
const videoDetails = data.videoDetails;
|
||||
if (videoDetails) {
|
||||
songInfo.title = cleanupName(videoDetails.title);
|
||||
songInfo.artist = cleanupName(videoDetails.author);
|
||||
songInfo.views = videoDetails.viewCount;
|
||||
songInfo.songDuration = videoDetails.lengthSeconds;
|
||||
songInfo.elapsedSeconds = videoDetails.elapsedSeconds;
|
||||
songInfo.isPaused = videoDetails.isPaused;
|
||||
songInfo.videoId = videoDetails.videoId;
|
||||
songInfo.album = data?.videoDetails?.album; // Will be undefined if video exist
|
||||
const { videoDetails } = data;
|
||||
if (videoDetails) {
|
||||
songInfo.title = cleanupName(videoDetails.title);
|
||||
songInfo.artist = cleanupName(videoDetails.author);
|
||||
songInfo.views = videoDetails.viewCount;
|
||||
songInfo.songDuration = videoDetails.lengthSeconds;
|
||||
songInfo.elapsedSeconds = videoDetails.elapsedSeconds;
|
||||
songInfo.isPaused = videoDetails.isPaused;
|
||||
songInfo.videoId = videoDetails.videoId;
|
||||
songInfo.album = data?.videoDetails?.album; // Will be undefined if video exist
|
||||
|
||||
const thumbnails = videoDetails.thumbnail?.thumbnails;
|
||||
songInfo.imageSrc = thumbnails[thumbnails.length - 1]?.url.split("?")[0];
|
||||
songInfo.image = await getImage(songInfo.imageSrc);
|
||||
|
||||
win.webContents.send("update-song-info", JSON.stringify(songInfo));
|
||||
}
|
||||
const thumbnails = videoDetails.thumbnail?.thumbnails;
|
||||
songInfo.imageSrc = thumbnails.at(-1)?.url.split('?')[0];
|
||||
songInfo.image = await getImage(songInfo.imageSrc);
|
||||
|
||||
win.webContents.send('update-song-info', JSON.stringify(songInfo));
|
||||
}
|
||||
};
|
||||
|
||||
// This variable will be filled with the callbacks once they register
|
||||
@ -88,48 +88,55 @@ const callbacks = [];
|
||||
* @param {songInfoCallback} callback
|
||||
*/
|
||||
const registerCallback = (callback) => {
|
||||
callbacks.push(callback);
|
||||
callbacks.push(callback);
|
||||
};
|
||||
|
||||
let handlingData = false;
|
||||
|
||||
const registerProvider = (win) => {
|
||||
// This will be called when the song-info-front finds a new request with song data
|
||||
ipcMain.on("video-src-changed", async (_, responseText) => {
|
||||
handlingData = true;
|
||||
await handleData(responseText, win);
|
||||
handlingData = false;
|
||||
callbacks.forEach((c) => {
|
||||
c(songInfo, "video-src-changed");
|
||||
});
|
||||
});
|
||||
ipcMain.on("playPaused", (_, { isPaused, elapsedSeconds }) => {
|
||||
songInfo.isPaused = isPaused;
|
||||
songInfo.elapsedSeconds = elapsedSeconds;
|
||||
if (handlingData) return;
|
||||
callbacks.forEach((c) => {
|
||||
c(songInfo, "playPaused");
|
||||
});
|
||||
})
|
||||
// This will be called when the song-info-front finds a new request with song data
|
||||
ipcMain.on('video-src-changed', async (_, responseText) => {
|
||||
handlingData = true;
|
||||
await handleData(responseText, win);
|
||||
handlingData = false;
|
||||
for (const c of callbacks) {
|
||||
c(songInfo, 'video-src-changed');
|
||||
}
|
||||
});
|
||||
ipcMain.on('playPaused', (_, { isPaused, elapsedSeconds }) => {
|
||||
songInfo.isPaused = isPaused;
|
||||
songInfo.elapsedSeconds = elapsedSeconds;
|
||||
if (handlingData) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const c of callbacks) {
|
||||
c(songInfo, 'playPaused');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const suffixesToRemove = [
|
||||
" - topic",
|
||||
"vevo",
|
||||
" (performance video)",
|
||||
" (clip officiel)",
|
||||
' - topic',
|
||||
'vevo',
|
||||
' (performance video)',
|
||||
' (clip officiel)',
|
||||
];
|
||||
|
||||
function cleanupName(name) {
|
||||
if (!name) return name;
|
||||
name = name.replace(/\((?:official)?[ ]?(?:music)?[ ]?(?:lyric[s]?)?[ ]?(?:video)?\)$/i, '')
|
||||
const lowCaseName = name.toLowerCase();
|
||||
for (const suffix of suffixesToRemove) {
|
||||
if (lowCaseName.endsWith(suffix)) {
|
||||
return name.slice(0, -suffix.length);
|
||||
}
|
||||
}
|
||||
return name;
|
||||
if (!name) {
|
||||
return name;
|
||||
}
|
||||
|
||||
name = name.replace(/\((?:official)? ?(?:music)? ?(?:lyrics?)? ?(?:video)?\)$/i, '');
|
||||
const lowCaseName = name.toLowerCase();
|
||||
for (const suffix of suffixesToRemove) {
|
||||
if (lowCaseName.endsWith(suffix)) {
|
||||
return name.slice(0, -suffix.length);
|
||||
}
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
module.exports = registerCallback;
|
||||
|
||||
Reference in New Issue
Block a user