vd-plugins/plugins/Last.fm/src/utils.ts

220 lines
6.4 KiB
TypeScript
Raw Normal View History

import { findByProps, findByStoreName } from "@vendetta/metro";
2023-02-12 20:31:51 +08:00
import { FluxDispatcher } from "@vendetta/metro/common";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { showToast } from "@vendetta/ui/toasts";
2023-04-05 16:47:26 +08:00
import { UserStore, currentSettings, global, verboseLog } from ".";
2023-02-12 20:31:51 +08:00
import Constants from "./constants";
2023-02-15 18:15:10 +08:00
const AssetManager = findByProps("getAssetIds");
const PresenceStore = findByStoreName("PresenceStore");
2023-02-15 18:15:10 +08:00
2023-02-12 20:31:51 +08:00
/** Fetches the latest user's scrobble */
async function fetchLatestScrobble(): Promise<Track> {
const params = new URLSearchParams({
2023-02-12 22:48:12 +08:00
"method": "user.getrecenttracks",
"user": currentSettings.username,
"api_key": Constants.LFM_API_KEY,
"format": "json",
"limit": "1",
"extended": "1"
2023-02-12 20:31:51 +08:00
}).toString();
2023-02-15 15:04:37 +08:00
const result = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`);
if (!result.ok) throw new Error(`Failed to fetch the latest scrobble: ${result.statusText}`);
const info = await result.json();
const lastTrack = info?.recenttracks?.track?.[0];
if (!lastTrack) throw info;
2023-02-12 20:31:51 +08:00
return {
name: lastTrack.name,
artist: lastTrack.artist.name,
2023-02-12 22:48:12 +08:00
album: lastTrack.album["#text"],
2023-02-13 21:48:12 +08:00
albumArt: await handleAlbumCover(lastTrack.image[3]["#text"]),
2023-02-12 20:31:51 +08:00
url: lastTrack.url,
2023-02-12 22:48:12 +08:00
date: lastTrack.date?.["#text"] ?? "now",
nowPlaying: Boolean(lastTrack["@attr"]?.nowplaying),
loved: lastTrack.loved === "1",
2023-02-12 20:31:51 +08:00
} as Track;
}
2023-02-12 21:47:31 +08:00
/**
* Currently ditches the default album covers
2023-02-12 20:31:51 +08:00
* @param cover The album cover given by Last.fm
*/
2023-02-13 21:48:12 +08:00
async function handleAlbumCover(cover: string): Promise<string> {
2023-02-12 21:47:31 +08:00
// If the cover is a default one, return null (remove the cover)
if (Constants.LFM_DEFAULT_COVER_HASHES.some(x => cover.includes(x))) {
2023-02-12 20:31:51 +08:00
return null;
}
return cover;
}
2023-02-15 18:15:10 +08:00
/** Clears the user's activity */
function clearActivity() {
global.lastActivity && verboseLog("--> Clearing activity...");
return sendRequest(null);
}
2023-02-12 20:31:51 +08:00
/** Sends the activity details to Discord */
2023-02-15 18:15:10 +08:00
function sendRequest(activity: Activity) {
2023-02-15 15:04:37 +08:00
if (global.pluginStopped) {
2023-02-12 20:31:51 +08:00
console.log("--> Plugin is unloaded, aborting...");
global.updateInterval && clearInterval(global.updateInterval);
activity = null;
}
global.lastActivity = activity;
2023-02-15 18:15:10 +08:00
FluxDispatcher.dispatch({
type: "LOCAL_ACTIVITY_UPDATE",
activity: activity
2023-02-12 20:31:51 +08:00
});
}
2023-02-15 18:15:10 +08:00
/** Fetches a Discord application's asset */
async function fetchAsset<T extends string | string[]>(asset: T, appId: string = Constants.APPLICATION_ID): Promise<T> {
2023-02-16 16:11:09 +08:00
if (!asset) return null;
2023-02-15 18:15:10 +08:00
if (typeof asset === "string") {
const assetId = await AssetManager.getAssetIds(appId, [asset]);
return assetId[0] as T;
}
return await AssetManager.getAssetIds(appId, asset);
2023-02-12 20:31:51 +08:00
}
/**
* Fetches the current scrobble and sets the activity.
* This function is actively called by the updateInterval.
*/
2023-02-15 18:15:10 +08:00
async function update() {
2023-02-12 20:31:51 +08:00
verboseLog("--> Fetching last track...");
if (!currentSettings.username) {
showToast("Last.fm username is not set!", getAssetIDByName("Small"));
2023-02-15 18:15:10 +08:00
flush(); // Flush as we always need reinitialization
2023-02-12 20:31:51 +08:00
throw new Error("Username is not set");
}
if (currentSettings.ignoreSpotify) {
2023-04-05 16:47:26 +08:00
for (const activity of PresenceStore.getActivities(UserStore.getCurrentUser().id)) {
if (activity?.type === ActivityType.LISTENING && activity.application_id !== Constants.APPLICATION_ID) {
verboseLog("--> Spotify is currently playing, aborting...");
clearActivity();
return;
}
}
}
2023-02-15 15:04:37 +08:00
const lastTrack = await fetchLatestScrobble().catch(async (err) => {
2023-02-12 20:31:51 +08:00
verboseLog("--> An error occurred while fetching the last track, aborting...");
2023-02-15 18:15:10 +08:00
clearActivity();
2023-02-12 20:31:51 +08:00
throw err;
});
if (!lastTrack.nowPlaying) {
verboseLog("--> Last track is not currently playing, aborting...");
2023-02-15 18:15:10 +08:00
clearActivity();
return;
2023-02-12 20:31:51 +08:00
}
2023-02-15 18:15:10 +08:00
verboseLog("--> Track fetched!");
2023-02-12 20:31:51 +08:00
if (global.lastTrackUrl === lastTrack.url) {
verboseLog("--> Last track is the same as the previous one, aborting...");
return;
}
const activity = {
2023-02-12 21:32:36 +08:00
name: currentSettings.appName || Constants.DEFAULT_APP_NAME,
2023-02-15 18:15:10 +08:00
flags: 0,
2023-02-12 20:31:51 +08:00
type: currentSettings.listeningTo ? 2 : 0,
details: lastTrack.name,
state: `by ${lastTrack.artist}`,
2023-02-15 18:15:10 +08:00
application_id: Constants.APPLICATION_ID,
2023-02-12 20:31:51 +08:00
} as Activity;
global.lastTrackUrl = lastTrack.url;
2023-02-16 21:58:36 +08:00
if (activity.name.includes("{{")) {
for (const key in lastTrack) {
activity.name = activity.name.replace(`{{${key}}}`, lastTrack[key]);
}
}
2023-02-12 20:31:51 +08:00
// Set timestamps
if (currentSettings.showTimestamp) {
activity.timestamps = {
start: Date.now() / 1000 | 0
};
}
2023-02-12 21:47:31 +08:00
if (lastTrack.album) {
2023-02-12 20:31:51 +08:00
activity.assets = {
2023-02-15 18:15:10 +08:00
large_image: await fetchAsset(lastTrack.albumArt),
2023-02-12 21:47:31 +08:00
large_text: `on ${lastTrack.album}`
2023-02-12 22:48:12 +08:00
};
2023-02-12 20:31:51 +08:00
}
2023-02-15 18:15:10 +08:00
verboseLog("--> Setting activity...");
verboseLog(activity);
try {
sendRequest(activity);
} catch (err) {
2023-02-12 20:31:51 +08:00
verboseLog("--> An error occurred while setting the activity");
2023-02-15 18:15:10 +08:00
clearActivity();
2023-02-12 20:31:51 +08:00
throw err;
2023-02-15 18:15:10 +08:00
}
2023-02-12 20:31:51 +08:00
verboseLog("--> Successfully set activity!");
}
2023-02-15 18:15:10 +08:00
/** Stops and reset everything, can be started again with `initialize()` */
export function flush() {
2023-02-12 20:31:51 +08:00
console.log("--> Flushing...");
global.lastActivity = null;
2023-02-15 15:04:37 +08:00
global.lastTrackUrl = null;
2023-02-12 20:31:51 +08:00
global.updateInterval && clearInterval(global.updateInterval);
2023-02-15 18:15:10 +08:00
clearActivity();
2023-02-12 20:31:51 +08:00
}
/** Initializes the plugin */
export async function initialize() {
console.log("--> Initializing...");
2023-02-15 15:04:37 +08:00
if (global.pluginStopped) {
2023-02-12 20:31:51 +08:00
throw new Error("Plugin is already stopped!");
}
2023-02-15 18:15:10 +08:00
flush();
2023-02-12 20:31:51 +08:00
let tries = 0;
2023-02-15 18:15:10 +08:00
await update().catch((err) => {
2023-02-12 20:31:51 +08:00
console.error(err);
tries++;
});
// Periodically fetches the current scrobble and sets the activity
global.updateInterval = setInterval(
() => update()
2023-02-15 18:15:10 +08:00
.then(() => {
tries = 0;
})
2023-02-12 20:31:51 +08:00
.catch(err => {
console.error(err);
if (++tries > 3) {
console.error("Failed to fetch/set activity 3 times, aborting...");
2023-02-15 18:15:10 +08:00
flush();
2023-02-12 20:31:51 +08:00
}
}),
2023-02-13 09:05:36 +08:00
(currentSettings.timeInterval || Constants.DEFAULT_TIME_INTERVAL) * 1000
2023-02-12 20:31:51 +08:00
);
}