import type { AniListAuthorisation, UserIdentity } from './identity'; import type { Media } from './media'; export interface WrappedMediaFormat { startYears: { startYear: number; minutesWatched: number; count: number; }; genres: { meanScore: number; minutesWatched: number; chaptersRead: number; genre: string; mediaIds: number[]; }[]; tags: { meanScore: number; minutesWatched: number; chaptersRead: number; tag: { name: string; }; mediaIds: number[]; }[]; } export interface Wrapped { statistics: { anime: WrappedMediaFormat; manga: WrappedMediaFormat; }; activities: { statusCount: number; messageCount: number; }; avatar: { large: string; }; } const profileActivities = async (user: AniListAuthorisation, identity: UserIdentity) => { const get = async (page: number) => await ( await fetch('https://graphql.anilist.co', { method: 'POST', headers: { Authorization: `${user.tokenType} ${user.accessToken}`, 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ query: `{ Page(page: ${page}) { activities(userId: ${identity.id}, type_in: [ TEXT, MESSAGE ]) { ... on TextActivity { type createdAt } ... on MessageActivity { type createdAt } } pageInfo { hasNextPage } } }` }) }) ).json(); const pages = []; let page = 1; let response = await get(page); pages.push(response['data']['Page']['activities']); while (response['data']['Page']['pageInfo']['hasNextPage']) { page += 1; response = await get(page); pages.push(response['data']['Page']['activities']); } return { statusCount: pages .flat() .filter( (activity) => activity.type == 'TEXT' && activity.createdAt > Math.floor(Date.now() / 1000) - 31556952 ).length, messageCount: pages .flat() .filter( (activity) => activity.type == 'MESSAGE' && activity.createdAt > Math.floor(Date.now() / 1000) - 31556952 ).length }; }; export const wrapped = async ( anilistAuthorisation: AniListAuthorisation, identity: UserIdentity ): Promise => { const wrappedResponse = await ( await fetch('https://graphql.anilist.co', { method: 'POST', headers: { Authorization: `${anilistAuthorisation.tokenType} ${anilistAuthorisation.accessToken}`, 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ query: `{ User(name: "${identity.name}") { avatar { large } statistics { anime { startYears { startYear minutesWatched count } genres(sort: [ MEAN_SCORE_DESC ]) { meanScore minutesWatched chaptersRead genre mediaIds } tags(sort: [ MEAN_SCORE_DESC ]) { meanScore minutesWatched chaptersRead tag { name } mediaIds } } manga { startYears { startYear chaptersRead count } genres(sort: [ MEAN_SCORE_DESC ]) { meanScore minutesWatched chaptersRead genre mediaIds } tags(sort: [ MEAN_SCORE_DESC ]) { meanScore minutesWatched chaptersRead tag { name } mediaIds } } } } }` }) }) ).json(); const { statusCount, messageCount } = await profileActivities(anilistAuthorisation, identity); return { statistics: wrappedResponse['data']['User']['statistics'], activities: { statusCount, messageCount }, avatar: wrappedResponse['data']['User']['avatar'] }; }; export interface TopMedia { genres: { genre: string; averageScore: number; }[]; tags: { tag: string; averageScore: number; }[]; topGenreMedia: Media; topTagMedia: Media; } export const tops = (media: Media[], amount: number, excludedKeywords: string[] = []): TopMedia => { const genresMap: { [genre: string]: { totalScore: number; count: number } } = {}; const tagsMap: { [tag: string]: { totalScore: number; count: number } } = {}; media.forEach((m) => { if (m.mediaListEntry && m.mediaListEntry.score) { m.genres.forEach((genre) => { if (!genresMap[genre]) genresMap[genre] = { totalScore: 0, count: 0 }; const score = m.mediaListEntry?.score; if (score) { genresMap[genre].totalScore += score; genresMap[genre].count++; } }); m.tags.forEach((tag) => { if (!tagsMap[tag.name]) tagsMap[tag.name] = { totalScore: 0, count: 0 }; const score = m.mediaListEntry?.score; if (score) { tagsMap[tag.name].totalScore += score; tagsMap[tag.name].count++; } }); } }); let genres = Object.keys(genresMap) .filter((genre) => !excludedKeywords.some((keyword) => genre.includes(keyword))) .map((genre) => ({ genre, averageScore: Math.round(genresMap[genre].totalScore / genresMap[genre].count) })); let tags = Object.keys(tagsMap) .filter((genre) => !excludedKeywords.some((keyword) => genre.includes(keyword))) .map((tag) => ({ tag, averageScore: Math.round(tagsMap[tag].totalScore / tagsMap[tag].count) })); genres = genres.slice(0, amount); tags = tags.slice(0, amount); genres.sort((a, b) => b.averageScore - a.averageScore); tags.sort((a, b) => b.averageScore - a.averageScore); const topGenreMedia = media.find((m) => m.genres.includes(genres[0].genre)) || media[0]; const topTagMedia = media.find((m) => m.tags.some((tag) => tag.name === tags[0].tag)) || media[0]; return { genres, tags, topGenreMedia, topTagMedia }; };