update showing errors as toast notifications
ofc zed thought why not use formatting again <3
This commit is contained in:
parent
a5686e815c
commit
dbcc2a4338
3 changed files with 382 additions and 382 deletions
|
|
@ -1,9 +1,5 @@
|
||||||
import { plugin } from "@vendetta";
|
import { plugin } from "@vendetta";
|
||||||
import { FluxDispatcher } from "@vendetta/metro/common";
|
import { FluxDispatcher } from "@vendetta/metro/common";
|
||||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
|
||||||
import { showToast } from "@vendetta/ui/toasts";
|
|
||||||
import { React } from "@vendetta/metro/common";
|
|
||||||
import { View } from "react-native";
|
|
||||||
import { Forms } from "@vendetta/ui/components";
|
import { Forms } from "@vendetta/ui/components";
|
||||||
|
|
||||||
import { LFMSettings } from "../../defs";
|
import { LFMSettings } from "../../defs";
|
||||||
|
|
@ -17,10 +13,10 @@ import Settings from "./ui/pages/Settings";
|
||||||
const { FormText } = Forms;
|
const { FormText } = Forms;
|
||||||
|
|
||||||
export const pluginState = {
|
export const pluginState = {
|
||||||
pluginStopped: false,
|
pluginStopped: false,
|
||||||
lastActivity: undefined,
|
lastActivity: undefined,
|
||||||
updateInterval: undefined,
|
updateInterval: undefined,
|
||||||
lastTrackUrl: undefined,
|
lastTrackUrl: undefined,
|
||||||
} as {
|
} as {
|
||||||
pluginStopped: boolean;
|
pluginStopped: boolean;
|
||||||
lastActivity?: any;
|
lastActivity?: any;
|
||||||
|
|
@ -31,18 +27,18 @@ export const pluginState = {
|
||||||
// Initialize default settings
|
// Initialize default settings
|
||||||
const defaultSettings: LFMSettings = Constants.DEFAULT_SETTINGS;
|
const defaultSettings: LFMSettings = Constants.DEFAULT_SETTINGS;
|
||||||
for (const key in defaultSettings) {
|
for (const key in defaultSettings) {
|
||||||
plugin.storage[key] =
|
plugin.storage[key] =
|
||||||
plugin.storage[key] ?? defaultSettings[key as keyof typeof defaultSettings];
|
plugin.storage[key] ?? defaultSettings[key as keyof typeof defaultSettings];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const currentSettings = new Proxy(plugin.storage, {
|
export const currentSettings = new Proxy(plugin.storage, {
|
||||||
get(target, prop) {
|
get(target, prop) {
|
||||||
return target[prop];
|
return target[prop];
|
||||||
},
|
},
|
||||||
set(target, prop, value) {
|
set(target, prop, value) {
|
||||||
target[prop] = value;
|
target[prop] = value;
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Connection status tracking
|
// Connection status tracking
|
||||||
|
|
@ -51,62 +47,66 @@ const MAX_CONNECTION_ATTEMPTS = 3;
|
||||||
const RECONNECT_DELAY = 5000;
|
const RECONNECT_DELAY = 5000;
|
||||||
|
|
||||||
async function tryInitialize() {
|
async function tryInitialize() {
|
||||||
try {
|
try {
|
||||||
await initialize();
|
await initialize();
|
||||||
connectionAttempts = 0;
|
connectionAttempts = 0;
|
||||||
} catch (error) {
|
console.log("[Last.fm] Successfully connected to Last.fm");
|
||||||
console.error("[Last.fm] Initialization error:", error);
|
} catch (error) {
|
||||||
connectionAttempts++;
|
console.error("[Last.fm] Initialization error:", error);
|
||||||
|
connectionAttempts++;
|
||||||
|
|
||||||
if (connectionAttempts < MAX_CONNECTION_ATTEMPTS) {
|
if (connectionAttempts < MAX_CONNECTION_ATTEMPTS) {
|
||||||
showToast("Retrying connection...", getAssetIDByName("ic_clock"));
|
console.log(
|
||||||
setTimeout(tryInitialize, RECONNECT_DELAY);
|
`[Last.fm] Retrying connection... (attempt ${connectionAttempts})`,
|
||||||
} else {
|
);
|
||||||
showToast("Failed to connect to Last.fm", getAssetIDByName("Small"));
|
setTimeout(tryInitialize, RECONNECT_DELAY);
|
||||||
}
|
} else {
|
||||||
|
console.error(
|
||||||
|
"[Last.fm] Failed to connect to Last.fm after multiple attempts",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLoad() {
|
onLoad() {
|
||||||
pluginState.pluginStopped = false;
|
pluginState.pluginStopped = false;
|
||||||
|
|
||||||
if (!currentSettings.username || !currentSettings.apiKey) {
|
if (!currentSettings.username || !currentSettings.apiKey) {
|
||||||
showToast(
|
console.log(
|
||||||
"Please configure Last.fm username and API key in settings",
|
"[Last.fm] Please configure Last.fm username and API key in settings",
|
||||||
getAssetIDByName("ic_warning"),
|
);
|
||||||
);
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
if (UserStore.getCurrentUser()) {
|
||||||
|
tryInitialize();
|
||||||
|
} else {
|
||||||
|
const waitForUser = () => {
|
||||||
if (UserStore.getCurrentUser()) {
|
if (UserStore.getCurrentUser()) {
|
||||||
tryInitialize();
|
tryInitialize();
|
||||||
} else {
|
FluxDispatcher.unsubscribe("CONNECTION_OPEN", waitForUser);
|
||||||
const waitForUser = () => {
|
}
|
||||||
if (UserStore.getCurrentUser()) {
|
};
|
||||||
tryInitialize();
|
|
||||||
FluxDispatcher.unsubscribe("CONNECTION_OPEN", waitForUser);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
FluxDispatcher.subscribe("CONNECTION_OPEN", waitForUser);
|
FluxDispatcher.subscribe("CONNECTION_OPEN", waitForUser);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onUnload() {
|
onUnload() {
|
||||||
pluginState.pluginStopped = true;
|
pluginState.pluginStopped = true;
|
||||||
stop();
|
stop();
|
||||||
},
|
},
|
||||||
// Handle settings updates
|
// Handle settings updates
|
||||||
onSettingsUpdate(newSettings: any) {
|
onSettingsUpdate(newSettings: any) {
|
||||||
Object.assign(currentSettings, newSettings);
|
Object.assign(currentSettings, newSettings);
|
||||||
tryInitialize();
|
tryInitialize();
|
||||||
},
|
},
|
||||||
// Handle Discord reconnection
|
// Handle Discord reconnection
|
||||||
onDiscordReconnect() {
|
onDiscordReconnect() {
|
||||||
if (!pluginState.pluginStopped) {
|
if (!pluginState.pluginStopped) {
|
||||||
tryInitialize();
|
tryInitialize();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Settings component
|
// Settings component
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
|
||||||
import { showToast } from "@vendetta/ui/toasts";
|
|
||||||
|
|
||||||
import { currentSettings, pluginState } from ".";
|
import { currentSettings, pluginState } from ".";
|
||||||
import { Activity } from "../../defs";
|
import { Activity } from "../../defs";
|
||||||
import Constants from "./constants";
|
import Constants from "./constants";
|
||||||
|
|
@ -9,9 +6,9 @@ import { clearActivity, fetchAsset, sendRequest } from "./utils/activity";
|
||||||
import { setDebugInfo } from "./utils/debug";
|
import { setDebugInfo } from "./utils/debug";
|
||||||
|
|
||||||
function formatTime(seconds: number): string {
|
function formatTime(seconds: number): string {
|
||||||
const minutes = Math.floor(seconds / 60);
|
const minutes = Math.floor(seconds / 60);
|
||||||
const remainingSeconds = Math.floor(seconds % 60);
|
const remainingSeconds = Math.floor(seconds % 60);
|
||||||
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
|
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
|
||||||
}
|
}
|
||||||
import { lastfmClient } from "./utils/lastfm";
|
import { lastfmClient } from "./utils/lastfm";
|
||||||
|
|
||||||
|
|
@ -23,210 +20,211 @@ enum ActivityType {
|
||||||
}
|
}
|
||||||
|
|
||||||
const verboseLog = (...message: any[]) =>
|
const verboseLog = (...message: any[]) =>
|
||||||
currentSettings.verboseLogging && console.log("[Last.fm]", ...message);
|
currentSettings.verboseLogging && console.log("[Last.fm]", ...message);
|
||||||
|
|
||||||
class PluginManager {
|
class PluginManager {
|
||||||
private static instance: PluginManager;
|
private static instance: PluginManager;
|
||||||
private updateTimer?: NodeJS.Timer;
|
private updateTimer?: NodeJS.Timer;
|
||||||
private reconnectTimer?: NodeJS.Timer;
|
private reconnectTimer?: NodeJS.Timer;
|
||||||
private consecutiveFailures = 0;
|
private consecutiveFailures = 0;
|
||||||
private isReconnecting = false;
|
private isReconnecting = false;
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
public static getInstance(): PluginManager {
|
public static getInstance(): PluginManager {
|
||||||
if (!PluginManager.instance) {
|
if (!PluginManager.instance) {
|
||||||
PluginManager.instance = new PluginManager();
|
PluginManager.instance = new PluginManager();
|
||||||
}
|
|
||||||
return PluginManager.instance;
|
|
||||||
}
|
}
|
||||||
|
return PluginManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the Discord activity with the current Last.fm track
|
* Updates the Discord activity with the current Last.fm track
|
||||||
*/
|
*/
|
||||||
private async updateActivity() {
|
private async updateActivity() {
|
||||||
if (pluginState.pluginStopped) {
|
if (pluginState.pluginStopped) {
|
||||||
verboseLog("Plugin is stopped, skipping update");
|
verboseLog("Plugin is stopped, skipping update");
|
||||||
this.stopUpdates();
|
this.stopUpdates();
|
||||||
return;
|
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) {
|
verboseLog("Fetching last track...");
|
||||||
this.consecutiveFailures++;
|
|
||||||
setDebugInfo("lastError", error);
|
|
||||||
|
|
||||||
if (this.consecutiveFailures >= Constants.MAX_RETRY_ATTEMPTS) {
|
try {
|
||||||
verboseLog(
|
if (!currentSettings.username || !currentSettings.apiKey) {
|
||||||
`Failed ${this.consecutiveFailures} times, initiating reconnection...`,
|
throw new Error("Username or API key not set");
|
||||||
);
|
}
|
||||||
this.startReconnection();
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
private startReconnection() {
|
const lastTrack = await lastfmClient.fetchLatestScrobble();
|
||||||
if (this.isReconnecting) return;
|
setDebugInfo("lastTrack", lastTrack);
|
||||||
|
|
||||||
this.isReconnecting = true;
|
if (!lastTrack.nowPlaying) {
|
||||||
this.stopUpdates();
|
verboseLog("Last track is not currently playing");
|
||||||
|
clearActivity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
verboseLog("Starting reconnection process");
|
if (pluginState.lastTrackUrl === lastTrack.url) {
|
||||||
this.reconnectTimer = setInterval(() => {
|
verboseLog("Track hasn't changed");
|
||||||
verboseLog("Attempting to reconnect...");
|
return;
|
||||||
this.initialize()
|
}
|
||||||
.then(() => {
|
|
||||||
verboseLog("Reconnection successful");
|
|
||||||
this.stopReconnection();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
verboseLog("Reconnection failed:", error);
|
|
||||||
});
|
|
||||||
}, Constants.RETRY_DELAY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private stopReconnection() {
|
const activity: Activity = {
|
||||||
if (this.reconnectTimer) {
|
name: currentSettings.appName || Constants.DEFAULT_APP_NAME,
|
||||||
clearInterval(this.reconnectTimer);
|
flags: 0,
|
||||||
this.reconnectTimer = undefined;
|
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] || "",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.isReconnecting = false;
|
}
|
||||||
this.consecutiveFailures = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// 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
|
* Stops all update and reconnection timers
|
||||||
*/
|
*/
|
||||||
private stopUpdates() {
|
private stopUpdates() {
|
||||||
if (this.updateTimer) {
|
if (this.updateTimer) {
|
||||||
clearInterval(this.updateTimer);
|
clearInterval(this.updateTimer);
|
||||||
this.updateTimer = undefined;
|
this.updateTimer = undefined;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the plugin and starts the update loop
|
* Initializes the plugin and starts the update loop
|
||||||
*/
|
*/
|
||||||
public async initialize() {
|
public async initialize() {
|
||||||
if (pluginState.pluginStopped) {
|
if (pluginState.pluginStopped) {
|
||||||
throw new Error("Plugin is stopped");
|
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);
|
|
||||||
verboseLog(`Update timer started with interval: ${interval}ms`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
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
|
* Stops the plugin and cleans up
|
||||||
*/
|
*/
|
||||||
public stop() {
|
public stop() {
|
||||||
pluginState.pluginStopped = true;
|
pluginState.pluginStopped = true;
|
||||||
this.stopUpdates();
|
this.stopUpdates();
|
||||||
this.stopReconnection();
|
this.stopReconnection();
|
||||||
clearActivity();
|
clearActivity();
|
||||||
}
|
console.log("[Last.fm] Plugin stopped");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export singleton instance methods
|
// Export singleton instance methods
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ import { currentSettings } from "..";
|
||||||
import { Track } from "../../../defs";
|
import { Track } from "../../../defs";
|
||||||
import Constants from "../constants";
|
import Constants from "../constants";
|
||||||
import { setDebugInfo } from "./debug";
|
import { setDebugInfo } from "./debug";
|
||||||
import { showToast } from "@vendetta/ui/toasts";
|
|
||||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
|
||||||
|
|
||||||
interface LastFMError {
|
interface LastFMError {
|
||||||
error: number;
|
error: number;
|
||||||
|
|
@ -44,161 +42,165 @@ interface LastFMTrack {
|
||||||
}
|
}
|
||||||
|
|
||||||
class LastFMClient {
|
class LastFMClient {
|
||||||
private static instance: LastFMClient;
|
private static instance: LastFMClient;
|
||||||
private retryCount = 0;
|
private retryCount = 0;
|
||||||
private lastError = 0;
|
private lastError = 0;
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
public static getInstance(): LastFMClient {
|
public static getInstance(): LastFMClient {
|
||||||
if (!LastFMClient.instance) {
|
if (!LastFMClient.instance) {
|
||||||
LastFMClient.instance = new LastFMClient();
|
LastFMClient.instance = new LastFMClient();
|
||||||
}
|
|
||||||
return LastFMClient.instance;
|
|
||||||
}
|
}
|
||||||
|
return LastFMClient.instance;
|
||||||
|
}
|
||||||
|
|
||||||
private validateSettings(): void {
|
private validateSettings(): void {
|
||||||
if (!currentSettings.username) {
|
if (!currentSettings.username) {
|
||||||
throw new Error("Last.fm username is not set");
|
throw new Error("Last.fm username is not set");
|
||||||
}
|
|
||||||
if (!currentSettings.apiKey) {
|
|
||||||
throw new Error("Last.fm API key is not set");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (!currentSettings.apiKey) {
|
||||||
|
throw new Error("Last.fm API key is not set");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async handleError(error: LastFMError): Promise<never> {
|
private async handleError(error: LastFMError): Promise<never> {
|
||||||
this.lastError = error.error;
|
this.lastError = error.error;
|
||||||
|
|
||||||
const errorMessages: { [key: number]: string } = {
|
const errorMessages: { [key: number]: string } = {
|
||||||
2: "Invalid API key",
|
2: "Invalid API key",
|
||||||
11: "Service temporarily unavailable",
|
11: "Service temporarily unavailable",
|
||||||
16: "Service temporarily unavailable",
|
16: "Service temporarily unavailable",
|
||||||
29: "Rate limit exceeded",
|
29: "Rate limit exceeded",
|
||||||
};
|
};
|
||||||
|
|
||||||
const message =
|
const message =
|
||||||
errorMessages[error.error] || error.message || "Unknown error";
|
errorMessages[error.error] || error.message || "Unknown error";
|
||||||
showToast(`Last.fm Error: ${message}`, getAssetIDByName("Small"));
|
|
||||||
|
|
||||||
throw new Error(`Last.fm API Error ${error.error}: ${message}`);
|
// Log instead of showing toast
|
||||||
|
console.log(`[Last.fm] API Error ${error.error}: ${message}`);
|
||||||
|
|
||||||
|
throw new Error(`Last.fm API Error ${error.error}: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async makeRequest(
|
||||||
|
params: Record<string, string>,
|
||||||
|
): Promise<LastFMResponse> {
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
...params,
|
||||||
|
api_key: currentSettings.apiKey,
|
||||||
|
format: "json",
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${Constants.LFM_API_BASE_URL}?${queryParams}`,
|
||||||
|
);
|
||||||
|
const data: LastFMResponse = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || data.error) {
|
||||||
|
await this.handleError(data as LastFMError);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async makeRequest(
|
return data;
|
||||||
params: Record<string, string>,
|
}
|
||||||
): Promise<LastFMResponse> {
|
|
||||||
const queryParams = new URLSearchParams({
|
|
||||||
...params,
|
|
||||||
api_key: currentSettings.apiKey,
|
|
||||||
format: "json",
|
|
||||||
}).toString();
|
|
||||||
|
|
||||||
const response = await fetch(
|
public async fetchLatestScrobble(): Promise<
|
||||||
`${Constants.LFM_API_BASE_URL}?${queryParams}`,
|
|
||||||
);
|
|
||||||
const data: LastFMResponse = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok || data.error) {
|
|
||||||
await this.handleError(data as LastFMError);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async fetchLatestScrobble(): Promise<
|
|
||||||
Track & { from: number; to: number | null }
|
Track & { from: number; to: number | null }
|
||||||
> {
|
> {
|
||||||
try {
|
try {
|
||||||
this.validateSettings();
|
this.validateSettings();
|
||||||
|
|
||||||
const data = await this.makeRequest({
|
const data = await this.makeRequest({
|
||||||
method: "user.getrecenttracks",
|
method: "user.getrecenttracks",
|
||||||
user: currentSettings.username,
|
user: currentSettings.username,
|
||||||
limit: "1",
|
limit: "1",
|
||||||
extended: "1",
|
extended: "1",
|
||||||
});
|
});
|
||||||
|
|
||||||
const lastTrack = data?.recenttracks?.track?.[0];
|
const lastTrack = data?.recenttracks?.track?.[0];
|
||||||
setDebugInfo("lastAPIResponse", lastTrack);
|
setDebugInfo("lastAPIResponse", lastTrack);
|
||||||
|
|
||||||
if (!lastTrack) {
|
if (!lastTrack) {
|
||||||
throw new Error("No tracks found");
|
console.log("[Last.fm] No tracks found for user");
|
||||||
}
|
throw new Error("No tracks found");
|
||||||
|
}
|
||||||
|
|
||||||
// Reset retry count on successful request
|
// Reset retry count on successful request
|
||||||
this.retryCount = 0;
|
this.retryCount = 0;
|
||||||
|
|
||||||
const isNowPlaying = Boolean(lastTrack["@attr"]?.nowplaying);
|
const isNowPlaying = Boolean(lastTrack["@attr"]?.nowplaying);
|
||||||
const from = isNowPlaying
|
const from = isNowPlaying
|
||||||
? Math.floor(Date.now() / 1000)
|
? Math.floor(Date.now() / 1000)
|
||||||
: parseInt(lastTrack?.date?.uts);
|
: parseInt(lastTrack?.date?.uts);
|
||||||
|
|
||||||
let to: number | null = null;
|
let to: number | null = null;
|
||||||
try {
|
try {
|
||||||
const trackInfo = await this.makeRequest({
|
const trackInfo = await this.makeRequest({
|
||||||
method: "track.getInfo",
|
method: "track.getInfo",
|
||||||
track: lastTrack.name,
|
track: lastTrack.name,
|
||||||
artist: lastTrack.artist.name,
|
artist: lastTrack.artist.name,
|
||||||
username: currentSettings.username,
|
username: currentSettings.username,
|
||||||
});
|
});
|
||||||
|
|
||||||
const duration = parseInt(trackInfo?.track?.duration);
|
const duration = parseInt(trackInfo?.track?.duration);
|
||||||
if (duration > 0) {
|
if (duration > 0) {
|
||||||
to = from + Math.floor(duration / 1000);
|
to = from + Math.floor(duration / 1000);
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn("Failed to fetch track duration", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: lastTrack.name,
|
|
||||||
artist: lastTrack.artist.name,
|
|
||||||
album: lastTrack.album["#text"],
|
|
||||||
albumArt: await this.handleAlbumCover(
|
|
||||||
lastTrack.image?.find((x) => x.size === "large")?.["#text"],
|
|
||||||
),
|
|
||||||
url: lastTrack.url,
|
|
||||||
date: lastTrack.date?.["#text"] ?? "now",
|
|
||||||
nowPlaying: isNowPlaying,
|
|
||||||
loved: lastTrack.loved === "1",
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
// Increment retry count and throw if we've exceeded max retries
|
|
||||||
this.retryCount++;
|
|
||||||
if (this.retryCount > Constants.MAX_RETRY_ATTEMPTS) {
|
|
||||||
this.retryCount = 0;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait before retrying
|
|
||||||
await new Promise((resolve) =>
|
|
||||||
setTimeout(resolve, Constants.RETRY_DELAY),
|
|
||||||
);
|
|
||||||
return this.fetchLatestScrobble();
|
|
||||||
}
|
}
|
||||||
}
|
} catch (err) {
|
||||||
|
console.warn("[Last.fm] Failed to fetch track duration", err);
|
||||||
|
}
|
||||||
|
|
||||||
private async handleAlbumCover(cover?: string): Promise<string | null> {
|
return {
|
||||||
if (!cover) return null;
|
name: lastTrack.name,
|
||||||
|
artist: lastTrack.artist.name,
|
||||||
// If the cover is a default one, return null
|
album: lastTrack.album["#text"],
|
||||||
if (
|
albumArt: await this.handleAlbumCover(
|
||||||
Constants.LFM_DEFAULT_COVER_HASHES.some((hash) => cover.includes(hash))
|
lastTrack.image?.find((x) => x.size === "large")?.["#text"],
|
||||||
) {
|
),
|
||||||
return null;
|
url: lastTrack.url,
|
||||||
}
|
date: lastTrack.date?.["#text"] ?? "now",
|
||||||
|
nowPlaying: isNowPlaying,
|
||||||
return cover;
|
loved: lastTrack.loved === "1",
|
||||||
}
|
from,
|
||||||
|
to,
|
||||||
public getLastError(): number {
|
};
|
||||||
return this.lastError;
|
} catch (error) {
|
||||||
}
|
// Increment retry count and throw if we've exceeded max retries
|
||||||
|
this.retryCount++;
|
||||||
public resetRetryCount(): void {
|
if (this.retryCount > Constants.MAX_RETRY_ATTEMPTS) {
|
||||||
this.retryCount = 0;
|
this.retryCount = 0;
|
||||||
|
console.error("[Last.fm] Max retry attempts reached");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait before retrying
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, Constants.RETRY_DELAY),
|
||||||
|
);
|
||||||
|
return this.fetchLatestScrobble();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleAlbumCover(cover?: string): Promise<string | null> {
|
||||||
|
if (!cover) return null;
|
||||||
|
|
||||||
|
// If the cover is a default one, return null
|
||||||
|
if (
|
||||||
|
Constants.LFM_DEFAULT_COVER_HASHES.some((hash) => cover.includes(hash))
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLastError(): number {
|
||||||
|
return this.lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetRetryCount(): void {
|
||||||
|
this.retryCount = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export a singleton instance
|
// Export a singleton instance
|
||||||
|
|
@ -207,8 +209,8 @@ export const lastfmClient = LastFMClient.getInstance();
|
||||||
/** Fetches the latest user's scrobble */
|
/** Fetches the latest user's scrobble */
|
||||||
export async function fetchLatestScrobble(): Promise<
|
export async function fetchLatestScrobble(): Promise<
|
||||||
Track & { from: number; to: number | null }
|
Track & { from: number; to: number | null }
|
||||||
> {
|
> {
|
||||||
return lastfmClient.fetchLatestScrobble();
|
return lastfmClient.fetchLatestScrobble();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -216,5 +218,5 @@ export async function fetchLatestScrobble(): Promise<
|
||||||
* @param cover The album cover given by Last.fm
|
* @param cover The album cover given by Last.fm
|
||||||
*/
|
*/
|
||||||
export async function handleAlbumCover(cover: string): Promise<string> {
|
export async function handleAlbumCover(cover: string): Promise<string> {
|
||||||
return lastfmClient.handleAlbumCover(cover);
|
return lastfmClient.handleAlbumCover(cover);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue