aboutsummaryrefslogtreecommitdiff
path: root/src/lib/Data/AniList/wrapped.ts
diff options
context:
space:
mode:
authorFuwn <[email protected]>2024-02-08 00:01:24 -0800
committerFuwn <[email protected]>2024-02-08 00:01:24 -0800
commitf78f5f4857f24ee5338fb1643c666a6b18d75769 (patch)
tree57b1b09f20b6b261a3b1ae15bfa441965f71ecd9 /src/lib/Data/AniList/wrapped.ts
parentrefactor(data): move static data to module (diff)
downloaddue.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.ts301
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
+ };
+};