update showing errors as toast notifications

ofc zed thought why not use formatting again <3
This commit is contained in:
kmmiio99o 2026-01-02 19:26:46 +01:00
parent a5686e815c
commit dbcc2a4338
No known key found for this signature in database
GPG key ID: 163FE39F7772D2EC
3 changed files with 382 additions and 382 deletions

View file

@ -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,
}; };

View file

@ -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

View file

@ -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);
} }