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 { 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 { LFMSettings } from "../../defs";
|
||||
|
|
@ -17,10 +13,10 @@ import Settings from "./ui/pages/Settings";
|
|||
const { FormText } = Forms;
|
||||
|
||||
export const pluginState = {
|
||||
pluginStopped: false,
|
||||
lastActivity: undefined,
|
||||
updateInterval: undefined,
|
||||
lastTrackUrl: undefined,
|
||||
pluginStopped: false,
|
||||
lastActivity: undefined,
|
||||
updateInterval: undefined,
|
||||
lastTrackUrl: undefined,
|
||||
} as {
|
||||
pluginStopped: boolean;
|
||||
lastActivity?: any;
|
||||
|
|
@ -31,18 +27,18 @@ export const pluginState = {
|
|||
// Initialize default settings
|
||||
const defaultSettings: LFMSettings = Constants.DEFAULT_SETTINGS;
|
||||
for (const key in defaultSettings) {
|
||||
plugin.storage[key] =
|
||||
plugin.storage[key] =
|
||||
plugin.storage[key] ?? defaultSettings[key as keyof typeof defaultSettings];
|
||||
}
|
||||
|
||||
export const currentSettings = new Proxy(plugin.storage, {
|
||||
get(target, prop) {
|
||||
return target[prop];
|
||||
},
|
||||
set(target, prop, value) {
|
||||
target[prop] = value;
|
||||
return true;
|
||||
},
|
||||
get(target, prop) {
|
||||
return target[prop];
|
||||
},
|
||||
set(target, prop, value) {
|
||||
target[prop] = value;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
// Connection status tracking
|
||||
|
|
@ -51,62 +47,66 @@ const MAX_CONNECTION_ATTEMPTS = 3;
|
|||
const RECONNECT_DELAY = 5000;
|
||||
|
||||
async function tryInitialize() {
|
||||
try {
|
||||
await initialize();
|
||||
connectionAttempts = 0;
|
||||
} catch (error) {
|
||||
console.error("[Last.fm] Initialization error:", error);
|
||||
connectionAttempts++;
|
||||
try {
|
||||
await initialize();
|
||||
connectionAttempts = 0;
|
||||
console.log("[Last.fm] Successfully connected to Last.fm");
|
||||
} catch (error) {
|
||||
console.error("[Last.fm] Initialization error:", error);
|
||||
connectionAttempts++;
|
||||
|
||||
if (connectionAttempts < MAX_CONNECTION_ATTEMPTS) {
|
||||
showToast("Retrying connection...", getAssetIDByName("ic_clock"));
|
||||
setTimeout(tryInitialize, RECONNECT_DELAY);
|
||||
} else {
|
||||
showToast("Failed to connect to Last.fm", getAssetIDByName("Small"));
|
||||
}
|
||||
if (connectionAttempts < MAX_CONNECTION_ATTEMPTS) {
|
||||
console.log(
|
||||
`[Last.fm] Retrying connection... (attempt ${connectionAttempts})`,
|
||||
);
|
||||
setTimeout(tryInitialize, RECONNECT_DELAY);
|
||||
} else {
|
||||
console.error(
|
||||
"[Last.fm] Failed to connect to Last.fm after multiple attempts",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
onLoad() {
|
||||
pluginState.pluginStopped = false;
|
||||
onLoad() {
|
||||
pluginState.pluginStopped = false;
|
||||
|
||||
if (!currentSettings.username || !currentSettings.apiKey) {
|
||||
showToast(
|
||||
"Please configure Last.fm username and API key in settings",
|
||||
getAssetIDByName("ic_warning"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!currentSettings.username || !currentSettings.apiKey) {
|
||||
console.log(
|
||||
"[Last.fm] Please configure Last.fm username and API key in settings",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (UserStore.getCurrentUser()) {
|
||||
tryInitialize();
|
||||
} else {
|
||||
const waitForUser = () => {
|
||||
if (UserStore.getCurrentUser()) {
|
||||
tryInitialize();
|
||||
} else {
|
||||
const waitForUser = () => {
|
||||
if (UserStore.getCurrentUser()) {
|
||||
tryInitialize();
|
||||
FluxDispatcher.unsubscribe("CONNECTION_OPEN", waitForUser);
|
||||
}
|
||||
};
|
||||
tryInitialize();
|
||||
FluxDispatcher.unsubscribe("CONNECTION_OPEN", waitForUser);
|
||||
}
|
||||
};
|
||||
|
||||
FluxDispatcher.subscribe("CONNECTION_OPEN", waitForUser);
|
||||
}
|
||||
},
|
||||
onUnload() {
|
||||
pluginState.pluginStopped = true;
|
||||
stop();
|
||||
},
|
||||
// Handle settings updates
|
||||
onSettingsUpdate(newSettings: any) {
|
||||
Object.assign(currentSettings, newSettings);
|
||||
tryInitialize();
|
||||
},
|
||||
// Handle Discord reconnection
|
||||
onDiscordReconnect() {
|
||||
if (!pluginState.pluginStopped) {
|
||||
tryInitialize();
|
||||
}
|
||||
},
|
||||
// Settings component
|
||||
settings: Settings,
|
||||
FluxDispatcher.subscribe("CONNECTION_OPEN", waitForUser);
|
||||
}
|
||||
},
|
||||
onUnload() {
|
||||
pluginState.pluginStopped = true;
|
||||
stop();
|
||||
},
|
||||
// Handle settings updates
|
||||
onSettingsUpdate(newSettings: any) {
|
||||
Object.assign(currentSettings, newSettings);
|
||||
tryInitialize();
|
||||
},
|
||||
// Handle Discord reconnection
|
||||
onDiscordReconnect() {
|
||||
if (!pluginState.pluginStopped) {
|
||||
tryInitialize();
|
||||
}
|
||||
},
|
||||
// Settings component
|
||||
settings: Settings,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { showToast } from "@vendetta/ui/toasts";
|
||||
|
||||
import { currentSettings, pluginState } from ".";
|
||||
import { Activity } from "../../defs";
|
||||
import Constants from "./constants";
|
||||
|
|
@ -9,9 +6,9 @@ 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")}`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = Math.floor(seconds % 60);
|
||||
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
|
||||
}
|
||||
import { lastfmClient } from "./utils/lastfm";
|
||||
|
||||
|
|
@ -23,210 +20,211 @@ enum ActivityType {
|
|||
}
|
||||
|
||||
const verboseLog = (...message: any[]) =>
|
||||
currentSettings.verboseLogging && console.log("[Last.fm]", ...message);
|
||||
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 static instance: PluginManager;
|
||||
private updateTimer?: NodeJS.Timer;
|
||||
private reconnectTimer?: NodeJS.Timer;
|
||||
private consecutiveFailures = 0;
|
||||
private isReconnecting = false;
|
||||
|
||||
private constructor() {}
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): PluginManager {
|
||||
if (!PluginManager.instance) {
|
||||
PluginManager.instance = new PluginManager();
|
||||
}
|
||||
return PluginManager.instance;
|
||||
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 async updateActivity() {
|
||||
if (pluginState.pluginStopped) {
|
||||
verboseLog("Plugin is stopped, skipping update");
|
||||
this.stopUpdates();
|
||||
return;
|
||||
}
|
||||
|
||||
private handleError(error: Error) {
|
||||
this.consecutiveFailures++;
|
||||
setDebugInfo("lastError", error);
|
||||
verboseLog("Fetching last track...");
|
||||
|
||||
if (this.consecutiveFailures >= Constants.MAX_RETRY_ATTEMPTS) {
|
||||
verboseLog(
|
||||
`Failed ${this.consecutiveFailures} times, initiating reconnection...`,
|
||||
);
|
||||
this.startReconnection();
|
||||
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);
|
||||
}
|
||||
|
||||
private startReconnection() {
|
||||
if (this.isReconnecting) return;
|
||||
const lastTrack = await lastfmClient.fetchLatestScrobble();
|
||||
setDebugInfo("lastTrack", lastTrack);
|
||||
|
||||
this.isReconnecting = true;
|
||||
this.stopUpdates();
|
||||
if (!lastTrack.nowPlaying) {
|
||||
verboseLog("Last track is not currently playing");
|
||||
clearActivity();
|
||||
return;
|
||||
}
|
||||
|
||||
verboseLog("Starting reconnection process");
|
||||
this.reconnectTimer = setInterval(() => {
|
||||
verboseLog("Attempting to reconnect...");
|
||||
this.initialize()
|
||||
.then(() => {
|
||||
verboseLog("Reconnection successful");
|
||||
this.stopReconnection();
|
||||
})
|
||||
.catch((error) => {
|
||||
verboseLog("Reconnection failed:", error);
|
||||
});
|
||||
}, Constants.RETRY_DELAY);
|
||||
}
|
||||
if (pluginState.lastTrackUrl === lastTrack.url) {
|
||||
verboseLog("Track hasn't changed");
|
||||
return;
|
||||
}
|
||||
|
||||
private stopReconnection() {
|
||||
if (this.reconnectTimer) {
|
||||
clearInterval(this.reconnectTimer);
|
||||
this.reconnectTimer = undefined;
|
||||
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] || "",
|
||||
);
|
||||
}
|
||||
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
|
||||
*/
|
||||
private stopUpdates() {
|
||||
if (this.updateTimer) {
|
||||
clearInterval(this.updateTimer);
|
||||
this.updateTimer = undefined;
|
||||
}
|
||||
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);
|
||||
verboseLog(`Update timer started with interval: ${interval}ms`);
|
||||
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();
|
||||
}
|
||||
public stop() {
|
||||
pluginState.pluginStopped = true;
|
||||
this.stopUpdates();
|
||||
this.stopReconnection();
|
||||
clearActivity();
|
||||
console.log("[Last.fm] Plugin stopped");
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance methods
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ import { currentSettings } from "..";
|
|||
import { Track } from "../../../defs";
|
||||
import Constants from "../constants";
|
||||
import { setDebugInfo } from "./debug";
|
||||
import { showToast } from "@vendetta/ui/toasts";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
|
||||
interface LastFMError {
|
||||
error: number;
|
||||
|
|
@ -44,161 +42,165 @@ interface LastFMTrack {
|
|||
}
|
||||
|
||||
class LastFMClient {
|
||||
private static instance: LastFMClient;
|
||||
private retryCount = 0;
|
||||
private lastError = 0;
|
||||
private static instance: LastFMClient;
|
||||
private retryCount = 0;
|
||||
private lastError = 0;
|
||||
|
||||
private constructor() {}
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): LastFMClient {
|
||||
if (!LastFMClient.instance) {
|
||||
LastFMClient.instance = new LastFMClient();
|
||||
}
|
||||
return LastFMClient.instance;
|
||||
public static getInstance(): LastFMClient {
|
||||
if (!LastFMClient.instance) {
|
||||
LastFMClient.instance = new LastFMClient();
|
||||
}
|
||||
return LastFMClient.instance;
|
||||
}
|
||||
|
||||
private validateSettings(): void {
|
||||
if (!currentSettings.username) {
|
||||
throw new Error("Last.fm username is not set");
|
||||
}
|
||||
if (!currentSettings.apiKey) {
|
||||
throw new Error("Last.fm API key is not set");
|
||||
}
|
||||
private validateSettings(): void {
|
||||
if (!currentSettings.username) {
|
||||
throw new Error("Last.fm username is not set");
|
||||
}
|
||||
if (!currentSettings.apiKey) {
|
||||
throw new Error("Last.fm API key is not set");
|
||||
}
|
||||
}
|
||||
|
||||
private async handleError(error: LastFMError): Promise<never> {
|
||||
this.lastError = error.error;
|
||||
private async handleError(error: LastFMError): Promise<never> {
|
||||
this.lastError = error.error;
|
||||
|
||||
const errorMessages: { [key: number]: string } = {
|
||||
2: "Invalid API key",
|
||||
11: "Service temporarily unavailable",
|
||||
16: "Service temporarily unavailable",
|
||||
29: "Rate limit exceeded",
|
||||
};
|
||||
const errorMessages: { [key: number]: string } = {
|
||||
2: "Invalid API key",
|
||||
11: "Service temporarily unavailable",
|
||||
16: "Service temporarily unavailable",
|
||||
29: "Rate limit exceeded",
|
||||
};
|
||||
|
||||
const message =
|
||||
const message =
|
||||
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(
|
||||
params: Record<string, string>,
|
||||
): Promise<LastFMResponse> {
|
||||
const queryParams = new URLSearchParams({
|
||||
...params,
|
||||
api_key: currentSettings.apiKey,
|
||||
format: "json",
|
||||
}).toString();
|
||||
return data;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public async fetchLatestScrobble(): Promise<
|
||||
public async fetchLatestScrobble(): Promise<
|
||||
Track & { from: number; to: number | null }
|
||||
> {
|
||||
try {
|
||||
this.validateSettings();
|
||||
> {
|
||||
try {
|
||||
this.validateSettings();
|
||||
|
||||
const data = await this.makeRequest({
|
||||
method: "user.getrecenttracks",
|
||||
user: currentSettings.username,
|
||||
limit: "1",
|
||||
extended: "1",
|
||||
});
|
||||
const data = await this.makeRequest({
|
||||
method: "user.getrecenttracks",
|
||||
user: currentSettings.username,
|
||||
limit: "1",
|
||||
extended: "1",
|
||||
});
|
||||
|
||||
const lastTrack = data?.recenttracks?.track?.[0];
|
||||
setDebugInfo("lastAPIResponse", lastTrack);
|
||||
const lastTrack = data?.recenttracks?.track?.[0];
|
||||
setDebugInfo("lastAPIResponse", lastTrack);
|
||||
|
||||
if (!lastTrack) {
|
||||
throw new Error("No tracks found");
|
||||
}
|
||||
if (!lastTrack) {
|
||||
console.log("[Last.fm] No tracks found for user");
|
||||
throw new Error("No tracks found");
|
||||
}
|
||||
|
||||
// Reset retry count on successful request
|
||||
this.retryCount = 0;
|
||||
// Reset retry count on successful request
|
||||
this.retryCount = 0;
|
||||
|
||||
const isNowPlaying = Boolean(lastTrack["@attr"]?.nowplaying);
|
||||
const from = isNowPlaying
|
||||
? Math.floor(Date.now() / 1000)
|
||||
: parseInt(lastTrack?.date?.uts);
|
||||
const isNowPlaying = Boolean(lastTrack["@attr"]?.nowplaying);
|
||||
const from = isNowPlaying
|
||||
? Math.floor(Date.now() / 1000)
|
||||
: parseInt(lastTrack?.date?.uts);
|
||||
|
||||
let to: number | null = null;
|
||||
try {
|
||||
const trackInfo = await this.makeRequest({
|
||||
method: "track.getInfo",
|
||||
track: lastTrack.name,
|
||||
artist: lastTrack.artist.name,
|
||||
username: currentSettings.username,
|
||||
});
|
||||
let to: number | null = null;
|
||||
try {
|
||||
const trackInfo = await this.makeRequest({
|
||||
method: "track.getInfo",
|
||||
track: lastTrack.name,
|
||||
artist: lastTrack.artist.name,
|
||||
username: currentSettings.username,
|
||||
});
|
||||
|
||||
const duration = parseInt(trackInfo?.track?.duration);
|
||||
if (duration > 0) {
|
||||
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();
|
||||
const duration = parseInt(trackInfo?.track?.duration);
|
||||
if (duration > 0) {
|
||||
to = from + Math.floor(duration / 1000);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[Last.fm] Failed to fetch track duration", err);
|
||||
}
|
||||
|
||||
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 {
|
||||
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;
|
||||
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
|
||||
|
|
@ -207,8 +209,8 @@ export const lastfmClient = LastFMClient.getInstance();
|
|||
/** Fetches the latest user's scrobble */
|
||||
export async function fetchLatestScrobble(): Promise<
|
||||
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
|
||||
*/
|
||||
export async function handleAlbumCover(cover: string): Promise<string> {
|
||||
return lastfmClient.handleAlbumCover(cover);
|
||||
return lastfmClient.handleAlbumCover(cover);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue