- Spotify: drop album line, normalize ";" separators to ",", add open-in-Spotify link and live progress bar - Games: resolve missing artwork via public Discord RPC app icons with placeholder fallback; add in-site game profile modal (IGDB + Steam + ITAD: artwork, details, reviews, prices) - Proxy game data through same-origin /api/game and /api/discord-app routes, CORS-locked to https://fristy.net - Lanyard: SSR REST first paint, then live WebSocket with heartbeat, reconnect backoff and polling fallback - Localize presence UI (tr/en/de): elapsed times, dates, numbers, currency, review labels, game descriptions - Sort projects by last update, descending - Slim dark scrollbars, compact Spotify progress bar
199 lines
5.5 KiB
TypeScript
199 lines
5.5 KiB
TypeScript
export const DISCORD_ID = "1014549805608992901";
|
|
|
|
const LANYARD_API = `https://api.lanyard.rest/v1/users/${DISCORD_ID}`;
|
|
|
|
export const LANYARD_WS = "wss://api.lanyard.rest/socket";
|
|
|
|
export type DiscordStatus = "online" | "idle" | "dnd" | "offline";
|
|
|
|
export interface LanyardEmoji {
|
|
name: string;
|
|
id?: string;
|
|
animated?: boolean;
|
|
}
|
|
|
|
export interface LanyardActivityAssets {
|
|
large_image?: string;
|
|
large_text?: string;
|
|
small_image?: string;
|
|
small_text?: string;
|
|
}
|
|
|
|
export interface LanyardActivity {
|
|
id: string;
|
|
name: string;
|
|
type: number;
|
|
state?: string | null;
|
|
details?: string | null;
|
|
emoji?: LanyardEmoji | null;
|
|
timestamps?: { start?: number; end?: number } | null;
|
|
application_id?: string | null;
|
|
assets?: LanyardActivityAssets | null;
|
|
}
|
|
|
|
export function activityImageUrl(activity: LanyardActivity, small: boolean): string | null {
|
|
const key = small ? activity.assets?.small_image : activity.assets?.large_image;
|
|
if (!key) {
|
|
return null;
|
|
}
|
|
if (key.startsWith("mp:")) {
|
|
return `https://media.discordapp.net/external/${key.slice(3)}`;
|
|
}
|
|
if (key.startsWith("spotify:")) {
|
|
return `https://i.scdn.co/image/${key.slice("spotify:".length)}`;
|
|
}
|
|
if (activity.application_id) {
|
|
return `https://cdn.discordapp.com/app-assets/${activity.application_id}/${key}.png`;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export interface LanyardSpotify {
|
|
track_id: string;
|
|
timestamps: {
|
|
start: number;
|
|
end: number;
|
|
};
|
|
song: string;
|
|
artist: string;
|
|
album: string;
|
|
album_art_url: string | null;
|
|
}
|
|
|
|
export function formatSpotifyList(value: string | null | undefined): string {
|
|
if (!value) {
|
|
return "";
|
|
}
|
|
return value
|
|
.replace(/;/g, ",")
|
|
.replace(/,[ \t]+,/g, ",")
|
|
.replace(/\s{2,}/g, " ")
|
|
.trim();
|
|
}
|
|
|
|
export function spotifyTrackUrl(trackId: string): string {
|
|
return `https://open.spotify.com/track/${trackId}`;
|
|
}
|
|
|
|
export function discordAppIconUrl(
|
|
applicationId: string,
|
|
iconHash: string | null,
|
|
size = 128,
|
|
): string | null {
|
|
if (!iconHash) {
|
|
return null;
|
|
}
|
|
const extension = iconHash.startsWith("a_") ? "gif" : "png";
|
|
return `https://cdn.discordapp.com/app-icons/${applicationId}/${iconHash}.${extension}?size=${size}`;
|
|
}
|
|
|
|
export function discordGameUrl(gameId: string): string {
|
|
return `https://discord.com/games/${gameId}`;
|
|
}
|
|
|
|
export interface DiscordAppInfo {
|
|
id: string;
|
|
name: string;
|
|
icon: string | null;
|
|
iconUrl: string | null;
|
|
coverImage: string | null;
|
|
coverUrl: string | null;
|
|
gameId: string;
|
|
gameUrl: string;
|
|
}
|
|
|
|
interface DiscordRpcResponse {
|
|
id: string;
|
|
name: string;
|
|
icon?: string | null;
|
|
cover_image?: string | null;
|
|
linked_games?: Array<{ id: string }> | null;
|
|
}
|
|
|
|
export async function getDiscordAppInfo(applicationId: string): Promise<DiscordAppInfo | null> {
|
|
if (!/^\d+$/.test(applicationId)) {
|
|
return null;
|
|
}
|
|
try {
|
|
const response = await fetch(
|
|
`https://discord.com/api/v10/applications/${applicationId}/rpc`,
|
|
{
|
|
headers: { "User-Agent": "fristy.net (personal site)" },
|
|
next: { revalidate: 86_400 },
|
|
},
|
|
);
|
|
if (!response.ok) {
|
|
return null;
|
|
}
|
|
const json = (await response.json()) as DiscordRpcResponse;
|
|
const gameId = json.linked_games?.[0]?.id ?? json.id ?? applicationId;
|
|
return {
|
|
id: json.id ?? applicationId,
|
|
name: json.name ?? "",
|
|
icon: json.icon ?? null,
|
|
iconUrl: discordAppIconUrl(applicationId, json.icon ?? null),
|
|
coverImage: json.cover_image ?? null,
|
|
coverUrl: json.cover_image
|
|
? `https://cdn.discordapp.com/app-icons/${applicationId}/${json.cover_image}.png?size=512`
|
|
: null,
|
|
gameId,
|
|
gameUrl: discordGameUrl(gameId),
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export interface LanyardData {
|
|
discord_user: {
|
|
id: string;
|
|
username: string;
|
|
global_name: string | null;
|
|
display_name: string | null;
|
|
discriminator: string;
|
|
avatar: string | null;
|
|
bot: boolean;
|
|
};
|
|
activities: LanyardActivity[];
|
|
discord_status: DiscordStatus;
|
|
active_on_discord_desktop: boolean;
|
|
active_on_discord_mobile: boolean;
|
|
active_on_discord_web: boolean;
|
|
listening_to_spotify: boolean;
|
|
spotify: LanyardSpotify | null;
|
|
}
|
|
|
|
interface LanyardResponse {
|
|
success: boolean;
|
|
data: LanyardData;
|
|
}
|
|
|
|
export async function getLanyard(): Promise<LanyardData | null> {
|
|
try {
|
|
const response = await fetch(LANYARD_API, { next: { revalidate: 10 } });
|
|
if (!response.ok) {
|
|
return null;
|
|
}
|
|
const json = (await response.json()) as LanyardResponse;
|
|
return json.success ? json.data : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function lanyardUrl(): string {
|
|
return LANYARD_API;
|
|
}
|
|
|
|
export function discordAvatarUrl(userId: string, avatarHash: string | null, size = 128): string {
|
|
if (!avatarHash) {
|
|
return `https://cdn.discordapp.com/embed/avatars/0.png?size=${size}`;
|
|
}
|
|
const extension = avatarHash.startsWith("a_") ? "gif" : "png";
|
|
return `https://cdn.discordapp.com/avatars/${userId}/${avatarHash}.${extension}?size=${size}`;
|
|
}
|
|
|
|
export function discordProfileUrl(userId: string): string {
|
|
return `https://discord.com/users/${userId}`;
|
|
}
|