Equicord/src/equicordplugins/absRPC/index.tsx
prism 050dd1cdcc
feat(eslint): migrate (#596)
* eslint: migrate to defineConfig and add no-multiple-empty-lines rule

* chore: fix multiple empty lines
2025-12-25 13:27:36 -05:00

251 lines
7.6 KiB
TypeScript

/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
// alot of the code is from JellyfinRPC
import { definePluginSettings } from "@api/Settings";
import { HeadingSecondary } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import { EquicordDevs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
import { ApplicationAssetUtils, FluxDispatcher, showToast } from "@webpack/common";
interface ActivityAssets {
large_image?: string;
large_text?: string;
small_image?: string;
small_text?: string;
}
interface Activity {
state: string;
details?: string;
timestamps?: {
start?: number;
};
assets?: ActivityAssets;
name: string;
application_id: string;
metadata?: {
button_urls?: Array<string>;
};
type: number;
flags: number;
}
interface MediaData {
name: string;
type: string;
author?: string;
series?: string;
duration?: number;
currentTime?: number;
progress?: number;
url?: string;
imageUrl?: string;
isFinished?: boolean;
}
const settings = definePluginSettings({
serverUrl: {
description: "AudioBookShelf server URL (e.g., https://abs.example.com)",
type: OptionType.STRING,
},
username: {
description: "AudioBookShelf username",
type: OptionType.STRING,
},
password: {
description: "AudioBookShelf password",
type: OptionType.STRING,
},
});
const applicationId = "1381423044907503636";
const logger = new Logger("AudioBookShelfRichPresence");
let authToken: string | null = null;
let updateInterval: NodeJS.Timeout | undefined;
async function getApplicationAsset(key: string): Promise<string> {
return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];
}
function setActivity(activity: Activity | null) {
FluxDispatcher.dispatch({
type: "LOCAL_ACTIVITY_UPDATE",
activity,
socketId: "ABSRPC",
});
}
export default definePlugin({
name: "AudioBookShelfRichPresence",
description: "Rich presence for AudioBookShelf media server",
authors: [EquicordDevs.vmohammad],
settingsAboutComponent: () => (
<>
<HeadingSecondary>How to connect to AudioBookShelf</HeadingSecondary>
<Paragraph>
Enter your AudioBookShelf server URL, username, and password to display your currently playing audiobooks as Discord Rich Presence.
<br /><br />
The plugin will automatically authenticate and fetch your listening progress.
</Paragraph>
</>
),
settings,
start() {
this.updatePresence();
updateInterval = setInterval(() => { this.updatePresence(); }, 10000);
},
stop() {
clearInterval(updateInterval);
updateInterval = undefined;
},
async authenticate(): Promise<boolean> {
if (!settings.store.serverUrl || !settings.store.username || !settings.store.password) {
logger.warn("AudioBookShelf server URL, username, or password is not set in settings.");
showToast("AudioBookShelf RPC is not configured.", "failure", {
duration: 15000,
});
return false;
}
try {
const baseUrl = settings.store.serverUrl.replace(/\/$/, "");
const url = `${baseUrl}/login`;
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: settings.store.username,
password: settings.store.password,
}),
});
if (!res.ok) throw `${res.status} ${res.statusText}`;
const data = await res.json();
authToken = data.user?.token;
return !!authToken;
} catch (e) {
logger.error("Failed to authenticate with AudioBookShelf", e);
authToken = null;
return false;
}
},
async fetchMediaData(): Promise<MediaData | null> {
if (!authToken && !(await this.authenticate())) {
return null;
}
const isPlayingNow = session => {
const now = Date.now();
const lastUpdate = session.updatedAt;
const diffSeconds = (now - lastUpdate) / 1000;
return diffSeconds <= 30;
};
try {
const baseUrl = settings.store.serverUrl!.replace(/\/$/, "");
const url = `${baseUrl}/api/me/listening-sessions`;
const res = await fetch(url, {
headers: {
"Authorization": `Bearer ${authToken}`,
},
});
if (!res.ok) {
if (res.status === 401) {
authToken = null;
if (await this.authenticate()) {
return this.fetchMediaData();
}
}
throw `${res.status} ${res.statusText}`;
}
const { sessions } = await res.json();
const activeSession = sessions.find((session: any) =>
session.updatedAt && !session.isFinished
);
if (!activeSession || !isPlayingNow(activeSession)) return null;
const { mediaMetadata: media, mediaType, duration, currentTime, libraryItemId } = activeSession;
if (!media) return null;
console.log(media);
return {
name: media.title || "Unknown",
type: mediaType || "book",
author: media.author || media.publisher,
series: media.series[0]?.name,
duration,
currentTime,
imageUrl: libraryItemId ? `${baseUrl}/api/items/${libraryItemId}/cover` : undefined,
isFinished: activeSession.isFinished || false,
};
} catch (e) {
logger.error("Failed to query AudioBookShelf API", e);
return null;
}
},
async updatePresence() {
setActivity(await this.getActivity());
},
async getActivity(): Promise<Activity | null> {
const mediaData = await this.fetchMediaData();
if (!mediaData || mediaData.isFinished) return null;
const largeImage = mediaData.imageUrl;
console.log("Large Image URL:", largeImage);
const assets: ActivityAssets = {
large_image: largeImage ? await getApplicationAsset(largeImage) : await getApplicationAsset("audiobookshelf"),
large_text: mediaData.series || mediaData.author || undefined,
};
const getDetails = () => {
return mediaData.name;
};
const getState = () => {
if (mediaData.series && mediaData.author) {
return `${mediaData.series} • ${mediaData.author}`;
}
return mediaData.author || "AudioBook";
};
const timestamps = mediaData.currentTime && mediaData.duration ? {
start: Date.now() - (mediaData.currentTime * 1000),
end: Date.now() + ((mediaData.duration - mediaData.currentTime) * 1000)
} : undefined;
return {
application_id: applicationId,
name: "AudioBookShelf",
details: getDetails(),
state: getState(),
assets,
timestamps,
type: 2,
flags: 1,
};
}
});