test
This commit is contained in:
parent
f4be7b2743
commit
a6156a928e
6 changed files with 677 additions and 435 deletions
|
|
@ -1,15 +1,32 @@
|
||||||
const Constants = {
|
const Constants = {
|
||||||
DEFAULT_APP_NAME: "Music",
|
DEFAULT_APP_NAME: "Music",
|
||||||
DEFAULT_TIME_INTERVAL: 5,
|
DEFAULT_TIME_INTERVAL: 5,
|
||||||
/** The application ID of the Discord's developer app */
|
/** The application ID of the Discord's developer app */
|
||||||
APPLICATION_ID: "1054951789318909972",
|
APPLICATION_ID: "1054951789318909972",
|
||||||
/** Last.fm api key */
|
/** The minimum update interval in seconds to prevent rate limiting */
|
||||||
LFM_API_KEY: "014ffe8a614370f000d85d95ec30e1be",
|
MIN_UPDATE_INTERVAL: 3,
|
||||||
/** These are the default album covers that are used by Last.fm */
|
/** Maximum retry attempts for API calls */
|
||||||
LFM_DEFAULT_COVER_HASHES: [
|
MAX_RETRY_ATTEMPTS: 3,
|
||||||
"2a96cbd8b46e442fc41c2b86b821562f",
|
/** Retry delay in milliseconds */
|
||||||
"c6f59c1e5e7240a4c0d427abd71f3dbb",
|
RETRY_DELAY: 5000,
|
||||||
]
|
/** Base URL for Last.fm API */
|
||||||
|
LFM_API_BASE_URL: "https://ws.audioscrobbler.com/2.0",
|
||||||
|
/** These are the default album covers that are used by Last.fm */
|
||||||
|
LFM_DEFAULT_COVER_HASHES: [
|
||||||
|
"2a96cbd8b46e442fc41c2b86b821562f",
|
||||||
|
"c6f59c1e5e7240a4c0d427abd71f3dbb",
|
||||||
|
],
|
||||||
|
/** Default settings */
|
||||||
|
DEFAULT_SETTINGS: {
|
||||||
|
username: "",
|
||||||
|
apiKey: "",
|
||||||
|
appName: "Music",
|
||||||
|
timeInterval: 5,
|
||||||
|
showTimestamp: true,
|
||||||
|
listeningTo: true,
|
||||||
|
ignoreSpotify: true,
|
||||||
|
verboseLogging: false,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default Constants;
|
export default Constants;
|
||||||
|
|
|
||||||
|
|
@ -1,121 +1,106 @@
|
||||||
import { plugin } from "@vendetta";
|
import { plugin } from "@vendetta";
|
||||||
import { FluxDispatcher, React } from "@vendetta/metro/common";
|
import { FluxDispatcher } from "@vendetta/metro/common";
|
||||||
|
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||||
|
import { showToast } from "@vendetta/ui/toasts";
|
||||||
|
|
||||||
import { lazy, Suspense } from "react";
|
import { lazy } from "react";
|
||||||
import { Activity, LFMSettings } from "../../defs";
|
import { LFMSettings } from "../../defs";
|
||||||
import { flush, initialize } from "./manager";
|
import Constants from "./constants";
|
||||||
|
import { initialize, stop } from "./manager";
|
||||||
import { UserStore } from "./modules";
|
import { UserStore } from "./modules";
|
||||||
import { Forms } from "@vendetta/ui/components";
|
|
||||||
import { ScrollView } from "react-native";
|
|
||||||
|
|
||||||
const { FormText } = Forms;
|
export const pluginState = {
|
||||||
|
pluginStopped: false,
|
||||||
|
lastActivity: undefined,
|
||||||
|
updateInterval: undefined,
|
||||||
|
lastTrackUrl: undefined,
|
||||||
|
} as {
|
||||||
|
pluginStopped: boolean;
|
||||||
|
lastActivity?: any;
|
||||||
|
updateInterval?: NodeJS.Timer;
|
||||||
|
lastTrackUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
// Error boundary component for settings
|
// Initialize default settings
|
||||||
function SettingsErrorBoundary({ children }: { children: React.ReactNode }) {
|
const defaultSettings: LFMSettings = Constants.DEFAULT_SETTINGS;
|
||||||
const [hasError, setHasError] = React.useState(false);
|
for (const key in defaultSettings) {
|
||||||
|
plugin.storage[key] =
|
||||||
React.useEffect(() => {
|
plugin.storage[key] ?? defaultSettings[key as keyof typeof defaultSettings];
|
||||||
const errorHandler = (error: any) => {
|
|
||||||
console.error("Settings component error:", error);
|
|
||||||
setHasError(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
setHasError(false);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (hasError) {
|
|
||||||
return (
|
|
||||||
<ScrollView style={{ padding: 12 }}>
|
|
||||||
<FormText style={{ color: "#FF0000" }}>
|
|
||||||
Failed to load Last.fm settings. Please try reloading the plugin.
|
|
||||||
</FormText>
|
|
||||||
</ScrollView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Suspense fallback={
|
|
||||||
<ScrollView style={{ padding: 12 }}>
|
|
||||||
<FormText>Loading Last.fm settings...</FormText>
|
|
||||||
</ScrollView>
|
|
||||||
}>
|
|
||||||
{children}
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lazy-loaded Settings component with error handling
|
export const currentSettings = new Proxy(plugin.storage, {
|
||||||
const LazySettings = lazy(() => {
|
get(target, prop) {
|
||||||
try {
|
return target[prop];
|
||||||
return import("./ui/pages/Settings").catch((error) => {
|
},
|
||||||
console.error("Failed to load Settings component:", error);
|
set(target, prop, value) {
|
||||||
// Return a fallback component
|
target[prop] = value;
|
||||||
return {
|
return true;
|
||||||
default: () => (
|
},
|
||||||
<ScrollView style={{ padding: 12 }}>
|
|
||||||
<FormText style={{ color: "#FF0000" }}>
|
|
||||||
Error: Could not load Last.fm settings component.
|
|
||||||
{"\n"}Please check console for details.
|
|
||||||
</FormText>
|
|
||||||
</ScrollView>
|
|
||||||
)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Critical error in lazy loading:", error);
|
|
||||||
// Return fallback component for critical errors
|
|
||||||
return Promise.resolve({
|
|
||||||
default: () => (
|
|
||||||
<ScrollView style={{ padding: 12 }}>
|
|
||||||
<FormText style={{ color: "#FF0000" }}>
|
|
||||||
Critical error loading settings. Please restart the app.
|
|
||||||
</FormText>
|
|
||||||
</ScrollView>
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wrapped settings component
|
const LazySettings = lazy(() => import("./ui/pages/Settings"));
|
||||||
function WrappedSettings() {
|
|
||||||
return (
|
// Connection status tracking
|
||||||
<SettingsErrorBoundary>
|
let connectionAttempts = 0;
|
||||||
<LazySettings />
|
const MAX_CONNECTION_ATTEMPTS = 3;
|
||||||
</SettingsErrorBoundary>
|
const RECONNECT_DELAY = 5000;
|
||||||
);
|
|
||||||
|
async function tryInitialize() {
|
||||||
|
try {
|
||||||
|
await initialize();
|
||||||
|
connectionAttempts = 0;
|
||||||
|
} 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const pluginState = {} as {
|
|
||||||
pluginStopped?: boolean,
|
|
||||||
lastActivity?: Activity,
|
|
||||||
updateInterval?: NodeJS.Timer,
|
|
||||||
lastTrackUrl?: string,
|
|
||||||
};
|
|
||||||
|
|
||||||
plugin.storage.ignoreSpotify ??= true;
|
|
||||||
export const currentSettings = { ...plugin.storage } as LFMSettings;
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
settings: WrappedSettings,
|
settings: LazySettings,
|
||||||
onLoad() {
|
onLoad() {
|
||||||
pluginState.pluginStopped = false;
|
pluginState.pluginStopped = false;
|
||||||
|
|
||||||
if (UserStore.getCurrentUser()) {
|
if (!currentSettings.username || !currentSettings.apiKey) {
|
||||||
initialize().catch(console.error);
|
showToast(
|
||||||
} else {
|
"Please configure Last.fm username and API key in settings",
|
||||||
const callback = () => {
|
getAssetIDByName("ic_warning"),
|
||||||
initialize().catch(console.error);
|
);
|
||||||
FluxDispatcher.unsubscribe(callback);
|
return;
|
||||||
};
|
|
||||||
|
|
||||||
FluxDispatcher.subscribe("CONNECTION_OPEN", callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
onUnload() {
|
|
||||||
pluginState.pluginStopped = true;
|
|
||||||
flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (UserStore.getCurrentUser()) {
|
||||||
|
tryInitialize();
|
||||||
|
} else {
|
||||||
|
const waitForUser = () => {
|
||||||
|
if (UserStore.getCurrentUser()) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,157 +7,217 @@ import Constants from "./constants";
|
||||||
import { SelfPresenceStore } from "./modules";
|
import { SelfPresenceStore } from "./modules";
|
||||||
import { clearActivity, fetchAsset, sendRequest } from "./utils/activity";
|
import { clearActivity, fetchAsset, sendRequest } from "./utils/activity";
|
||||||
import { setDebugInfo } from "./utils/debug";
|
import { setDebugInfo } from "./utils/debug";
|
||||||
import { fetchLatestScrobble } from "./utils/lastfm";
|
import { lastfmClient } from "./utils/lastfm";
|
||||||
|
|
||||||
enum ActivityType {
|
enum ActivityType {
|
||||||
PLAYING = 0,
|
PLAYING = 0,
|
||||||
STREAMING = 1,
|
STREAMING = 1,
|
||||||
LISTENING = 2,
|
LISTENING = 2,
|
||||||
COMPETING = 5
|
COMPETING = 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
const verboseLog = (...message: any) => currentSettings.verboseLogging && console.log(...message);
|
const verboseLog = (...message: any[]) =>
|
||||||
|
currentSettings.verboseLogging && console.log("[Last.fm]", ...message);
|
||||||
|
|
||||||
/**
|
class PluginManager {
|
||||||
* Fetches the current scrobble and sets the activity.
|
private static instance: PluginManager;
|
||||||
* This function is actively called by the updateInterval.
|
private updateTimer?: NodeJS.Timer;
|
||||||
*/
|
private reconnectTimer?: NodeJS.Timer;
|
||||||
async function update() {
|
private consecutiveFailures: number = 0;
|
||||||
|
private isReconnecting: boolean = 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) {
|
if (pluginState.pluginStopped) {
|
||||||
verboseLog("--> Plugin is unloaded, aborting update()...");
|
verboseLog("Plugin is stopped, skipping update");
|
||||||
flush();
|
this.stopUpdates();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
verboseLog("--> Fetching last track...");
|
verboseLog("Fetching last track...");
|
||||||
|
|
||||||
if (!currentSettings.username) {
|
|
||||||
showToast("Last.fm username is not set!", getAssetIDByName("Small"));
|
|
||||||
flush(); // Flush as we always need reinitialization
|
|
||||||
throw new Error("Username is not set");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentSettings.ignoreSpotify) {
|
|
||||||
const spotifyActivity = SelfPresenceStore.findActivity(act => act.sync_id);
|
|
||||||
if (spotifyActivity) {
|
|
||||||
verboseLog("--> Spotify is currently playing, aborting...");
|
|
||||||
setDebugInfo("isSpotifyIgnored", true);
|
|
||||||
|
|
||||||
clearActivity();
|
|
||||||
return;
|
|
||||||
} else setDebugInfo("isSpotifyIgnored", false);
|
|
||||||
} else {
|
|
||||||
setDebugInfo("isSpotifyIgnored", undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastTrack = await fetchLatestScrobble().catch(async (err) => {
|
|
||||||
verboseLog("--> An error occurred while fetching the last track, aborting...");
|
|
||||||
clearActivity();
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
setDebugInfo("lastTrack", lastTrack);
|
|
||||||
|
|
||||||
if (!lastTrack.nowPlaying) {
|
|
||||||
verboseLog("--> Last track is not currently playing, aborting...");
|
|
||||||
clearActivity();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
verboseLog("--> Track fetched!");
|
|
||||||
|
|
||||||
if (pluginState.lastTrackUrl === lastTrack.url) {
|
|
||||||
verboseLog("--> Last track is the same as the previous one, aborting...");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activity = {
|
|
||||||
name: currentSettings.appName || Constants.DEFAULT_APP_NAME,
|
|
||||||
flags: 0,
|
|
||||||
type: currentSettings.listeningTo ? ActivityType.LISTENING : ActivityType.PLAYING,
|
|
||||||
details: lastTrack.name,
|
|
||||||
state: `${lastTrack.artist}`,
|
|
||||||
status_display_type: 1,
|
|
||||||
application_id: Constants.APPLICATION_ID,
|
|
||||||
} as Activity;
|
|
||||||
|
|
||||||
pluginState.lastTrackUrl = lastTrack.url;
|
|
||||||
|
|
||||||
if (activity.name.includes("{{")) {
|
|
||||||
for (const key in lastTrack) {
|
|
||||||
activity.name = activity.name.replace(`{{${key}}}`, lastTrack[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set timestamps
|
|
||||||
if (currentSettings.showTimestamp) {
|
|
||||||
activity.timestamps = {
|
|
||||||
start: Date.now()| 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastTrack.album) {
|
|
||||||
const asset = await fetchAsset([lastTrack.albumArt]);
|
|
||||||
|
|
||||||
activity.assets = {
|
|
||||||
large_image: asset[0],
|
|
||||||
large_text: `${lastTrack.album}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
verboseLog("--> Setting activity...");
|
|
||||||
setDebugInfo("lastActivity", activity);
|
|
||||||
verboseLog(activity);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sendRequest(activity);
|
if (!currentSettings.username || !currentSettings.apiKey) {
|
||||||
} catch (err) {
|
throw new Error("Username or API key not set");
|
||||||
verboseLog("--> An error occurred while setting the activity");
|
}
|
||||||
|
|
||||||
|
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();
|
clearActivity();
|
||||||
throw err;
|
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: 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
|
||||||
|
if (currentSettings.showTimestamp) {
|
||||||
|
activity.timestamps = {
|
||||||
|
start: Date.now() | 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle album art
|
||||||
|
if (lastTrack.album) {
|
||||||
|
const asset = await fetchAsset([lastTrack.albumArt]);
|
||||||
|
if (asset[0]) {
|
||||||
|
activity.assets = {
|
||||||
|
large_image: asset[0],
|
||||||
|
large_text: lastTrack.album,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
verboseLog("--> Successfully set activity!");
|
private handleError(error: Error) {
|
||||||
}
|
this.consecutiveFailures++;
|
||||||
|
setDebugInfo("lastError", error);
|
||||||
|
|
||||||
/** Stops and reset everything, can be started again with `initialize()` */
|
if (this.consecutiveFailures >= Constants.MAX_RETRY_ATTEMPTS) {
|
||||||
export function flush(isClearActivity = false) {
|
verboseLog(
|
||||||
pluginState.lastActivity = null;
|
`Failed ${this.consecutiveFailures} times, initiating reconnection...`,
|
||||||
pluginState.lastTrackUrl = null;
|
);
|
||||||
|
this.startReconnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pluginState.updateInterval && clearInterval(pluginState.updateInterval);
|
private startReconnection() {
|
||||||
!isClearActivity && clearActivity();
|
if (this.isReconnecting) return;
|
||||||
}
|
|
||||||
|
|
||||||
/** Initializes the plugin */
|
this.isReconnecting = true;
|
||||||
export async function initialize() {
|
this.stopUpdates();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
if (pluginState.pluginStopped) {
|
||||||
throw new Error("Plugin is already stopped!");
|
throw new Error("Plugin is stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
flush();
|
this.stopUpdates();
|
||||||
|
await this.updateActivity();
|
||||||
|
|
||||||
let tries = 0;
|
const interval = Math.max(
|
||||||
|
(Number(currentSettings.timeInterval) ||
|
||||||
await update().catch((err) => {
|
Constants.DEFAULT_SETTINGS.timeInterval) * 1000,
|
||||||
console.error(err);
|
Constants.MIN_UPDATE_INTERVAL * 1000,
|
||||||
tries++;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Periodically fetches the current scrobble and sets the activity
|
|
||||||
pluginState.updateInterval = setInterval(
|
|
||||||
() => update()
|
|
||||||
.then(() => {
|
|
||||||
tries = 0;
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
|
|
||||||
if (++tries > 3) {
|
|
||||||
console.error("Failed to fetch/set activity 3 times, aborting...");
|
|
||||||
flush();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
(Number(currentSettings.timeInterval) || Constants.DEFAULT_TIME_INTERVAL) * 1000
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.updateTimer = setInterval(() => this.updateActivity(), interval);
|
||||||
|
verboseLog(`Update timer started with interval: ${interval}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the plugin and cleans up
|
||||||
|
*/
|
||||||
|
public stop() {
|
||||||
|
pluginState.pluginStopped = true;
|
||||||
|
this.stopUpdates();
|
||||||
|
this.stopReconnection();
|
||||||
|
clearActivity();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export singleton instance methods
|
||||||
|
const manager = PluginManager.getInstance();
|
||||||
|
export const initialize = () => manager.initialize();
|
||||||
|
export const stop = () => manager.stop();
|
||||||
|
|
|
||||||
|
|
@ -1,162 +1,192 @@
|
||||||
import { NavigationNative, React } from "@vendetta/metro/common";
|
import { plugin } from "@vendetta";
|
||||||
import { storage } from "@vendetta/plugin";
|
import { Forms } from "@vendetta/ui/components";
|
||||||
import { useProxy } from "@vendetta/storage";
|
import { useProxy } from "@vendetta/storage";
|
||||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||||
import { Forms } from "@vendetta/ui/components";
|
|
||||||
import { showToast } from "@vendetta/ui/toasts";
|
import { showToast } from "@vendetta/ui/toasts";
|
||||||
|
import { React } from "@vendetta/metro/common";
|
||||||
|
import { ScrollView } from "react-native";
|
||||||
|
|
||||||
import { lazy, useEffect } from "react";
|
import { currentSettings, pluginState } from "../..";
|
||||||
import { ScrollView, Text, TouchableOpacity } from "react-native";
|
|
||||||
import { currentSettings } from "../..";
|
|
||||||
import { LFMSettings } from "../../../../defs";
|
|
||||||
import Constants from "../../constants";
|
import Constants from "../../constants";
|
||||||
import { initialize } from "../../manager";
|
import { initialize } from "../../manager";
|
||||||
import {
|
import { lastfmClient } from "../../utils/lastfm";
|
||||||
logComponentMount,
|
|
||||||
logComponentError,
|
|
||||||
logNavigationError,
|
|
||||||
incrementSettingsLoad,
|
|
||||||
} from "../../utils/debug";
|
|
||||||
|
|
||||||
const { FormRow, FormInput, FormDivider, FormSwitchRow, FormText, FormIcon } =
|
const { FormInput, FormRow, FormSwitch, FormSection, FormDivider, FormText } =
|
||||||
Forms;
|
Forms;
|
||||||
|
|
||||||
function UpdateButton() {
|
export default function Settings() {
|
||||||
async function onPressCallback() {
|
useProxy(currentSettings);
|
||||||
try {
|
|
||||||
incrementSettingsLoad();
|
|
||||||
for (const key in storage) {
|
|
||||||
if (storage[key] != null) {
|
|
||||||
currentSettings[key] = storage[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
|
||||||
|
const saveSettings = (key: string, value: any) => {
|
||||||
|
currentSettings[key] = value;
|
||||||
|
plugin.storage[key] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const testConnection = async () => {
|
||||||
|
if (!currentSettings.username || !currentSettings.apiKey) {
|
||||||
|
showToast(
|
||||||
|
"Please enter both username and API key",
|
||||||
|
getAssetIDByName("Small"),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await lastfmClient.fetchLatestScrobble();
|
||||||
|
showToast("Connection successful!", getAssetIDByName("Check"));
|
||||||
|
|
||||||
|
// Reinitialize the plugin with new settings
|
||||||
await initialize();
|
await initialize();
|
||||||
showToast("Settings updated!", getAssetIDByName("Check"));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logComponentError("UpdateButton", error);
|
showToast(
|
||||||
showToast("Failed to update settings", getAssetIDByName("Small"));
|
`Connection failed: ${error.message}`,
|
||||||
|
getAssetIDByName("Small"),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity onPress={onPressCallback}>
|
<ScrollView style={{ flex: 1 }}>
|
||||||
<FormText style={{ marginRight: 12 }}>UPDATE</FormText>
|
<FormSection title="Account Settings">
|
||||||
</TouchableOpacity>
|
<FormInput
|
||||||
);
|
title="Last.fm Username"
|
||||||
}
|
placeholder="Enter your Last.fm username"
|
||||||
|
value={currentSettings.username}
|
||||||
|
onChange={(value: string) => saveSettings("username", value)}
|
||||||
|
/>
|
||||||
|
<FormDivider />
|
||||||
|
<FormInput
|
||||||
|
title="API Key"
|
||||||
|
placeholder="Enter your Last.fm API key"
|
||||||
|
value={currentSettings.apiKey}
|
||||||
|
onChange={(value: string) => saveSettings("apiKey", value)}
|
||||||
|
secureTextEntry={true}
|
||||||
|
/>
|
||||||
|
<FormText style={{ padding: 8, opacity: 0.5 }}>
|
||||||
|
Get your API key at: https://www.last.fm/api/account/create
|
||||||
|
</FormText>
|
||||||
|
<FormRow
|
||||||
|
label="Test Connection"
|
||||||
|
leading={<FormRow.Icon source={getAssetIDByName("ic_connection")} />}
|
||||||
|
trailing={loading ? <FormRow.Arrow loading /> : <FormRow.Arrow />}
|
||||||
|
onPress={testConnection}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
export default React.memo(function Settings() {
|
<FormSection title="Display Settings">
|
||||||
const settings = useProxy(storage) as LFMSettings;
|
<FormInput
|
||||||
const navigation = NavigationNative.useNavigation();
|
title="App Name"
|
||||||
|
placeholder={Constants.DEFAULT_SETTINGS.appName}
|
||||||
|
value={currentSettings.appName}
|
||||||
|
onChange={(value: string) => saveSettings("appName", value)}
|
||||||
|
/>
|
||||||
|
<FormDivider />
|
||||||
|
<FormInput
|
||||||
|
title="Update Interval (seconds)"
|
||||||
|
placeholder={String(Constants.DEFAULT_SETTINGS.timeInterval)}
|
||||||
|
value={String(currentSettings.timeInterval)}
|
||||||
|
onChange={(value: string) => {
|
||||||
|
const interval = Number(value);
|
||||||
|
if (interval >= Constants.MIN_UPDATE_INTERVAL) {
|
||||||
|
saveSettings("timeInterval", interval);
|
||||||
|
} else {
|
||||||
|
showToast(
|
||||||
|
`Minimum interval is ${Constants.MIN_UPDATE_INTERVAL} seconds`,
|
||||||
|
getAssetIDByName("Small"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
keyboardType="numeric"
|
||||||
|
/>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
// Log component mount
|
<FormSection title="Options">
|
||||||
useEffect(() => {
|
<FormRow
|
||||||
logComponentMount("Settings");
|
label="Show 'Listening to' instead of 'Playing'"
|
||||||
}, []);
|
leading={<FormRow.Icon source={getAssetIDByName("ic_music")} />}
|
||||||
|
trailing={
|
||||||
// Safety check to ensure settings is properly initialized
|
<FormSwitch
|
||||||
if (!settings) {
|
value={
|
||||||
logComponentError("Settings", "Settings proxy is null or undefined");
|
currentSettings.listeningTo ??
|
||||||
return (
|
Constants.DEFAULT_SETTINGS.listeningTo
|
||||||
<ScrollView>
|
}
|
||||||
<FormText>Loading...</FormText>
|
onValueChange={(value: boolean) =>
|
||||||
</ScrollView>
|
saveSettings("listeningTo", value)
|
||||||
);
|
}
|
||||||
}
|
/>
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
try {
|
|
||||||
navigation.setOptions({
|
|
||||||
title: "Last.fm Configuration",
|
|
||||||
headerRight: () => <UpdateButton />,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logComponentError("Settings-Navigation", error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollView>
|
|
||||||
<FormInput
|
|
||||||
value={settings.appName || undefined}
|
|
||||||
onChangeText={(value: string) => (settings.appName = value.trim())}
|
|
||||||
title="Discord Application Name"
|
|
||||||
placeholder={Constants.DEFAULT_APP_NAME}
|
|
||||||
returnKeyType="done"
|
|
||||||
/>
|
|
||||||
<FormDivider />
|
|
||||||
<FormInput
|
|
||||||
required
|
|
||||||
value={settings.username || undefined}
|
|
||||||
onChangeText={(value: string) => (settings.username = value.trim())}
|
|
||||||
title="Last.fm username"
|
|
||||||
helpText={
|
|
||||||
!settings.username && (
|
|
||||||
<Text style={{ color: "#FF0000" }}>
|
|
||||||
{"This field is required!"}
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
placeholder="wumpus123"
|
|
||||||
returnKeyType="done"
|
|
||||||
/>
|
|
||||||
<FormDivider />
|
|
||||||
<FormInput
|
|
||||||
value={settings.timeInterval}
|
|
||||||
onChangeText={(value: string) => (settings.timeInterval = value)}
|
|
||||||
title="Update interval (in seconds)"
|
|
||||||
placeholder={Constants.DEFAULT_TIME_INTERVAL.toString()}
|
|
||||||
keyboardType="numeric"
|
|
||||||
returnKeyType="done"
|
|
||||||
/>
|
|
||||||
<FormDivider />
|
|
||||||
<FormSwitchRow
|
|
||||||
label="Show time elapsed"
|
|
||||||
subLabel="Show the time elapsed since the song started playing"
|
|
||||||
leading={<FormIcon source={getAssetIDByName("clock")} />}
|
|
||||||
value={settings.showTimestamp}
|
|
||||||
onValueChange={(value: boolean) => (settings.showTimestamp = value)}
|
|
||||||
/>
|
|
||||||
<FormDivider />
|
|
||||||
<FormSwitchRow
|
|
||||||
label="Set status as listening"
|
|
||||||
subLabel='Set your status as "Listening to" instead of "Playing"'
|
|
||||||
leading={<FormIcon source={getAssetIDByName("ic_headset_neutral")} />}
|
|
||||||
value={settings.listeningTo}
|
|
||||||
onValueChange={(value: boolean) => (settings.listeningTo = value)}
|
|
||||||
/>
|
|
||||||
<FormDivider />
|
|
||||||
<FormSwitchRow
|
|
||||||
label="Hide when Spotify is running"
|
|
||||||
subLabel="Hide the status when a Spotify activity is detected"
|
|
||||||
leading={
|
|
||||||
<FormIcon
|
|
||||||
source={getAssetIDByName("img_account_sync_spotify_light_and_dark")}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value={settings.ignoreSpotify}
|
|
||||||
onValueChange={(value: boolean) => (settings.ignoreSpotify = value)}
|
|
||||||
/>
|
|
||||||
<FormDivider />
|
|
||||||
<FormRow
|
|
||||||
label="Debug"
|
|
||||||
subLabel="View debug information"
|
|
||||||
leading={<FormIcon source={getAssetIDByName("debug")} />}
|
|
||||||
trailing={FormRow.Arrow}
|
|
||||||
onPress={() => {
|
|
||||||
try {
|
|
||||||
navigation.push("VendettaCustomPage", {
|
|
||||||
title: "Debug",
|
|
||||||
render: lazy(() => import("./Debug")),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logNavigationError(error);
|
|
||||||
console.error("Failed to navigate to Debug page:", error);
|
|
||||||
showToast("Failed to open debug page", getAssetIDByName("Small"));
|
|
||||||
}
|
}
|
||||||
}}
|
/>
|
||||||
/>
|
<FormDivider />
|
||||||
|
<FormRow
|
||||||
|
label="Show Timestamp"
|
||||||
|
leading={<FormRow.Icon source={getAssetIDByName("ic_timeline")} />}
|
||||||
|
trailing={
|
||||||
|
<FormSwitch
|
||||||
|
value={
|
||||||
|
currentSettings.showTimestamp ??
|
||||||
|
Constants.DEFAULT_SETTINGS.showTimestamp
|
||||||
|
}
|
||||||
|
onValueChange={(value: boolean) =>
|
||||||
|
saveSettings("showTimestamp", value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FormDivider />
|
||||||
|
<FormRow
|
||||||
|
label="Ignore when Spotify is playing"
|
||||||
|
leading={
|
||||||
|
<FormRow.Icon source={getAssetIDByName("ic_spotify_white_24px")} />
|
||||||
|
}
|
||||||
|
trailing={
|
||||||
|
<FormSwitch
|
||||||
|
value={
|
||||||
|
currentSettings.ignoreSpotify ??
|
||||||
|
Constants.DEFAULT_SETTINGS.ignoreSpotify
|
||||||
|
}
|
||||||
|
onValueChange={(value: boolean) =>
|
||||||
|
saveSettings("ignoreSpotify", value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FormDivider />
|
||||||
|
<FormRow
|
||||||
|
label="Verbose Logging"
|
||||||
|
leading={
|
||||||
|
<FormRow.Icon source={getAssetIDByName("ic_message_copy")} />
|
||||||
|
}
|
||||||
|
trailing={
|
||||||
|
<FormSwitch
|
||||||
|
value={
|
||||||
|
currentSettings.verboseLogging ??
|
||||||
|
Constants.DEFAULT_SETTINGS.verboseLogging
|
||||||
|
}
|
||||||
|
onValueChange={(value: boolean) =>
|
||||||
|
saveSettings("verboseLogging", value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
<FormSection title="Plugin Status">
|
||||||
|
<FormText style={{ padding: 8 }}>
|
||||||
|
Status: {pluginState.pluginStopped ? "Stopped" : "Running"}
|
||||||
|
{"\n"}
|
||||||
|
Last Update: {pluginState.lastTrackUrl ? "Success" : "No track data"}
|
||||||
|
</FormText>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
<FormText style={{ padding: 16, opacity: 0.5, textAlign: "center" }}>
|
||||||
|
Version 2.0.0
|
||||||
|
</FormText>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,34 +3,42 @@ import { FluxDispatcher } from "@vendetta/metro/common";
|
||||||
import { pluginState } from "..";
|
import { pluginState } from "..";
|
||||||
import { Activity } from "../../../defs";
|
import { Activity } from "../../../defs";
|
||||||
import Constants from "../constants";
|
import Constants from "../constants";
|
||||||
import { flush } from "../manager";
|
import { stop } from "../manager";
|
||||||
import { AssetManager } from "../modules";
|
import { AssetManager } from "../modules";
|
||||||
|
|
||||||
/** Clears the user's activity */
|
/** Clears the user's activity */
|
||||||
export function clearActivity() {
|
export function clearActivity() {
|
||||||
return sendRequest(null);
|
return sendRequest(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sends the activity details to Discord */
|
/** Sends the activity details to Discord */
|
||||||
export function sendRequest(activity: Activity) {
|
export function sendRequest(activity: Activity | null) {
|
||||||
if (pluginState.pluginStopped) {
|
if (pluginState.pluginStopped) {
|
||||||
flush(true);
|
stop();
|
||||||
activity = null;
|
activity = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginState.lastActivity = activity;
|
pluginState.lastActivity = activity;
|
||||||
|
|
||||||
FluxDispatcher.dispatch({
|
FluxDispatcher.dispatch({
|
||||||
type: "LOCAL_ACTIVITY_UPDATE",
|
type: "LOCAL_ACTIVITY_UPDATE",
|
||||||
activity: activity,
|
activity: activity,
|
||||||
pid: 2312,
|
pid: 2312,
|
||||||
socketId: "Last.fm@Vendetta"
|
socketId: "Last.fm@Vendetta",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetches a Discord application's asset */
|
/** Fetches a Discord application's asset */
|
||||||
export async function fetchAsset(asset: string[], appId: string = Constants.APPLICATION_ID): Promise<string[]> {
|
export async function fetchAsset(
|
||||||
if (!asset) return [];
|
asset: string[],
|
||||||
|
appId: string = Constants.APPLICATION_ID,
|
||||||
|
): Promise<string[]> {
|
||||||
|
if (!asset?.length) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
return await AssetManager.fetchAssetIds(appId, asset);
|
return await AssetManager.fetchAssetIds(appId, asset);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Last.fm] Failed to fetch assets:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,48 +2,190 @@ 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";
|
||||||
|
|
||||||
/** Fetches the latest user's scrobble */
|
interface LastFMError {
|
||||||
export async function fetchLatestScrobble(): Promise<Track> {
|
error: number;
|
||||||
const params = new URLSearchParams({
|
message: string;
|
||||||
"method": "user.getrecenttracks",
|
}
|
||||||
"user": currentSettings.username,
|
|
||||||
"api_key": Constants.LFM_API_KEY,
|
interface LastFMResponse {
|
||||||
"format": "json",
|
recenttracks?: {
|
||||||
"limit": "1",
|
track: LastFMTrack[];
|
||||||
"extended": "1"
|
};
|
||||||
|
error?: number;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LastFMTrack {
|
||||||
|
name: string;
|
||||||
|
artist: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
album: {
|
||||||
|
"#text": string;
|
||||||
|
};
|
||||||
|
image?: {
|
||||||
|
size: string;
|
||||||
|
"#text": string;
|
||||||
|
}[];
|
||||||
|
url: string;
|
||||||
|
date?: {
|
||||||
|
"#text": string;
|
||||||
|
};
|
||||||
|
"@attr"?: {
|
||||||
|
nowplaying: boolean;
|
||||||
|
};
|
||||||
|
loved: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class LastFMClient {
|
||||||
|
private static instance: LastFMClient;
|
||||||
|
private retryCount: number = 0;
|
||||||
|
private lastError: number = 0;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
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 async handleError(error: LastFMError): Promise<never> {
|
||||||
|
this.lastError = error.error;
|
||||||
|
|
||||||
|
const errorMessages: { [key: number]: string } = {
|
||||||
|
2: "Invalid API key",
|
||||||
|
6: "User not found",
|
||||||
|
8: "Operation failed - Backend error",
|
||||||
|
11: "Service temporarily unavailable",
|
||||||
|
16: "Service temporarily unavailable",
|
||||||
|
29: "Rate limit exceeded",
|
||||||
|
};
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async makeRequest(
|
||||||
|
params: Record<string, string>,
|
||||||
|
): Promise<LastFMResponse> {
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
...params,
|
||||||
|
api_key: currentSettings.apiKey,
|
||||||
|
format: "json",
|
||||||
}).toString();
|
}).toString();
|
||||||
|
|
||||||
const result = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`);
|
const response = await fetch(
|
||||||
if (!result.ok) throw new Error(`Failed to fetch the latest scrobble: ${result.statusText}`);
|
`${Constants.LFM_API_BASE_URL}?${queryParams}`,
|
||||||
|
);
|
||||||
|
const data: LastFMResponse = await response.json();
|
||||||
|
|
||||||
const info = await result.json();
|
if (!response.ok || data.error) {
|
||||||
|
await this.handleError(data as LastFMError);
|
||||||
|
}
|
||||||
|
|
||||||
const lastTrack = info?.recenttracks?.track?.[0];
|
return data;
|
||||||
setDebugInfo("lastAPIResponse", lastTrack);
|
}
|
||||||
|
|
||||||
if (!lastTrack) throw info;
|
public async fetchLatestScrobble(): Promise<Track> {
|
||||||
|
try {
|
||||||
|
this.validateSettings();
|
||||||
|
|
||||||
return {
|
const data = await this.makeRequest({
|
||||||
|
method: "user.getrecenttracks",
|
||||||
|
user: currentSettings.username,
|
||||||
|
limit: "1",
|
||||||
|
extended: "1",
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastTrack = data?.recenttracks?.track?.[0];
|
||||||
|
setDebugInfo("lastAPIResponse", lastTrack);
|
||||||
|
|
||||||
|
if (!lastTrack) {
|
||||||
|
throw new Error("No tracks found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset retry count on successful request
|
||||||
|
this.retryCount = 0;
|
||||||
|
|
||||||
|
return {
|
||||||
name: lastTrack.name,
|
name: lastTrack.name,
|
||||||
artist: lastTrack.artist.name,
|
artist: lastTrack.artist.name,
|
||||||
album: lastTrack.album["#text"],
|
album: lastTrack.album["#text"],
|
||||||
albumArt: await handleAlbumCover(lastTrack.image?.find((x: any) => x.size === "large")?.["#text"]),
|
albumArt: await this.handleAlbumCover(
|
||||||
|
lastTrack.image?.find((x) => x.size === "large")?.["#text"],
|
||||||
|
),
|
||||||
url: lastTrack.url,
|
url: lastTrack.url,
|
||||||
date: lastTrack.date?.["#text"] ?? "now",
|
date: lastTrack.date?.["#text"] ?? "now",
|
||||||
nowPlaying: Boolean(lastTrack["@attr"]?.nowplaying),
|
nowPlaying: Boolean(lastTrack["@attr"]?.nowplaying),
|
||||||
loved: lastTrack.loved === "1",
|
loved: lastTrack.loved === "1",
|
||||||
} as Track;
|
} as Track;
|
||||||
|
} 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
* Currently ditches the default album covers
|
export const lastfmClient = LastFMClient.getInstance();
|
||||||
* @param cover The album cover given by Last.fm
|
|
||||||
*/
|
/** Fetches the latest user's scrobble */
|
||||||
export async function handleAlbumCover(cover: string): Promise<string> {
|
export async function fetchLatestScrobble(): Promise<Track> {
|
||||||
// If the cover is a default one, return null (remove the cover)
|
return lastfmClient.fetchLatestScrobble();
|
||||||
if (Constants.LFM_DEFAULT_COVER_HASHES.some(x => cover.includes(x))) {
|
}
|
||||||
return null;
|
|
||||||
}
|
/**
|
||||||
return cover;
|
* Handles album cover processing
|
||||||
|
* @param cover The album cover given by Last.fm
|
||||||
|
*/
|
||||||
|
export async function handleAlbumCover(cover: string): Promise<string> {
|
||||||
|
return lastfmClient.handleAlbumCover(cover);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue