233 lines
7.5 KiB
TypeScript
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();
|