diff options
| author | Fuwn <[email protected]> | 2024-02-08 00:01:24 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2024-02-08 00:01:24 -0800 |
| commit | f78f5f4857f24ee5338fb1643c666a6b18d75769 (patch) | |
| tree | 57b1b09f20b6b261a3b1ae15bfa441965f71ecd9 /src/lib/Data/AniList/wrapped.ts | |
| parent | refactor(data): move static data to module (diff) | |
| download | due.moe-f78f5f4857f24ee5338fb1643c666a6b18d75769.tar.xz due.moe-f78f5f4857f24ee5338fb1643c666a6b18d75769.zip | |
refactor(anilist): move to data module
Diffstat (limited to 'src/lib/Data/AniList/wrapped.ts')
| -rw-r--r-- | src/lib/Data/AniList/wrapped.ts | 301 |
1 files changed, 301 insertions, 0 deletions
diff --git a/src/lib/Data/AniList/wrapped.ts b/src/lib/Data/AniList/wrapped.ts new file mode 100644 index 00000000..72c81cd8 --- /dev/null +++ b/src/lib/Data/AniList/wrapped.ts @@ -0,0 +1,301 @@ +import type { AniListAuthorisation, UserIdentity } from './identity'; +import type { Media } from './media'; + +export enum SortOptions { + SCORE, + MINUTES_WATCHED, + COUNT +} + +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, + date = new Date() +) => { + const now = date.getTime(); + 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 ], createdAt_greater: ${Math.floor( + new Date(date.getFullYear(), 0, 1).getTime() / 1000 + )}, createdAt_lesser: ${Math.floor( + new Date(date.getFullYear(), 7, 1).getTime() / 1000 + )}) { + ... on TextActivity { + type + createdAt + } + ... on MessageActivity { + type + createdAt + } + } + pageInfo { + hasNextPage + } + } +}` + }) + }) + ).json(); + const pages = []; + let page = 1; + let response = await get(page); + const beginningOfYear = new Date(now).setMonth(0, 1) / 1000; + + 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 > beginningOfYear && + activity.createdAt < now / 1000 + ).length, + messageCount: pages + .flat() + .filter( + (activity) => + activity.type == 'MESSAGE' && + activity.createdAt > beginningOfYear && + activity.createdAt < now / 1000 + ).length + }; +}; + +export const wrapped = async ( + anilistAuthorisation: AniListAuthorisation | undefined, + identity: UserIdentity, + year = new Date().getFullYear(), + skipActivities = false +): Promise<Wrapped> => { + const headers: { [key: string]: string } = { + 'Content-Type': 'application/json', + Accept: 'application/json' + }; + + if (anilistAuthorisation) { + headers[ + 'Authorization' + ] = `${anilistAuthorisation.tokenType} ${anilistAuthorisation.accessToken}`; + } + + const wrappedResponse = await ( + await fetch('https://graphql.anilist.co', { + method: 'POST', + headers, + 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(); + let statusCountActivities = 0; + let messageCountActivities = 0; + + if (!skipActivities && anilistAuthorisation) { + const { statusCount, messageCount } = await profileActivities( + anilistAuthorisation, + identity, + new Date(year, 11, 31) + ); + + statusCountActivities = statusCount; + messageCountActivities = messageCount; + } + + return { + statistics: wrappedResponse['data']['User']['statistics'], + activities: { + statusCount: statusCountActivities, + messageCount: messageCountActivities + }, + 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, + sortMode: SortOptions, + excludedKeywords: string[] = [] +): TopMedia => { + const genresMap: { + [genre: string]: { totalScore: number; count: number; minutesWatched: number }; + } = {}; + const tagsMap: { [tag: string]: { totalScore: number; count: number; minutesWatched: number } } = + {}; + + media.forEach((m) => { + if (m.mediaListEntry && m.mediaListEntry.score) { + m.genres.forEach((genre) => { + if (!genresMap[genre]) genresMap[genre] = { totalScore: 0, count: 0, minutesWatched: 0 }; + + const score = m.mediaListEntry?.score; + + if (score) { + genresMap[genre].totalScore += score; + genresMap[genre].minutesWatched += m.duration; + genresMap[genre].count++; + } + }); + + m.tags.forEach((tag) => { + if (tag.rank < 50) return; + + if (!tagsMap[tag.name]) tagsMap[tag.name] = { totalScore: 0, count: 0, minutesWatched: 0 }; + + const score = m.mediaListEntry?.score; + + if (score) { + tagsMap[tag.name].totalScore += score; + tagsMap[tag.name].minutesWatched += m.duration; + 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) + })); + + switch (sortMode) { + case SortOptions.SCORE: + genres = genres.sort((a, b) => b.averageScore - a.averageScore); + tags = tags.sort((a, b) => b.averageScore - a.averageScore); + + break; + case SortOptions.MINUTES_WATCHED: + genres = genres.sort( + (a, b) => genresMap[b.genre].minutesWatched - genresMap[a.genre].minutesWatched + ); + tags = tags.sort((a, b) => tagsMap[b.tag].minutesWatched - tagsMap[a.tag].minutesWatched); + + break; + case SortOptions.COUNT: + genres = genres.sort((a, b) => genresMap[b.genre].count - genresMap[a.genre].count); + tags = tags.sort((a, b) => tagsMap[b.tag].count - tagsMap[a.tag].count); + + break; + } + + genres = genres.slice(0, amount); + tags = tags.slice(0, amount); + + let topGenreMedia; + + try { + topGenreMedia = media.find((m) => m.genres.includes(genres[0].genre)) || media[0]; + } catch { + topGenreMedia = media[0]; + } + + let topTagMedia; + + try { + topTagMedia = media.find((m) => m.tags.some((tag) => tag.name === tags[0].tag)) || media[0]; + } catch { + topTagMedia = media[0]; + } + + return { + genres, + tags, + topGenreMedia, + topTagMedia + }; +}; |