mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-16 12:42:06 +00:00
refactor(discord): centralize Discord logic and improve activity update handling
This commit is contained in:
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable stylistic/no-mixed-operators */
|
||||||
import { app, dialog } from 'electron';
|
import { app, dialog } from 'electron';
|
||||||
import { Client as DiscordClient } from '@xhayper/discord-rpc';
|
import { Client as DiscordClient } from '@xhayper/discord-rpc';
|
||||||
import { dev } from 'electron-is';
|
import { dev } from 'electron-is';
|
||||||
@ -17,158 +18,12 @@ import type { DiscordPluginConfig } from './index';
|
|||||||
// Application ID registered by @th-ch/youtube-music dev team
|
// Application ID registered by @th-ch/youtube-music dev team
|
||||||
const clientId = '1177081335727267940';
|
const clientId = '1177081335727267940';
|
||||||
|
|
||||||
export interface Info {
|
// --- Factored utilities ---
|
||||||
rpc: DiscordClient;
|
function buildDiscordButtons(
|
||||||
ready: boolean;
|
|
||||||
autoReconnect: boolean;
|
|
||||||
lastSongInfo?: SongInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
const info: Info = {
|
|
||||||
rpc: new DiscordClient({
|
|
||||||
clientId,
|
|
||||||
}),
|
|
||||||
ready: false,
|
|
||||||
autoReconnect: true,
|
|
||||||
lastSongInfo: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {(() => void)[]}
|
|
||||||
*/
|
|
||||||
const refreshCallbacks: (() => void)[] = [];
|
|
||||||
|
|
||||||
const truncateString = (str: string, length: number): string => {
|
|
||||||
if (str.length > length) return `${str.substring(0, length - 3)}...`;
|
|
||||||
return str;
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetInfo = () => {
|
|
||||||
info.ready = false;
|
|
||||||
clearTimeout(clearActivity);
|
|
||||||
if (dev()) {
|
|
||||||
console.log(LoggerPrefix, t('plugins.discord.backend.disconnected'));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const cb of refreshCallbacks) {
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const connectTimeout = () =>
|
|
||||||
new Promise((resolve, reject) =>
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!info.autoReconnect || info.rpc.isConnected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
info.rpc.login().then(resolve).catch(reject);
|
|
||||||
}, 5000),
|
|
||||||
);
|
|
||||||
const connectRecursive = () => {
|
|
||||||
if (!info.autoReconnect || info.rpc.isConnected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
connectTimeout().catch(connectRecursive);
|
|
||||||
};
|
|
||||||
|
|
||||||
let window: Electron.BrowserWindow;
|
|
||||||
export const connect = (showError = false) => {
|
|
||||||
if (info.rpc.isConnected) {
|
|
||||||
if (dev()) {
|
|
||||||
console.log(LoggerPrefix, t('plugins.discord.backend.already-connected'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
info.ready = false;
|
|
||||||
|
|
||||||
// Startup the rpc client
|
|
||||||
info.rpc.login().catch((error: Error) => {
|
|
||||||
resetInfo();
|
|
||||||
if (dev()) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.autoReconnect) {
|
|
||||||
connectRecursive();
|
|
||||||
} else if (showError) {
|
|
||||||
dialog.showMessageBox(window, {
|
|
||||||
title: 'Connection failed',
|
|
||||||
message: error.message || String(error),
|
|
||||||
type: 'error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let clearActivity: NodeJS.Timeout | undefined;
|
|
||||||
|
|
||||||
export const clear = () => {
|
|
||||||
if (info.rpc) {
|
|
||||||
info.rpc.user?.clearActivity();
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(clearActivity);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const registerRefresh = (cb: () => void) => refreshCallbacks.push(cb);
|
|
||||||
export const isConnected = () => info.rpc?.isConnected;
|
|
||||||
|
|
||||||
let lastActivitySongId: string | null = null;
|
|
||||||
let lastPausedState: boolean = false; // Initialize with a default boolean
|
|
||||||
let lastElapsedSeconds: number = 0;
|
|
||||||
let updateTimeout: NodeJS.Timeout | null = null;
|
|
||||||
let lastProgressUpdate: number = 0; // timestamp of the last throttled update
|
|
||||||
|
|
||||||
const PROGRESS_THROTTLE_MS = 15000; // 15s to respect Discord's rate limit
|
|
||||||
|
|
||||||
function isSeek(oldSec: number, newSec: number) {
|
|
||||||
return Math.abs((newSec ?? 0) - (oldSec ?? 0)) > 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
function manageActivityClearTimer(
|
|
||||||
isPaused: boolean | undefined,
|
|
||||||
config: DiscordPluginConfig,
|
config: DiscordPluginConfig,
|
||||||
) {
|
|
||||||
clearTimeout(clearActivity); // Clear any existing timer
|
|
||||||
// Set a new timer only if the song is paused and the feature is enabled
|
|
||||||
if (isPaused === true && config.activityTimeoutEnabled) {
|
|
||||||
clearActivity = setTimeout(
|
|
||||||
() => info.rpc.user?.clearActivity().catch(console.error),
|
|
||||||
config.activityTimeoutTime ?? 10_000,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendActivityToDiscord(
|
|
||||||
songInfo: SongInfo,
|
songInfo: SongInfo,
|
||||||
config: DiscordPluginConfig,
|
): GatewayActivityButton[] | undefined {
|
||||||
) {
|
const buttons: GatewayActivityButton[] = [];
|
||||||
if (songInfo.title.length === 0 && songInfo.artist.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
info.lastSongInfo = songInfo;
|
|
||||||
clearTimeout(clearActivity);
|
|
||||||
if (!info.rpc || !info.ready) {
|
|
||||||
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
|
|
||||||
const paddedInfoKeys: (keyof SongInfo)[] = ['title', 'artist', 'album'];
|
|
||||||
for (const key of paddedInfoKeys) {
|
|
||||||
const keyLength = (songInfo[key] as string)?.length;
|
|
||||||
if (keyLength < 2) {
|
|
||||||
(songInfo[key] as string) += hangulFillerUnicodeCharacter.repeat(
|
|
||||||
2 - keyLength,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let buttons: GatewayActivityButton[] | undefined = [];
|
|
||||||
if (config.playOnYouTubeMusic) {
|
if (config.playOnYouTubeMusic) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
label: 'Play on YouTube Music',
|
label: 'Play on YouTube Music',
|
||||||
@ -181,27 +36,277 @@ function sendActivityToDiscord(
|
|||||||
url: 'https://github.com/th-ch/youtube-music',
|
url: 'https://github.com/th-ch/youtube-music',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (buttons.length === 0) {
|
return buttons.length ? buttons : undefined;
|
||||||
buttons = undefined;
|
}
|
||||||
|
|
||||||
|
function padHangulFields(songInfo: SongInfo): void {
|
||||||
|
const hangulFiller = '\u3164';
|
||||||
|
(['title', 'artist', 'album'] as (keyof SongInfo)[]).forEach((key) => {
|
||||||
|
const value = songInfo[key];
|
||||||
|
if (typeof value === 'string' && value.length < 2) {
|
||||||
|
// @ts-expect-error: dynamic assignment for SongInfo fields
|
||||||
|
songInfo[key] = value + hangulFiller.repeat(2 - value.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Centralized timer management
|
||||||
|
const TimerManager = {
|
||||||
|
timers: new Map<string, NodeJS.Timeout>(),
|
||||||
|
set(key: string, fn: () => void, delay: number) {
|
||||||
|
this.clear(key);
|
||||||
|
this.timers.set(key, setTimeout(fn, delay));
|
||||||
|
},
|
||||||
|
clear(key: string) {
|
||||||
|
const t = this.timers.get(key);
|
||||||
|
if (t) clearTimeout(t);
|
||||||
|
this.timers.delete(key);
|
||||||
|
},
|
||||||
|
clearAll() {
|
||||||
|
for (const t of this.timers.values()) clearTimeout(t);
|
||||||
|
this.timers.clear();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Centralization of Discord logic ---
|
||||||
|
interface DiscordState {
|
||||||
|
rpc: DiscordClient;
|
||||||
|
ready: boolean;
|
||||||
|
autoReconnect: boolean;
|
||||||
|
lastSongInfo?: SongInfo;
|
||||||
|
}
|
||||||
|
const discordState: DiscordState = {
|
||||||
|
rpc: new DiscordClient({
|
||||||
|
clientId,
|
||||||
|
}),
|
||||||
|
ready: false,
|
||||||
|
autoReconnect: true,
|
||||||
|
lastSongInfo: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mainWindow: Electron.BrowserWindow;
|
||||||
|
|
||||||
|
let lastActivitySongId: string | null = null;
|
||||||
|
let lastPausedState: boolean = false;
|
||||||
|
let lastElapsedSeconds: number = 0;
|
||||||
|
let lastProgressUpdate: number = 0;
|
||||||
|
const PROGRESS_THROTTLE_MS = 15000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main Discord presence update logic.
|
||||||
|
* Handles throttling, snapshotting, and timer management to avoid spamming Discord's API.
|
||||||
|
* - Throttling: Ensures updates are not sent more than once every PROGRESS_THROTTLE_MS ms.
|
||||||
|
* - Snapshot: If an update is requested too soon, schedules a delayed update with a snapshot of the current song state.
|
||||||
|
* - Timers: Uses TimerManager to ensure only one timer per type is active and to clean up properly.
|
||||||
|
*/
|
||||||
|
function updateDiscordRichPresence(
|
||||||
|
songInfo: SongInfo,
|
||||||
|
config: DiscordPluginConfig,
|
||||||
|
) {
|
||||||
|
if (songInfo.title.length === 0 && songInfo.artist.length === 0) return;
|
||||||
|
discordState.lastSongInfo = songInfo;
|
||||||
|
// Always clear any pending activity-clear timer before updating presence
|
||||||
|
TimerManager.clear('clearActivity');
|
||||||
|
if (!discordState.rpc || !discordState.ready) return;
|
||||||
|
const now = Date.now();
|
||||||
|
const songChanged = songInfo.videoId !== lastActivitySongId;
|
||||||
|
const pauseChanged = songInfo.isPaused !== lastPausedState;
|
||||||
|
const seeked = isSeek(lastElapsedSeconds, songInfo.elapsedSeconds ?? 0);
|
||||||
|
// If the song changed, pause state changed, or user seeked, update immediately and reset throttle
|
||||||
|
if (songChanged || pauseChanged || seeked) {
|
||||||
|
// Cancel any pending throttled update
|
||||||
|
TimerManager.clear('updateTimeout');
|
||||||
|
padHangulFields(songInfo);
|
||||||
|
const activityInfo: SetActivity = {
|
||||||
|
type: ActivityType.Listening,
|
||||||
|
details: truncateString(songInfo.title, 128),
|
||||||
|
state: truncateString(songInfo.artist, 128),
|
||||||
|
largeImageKey: songInfo.imageSrc ?? '',
|
||||||
|
largeImageText: songInfo.album ?? '',
|
||||||
|
buttons: buildDiscordButtons(config, songInfo),
|
||||||
|
};
|
||||||
|
if (songInfo.isPaused) {
|
||||||
|
activityInfo.smallImageKey = 'paused';
|
||||||
|
activityInfo.smallImageText = 'Paused';
|
||||||
|
} else if (!config.hideDurationLeft) {
|
||||||
|
// Set start/end timestamps for progress bar
|
||||||
|
const songStartTime = Date.now() - (songInfo.elapsedSeconds ?? 0) * 1000;
|
||||||
|
activityInfo.startTimestamp = songStartTime;
|
||||||
|
activityInfo.endTimestamp = songStartTime + songInfo.songDuration * 1000;
|
||||||
|
}
|
||||||
|
discordState.rpc.user?.setActivity(activityInfo).catch(console.error);
|
||||||
|
lastActivitySongId = songInfo.videoId;
|
||||||
|
if (typeof songInfo.isPaused === 'boolean') {
|
||||||
|
lastPausedState = songInfo.isPaused;
|
||||||
|
}
|
||||||
|
lastElapsedSeconds = songInfo.elapsedSeconds ?? 0;
|
||||||
|
lastProgressUpdate = now;
|
||||||
|
setActivityTimeoutCentral(songInfo.isPaused, config);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const activityInfo: SetActivity = {
|
// Throttling: Only allow a full update if enough time has passed
|
||||||
type: ActivityType.Listening,
|
if (now - lastProgressUpdate > PROGRESS_THROTTLE_MS) {
|
||||||
details: truncateString(songInfo.title, 128),
|
padHangulFields(songInfo);
|
||||||
state: truncateString(songInfo.artist, 128),
|
const activityInfo: SetActivity = {
|
||||||
largeImageKey: songInfo.imageSrc ?? '',
|
type: ActivityType.Listening,
|
||||||
largeImageText: songInfo.album ?? '',
|
details: truncateString(songInfo.title, 128),
|
||||||
buttons,
|
state: truncateString(songInfo.artist, 128),
|
||||||
};
|
largeImageKey: songInfo.imageSrc ?? '',
|
||||||
if (songInfo.isPaused) {
|
largeImageText: songInfo.album ?? '',
|
||||||
activityInfo.smallImageKey = 'paused';
|
buttons: buildDiscordButtons(config, songInfo),
|
||||||
activityInfo.smallImageText = 'Paused';
|
};
|
||||||
// No timestamps when paused
|
if (songInfo.isPaused) {
|
||||||
} else if (!config.hideDurationLeft) {
|
activityInfo.smallImageKey = 'paused';
|
||||||
const songStartTime = Date.now() - (songInfo.elapsedSeconds ?? 0) * 1000;
|
activityInfo.smallImageText = 'Paused';
|
||||||
activityInfo.startTimestamp = songStartTime;
|
} else if (!config.hideDurationLeft) {
|
||||||
activityInfo.endTimestamp = songStartTime + songInfo.songDuration * 1000;
|
// Set start/end timestamps for progress bar
|
||||||
|
const songStartTime = Date.now() - (songInfo.elapsedSeconds ?? 0) * 1000;
|
||||||
|
activityInfo.startTimestamp = songStartTime;
|
||||||
|
activityInfo.endTimestamp = songStartTime + songInfo.songDuration * 1000;
|
||||||
|
}
|
||||||
|
discordState.rpc.user?.setActivity(activityInfo).catch(console.error);
|
||||||
|
lastProgressUpdate = now;
|
||||||
|
lastElapsedSeconds = songInfo.elapsedSeconds ?? 0;
|
||||||
|
setActivityTimeoutCentral(songInfo.isPaused, config);
|
||||||
|
} else {
|
||||||
|
// Snapshot logic: If throttled, schedule a delayed update with a snapshot of the current song state
|
||||||
|
TimerManager.clear('updateTimeout');
|
||||||
|
const songInfoSnapshot = { ...songInfo };
|
||||||
|
TimerManager.set(
|
||||||
|
'updateTimeout',
|
||||||
|
() => {
|
||||||
|
// Only send if the global state still matches the snapshot
|
||||||
|
if (
|
||||||
|
discordState.lastSongInfo?.videoId === songInfoSnapshot.videoId &&
|
||||||
|
discordState.lastSongInfo?.isPaused === songInfoSnapshot.isPaused
|
||||||
|
) {
|
||||||
|
padHangulFields(songInfoSnapshot);
|
||||||
|
const activityInfo: SetActivity = {
|
||||||
|
type: ActivityType.Listening,
|
||||||
|
details: truncateString(songInfoSnapshot.title, 128),
|
||||||
|
state: truncateString(songInfoSnapshot.artist, 128),
|
||||||
|
largeImageKey: songInfoSnapshot.imageSrc ?? '',
|
||||||
|
largeImageText: songInfoSnapshot.album ?? '',
|
||||||
|
buttons: buildDiscordButtons(config, songInfoSnapshot),
|
||||||
|
};
|
||||||
|
if (songInfoSnapshot.isPaused) {
|
||||||
|
activityInfo.smallImageKey = 'paused';
|
||||||
|
activityInfo.smallImageText = 'Paused';
|
||||||
|
} else if (!config.hideDurationLeft) {
|
||||||
|
// Set start/end timestamps for progress bar
|
||||||
|
const songStartTime =
|
||||||
|
Date.now() - (songInfoSnapshot.elapsedSeconds ?? 0) * 1000;
|
||||||
|
activityInfo.startTimestamp = songStartTime;
|
||||||
|
activityInfo.endTimestamp =
|
||||||
|
songStartTime + songInfoSnapshot.songDuration * 1000;
|
||||||
|
}
|
||||||
|
discordState.rpc.user?.setActivity(activityInfo).catch(console.error);
|
||||||
|
lastProgressUpdate = Date.now();
|
||||||
|
lastElapsedSeconds = songInfoSnapshot.elapsedSeconds ?? 0;
|
||||||
|
setActivityTimeoutCentral(songInfoSnapshot.isPaused, config);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PROGRESS_THROTTLE_MS - (now - lastProgressUpdate),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
info.rpc.user?.setActivity(activityInfo).catch(console.error);
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a timer to clear Discord activity if paused for too long.
|
||||||
|
* Uses TimerManager to ensure only one clear-activity timer is active at a time.
|
||||||
|
*/
|
||||||
|
function setActivityTimeoutCentral(
|
||||||
|
isPaused: boolean | undefined,
|
||||||
|
config: DiscordPluginConfig,
|
||||||
|
) {
|
||||||
|
TimerManager.clear('clearActivity');
|
||||||
|
if (isPaused === true && config.activityTimeoutEnabled) {
|
||||||
|
TimerManager.set(
|
||||||
|
'clearActivity',
|
||||||
|
() => {
|
||||||
|
discordState.rpc.user?.clearActivity().catch(console.error);
|
||||||
|
},
|
||||||
|
config.activityTimeoutTime ?? 10_000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const truncateString = (str: string, length: number): string => {
|
||||||
|
if (str.length > length) return `${str.substring(0, length - 3)}...`;
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetInfo = () => {
|
||||||
|
discordState.ready = false;
|
||||||
|
TimerManager.clearAll();
|
||||||
|
if (dev()) {
|
||||||
|
console.log(LoggerPrefix, t('plugins.discord.backend.disconnected'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectTimeout = () =>
|
||||||
|
new Promise((resolve, reject) =>
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!discordState.autoReconnect || discordState.rpc.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
discordState.rpc.login().then(resolve).catch(reject);
|
||||||
|
}, 5000),
|
||||||
|
);
|
||||||
|
const connectRecursive = () => {
|
||||||
|
if (!discordState.autoReconnect || discordState.rpc.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectTimeout().catch(connectRecursive);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const connect = (showError = false) => {
|
||||||
|
if (discordState.rpc.isConnected) {
|
||||||
|
if (dev()) {
|
||||||
|
console.log(LoggerPrefix, t('plugins.discord.backend.already-connected'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
discordState.ready = false;
|
||||||
|
|
||||||
|
// Startup the rpc client
|
||||||
|
discordState.rpc.login().catch((error: Error) => {
|
||||||
|
resetInfo();
|
||||||
|
if (dev()) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (discordState.autoReconnect) {
|
||||||
|
connectRecursive();
|
||||||
|
} else if (showError) {
|
||||||
|
dialog.showMessageBox(mainWindow, {
|
||||||
|
title: 'Connection failed',
|
||||||
|
message: error.message || String(error),
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clear = () => {
|
||||||
|
if (discordState.rpc) {
|
||||||
|
discordState.rpc.user?.clearActivity();
|
||||||
|
}
|
||||||
|
|
||||||
|
TimerManager.clearAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const registerRefresh = (cb: () => void) => refreshCallbacks.push(cb);
|
||||||
|
export const isConnected = () => discordState.rpc?.isConnected;
|
||||||
|
|
||||||
|
const refreshCallbacks: (() => void)[] = [];
|
||||||
|
|
||||||
|
function isSeek(oldSec: number, newSec: number) {
|
||||||
|
return Math.abs((newSec ?? 0) - (oldSec ?? 0)) > 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const backend = createBackend<
|
export const backend = createBackend<
|
||||||
@ -211,70 +316,13 @@ export const backend = createBackend<
|
|||||||
},
|
},
|
||||||
DiscordPluginConfig
|
DiscordPluginConfig
|
||||||
>({
|
>({
|
||||||
/**
|
updateActivity: (songInfo, config) =>
|
||||||
* We get multiple events
|
updateDiscordRichPresence(songInfo, config),
|
||||||
* Next song: PAUSE(n), PAUSE(n+1), PLAY(n+1)
|
|
||||||
* Skip time: PAUSE(N), PLAY(N)
|
|
||||||
*/
|
|
||||||
updateActivity: (songInfo, config) => {
|
|
||||||
if (songInfo.title.length === 0 && songInfo.artist.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
info.lastSongInfo = songInfo;
|
|
||||||
clearTimeout(clearActivity);
|
|
||||||
if (!info.rpc || !info.ready) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const now = Date.now();
|
|
||||||
const songChanged = songInfo.videoId !== lastActivitySongId;
|
|
||||||
const pauseChanged = songInfo.isPaused !== lastPausedState;
|
|
||||||
const seeked = isSeek(lastElapsedSeconds, songInfo.elapsedSeconds ?? 0);
|
|
||||||
if (songChanged || pauseChanged || seeked) {
|
|
||||||
// Always cancel any pending throttle on important event
|
|
||||||
if (updateTimeout) clearTimeout(updateTimeout);
|
|
||||||
updateTimeout = null;
|
|
||||||
sendActivityToDiscord(songInfo, config);
|
|
||||||
lastActivitySongId = songInfo.videoId;
|
|
||||||
// Update lastPausedState only if songInfo.isPaused is a boolean
|
|
||||||
if (typeof songInfo.isPaused === 'boolean') {
|
|
||||||
lastPausedState = songInfo.isPaused;
|
|
||||||
}
|
|
||||||
lastElapsedSeconds = songInfo.elapsedSeconds ?? 0;
|
|
||||||
lastProgressUpdate = now;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Normal progression: throttle
|
|
||||||
if (now - lastProgressUpdate > PROGRESS_THROTTLE_MS) {
|
|
||||||
sendActivityToDiscord(songInfo, config);
|
|
||||||
lastProgressUpdate = now;
|
|
||||||
lastElapsedSeconds = songInfo.elapsedSeconds ?? 0;
|
|
||||||
} else {
|
|
||||||
if (updateTimeout) clearTimeout(updateTimeout);
|
|
||||||
// Capture a snapshot of songInfo for the timeout callback
|
|
||||||
const songInfoSnapshot = { ...songInfo };
|
|
||||||
updateTimeout = setTimeout(
|
|
||||||
() => {
|
|
||||||
// Only send if the current global song state still matches what was expected
|
|
||||||
// at the time the timeout was set (i.e., the snapshot's state)
|
|
||||||
if (
|
|
||||||
info.lastSongInfo?.videoId === songInfoSnapshot.videoId &&
|
|
||||||
info.lastSongInfo?.isPaused === songInfoSnapshot.isPaused
|
|
||||||
) {
|
|
||||||
// Use the snapshot for sending activity and updating last elapsed time
|
|
||||||
sendActivityToDiscord(songInfoSnapshot, config);
|
|
||||||
lastProgressUpdate = Date.now();
|
|
||||||
lastElapsedSeconds = songInfoSnapshot.elapsedSeconds ?? 0;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
PROGRESS_THROTTLE_MS - (now - lastProgressUpdate),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
manageActivityClearTimer(songInfo.isPaused, config);
|
|
||||||
},
|
|
||||||
async start(ctx) {
|
async start(ctx) {
|
||||||
this.config = await ctx.getConfig();
|
this.config = await ctx.getConfig();
|
||||||
|
|
||||||
info.rpc.on('connected', () => {
|
discordState.rpc.on('connected', () => {
|
||||||
if (dev()) {
|
if (dev()) {
|
||||||
console.log(LoggerPrefix, t('plugins.discord.backend.connected'));
|
console.log(LoggerPrefix, t('plugins.discord.backend.connected'));
|
||||||
}
|
}
|
||||||
@ -284,31 +332,31 @@ export const backend = createBackend<
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
info.rpc.on('ready', () => {
|
discordState.rpc.on('ready', () => {
|
||||||
info.ready = true;
|
discordState.ready = true;
|
||||||
if (info.lastSongInfo && this.config) {
|
if (discordState.lastSongInfo && this.config) {
|
||||||
this.updateActivity(info.lastSongInfo, this.config);
|
this.updateActivity(discordState.lastSongInfo, this.config);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
info.rpc.on('disconnected', () => {
|
discordState.rpc.on('disconnected', () => {
|
||||||
resetInfo();
|
resetInfo();
|
||||||
|
|
||||||
if (info.autoReconnect) {
|
if (discordState.autoReconnect) {
|
||||||
connectTimeout();
|
connectTimeout();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
info.autoReconnect = this.config.autoReconnect;
|
discordState.autoReconnect = this.config.autoReconnect;
|
||||||
|
|
||||||
window = ctx.window;
|
mainWindow = ctx.window;
|
||||||
|
|
||||||
// If the page is ready, register the callback
|
// If the page is ready, register the callback
|
||||||
ctx.window.once('ready-to-show', () => {
|
ctx.window.once('ready-to-show', () => {
|
||||||
let lastSent = Date.now();
|
let lastSent = Date.now();
|
||||||
registerCallback((songInfo, event) => {
|
registerCallback((songInfo, event) => {
|
||||||
if (event !== SongInfoEvent.TimeChanged) {
|
if (event !== SongInfoEvent.TimeChanged) {
|
||||||
info.lastSongInfo = songInfo;
|
discordState.lastSongInfo = songInfo;
|
||||||
if (this.config) this.updateActivity(songInfo, this.config);
|
if (this.config) this.updateActivity(songInfo, this.config);
|
||||||
} else {
|
} else {
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
@ -316,7 +364,7 @@ export const backend = createBackend<
|
|||||||
if (currentTime - lastSent > 5000) {
|
if (currentTime - lastSent > 5000) {
|
||||||
lastSent = currentTime;
|
lastSent = currentTime;
|
||||||
if (songInfo) {
|
if (songInfo) {
|
||||||
info.lastSongInfo = songInfo;
|
discordState.lastSongInfo = songInfo;
|
||||||
if (this.config) this.updateActivity(songInfo, this.config);
|
if (this.config) this.updateActivity(songInfo, this.config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -334,9 +382,9 @@ export const backend = createBackend<
|
|||||||
},
|
},
|
||||||
onConfigChange(newConfig) {
|
onConfigChange(newConfig) {
|
||||||
this.config = newConfig;
|
this.config = newConfig;
|
||||||
info.autoReconnect = newConfig.autoReconnect;
|
discordState.autoReconnect = newConfig.autoReconnect;
|
||||||
if (info.lastSongInfo) {
|
if (discordState.lastSongInfo) {
|
||||||
this.updateActivity(info.lastSongInfo, newConfig);
|
this.updateActivity(discordState.lastSongInfo, newConfig);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user