mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-13 03:11:46 +00:00
feat: migrate to new plugin api
Co-authored-by: Su-Yong <simssy2205@gmail.com>
This commit is contained in:
55
src/plugins/discord/index.ts
Normal file
55
src/plugins/discord/index.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { createPluginBuilder } from '../utils/builder';
|
||||
|
||||
export type DiscordPluginConfig = {
|
||||
enabled: boolean;
|
||||
/**
|
||||
* If enabled, will try to reconnect to discord every 5 seconds after disconnecting or failing to connect
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
autoReconnect: boolean;
|
||||
/**
|
||||
* If enabled, the discord rich presence gets cleared when music paused after the time specified below
|
||||
*/
|
||||
activityTimoutEnabled: boolean;
|
||||
/**
|
||||
* The time in milliseconds after which the discord rich presence gets cleared when music paused
|
||||
*
|
||||
* @default 10 * 60 * 1000 (10 minutes)
|
||||
*/
|
||||
activityTimoutTime: number;
|
||||
/**
|
||||
* Add a "Play on YouTube Music" button to rich presence
|
||||
*/
|
||||
playOnYouTubeMusic: boolean;
|
||||
/**
|
||||
* Hide the "View App On GitHub" button in the rich presence
|
||||
*/
|
||||
hideGitHubButton: boolean;
|
||||
/**
|
||||
* Hide the "duration left" in the rich presence
|
||||
*/
|
||||
hideDurationLeft: boolean;
|
||||
}
|
||||
|
||||
const builder = createPluginBuilder('discord', {
|
||||
name: 'Discord Rich Presence',
|
||||
restartNeeded: false,
|
||||
config: {
|
||||
enabled: false,
|
||||
autoReconnect: true,
|
||||
activityTimoutEnabled: true,
|
||||
activityTimoutTime: 10 * 60 * 1000,
|
||||
playOnYouTubeMusic: true,
|
||||
hideGitHubButton: false,
|
||||
hideDurationLeft: false,
|
||||
} as DiscordPluginConfig,
|
||||
});
|
||||
|
||||
export default builder;
|
||||
|
||||
declare global {
|
||||
interface PluginBuilderList {
|
||||
[builder.id]: typeof builder;
|
||||
}
|
||||
}
|
||||
@ -4,9 +4,9 @@ import { dev } from 'electron-is';
|
||||
|
||||
import { SetActivity } from '@xhayper/discord-rpc/dist/structures/ClientUser';
|
||||
|
||||
import registerCallback, { type SongInfoCallback, type SongInfo } from '../../providers/song-info';
|
||||
import builder from './index';
|
||||
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
import registerCallback, { type SongInfoCallback, type SongInfo } from '../../providers/song-info';
|
||||
|
||||
// Application ID registered by @Zo-Bro-23
|
||||
const clientId = '1043858434585526382';
|
||||
@ -51,7 +51,6 @@ const connectTimeout = () => new Promise((resolve, reject) => setTimeout(() => {
|
||||
|
||||
info.rpc.login().then(resolve).catch(reject);
|
||||
}, 5000));
|
||||
|
||||
const connectRecursive = () => {
|
||||
if (!info.autoReconnect || info.rpc.isConnected) {
|
||||
return;
|
||||
@ -94,129 +93,133 @@ export const connect = (showError = false) => {
|
||||
let clearActivity: NodeJS.Timeout | undefined;
|
||||
let updateActivity: SongInfoCallback;
|
||||
|
||||
type DiscordOptions = ConfigType<'discord'>;
|
||||
export default builder.createMain(({ getConfig }) => {
|
||||
return {
|
||||
async onLoad(win) {
|
||||
const options = await getConfig();
|
||||
|
||||
export default (
|
||||
win: Electron.BrowserWindow,
|
||||
options: DiscordOptions,
|
||||
) => {
|
||||
info.rpc.on('connected', () => {
|
||||
if (dev()) {
|
||||
console.log('discord connected');
|
||||
}
|
||||
|
||||
for (const cb of refreshCallbacks) {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
|
||||
info.rpc.on('ready', () => {
|
||||
info.ready = true;
|
||||
if (info.lastSongInfo) {
|
||||
updateActivity(info.lastSongInfo);
|
||||
}
|
||||
});
|
||||
|
||||
info.rpc.on('disconnected', () => {
|
||||
resetInfo();
|
||||
|
||||
if (info.autoReconnect) {
|
||||
connectTimeout();
|
||||
}
|
||||
});
|
||||
|
||||
info.autoReconnect = options.autoReconnect;
|
||||
|
||||
window = win;
|
||||
// We get multiple events
|
||||
// Next song: PAUSE(n), PAUSE(n+1), PLAY(n+1)
|
||||
// Skip time: PAUSE(N), PLAY(N)
|
||||
updateActivity = (songInfo) => {
|
||||
if (songInfo.title.length === 0 && songInfo.artist.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
info.lastSongInfo = songInfo;
|
||||
|
||||
// Stop the clear activity timout
|
||||
clearTimeout(clearActivity);
|
||||
|
||||
// Stop early if discord connection is not ready
|
||||
// do this after clearTimeout to avoid unexpected clears
|
||||
if (!info.rpc || !info.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear directly if timeout is 0
|
||||
if (songInfo.isPaused && options.activityTimoutEnabled && options.activityTimoutTime === 0) {
|
||||
info.rpc.user?.clearActivity().catch(console.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Song information changed, so lets update the rich presence
|
||||
// @see https://discord.com/developers/docs/topics/gateway#activity-object
|
||||
// not all options are transfered through https://github.com/discordjs/RPC/blob/6f83d8d812c87cb7ae22064acd132600407d7d05/src/client.js#L518-530
|
||||
const hangulFillerUnicodeCharacter = '\u3164'; // This is an empty character
|
||||
if (songInfo.title.length < 2) {
|
||||
songInfo.title += hangulFillerUnicodeCharacter.repeat(2 - songInfo.title.length);
|
||||
}
|
||||
if (songInfo.artist.length < 2) {
|
||||
songInfo.artist += hangulFillerUnicodeCharacter.repeat(2 - songInfo.title.length);
|
||||
}
|
||||
|
||||
const activityInfo: SetActivity = {
|
||||
details: songInfo.title,
|
||||
state: songInfo.artist,
|
||||
largeImageKey: songInfo.imageSrc ?? '',
|
||||
largeImageText: songInfo.album ?? '',
|
||||
buttons: [
|
||||
...(options.playOnYouTubeMusic ? [{ label: 'Play on YouTube Music', url: songInfo.url ?? '' }] : []),
|
||||
...(options.hideGitHubButton ? [] : [{ label: 'View App On GitHub', url: 'https://github.com/th-ch/youtube-music' }]),
|
||||
],
|
||||
};
|
||||
|
||||
if (songInfo.isPaused) {
|
||||
// Add a paused icon to show that the song is paused
|
||||
activityInfo.smallImageKey = 'paused';
|
||||
activityInfo.smallImageText = 'Paused';
|
||||
// Set start the timer so the activity gets cleared after a while if enabled
|
||||
if (options.activityTimoutEnabled) {
|
||||
clearActivity = setTimeout(() => info.rpc.user?.clearActivity().catch(console.error), options.activityTimoutTime ?? 10_000);
|
||||
}
|
||||
} else if (!options.hideDurationLeft) {
|
||||
// Add the start and end time of the song
|
||||
const songStartTime = Date.now() - ((songInfo.elapsedSeconds ?? 0) * 1000);
|
||||
activityInfo.startTimestamp = songStartTime;
|
||||
activityInfo.endTimestamp
|
||||
= songStartTime + (songInfo.songDuration * 1000);
|
||||
}
|
||||
|
||||
info.rpc.user?.setActivity(activityInfo).catch(console.error);
|
||||
};
|
||||
|
||||
// If the page is ready, register the callback
|
||||
win.once('ready-to-show', () => {
|
||||
let lastSongInfo: SongInfo;
|
||||
registerCallback((songInfo) => {
|
||||
lastSongInfo = songInfo;
|
||||
updateActivity(songInfo);
|
||||
});
|
||||
connect();
|
||||
let lastSent = Date.now();
|
||||
ipcMain.on('timeChanged', (_, t: number) => {
|
||||
const currentTime = Date.now();
|
||||
// if lastSent is more than 5 seconds ago, send the new time
|
||||
if (currentTime - lastSent > 5000) {
|
||||
lastSent = currentTime;
|
||||
if (lastSongInfo) {
|
||||
lastSongInfo.elapsedSeconds = t;
|
||||
updateActivity(lastSongInfo);
|
||||
info.rpc.on('connected', () => {
|
||||
if (dev()) {
|
||||
console.log('discord connected');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
app.on('window-all-closed', clear);
|
||||
};
|
||||
|
||||
for (const cb of refreshCallbacks) {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
|
||||
info.rpc.on('ready', () => {
|
||||
info.ready = true;
|
||||
if (info.lastSongInfo) {
|
||||
updateActivity(info.lastSongInfo);
|
||||
}
|
||||
});
|
||||
|
||||
info.rpc.on('disconnected', () => {
|
||||
resetInfo();
|
||||
|
||||
if (info.autoReconnect) {
|
||||
connectTimeout();
|
||||
}
|
||||
});
|
||||
|
||||
info.autoReconnect = options.autoReconnect;
|
||||
|
||||
window = win;
|
||||
// We get multiple events
|
||||
// Next song: PAUSE(n), PAUSE(n+1), PLAY(n+1)
|
||||
// Skip time: PAUSE(N), PLAY(N)
|
||||
updateActivity = (songInfo) => {
|
||||
if (songInfo.title.length === 0 && songInfo.artist.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
info.lastSongInfo = songInfo;
|
||||
|
||||
// Stop the clear activity timout
|
||||
clearTimeout(clearActivity);
|
||||
|
||||
// Stop early if discord connection is not ready
|
||||
// do this after clearTimeout to avoid unexpected clears
|
||||
if (!info.rpc || !info.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear directly if timeout is 0
|
||||
if (songInfo.isPaused && options.activityTimoutEnabled && options.activityTimoutTime === 0) {
|
||||
info.rpc.user?.clearActivity().catch(console.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Song information changed, so lets update the rich presence
|
||||
// @see https://discord.com/developers/docs/topics/gateway#activity-object
|
||||
// not all options are transfered through https://github.com/discordjs/RPC/blob/6f83d8d812c87cb7ae22064acd132600407d7d05/src/client.js#L518-530
|
||||
const hangulFillerUnicodeCharacter = '\u3164'; // This is an empty character
|
||||
if (songInfo.title.length < 2) {
|
||||
songInfo.title += hangulFillerUnicodeCharacter.repeat(2 - songInfo.title.length);
|
||||
}
|
||||
if (songInfo.artist.length < 2) {
|
||||
songInfo.artist += hangulFillerUnicodeCharacter.repeat(2 - songInfo.title.length);
|
||||
}
|
||||
|
||||
const activityInfo: SetActivity = {
|
||||
details: songInfo.title,
|
||||
state: songInfo.artist,
|
||||
largeImageKey: songInfo.imageSrc ?? '',
|
||||
largeImageText: songInfo.album ?? '',
|
||||
buttons: [
|
||||
...(options.playOnYouTubeMusic ? [{ label: 'Play on YouTube Music', url: songInfo.url ?? '' }] : []),
|
||||
...(options.hideGitHubButton ? [] : [{ label: 'View App On GitHub', url: 'https://github.com/th-ch/youtube-music' }]),
|
||||
],
|
||||
};
|
||||
|
||||
if (songInfo.isPaused) {
|
||||
// Add a paused icon to show that the song is paused
|
||||
activityInfo.smallImageKey = 'paused';
|
||||
activityInfo.smallImageText = 'Paused';
|
||||
// Set start the timer so the activity gets cleared after a while if enabled
|
||||
if (options.activityTimoutEnabled) {
|
||||
clearActivity = setTimeout(() => info.rpc.user?.clearActivity().catch(console.error), options.activityTimoutTime ?? 10_000);
|
||||
}
|
||||
} else if (!options.hideDurationLeft) {
|
||||
// Add the start and end time of the song
|
||||
const songStartTime = Date.now() - ((songInfo.elapsedSeconds ?? 0) * 1000);
|
||||
activityInfo.startTimestamp = songStartTime;
|
||||
activityInfo.endTimestamp
|
||||
= songStartTime + (songInfo.songDuration * 1000);
|
||||
}
|
||||
|
||||
info.rpc.user?.setActivity(activityInfo).catch(console.error);
|
||||
};
|
||||
|
||||
// If the page is ready, register the callback
|
||||
win.once('ready-to-show', () => {
|
||||
let lastSongInfo: SongInfo;
|
||||
registerCallback((songInfo) => {
|
||||
lastSongInfo = songInfo;
|
||||
updateActivity(songInfo);
|
||||
});
|
||||
connect();
|
||||
let lastSent = Date.now();
|
||||
ipcMain.on('timeChanged', (_, t: number) => {
|
||||
const currentTime = Date.now();
|
||||
// if lastSent is more than 5 seconds ago, send the new time
|
||||
if (currentTime - lastSent > 5000) {
|
||||
lastSent = currentTime;
|
||||
if (lastSongInfo) {
|
||||
lastSongInfo.elapsedSeconds = t;
|
||||
updateActivity(lastSongInfo);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
app.on('window-all-closed', clear);
|
||||
},
|
||||
onUnload() {
|
||||
resetInfo();
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const clear = () => {
|
||||
if (info.rpc) {
|
||||
|
||||
@ -2,11 +2,13 @@ import prompt from 'custom-electron-prompt';
|
||||
|
||||
import { clear, connect, isConnected, registerRefresh } from './main';
|
||||
|
||||
import builder from './index';
|
||||
|
||||
import { setMenuOptions } from '../../config/plugins';
|
||||
import promptOptions from '../../providers/prompt-options';
|
||||
import { singleton } from '../../providers/decorators';
|
||||
import { MenuTemplate } from '../../menu';
|
||||
|
||||
import type { MenuTemplate } from '../../menu';
|
||||
import type { ConfigType } from '../../config/dynamic';
|
||||
|
||||
const registerRefreshOnce = singleton((refreshMenu: () => void) => {
|
||||
@ -15,8 +17,9 @@ const registerRefreshOnce = singleton((refreshMenu: () => void) => {
|
||||
|
||||
type DiscordOptions = ConfigType<'discord'>;
|
||||
|
||||
export default (win: Electron.BrowserWindow, options: DiscordOptions, refreshMenu: () => void): MenuTemplate => {
|
||||
registerRefreshOnce(refreshMenu);
|
||||
export default builder.createMenu(async ({ window, getConfig, setConfig, refresh }): Promise<MenuTemplate> => {
|
||||
const config = await getConfig();
|
||||
registerRefreshOnce(refresh);
|
||||
|
||||
return [
|
||||
{
|
||||
@ -27,10 +30,11 @@ export default (win: Electron.BrowserWindow, options: DiscordOptions, refreshMen
|
||||
{
|
||||
label: 'Auto reconnect',
|
||||
type: 'checkbox',
|
||||
checked: options.autoReconnect,
|
||||
checked: config.autoReconnect,
|
||||
click(item: Electron.MenuItem) {
|
||||
options.autoReconnect = item.checked;
|
||||
setMenuOptions('discord', options);
|
||||
setConfig({
|
||||
autoReconnect: item.checked,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -40,45 +44,49 @@ export default (win: Electron.BrowserWindow, options: DiscordOptions, refreshMen
|
||||
{
|
||||
label: 'Clear activity after timeout',
|
||||
type: 'checkbox',
|
||||
checked: options.activityTimoutEnabled,
|
||||
checked: config.activityTimoutEnabled,
|
||||
click(item: Electron.MenuItem) {
|
||||
options.activityTimoutEnabled = item.checked;
|
||||
setMenuOptions('discord', options);
|
||||
setConfig({
|
||||
activityTimoutEnabled: item.checked,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Play on YouTube Music',
|
||||
type: 'checkbox',
|
||||
checked: options.playOnYouTubeMusic,
|
||||
checked: config.playOnYouTubeMusic,
|
||||
click(item: Electron.MenuItem) {
|
||||
options.playOnYouTubeMusic = item.checked;
|
||||
setMenuOptions('discord', options);
|
||||
setConfig({
|
||||
playOnYouTubeMusic: item.checked,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Hide GitHub link Button',
|
||||
type: 'checkbox',
|
||||
checked: options.hideGitHubButton,
|
||||
checked: config.hideGitHubButton,
|
||||
click(item: Electron.MenuItem) {
|
||||
options.hideGitHubButton = item.checked;
|
||||
setMenuOptions('discord', options);
|
||||
setConfig({
|
||||
hideGitHubButton: item.checked,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Hide duration left',
|
||||
type: 'checkbox',
|
||||
checked: options.hideDurationLeft,
|
||||
checked: config.hideDurationLeft,
|
||||
click(item: Electron.MenuItem) {
|
||||
options.hideDurationLeft = item.checked;
|
||||
setMenuOptions('discord', options);
|
||||
setConfig({
|
||||
hideGitHubButton: item.checked,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Set inactivity timeout',
|
||||
click: () => setInactivityTimeout(win, options),
|
||||
click: () => setInactivityTimeout(window, config),
|
||||
},
|
||||
];
|
||||
};
|
||||
});
|
||||
|
||||
async function setInactivityTimeout(win: Electron.BrowserWindow, options: DiscordOptions) {
|
||||
const output = await prompt({
|
||||
|
||||
Reference in New Issue
Block a user