2023-02-12 20:31:51 +08:00
|
|
|
import { patcher } from "@vendetta";
|
|
|
|
|
import { FluxDispatcher } from "@vendetta/metro/common";
|
|
|
|
|
import { getAssetIDByName } from "@vendetta/ui/assets";
|
|
|
|
|
import { showToast } from "@vendetta/ui/toasts";
|
|
|
|
|
import { currentSettings, global, SET_ACTIVITY, verboseLog } from ".";
|
|
|
|
|
import Constants from "./constants";
|
|
|
|
|
|
|
|
|
|
/** 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Sends the activity details to Discord */
|
|
|
|
|
async function sendRequest(activity: Activity): Promise<ResultActivity> {
|
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;
|
|
|
|
|
|
|
|
|
|
return await SET_ACTIVITY.handler({
|
|
|
|
|
isSocketConnected: () => true,
|
|
|
|
|
socket: {
|
|
|
|
|
id: 120,
|
|
|
|
|
application: {
|
|
|
|
|
id: Constants.APPLICATION_ID,
|
|
|
|
|
name: activity?.name || "RichPresence"
|
|
|
|
|
},
|
|
|
|
|
transport: "ipc"
|
|
|
|
|
},
|
|
|
|
|
args: {
|
|
|
|
|
pid: 120,
|
|
|
|
|
activity: activity && { ...activity } || null
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Clears the user's activity */
|
|
|
|
|
function clearActivity(): Promise<ResultActivity> {
|
|
|
|
|
global.lastActivity && verboseLog("--> Clearing activity...");
|
|
|
|
|
return sendRequest(null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fetches the current scrobble and sets the activity.
|
|
|
|
|
* This function is actively called by the updateInterval.
|
|
|
|
|
*/
|
|
|
|
|
export async function update() {
|
|
|
|
|
verboseLog("--> Fetching last track...");
|
|
|
|
|
|
|
|
|
|
if (!currentSettings.username) {
|
|
|
|
|
showToast("Last.fm username is not set!", getAssetIDByName("Small"));
|
2023-02-12 21:32:36 +08:00
|
|
|
await flush(); // Flush as we always need reinitialization
|
2023-02-12 20:31:51 +08:00
|
|
|
throw new Error("Username is not set");
|
|
|
|
|
}
|
|
|
|
|
|
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...");
|
|
|
|
|
await clearActivity().catch();
|
|
|
|
|
throw err;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!lastTrack.nowPlaying) {
|
|
|
|
|
verboseLog("--> Last track is not currently playing, aborting...");
|
|
|
|
|
return await clearActivity();
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-12 21:47:31 +08:00
|
|
|
verboseLog("--> Track fetched! Setting activity...");
|
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-12 20:31:51 +08:00
|
|
|
type: currentSettings.listeningTo ? 2 : 0,
|
|
|
|
|
details: lastTrack.name,
|
|
|
|
|
state: `by ${lastTrack.artist}`,
|
|
|
|
|
} as Activity;
|
|
|
|
|
|
|
|
|
|
global.lastTrackUrl = lastTrack.url;
|
|
|
|
|
|
|
|
|
|
// 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 = {
|
|
|
|
|
large_image: 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 15:04:37 +08:00
|
|
|
const response = await sendRequest(activity).catch(async (err) => {
|
2023-02-12 20:31:51 +08:00
|
|
|
verboseLog("--> An error occurred while setting the activity");
|
|
|
|
|
await clearActivity().catch();
|
|
|
|
|
throw err;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
verboseLog("--> Successfully set activity!");
|
|
|
|
|
verboseLog(response);
|
|
|
|
|
return response;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Stops everything */
|
|
|
|
|
export function flush(): Promise<ResultActivity> {
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
return clearActivity();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 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 15:04:37 +08:00
|
|
|
await flush();
|
2023-02-12 20:31:51 +08:00
|
|
|
|
|
|
|
|
let tries = 0;
|
|
|
|
|
|
|
|
|
|
update().catch((err) => {
|
|
|
|
|
console.error(err);
|
|
|
|
|
tries++;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Periodically fetches the current scrobble and sets the activity
|
|
|
|
|
global.updateInterval = setInterval(
|
|
|
|
|
() => update()
|
|
|
|
|
.then(() => tries = 0)
|
|
|
|
|
.catch(err => {
|
|
|
|
|
console.error(err);
|
|
|
|
|
|
|
|
|
|
if (++tries > 3) {
|
|
|
|
|
console.error("Failed to fetch/set activity 3 times, aborting...");
|
|
|
|
|
clearInterval(global.updateInterval);
|
|
|
|
|
}
|
|
|
|
|
}),
|
2023-02-13 09:05:36 +08:00
|
|
|
(currentSettings.timeInterval || Constants.DEFAULT_TIME_INTERVAL) * 1000
|
2023-02-12 20:31:51 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Discord doesn't allow us to set the activity type to "Listening to" so we have to patch it
|
|
|
|
|
export function patchActivityType(): () => boolean {
|
|
|
|
|
if (global.activityTypeUnpatch) {
|
|
|
|
|
console.warn("This is weird... activity type override is already defined?");
|
|
|
|
|
global.activityTypeUnpatch();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log("Patching activity type...");
|
|
|
|
|
|
|
|
|
|
const nodes = Object.values(FluxDispatcher._actionHandlers._dependencyGraph.nodes);
|
|
|
|
|
const { actionHandler } = nodes.find((x: any) => x.name === "LocalActivityStore") as any;
|
|
|
|
|
|
|
|
|
|
return patcher.before("LOCAL_ACTIVITY_UPDATE", actionHandler, ([{ activity }]) => {
|
|
|
|
|
if (activity && activity.name === global.lastActivity.name) {
|
|
|
|
|
activity.type = global.lastActivity.type;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|