aboutsummaryrefslogtreecommitdiff
path: root/src/lib/AniList
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/AniList
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/AniList')
-rw-r--r--src/lib/AniList/activity.ts306
-rw-r--r--src/lib/AniList/character.ts84
-rw-r--r--src/lib/AniList/follow.ts48
-rw-r--r--src/lib/AniList/following.ts56
-rw-r--r--src/lib/AniList/forum.ts75
-rw-r--r--src/lib/AniList/identity.ts34
-rw-r--r--src/lib/AniList/media.ts441
-rw-r--r--src/lib/AniList/notifications.ts78
-rw-r--r--src/lib/AniList/prequels.ts175
-rw-r--r--src/lib/AniList/schedule.ts105
-rw-r--r--src/lib/AniList/user.ts92
-rw-r--r--src/lib/AniList/wrapped.ts301
12 files changed, 0 insertions, 1795 deletions
diff --git a/src/lib/AniList/activity.ts b/src/lib/AniList/activity.ts
deleted file mode 100644
index fb15521e..00000000
--- a/src/lib/AniList/activity.ts
+++ /dev/null
@@ -1,306 +0,0 @@
-import { database } from '$lib/Database/activities';
-import type { User } from './follow';
-import type { AniListAuthorisation, UserIdentity } from './identity';
-
-export interface ActivityHistoryEntry {
- date: number;
- amount: number;
-}
-
-interface ActivityHistoryOptions {
- stats: {
- activityHistory: ActivityHistoryEntry[];
- };
- options: {
- timezone: string;
- };
-}
-
-interface LastActivity {
- date: Date;
- timezone: string;
-}
-
-export const fillMissingDays = (
- inputActivities: ActivityHistoryEntry[],
- startOfYear = false,
- year = new Date().getFullYear()
-): ActivityHistoryEntry[] => {
- const yearDate = new Date(year, 0, 0);
-
- if (inputActivities.length === 0)
- return startOfYear
- ? fillDateRange(
- new Date(yearDate.getUTCFullYear(), 0, 1),
- new Date(yearDate.getUTCFullYear() + 1, 0, 1)
- )
- : [];
-
- const sortedActivities = [...inputActivities].sort((a, b) => a.date - b.date);
- const endDate = new Date(sortedActivities[sortedActivities.length - 1].date * 1000);
-
- endDate.setUTCDate(endDate.getUTCDate() + 1);
-
- return fillDateRange(
- startOfYear
- ? new Date(yearDate.getUTCFullYear(), 0, 1)
- : new Date(sortedActivities[0].date * 1000),
- endDate,
- sortedActivities
- );
-};
-
-const fillDateRange = (
- startDate: Date,
- endDate: Date,
- existingActivities: ActivityHistoryEntry[] = []
-): ActivityHistoryEntry[] => {
- const outputActivities: ActivityHistoryEntry[] = [];
-
- for (let dt = new Date(startDate); dt < endDate; dt.setUTCDate(dt.getUTCDate() + 1)) {
- const dateString = dt.toDateString();
-
- if (
- !new Set(
- existingActivities.map((activity) => new Date(activity.date * 1000).toDateString())
- ).has(dateString)
- ) {
- outputActivities.push({ date: Math.floor(dt.getTime() / 1000), amount: 0 });
- } else {
- const activity = existingActivities.find(
- (activity) => new Date(activity.date * 1000).toDateString() === dateString
- );
-
- if (activity) outputActivities.push(activity);
- }
- }
-
- return outputActivities;
-};
-
-export const activityHistoryOptions = async (
- userIdentity: UserIdentity,
- authorisation?: AniListAuthorisation
-): Promise<ActivityHistoryOptions> => {
- return (
- await (
- await fetch('https://graphql.anilist.co', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json',
- ...(authorisation
- ? { Authorization: `${authorisation.tokenType} ${authorisation.accessToken}` }
- : {})
- },
- body: JSON.stringify({
- query: `{ User(id: ${userIdentity.id}) {
- stats { activityHistory { date amount } }
- ${authorisation ? 'options { timezone }' : ''}
- } }`
- })
- })
- ).json()
- )['data']['User'];
-};
-
-const convertToTimezoneOffset = (timeStr: string) => {
- const [hours, minutes] = timeStr.split(':');
- let totalMinutes = parseInt(hours, 10) * 60 + parseInt(minutes, 10);
-
- totalMinutes = -totalMinutes;
-
- return totalMinutes;
-};
-
-export const activityHistory = async (
- userIdentity: UserIdentity,
- authorisation?: AniListAuthorisation
-): Promise<ActivityHistoryEntry[]> =>
- (await activityHistoryOptions(userIdentity, authorisation)).stats.activityHistory;
-
-export const lastActivityDate = async (
- userIdentity: UserIdentity,
- authorisation: AniListAuthorisation
-): Promise<LastActivity> => {
- if (userIdentity.id === -1 || userIdentity.id === -2)
- return { date: new Date(8640000000000000), timezone: '' };
-
- const history = await activityHistoryOptions(userIdentity, authorisation);
- const date = new Date(
- Number(history.stats.activityHistory[history.stats.activityHistory.length - 1].date) * 1000 +
- convertToTimezoneOffset(history.options.timezone)
- );
-
- date.setDate(date.getDate() + 1);
-
- return { date, timezone: history.options.timezone };
-};
-
-export interface ActivitiesPage {
- data: {
- Page: {
- pageInfo: {
- hasNextPage: boolean;
- };
- activities: {
- createdAt: number;
- }[];
- };
- };
-}
-
-const activitiesPage = async (
- page: number,
- anilistAuthorisation: AniListAuthorisation,
- userIdentity: UserIdentity,
- year = new Date().getFullYear()
-): Promise<ActivitiesPage> =>
- 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: `{
- Page(page: ${page}) {
- pageInfo { hasNextPage }
- activities(userId: ${userIdentity.id}, createdAt_greater: ${Math.floor(
- new Date(year, 0, 1).getTime() / 1000
- )}, createdAt_lesser: ${Math.floor(new Date(year, 7, 1).getTime() / 1000)}) {
- ... on TextActivity { createdAt }
- ... on ListActivity { createdAt }
- ... on MessageActivity { createdAt }
- }
- }
- }`
- })
- })
- ).json();
-
-export const fullActivityHistory = async (
- anilistAuthorisation: AniListAuthorisation,
- userIdentity: UserIdentity,
- year = new Date().getFullYear()
-): Promise<ActivityHistoryEntry[]> => {
- const activities = [];
- let page = 1;
- let currentDatabasePage = await database.activities.get(page);
- let currentPage;
-
- if (currentDatabasePage) currentPage = currentDatabasePage.data;
- else {
- currentPage = await activitiesPage(page, anilistAuthorisation, userIdentity, year);
- database.activities.add({
- page,
- data: currentPage
- });
- }
-
- for (const activity of currentPage.data.Page.activities) activities.push(activity);
-
- while (currentPage['data']['Page']['pageInfo']['hasNextPage']) {
- for (const activity of currentPage.data.Page.activities) activities.push(activity);
-
- page += 1;
- currentDatabasePage = await database.activities.get(page);
-
- if (currentDatabasePage) currentPage = currentDatabasePage.data;
- else {
- currentPage = await activitiesPage(page, anilistAuthorisation, userIdentity, year);
- database.activities.add({
- page,
- data: currentPage
- });
- }
- }
-
- for (const activity of currentPage.data.Page.activities) activities.push(activity);
-
- let fullLocalActivityHistory: ActivityHistoryEntry[] = [];
-
- for (const activity of activities) {
- const date = new Date(activity.createdAt * 1000);
- const dateString = date.toDateString();
-
- const activityHistoryEntry = fullLocalActivityHistory.find(
- (activityHistoryEntry) =>
- new Date(activityHistoryEntry.date * 1000).toDateString() === dateString
- );
-
- if (activityHistoryEntry) activityHistoryEntry.amount += 1;
- else fullLocalActivityHistory.push({ date: Math.floor(date.getTime() / 1000), amount: 1 });
- }
-
- fullLocalActivityHistory = fullLocalActivityHistory.filter((a) => !isNaN(a.date));
-
- if (new Date().getMonth() > 6)
- fullLocalActivityHistory.push(...(await activityHistory(userIdentity)));
-
- fullLocalActivityHistory = fullLocalActivityHistory.filter(
- (activityHistoryEntry, index, self) =>
- self.findIndex(
- (a) =>
- new Date(a.date * 1000).toDateString() ===
- new Date(activityHistoryEntry.date * 1000).toDateString()
- ) === index
- );
-
- fullLocalActivityHistory = fullLocalActivityHistory.filter(
- (activityHistoryEntry) => new Date(activityHistoryEntry.date * 1000).getFullYear() === year
- );
-
- return fullLocalActivityHistory;
-};
-
-export const activityLikes = async (id: number): Promise<Partial<User>[]> => {
- const activityResponse = await (
- await fetch('https://graphql.anilist.co', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json'
- },
- body: JSON.stringify({
- query: `{
- Activity(id: ${id}) {
- __typename
- ... on TextActivity { likes { name avatar { large } } }
- ... on ListActivity { likes { name avatar { large } } }
- ... on MessageActivity { likes { name avatar { large } } }
- }
- }`
- })
- })
- ).json();
-
- return activityResponse['data']['Activity']['likes'];
-};
-
-export const activityText = async (id: number, replies = false): Promise<string> => {
- const activityResponse = await (
- await fetch('https://graphql.anilist.co', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json'
- },
- body: JSON.stringify({
- query: `{
- Activity(id: ${id}) {
- ... on TextActivity { text(asHtml: true) ${replies ? 'replies { text(asHtml: true) }' : ''} }
- }
- }`
- })
- })
- ).json();
- let text = activityResponse['data']['Activity']['text'];
-
- if (replies)
- for (const reply of activityResponse['data']['Activity']['replies']) text += reply.text;
-
- return text;
-};
diff --git a/src/lib/AniList/character.ts b/src/lib/AniList/character.ts
deleted file mode 100644
index 082af47a..00000000
--- a/src/lib/AniList/character.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-export interface Character {
- name: {
- full: string;
- };
- id: number;
- image: {
- large: string;
- };
-}
-
-export interface CharactersPage {
- data: {
- Page: {
- characters: Character[];
- pageInfo: {
- hasNextPage: boolean;
- currentPage: number;
- };
- };
- };
-}
-
-const charactersPage = async (page: number): Promise<CharactersPage> =>
- await (
- await fetch('https://graphql.anilist.co', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json'
- },
- body: JSON.stringify({
- query: `{ Page(page: ${page}, perPage: 50) {
- characters(isBirthday: true) { name { full } id image { large } }
- pageInfo { hasNextPage currentPage }
- } }`
- })
- })
- ).json();
-
-export const todaysCharacterBirthdays = async (): Promise<Character[]> => {
- const characters = [];
- let page = 1;
- let currentPage = await charactersPage(page);
-
- for (const character of currentPage['data']['Page']['characters'])
- characters.push({
- id: character['id'],
- name: {
- full: character['name']['full']
- },
- image: {
- large: character['image']['large']
- }
- });
-
- while (currentPage['data']['Page']['pageInfo']['hasNextPage']) {
- for (const character of currentPage['data']['Page']['characters'])
- characters.push({
- id: character['id'],
- name: {
- full: character['name']['full']
- },
- image: {
- large: character['image']['large']
- }
- });
-
- page += 1;
- currentPage = await charactersPage(page);
- }
-
- for (const character of currentPage['data']['Page']['characters'])
- characters.push({
- id: character['id'],
- name: {
- full: character['name']['full']
- },
- image: {
- large: character['image']['large']
- }
- });
-
- return characters;
-};
diff --git a/src/lib/AniList/follow.ts b/src/lib/AniList/follow.ts
deleted file mode 100644
index 3601d510..00000000
--- a/src/lib/AniList/follow.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import type { AniListAuthorisation } from './identity';
-
-export interface User {
- id: number;
- name: string;
- isFollowing: boolean;
- isFollower: boolean;
- avatar: {
- large: string;
- };
-}
-
-export const toggleFollow = async (
- anilistAuthorisation: AniListAuthorisation,
- username: string
-): Promise<User> => {
- const {
- data: { User: user }
- } = 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: "${username}") { id } }`
- })
- })
- ).json();
-
- return (
- 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({
- mutation: `{ ToggleFollow(userId: ${user.id}) { id name isFollowing isFollower } }`
- })
- })
- ).json()
- )['data']['ToggleFollow'];
-};
diff --git a/src/lib/AniList/following.ts b/src/lib/AniList/following.ts
deleted file mode 100644
index 1bba66b9..00000000
--- a/src/lib/AniList/following.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { user, type User } from './user';
-
-export interface FollowingPage {
- data: {
- Page: {
- pageInfo: {
- hasNextPage: boolean;
- };
- following: Partial<User>[];
- };
- };
-}
-
-const followingPage = async (page: number, id: number): Promise<FollowingPage> =>
- await (
- await fetch('https://graphql.anilist.co', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json'
- },
- body: JSON.stringify({
- query: `{
- Page(page: ${page}) {
- pageInfo { hasNextPage }
- following(userId: ${id}) { name id }
- }
- }`
- })
- })
- ).json();
-
-export const followers = async (name: string): Promise<Partial<User>[]> => {
- const activities = [];
- let page = 1;
- const id = (await user(name)).id;
- let currentPage = await followingPage(page, id);
-
- for (const activity of currentPage.data.Page.following) activities.push(activity);
-
- while (currentPage['data']['Page']['pageInfo']['hasNextPage']) {
- for (const activity of currentPage.data.Page.following) activities.push(activity);
-
- page += 1;
- currentPage = await followingPage(page, id);
- }
-
- for (const activity of currentPage.data.Page.following) activities.push(activity);
-
- for (let i = activities.length - 1; i > 0; i--) {
- const j = Math.floor(Math.random() * (i + 1));
- [activities[i], activities[j]] = [activities[j], activities[i]];
- }
-
- return activities;
-};
diff --git a/src/lib/AniList/forum.ts b/src/lib/AniList/forum.ts
deleted file mode 100644
index 5a1bfc25..00000000
--- a/src/lib/AniList/forum.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import type { User } from './follow';
-import { user } from './user';
-
-export interface Thread {
- id: number;
- title: string;
- createdAt: number;
- mediaCategories: {
- coverImage: {
- extraLarge: string;
- };
- }[];
-}
-
-export interface ThreadPage {
- data: {
- Page: {
- threads: Thread[];
- pageInfo: {
- hasNextPage: boolean;
- currentPage: number;
- };
- };
- };
-}
-
-const threadPage = async (page: number, userId: number): Promise<ThreadPage> =>
- await (
- await fetch('https://graphql.anilist.co', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json'
- },
- body: JSON.stringify({
- query: `{ Page(perPage: 50, page: ${page}) {
- threads(userId: ${userId}) { id title createdAt mediaCategories { coverImage { extraLarge } } }
- pageInfo { hasNextPage }
-} }`
- })
- })
- ).json();
-
-export const threads = async (username: string): Promise<Thread[]> => {
- const allThreads = [];
- const userId = (await user(username)).id;
- let page = 1;
- let currentPage = await threadPage(page, userId);
-
- for (const thread of currentPage.data.Page.threads) allThreads.push(thread);
-
- while (currentPage.data.Page.pageInfo.hasNextPage) {
- page += 1;
- currentPage = await threadPage(page, userId);
-
- for (const thread of currentPage.data.Page.threads) allThreads.push(thread);
- }
-
- return allThreads;
-};
-
-export const threadLikes = async (id: number): Promise<Partial<User>[]> => {
- const activityResponse = await (
- await fetch('https://graphql.anilist.co', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json'
- },
- body: JSON.stringify({ query: `{ Thread(id: ${id}) { likes { name avatar { large } } } }` })
- })
- ).json();
-
- return activityResponse['data']['Thread']['likes'];
-};
diff --git a/src/lib/AniList/identity.ts b/src/lib/AniList/identity.ts
deleted file mode 100644
index 7214ffa4..00000000
--- a/src/lib/AniList/identity.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-export interface UserIdentity {
- id: number;
- name: string;
- avatar: string;
-}
-
-export interface AniListAuthorisation {
- tokenType: string;
- accessToken: string;
- expiresIn: number;
- refreshToken: string;
-}
-
-export const userIdentity = async (
- anilistAuthorisation: AniListAuthorisation
-): Promise<UserIdentity> => {
- const userIdResponse = 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: `{ Viewer { id name avatar { large } } }` })
- })
- ).json();
-
- return {
- id: userIdResponse['data']['Viewer']['id'],
- name: userIdResponse['data']['Viewer']['name'],
- avatar: userIdResponse['data']['Viewer']['avatar']['large']
- };
-};
diff --git a/src/lib/AniList/media.ts b/src/lib/AniList/media.ts
deleted file mode 100644
index b9d511bc..00000000
--- a/src/lib/AniList/media.ts
+++ /dev/null
@@ -1,441 +0,0 @@
-import type { AniListAuthorisation } from '$lib/AniList/identity';
-import type { UserIdentity } from './identity';
-import anime from '$stores/anime';
-import manga from '$stores/manga';
-import settings from '$stores/settings';
-import lastPruneTimes from '$stores/lastPruneTimes';
-import { options as getOptions, type Options } from '$lib/Notification/options';
-
-export enum Type {
- Anime,
- Manga
-}
-
-export interface MediaTitle {
- romaji: string;
- english: string;
- native: string;
-}
-
-export interface Media {
- id: number;
- idMal: number;
- status: 'FINISHED' | 'RELEASING' | 'NOT_YET_RELEASED' | 'CANCELLED' | 'HIATUS';
- type: 'ANIME' | 'MANGA';
- episodes: number;
- chapters: number;
- volumes: number;
- duration: number;
- format:
- | 'TV'
- | 'TV_SHORT'
- | 'MOVIE'
- | 'SPECIAL'
- | 'OVA'
- | 'ONA'
- | 'MUSIC'
- | 'MANGA'
- | 'NOVEL'
- | 'ONE_SHOT';
- title: MediaTitle;
- nextAiringEpisode?: {
- episode: number;
- airingAt?: number;
- nativeAiringAt?: number;
- };
- synonyms: string[];
- mediaListEntry?: {
- progress: number;
- progressVolumes: number;
- status: 'CURRENT' | 'PLANNING' | 'COMPLETED' | 'DROPPED' | 'PAUSED' | 'REPEATING';
- score: number;
- repeat: number;
- startedAt: {
- year: number;
- };
- completedAt: {
- year: number;
- };
- createdAt: number;
- updatedAt: number;
- };
- startDate: {
- year: number;
- };
- endDate: {
- year: number;
- };
- coverImage: {
- extraLarge: string;
- };
- tags: {
- name: string;
- rank: number;
- }[];
- genres: string[];
-}
-
-export const flattenLists = (
- lists: { name: string; entries: { media: Media }[] }[],
- all = false
-) => {
- if (lists === undefined) return [];
-
- const flattenedList: Media[] = [];
- const markedMediaIds: number[] = [];
- let dueInclude = false;
- const processedList = (list: Media[], include: boolean) =>
- list
- .filter((media) =>
- include ? markedMediaIds.includes(media.id) : !markedMediaIds.includes(media.id)
- )
- .filter(
- (item, index, self) =>
- self.findIndex((itemToCompare) => itemToCompare.id === item.id) === index
- );
-
- if (all) {
- for (const list of lists) flattenedList.push(...list.entries.map((entry) => entry.media));
- } else {
- for (const list of lists) {
- if (list.name.toLowerCase().includes('#dueinclude')) {
- dueInclude = true;
-
- markedMediaIds.splice(0, markedMediaIds.length);
- markedMediaIds.push(...list.entries.map((entry) => entry.media.id));
- }
-
- if (dueInclude) continue;
-
- if (list.name.toLowerCase().includes('#dueignore'))
- markedMediaIds.push(...list.entries.map((entry) => entry.media.id));
- else flattenedList.push(...list.entries.map((entry) => entry.media));
- }
- }
-
- return processedList(flattenedList, dueInclude);
-};
-
-const collectionQueryTemplate = (type: Type, userId: number, includeCompleted: boolean) =>
- `{
- MediaListCollection(
- userId: ${userId},
- type: ${type === Type.Anime ? 'ANIME' : 'MANGA'}
- ${includeCompleted ? '' : ', status_not_in: [ COMPLETED ]'}) {
- lists {
- name entries {
- media {
- id idMal status type episodes chapters format duration synonyms genres
- tags { name rank }
- title { romaji english native }
- nextAiringEpisode { episode airingAt }
- mediaListEntry {
- progress progressVolumes status repeat createdAt updatedAt
- score(format: POINT_100) startedAt { year } completedAt { year }
- }
- startDate { year }
- endDate { year }
- coverImage { extraLarge }
- }
- }
- }
- }
-}`;
-
-interface CollectionOptions {
- includeCompleted?: boolean;
- forcePrune?: boolean;
- all?: boolean;
- addNotification?: (preferences: Options) => void;
- notificationType?: string;
-}
-
-interface NonNullCollectionOptions {
- includeCompleted: boolean;
- forcePrune: boolean;
- all?: boolean;
- addNotification?: (preferences: Options) => void;
- notificationType?: string;
-}
-
-const assignDefaultOptions = (options: CollectionOptions) => {
- const nonNullOptions: NonNullCollectionOptions = {
- includeCompleted: false,
- forcePrune: false,
- all: false,
- addNotification: undefined,
- notificationType: undefined
- };
-
- if (options.includeCompleted !== undefined)
- nonNullOptions.includeCompleted = options.includeCompleted;
- if (options.forcePrune !== undefined) nonNullOptions.forcePrune = options.forcePrune;
- if (options.all !== undefined) nonNullOptions.all = options.all;
- if (options.addNotification !== undefined)
- nonNullOptions.addNotification = options.addNotification;
- if (options.notificationType !== undefined)
- nonNullOptions.notificationType = options.notificationType;
-
- return nonNullOptions;
-};
-
-export const mediaListCollection = async (
- anilistAuthorisation: AniListAuthorisation,
- userIdentity: UserIdentity,
- type: Type,
- mediaCache: string | undefined,
- currentLastPruneAt: string | number,
- inputOptions: CollectionOptions = {}
-): Promise<Media[]> => {
- if (userIdentity.id === -1 || userIdentity.id === -2) return [];
-
- const options = assignDefaultOptions(inputOptions);
-
- let currentCacheMinutes;
-
- settings.subscribe((value) => (currentCacheMinutes = value.cacheMinutes));
-
- if (String(currentLastPruneAt) === '') {
- if (type === Type.Anime) lastPruneTimes.setKey('anime', new Date().getTime());
- else lastPruneTimes.setKey('manga', new Date().getTime());
- } else {
- if (
- (new Date().getTime() - Number(currentLastPruneAt)) / 1000 / 60 >
- Number(currentCacheMinutes) ||
- options.forcePrune
- ) {
- if (type === Type.Anime) {
- lastPruneTimes.setKey('anime', new Date().getTime());
- anime.set('');
- } else {
- lastPruneTimes.setKey('manga', new Date().getTime());
- manga.set('');
- }
-
- mediaCache = '';
- }
- }
-
- if (mediaCache !== undefined && mediaCache !== '') return JSON.parse(mediaCache);
-
- const userIdResponse = 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: collectionQueryTemplate(type, userIdentity.id, options.includeCompleted)
- })
- })
- ).json();
-
- if (
- !userIdResponse['data'] ||
- !userIdResponse['data']['MediaListCollection'] ||
- !userIdResponse['data']['MediaListCollection']['lists']
- )
- return [];
-
- if (mediaCache === '')
- if (type === Type.Anime)
- anime.set(
- JSON.stringify(
- flattenLists(userIdResponse['data']['MediaListCollection']['lists'], options.all)
- )
- );
- else
- manga.set(
- JSON.stringify(
- flattenLists(userIdResponse['data']['MediaListCollection']['lists'], options.all)
- )
- );
-
- if (options.addNotification)
- options.addNotification(
- getOptions({
- heading: options.notificationType ? options.notificationType : Type[type],
- description: 'Re-cached media lists from AniList'
- })
- );
-
- return flattenLists(userIdResponse['data']['MediaListCollection']['lists'], options.all);
-};
-
-export const publicMediaListCollection = async (userId: number, type: Type): Promise<Media[]> =>
- flattenLists(
- (
- await (
- await fetch('https://graphql.anilist.co', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json'
- },
- body: JSON.stringify({
- query: collectionQueryTemplate(type, userId, false)
- })
- })
- ).json()
- )['data']['MediaListCollection']['lists']
- );
-
-const countMedian = (guesses: number[]) => {
- guesses.sort((a: number, b: number) => a - b);
-
- const mid = Math.floor(guesses.length / 2);
-
- return guesses.length % 2 !== 0 ? guesses[mid] : (guesses[mid - 1] + guesses[mid]) / 2;
-};
-
-const countIQR = (guesses: number[]) => {
- guesses.sort((a: number, b: number) => a - b);
-
- const q1 = guesses[Math.floor(guesses.length / 4)];
- const q3 = guesses[Math.ceil(guesses.length * (3 / 4))];
- const iqr = q3 - q1;
-
- return guesses.filter((guess: number) => guess >= q1 - 1.5 * iqr && guess <= q3 + 1.5 * iqr);
-};
-
-const countMode = (guesses: number[]) => {
- const frequency: { [key: number]: number } = {};
- let maximumFrequency = 0;
- let mode = 0;
-
- for (const guess of guesses) {
- frequency[guess] = (frequency[guess] || 0) + 1;
-
- if (frequency[guess] > maximumFrequency) {
- maximumFrequency = frequency[guess];
- mode = guess;
- }
- }
-
- return mode;
-};
-
-export const recentMediaActivities = async (
- userIdentity: UserIdentity,
- media: Media,
- method: 'median' | 'iqr_median' | 'iqr_mode' | 'mode'
-): Promise<number | null> => {
- const activities = await (
- await fetch('https://graphql.anilist.co', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json'
- },
- body: JSON.stringify({
- query: `{
- Page(page: 1) {
- activities(mediaId: ${media.id}, sort: ID_DESC) {
- ... on ListActivity { progress }
- }
- }
-
- MediaList(userId: ${userIdentity.id}, mediaId: ${media.id}) { progress }
- }`
- })
- })
- ).json();
- const guesses: number[] = [];
-
- activities['data']['Page']['activities'].forEach((activity: { progress: string }) => {
- if (activity.progress !== null) {
- const progress = Number((activity.progress.match(/\d+$/) || [0])[0]);
-
- if (progress !== 65535) guesses.push(progress);
- }
- });
- guesses.sort((a, b) => b - a);
-
- if (guesses.length) {
- let bestGuess;
-
- switch (method) {
- case 'median':
- {
- bestGuess = countMedian(guesses);
- }
- break;
- case 'iqr_median':
- {
- bestGuess = countMedian(countIQR(guesses));
- }
- break;
- case 'iqr_mode':
- {
- bestGuess = countMode(countIQR(guesses));
- }
- break;
- case 'mode':
- {
- bestGuess = countMode(guesses);
- }
- break;
- default:
- {
- bestGuess = countMedian(guesses);
- }
- break;
- }
-
- // if (guesses.length > 2) {
- // if (guesses.filter((val) => val < 7000).length) {
- // guesses = guesses.filter((val) => val < 7000);
- // }
-
- // const difference = guesses[0] - guesses[1];
- // const inverseDifference = 1 + Math.ceil(25 / (difference + 1));
-
- // if (guesses.length >= inverseDifference) {
- // if (
- // guesses[1] === guesses[inverseDifference] ||
- // guesses[0] - guesses[1] > 500 ||
- // (guesses[0] - guesses[1] > 100 && guesses[1] >= guesses[inverseDifference] - 1)
- // ) {
- // bestGuess = guesses[1];
-
- // if (
- // guesses.length > 15 &&
- // guesses[1] - guesses[2] > 50 &&
- // guesses[2] === guesses[guesses.length - 1]
- // ) {
- // bestGuess = guesses[2];
- // }
- // }
- // }
- // }
-
- // if (activities['data']['MediaList']['progress'] !== null) {
- if (activities['data']['MediaList']['progress'] > bestGuess)
- bestGuess = activities['data']['MediaList']['progress'];
- // }
-
- return Math.round(bestGuess);
- }
-
- return null;
-};
-
-export const mediaCover = async (id: number) =>
- (
- await (
- await fetch('https://graphql.anilist.co', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json'
- },
- body: JSON.stringify({
- query: `{
- Media(id: ${id}) { coverImage { extraLarge } }
- }`
- })
- })
- ).json()
- )['data']['Media']['coverImage']['extraLarge'];
diff --git a/src/lib/AniList/notifications.ts b/src/lib/AniList/notifications.ts
deleted file mode 100644
index bebf1b42..00000000
--- a/src/lib/AniList/notifications.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-export interface Notification {
- user: {
- name: string;
- avatar: {
- large: string;
- };
- };
- thread: {
- title: string;
- id: number;
- };
- activity: {
- id: number;
- };
- context: string;
- id: number;
- createdAt: number;
- type:
- | 'FOLLOWING'
- | 'ACTIVITY_MESSAGE'
- | 'ACTIVITY_MENTION'
- | 'ACTIVITY_REPLY'
- | 'ACTIVITY_REPLY_SUBSCRIBED'
- | 'ACTIVITY_LIKE'
- | 'ACTIVITY_REPLY_LIKE'
- | 'THREAD_COMMENT_MENTION'
- | 'THREAD_COMMENT_REPLY'
- | 'THREAD_SUBSCRIBED'
- | 'THREAD_COMMENT_LIKE'
- | 'THREAD_LIKE';
-}
-
-export const notifications = async (accessToken: string): Promise<Notification[] | null> => {
- const activityNotification = (type: string, extend = '') => `... on ${type} {
- id user { name avatar { large } } context createdAt type ${extend}
- }`;
- const richActivityNotification = (type: string) =>
- `${activityNotification(
- type,
- `activity {
- ... on TextActivity { id }
- ... on ListActivity { id }
- ... on MessageActivity { id }
- }`
- )}`;
- const threadNotification = (type: string) =>
- `${activityNotification(type, `thread { title id }`)}`;
- const data = await (
- await fetch('https://graphql.anilist.co', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${accessToken}`,
- Accept: 'application/json'
- },
- body: JSON.stringify({
- query: `{ Page(page: 1, perPage: 50) { notifications {
- ${activityNotification('FollowingNotification')}
- ${activityNotification('ActivityMessageNotification')}
- ${richActivityNotification('ActivityMentionNotification')}
- ${richActivityNotification('ActivityReplyNotification')}
- ${richActivityNotification('ActivityReplySubscribedNotification')}
- ${richActivityNotification('ActivityLikeNotification')}
- ${richActivityNotification('ActivityReplyLikeNotification')}
- ${threadNotification('ThreadCommentMentionNotification')}
- ${threadNotification('ThreadCommentReplyNotification')}
- ${threadNotification('ThreadCommentSubscribedNotification')}
- ${threadNotification('ThreadCommentLikeNotification')}
- ${threadNotification('ThreadLikeNotification')}
- } } }`
- })
- })
- ).json();
-
- if (data['errors']) return null;
-
- return data['data']['Page']['notifications'];
-};
diff --git a/src/lib/AniList/prequels.ts b/src/lib/AniList/prequels.ts
deleted file mode 100644
index 3009e9ba..00000000
--- a/src/lib/AniList/prequels.ts
+++ /dev/null
@@ -1,175 +0,0 @@
-import type { AniListAuthorisation } from './identity';
-import type { MediaTitle } from './media';
-
-export interface MediaPrequel {
- id: number;
- title: MediaTitle;
- episodes: number;
- seen: number;
- nextAiringEpisode?: {
- episode: number;
- airingAt?: number;
- };
- startDate: {
- year: number;
- month: number;
- day: number;
- };
- coverImage: {
- extraLarge: string;
- };
-}
-
-interface PrequelRelations {
- edges: {
- relationType: string;
- node: {
- title: MediaTitle;
- episodes: number;
- mediaListEntry: {
- status: string;
- progress: number;
- };
- coverImage: {
- extraLarge: string;
- };
- };
- }[];
-}
-
-interface PrequelsPage {
- data: {
- Page: {
- media: {
- title: MediaTitle;
- id: number;
- relations: PrequelRelations;
- nextAiringEpisode?: {
- episode: number;
- airingAt?: number;
- };
- startDate: {
- year: number;
- month: number;
- day: number;
- };
- coverImage: {
- extraLarge: string;
- };
- }[];
- pageInfo: {
- hasNextPage: boolean;
- };
- };
- };
-}
-
-const prequelsPage = async (
- page: number,
- anilistAuthorisation: AniListAuthorisation,
- year: number,
- season: 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'
-): Promise<PrequelsPage> =>
- 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: `{
- Page(page: ${page}) {
- pageInfo {
- hasNextPage
- }
- media(season: ${season}, seasonYear: ${year}) {
- title {
- english
- romaji
- }
- coverImage { extraLarge }
- id
- nextAiringEpisode { episode airingAt }
- startDate { year month day }
- relations {
- edges {
- relationType
- node {
- title {
- english
- romaji
- }
- episodes
- mediaListEntry {
- status
- progress
- }
- coverImage { extraLarge }
- }
- }
- }
- }
- }
-}`
- })
- })
- ).json();
-
-export const prequels = async (
- anilistAuthorisation: AniListAuthorisation,
- year: number,
- season: 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'
-): Promise<MediaPrequel[]> => {
- const candidates = [];
- let page = 1;
- let currentPage = await prequelsPage(page, anilistAuthorisation, year, season);
-
- for (const candidate of currentPage.data.Page.media) candidates.push(candidate);
-
- while (currentPage['data']['Page']['pageInfo']['hasNextPage']) {
- for (const candidate of currentPage.data.Page.media) candidates.push(candidate);
-
- page += 1;
- currentPage = await prequelsPage(page, anilistAuthorisation, year, season);
- }
-
- for (const candidate of currentPage.data.Page.media) candidates.push(candidate);
-
- const media: MediaPrequel[] = [];
-
- for (const candidate of candidates) {
- let episodes = 0;
- let seen = 0;
-
- for (const relation of candidate.relations.edges) {
- if (relation.relationType === 'PREQUEL' || relation.relationType === 'PARENT') {
- if (
- relation.node.mediaListEntry === null ||
- relation.node.mediaListEntry.status !== 'COMPLETED'
- ) {
- episodes += relation.node.episodes;
-
- if (relation.node.mediaListEntry !== null)
- seen += relation.node.mediaListEntry.progress || 0;
- }
- }
- }
-
- if (media.some((m) => m.id === candidate.id)) continue;
-
- if (episodes !== 0)
- media.push({
- id: candidate.id,
- title: candidate.title,
- episodes,
- seen,
- nextAiringEpisode: candidate.nextAiringEpisode,
- startDate: candidate.startDate,
- coverImage: candidate.coverImage
- });
- }
-
- return media;
-};
diff --git a/src/lib/AniList/schedule.ts b/src/lib/AniList/schedule.ts
deleted file mode 100644
index 0db130ec..00000000
--- a/src/lib/AniList/schedule.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import type { Media, MediaTitle } from './media';
-
-interface SchedulePage {
- data: {
- Page: {
- media: {
- title: MediaTitle;
- synonyms: string[];
- id: number;
- idMal: number;
- episodes: number;
- nextAiringEpisode?: {
- episode: number;
- airingAt?: number;
- };
- coverImage: {
- extraLarge: string;
- };
- }[];
- pageInfo: {
- hasNextPage: boolean;
- };
- };
- };
-}
-
-const schedulePage = async (
- page: number,
- year: number,
- season: 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'
-): Promise<SchedulePage> =>
- await (
- await fetch('https://graphql.anilist.co', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json'
- },
- body: JSON.stringify({
- query: `{
- Page(page: ${page}) {
- pageInfo {
- hasNextPage
- }
- media(season: ${season}, seasonYear: ${year}) {
- id idMal episodes synonyms
- title { english romaji native }
- nextAiringEpisode { episode airingAt }
- coverImage { extraLarge }
- }
- }
-}`
- })
- })
- ).json();
-
-type Season = 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL';
-
-export const scheduleMediaListCollection = async (
- year: number,
- season: Season,
- includeLastSeason = false
-) => {
- const scheduledMedia = [];
- let page = 1;
- let currentPage = await schedulePage(page, year, season);
-
- for (const candidate of currentPage.data.Page.media) scheduledMedia.push(candidate);
-
- while (currentPage['data']['Page']['pageInfo']['hasNextPage']) {
- for (const candidate of currentPage.data.Page.media) scheduledMedia.push(candidate);
-
- page += 1;
- currentPage = await schedulePage(page, year, season);
- }
-
- for (const candidate of currentPage.data.Page.media) scheduledMedia.push(candidate);
-
- if (includeLastSeason) {
- const lastSeason = {
- WINTER: 'FALL',
- SPRING: 'WINTER',
- SUMMER: 'SPRING',
- FALL: 'SUMMER'
- }[season];
-
- const lastSeasonYear = season === 'WINTER' ? year - 1 : year;
-
- let page = 1;
- let currentPage = await schedulePage(page, lastSeasonYear, lastSeason as Season);
-
- for (const candidate of currentPage.data.Page.media) scheduledMedia.push(candidate);
-
- while (currentPage['data']['Page']['pageInfo']['hasNextPage']) {
- for (const candidate of currentPage.data.Page.media) scheduledMedia.push(candidate);
-
- page += 1;
- currentPage = await schedulePage(page, lastSeasonYear, lastSeason as Season);
- }
-
- for (const candidate of currentPage.data.Page.media) scheduledMedia.push(candidate);
- }
-
- return scheduledMedia as Partial<Media[]>;
-};
diff --git a/src/lib/AniList/user.ts b/src/lib/AniList/user.ts
deleted file mode 100644
index d2f06354..00000000
--- a/src/lib/AniList/user.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-export interface User {
- name: string;
- id: number;
- statistics: {
- anime: {
- count: number;
- meanScore: number;
- minutesWatched: number;
- episodesWatched: number;
- };
- manga: {
- count: number;
- meanScore: number;
- chaptersRead: number;
- volumesRead: number;
- };
- };
- avatar: {
- large: string;
- };
- bannerImage: string;
-}
-
-export interface FullUser {
- id: number;
- name: string;
- avatar: {
- large: string;
- medium: string;
- };
- bans: JSON;
- bannerImage: string;
- siteUrl: string;
- donatorTier: number;
- donatorBadge: string;
- moderatorRoles: string[];
- createAt: number;
- updatedAt: number;
- previousNames: {
- name: string;
- createdAt: number;
- updatedAt: string;
- }[];
- about: string;
-}
-
-export const user = async (username: string, id = false): Promise<User> =>
- (
- await (
- await fetch('https://graphql.anilist.co', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json'
- },
- body: JSON.stringify({
- query: `{ User(${id ? `id: ${username}` : `name: "${username}"`}) {
- name id bannerImage avatar { large } statistics {
- anime {
- count meanScore minutesWatched episodesWatched
- }
- manga {
- count meanScore chaptersRead volumesRead
- }
- }
- } }`
- })
- })
- ).json()
- )['data']['User'];
-
-export const dumpUser = async (username: string): Promise<FullUser> =>
- (
- await (
- await fetch('https://graphql.anilist.co', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json'
- },
- body: JSON.stringify({
- query: `{ User(name: "${username}") {
- id name about
- avatar { large medium }
- bannerImage bans siteUrl donatorTier donatorBadge moderatorRoles
- createdAt updatedAt
- previousNames { name createdAt updatedAt }
- } }`
- })
- })
- ).json()
- )['data']['User'];
diff --git a/src/lib/AniList/wrapped.ts b/src/lib/AniList/wrapped.ts
deleted file mode 100644
index 72c81cd8..00000000
--- a/src/lib/AniList/wrapped.ts
+++ /dev/null
@@ -1,301 +0,0 @@
-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
- };
-};