import type { AnimeEntry, AnimeProfile } from '#lib/anime/types'; const cacheDuration = 60 * 60 * 1000; const minutesPerDay = 60 * 24; const topRatedMinimumScore = 9; const statusLabels: Record = { COMPLETED: 'Completed', CURRENT: 'Watching', PLANNING: 'Planning', PAUSED: 'Paused', DROPPED: 'Dropped' }; const animeQuery = ` query ($userName: String) { User(name: $userName) { name siteUrl statistics { anime { count episodesWatched minutesWatched } } } MediaListCollection(userName: $userName, type: ANIME) { lists { status entries { score(format: POINT_10) progress media { id siteUrl episodes title { english romaji } coverImage { large } } } } } } `; interface AniListEntry { score: number; progress: number; media: { id: number; siteUrl: string; episodes: number | null; title: { english: string | null; romaji: string }; coverImage: { large: string }; }; } interface AniListResponse { data: { User: { name: string; siteUrl: string; statistics: { anime: { count: number; episodesWatched: number; minutesWatched: number } }; }; MediaListCollection: { lists: { status: string; entries: AniListEntry[] }[] }; }; } const profileCache = new Map(); function toAnimeEntry({ score, progress, media }: AniListEntry): AnimeEntry { return { id: media.id, title: media.title.english ?? media.title.romaji, url: media.siteUrl, cover: media.coverImage.large, score, progress, episodes: media.episodes }; } function entriesWithStatus(response: AniListResponse, status: string): AnimeEntry[] { const list = response.data.MediaListCollection.lists.find((item) => item.status === status); return (list?.entries ?? []).map(toAnimeEntry); } function toAnimeProfile(response: AniListResponse): AnimeProfile { const { name, siteUrl, statistics } = response.data.User; const completed = entriesWithStatus(response, 'COMPLETED'); const statusCounts = Object.entries(statusLabels).map(([status, label]) => ({ status, label, count: entriesWithStatus(response, status).length })); return { name, profileUrl: siteUrl, statusCounts, stats: { count: statistics.anime.count, episodesWatched: statistics.anime.episodesWatched, daysWatched: Math.round((statistics.anime.minutesWatched / minutesPerDay) * 10) / 10 }, watching: entriesWithStatus(response, 'CURRENT'), topRated: completed.filter((entry) => entry.score >= topRatedMinimumScore).sort((first, second) => second.score - first.score), completedCount: completed.length }; } export async function getAnimeProfile(userName: string): Promise { const cached = profileCache.get(userName); if (cached && Date.now() - cached.fetchedAt < cacheDuration) return cached.profile; try { const response = await fetch('https://graphql.anilist.co', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ query: animeQuery, variables: { userName } }) }); if (!response.ok) throw new Error(`AniList returned ${response.status}`); const profile = toAnimeProfile(await response.json()); profileCache.set(userName, { profile, fetchedAt: Date.now() }); return profile; } catch (error) { console.error(`AniList lookup failed for ${userName}:`, error); return cached?.profile ?? null; } }