vd-plugins/plugins/Last.fm/src/manager.ts
2026-05-09 10:20:55 +02:00

233 lines
7.5 KiB
TypeScript

import { currentSettings, pluginState } from ".";
import { Activity } from "../../defs";
import Constants from "./constants";
import { SelfPresenceStore } from "./modules";
import { clearActivity, fetchAsset, sendRequest } from "./utils/activity";
import { setDebugInfo } from "./utils/debug";
function formatTime(seconds: number): string {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60);
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
}
import { lastfmClient } from "./utils/lastfm";
enum ActivityType {
PLAYING = 0,
STREAMING = 1,
LISTENING = 2,
COMPETING = 5,
}
const verboseLog = (...message: any[]) =>
currentSettings.verboseLogging && console.log("[Last.fm]", ...message);
class PluginManager {
private static instance: PluginManager;
private updateTimer?: NodeJS.Timer;
private reconnectTimer?: NodeJS.Timer;
private consecutiveFailures = 0;
private isReconnecting = false;
private constructor() {}
public static getInstance(): PluginManager {
if (!PluginManager.instance) {
PluginManager.instance = new PluginManager();
}
return PluginManager.instance;
}
/**
* Updates the Discord activity with the current Last.fm track
*/
private async updateActivity() {
if (pluginState.pluginStopped) {
verboseLog("Plugin is stopped, skipping update");
this.stopUpdates();
return;
}
verboseLog("Fetching last track...");
try {
if (!currentSettings.username || !currentSettings.apiKey) {
throw new Error("Username or API key not set");
}
if (currentSettings.ignoreSpotify) {
const spotifyActivity = SelfPresenceStore.findActivity(
(act) => act.sync_id,
);
if (spotifyActivity) {
verboseLog("Spotify is currently playing, clearing activity");
setDebugInfo("isSpotifyIgnored", true);
clearActivity();
return;
}
setDebugInfo("isSpotifyIgnored", false);
}
const lastTrack = await lastfmClient.fetchLatestScrobble();
setDebugInfo("lastTrack", lastTrack);
if (!lastTrack.nowPlaying) {
verboseLog("Last track is not currently playing");
clearActivity();
return;
}
if (pluginState.lastTrackUrl === lastTrack.url) {
verboseLog("Track hasn't changed");
return;
}
const activity: Activity = {
name: currentSettings.appName || Constants.DEFAULT_APP_NAME,
flags: 0,
type: currentSettings.listeningTo
? ActivityType.LISTENING
: ActivityType.PLAYING,
details: lastTrack.name,
state: `by ${lastTrack.artist}`,
status_display_type: 1,
application_id: Constants.APPLICATION_ID,
};
// Handle dynamic app name
if (activity.name.includes("{{")) {
for (const key in lastTrack) {
activity.name = activity.name.replace(
`{{${key}}}`,
lastTrack[key as keyof typeof lastTrack] || "",
);
}
}
// Set timestamps if enabled
// Use `from` and `to` from track.getInfo
if (currentSettings.showTimestamp && typeof lastTrack.from === "number") {
activity.timestamps = {
start: lastTrack.from * 1000,
...(typeof lastTrack.to === "number" && { end: lastTrack.to * 1000 }),
};
}
// Handle album art
if (lastTrack.album) {
const asset = await fetchAsset([lastTrack.albumArt]);
if (asset[0]) {
activity.assets = {
large_image: asset[0],
large_text: `on ${lastTrack.album}${
currentSettings.showTimestamp && lastTrack.to
? ` • ${formatTime((lastTrack.to - lastTrack.from) / 1000)}`
: ""
}`,
};
}
}
verboseLog("Setting activity:", activity);
setDebugInfo("lastActivity", activity);
await sendRequest(activity);
pluginState.lastTrackUrl = lastTrack.url;
pluginState.lastActivity = activity;
this.consecutiveFailures = 0;
verboseLog("Activity set successfully!");
} catch (error) {
console.error("[Last.fm] Update error:", error);
this.handleError(error);
}
}
private handleError(error: Error) {
this.consecutiveFailures++;
setDebugInfo("lastError", error);
if (this.consecutiveFailures >= Constants.MAX_RETRY_ATTEMPTS) {
console.log(
`[Last.fm] Failed ${this.consecutiveFailures} times, initiating reconnection...`,
);
this.startReconnection();
}
}
private startReconnection() {
if (this.isReconnecting) return;
this.isReconnecting = true;
this.stopUpdates();
console.log("[Last.fm] Starting reconnection process");
this.reconnectTimer = setInterval(() => {
console.log("[Last.fm] Attempting to reconnect...");
this.initialize()
.then(() => {
console.log("[Last.fm] Reconnection successful");
this.stopReconnection();
})
.catch((error) => {
console.log("[Last.fm] Reconnection failed:", error);
});
}, Constants.RETRY_DELAY);
}
private stopReconnection() {
if (this.reconnectTimer) {
clearInterval(this.reconnectTimer);
this.reconnectTimer = undefined;
}
this.isReconnecting = false;
this.consecutiveFailures = 0;
}
/**
* Stops all update and reconnection timers
*/
private stopUpdates() {
if (this.updateTimer) {
clearInterval(this.updateTimer);
this.updateTimer = undefined;
}
}
/**
* Initializes the plugin and starts the update loop
*/
public async initialize() {
if (pluginState.pluginStopped) {
throw new Error("Plugin is stopped");
}
this.stopUpdates();
await this.updateActivity();
const interval = Math.max(
(Number(currentSettings.timeInterval) ||
Constants.DEFAULT_SETTINGS.timeInterval) * 1000,
Constants.MIN_UPDATE_INTERVAL * 1000,
);
this.updateTimer = setInterval(() => this.updateActivity(), interval);
console.log(`[Last.fm] Update timer started with interval: ${interval}ms`);
}
/**
* Stops the plugin and cleans up
*/
public stop() {
pluginState.pluginStopped = true;
this.stopUpdates();
this.stopReconnection();
clearActivity();
console.log("[Last.fm] Plugin stopped");
}
}
// Export singleton instance methods
const manager = PluginManager.getInstance();
export const initialize = () => manager.initialize();
export const stop = () => manager.stop();