aboutsummaryrefslogtreecommitdiff
path: root/src/lib/Data/AniList
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-03-01 16:20:51 -0800
committerFuwn <[email protected]>2026-03-01 16:21:02 -0800
commiteae5d24d9e79e59a19d4721caaeaa0ca650ecb33 (patch)
tree1b685bb248e051dfa26d2bfdebe6689402dd93c5 /src/lib/Data/AniList
parentchore(tooling): remove legacy eslint and prettier (diff)
downloaddue.moe-eae5d24d9e79e59a19d4721caaeaa0ca650ecb33.tar.xz
due.moe-eae5d24d9e79e59a19d4721caaeaa0ca650ecb33.zip
chore(biome): drop formatter style overrides
Diffstat (limited to 'src/lib/Data/AniList')
-rw-r--r--src/lib/Data/AniList/activity.ts526
-rw-r--r--src/lib/Data/AniList/character.ts142
-rw-r--r--src/lib/Data/AniList/follow.ts82
-rw-r--r--src/lib/Data/AniList/following.ts88
-rw-r--r--src/lib/Data/AniList/forum.ts122
-rw-r--r--src/lib/Data/AniList/identity.ts50
-rw-r--r--src/lib/Data/AniList/media.ts864
-rw-r--r--src/lib/Data/AniList/notifications.ts160
-rw-r--r--src/lib/Data/AniList/prequels.ts282
-rw-r--r--src/lib/Data/AniList/schedule.ts182
-rw-r--r--src/lib/Data/AniList/user.ts145
-rw-r--r--src/lib/Data/AniList/wrapped.ts523
12 files changed, 1660 insertions, 1506 deletions
diff --git a/src/lib/Data/AniList/activity.ts b/src/lib/Data/AniList/activity.ts
index 9a8d13ba..11466cc5 100644
--- a/src/lib/Data/AniList/activity.ts
+++ b/src/lib/Data/AniList/activity.ts
@@ -1,309 +1,353 @@
-import { database } from '$lib/Database/IDB/activities';
-import type { User } from './follow';
-import type { AniListAuthorisation, UserIdentity } from './identity';
+import { database } from "$lib/Database/IDB/activities";
+import type { User } from "./follow";
+import type { AniListAuthorisation, UserIdentity } from "./identity";
export interface ActivityHistoryEntry {
- date: number;
- amount: number;
+ date: number;
+ amount: number;
}
interface ActivityHistoryOptions {
- stats: {
- activityHistory: ActivityHistoryEntry[];
- };
- options: {
- timezone: string;
- };
+ stats: {
+ activityHistory: ActivityHistoryEntry[];
+ };
+ options: {
+ timezone: string;
+ };
}
interface LastActivity {
- date: Date;
- timezone: string;
+ date: Date;
+ timezone: string;
}
export const fillMissingDays = (
- inputActivities: ActivityHistoryEntry[],
- startOfYear = false,
- year = new Date().getFullYear()
+ 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 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[] = []
+ 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;
+ 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
+ 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}) {
+ 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'];
+ ${authorisation ? "options { timezone }" : ""}
+ } }`,
+ }),
+ })
+ ).json()
+ )["data"]["User"];
};
const convertToTimezoneOffset = (timeStr: string) => {
- const [hours, minutes] = timeStr.split(':');
- let totalMinutes = parseInt(hours, 10) * 60 + parseInt(minutes, 10);
+ const [hours, minutes] = timeStr.split(":");
+ let totalMinutes = parseInt(hours, 10) * 60 + parseInt(minutes, 10);
- totalMinutes = -totalMinutes;
+ totalMinutes = -totalMinutes;
- return totalMinutes;
+ return totalMinutes;
};
export const activityHistory = async (
- userIdentity: UserIdentity,
- authorisation?: AniListAuthorisation
+ userIdentity: UserIdentity,
+ authorisation?: AniListAuthorisation,
): Promise<ActivityHistoryEntry[]> =>
- (await activityHistoryOptions(userIdentity, authorisation)).stats.activityHistory;
+ (await activityHistoryOptions(userIdentity, authorisation)).stats
+ .activityHistory;
export const lastActivityDate = async (
- userIdentity: UserIdentity,
- authorisation: AniListAuthorisation
+ 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 };
+ 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;
- }[];
- };
- };
+ data: {
+ Page: {
+ pageInfo: {
+ hasNextPage: boolean;
+ };
+ activities: {
+ createdAt: number;
+ }[];
+ };
+ };
}
const activitiesPage = async (
- page: number,
- anilistAuthorisation: AniListAuthorisation,
- userIdentity: UserIdentity,
- year = new Date().getFullYear()
+ 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: `{
+ 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)}) {
+ 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();
+ }`,
+ }),
+ })
+ ).json();
export const fullActivityHistory = async (
- anilistAuthorisation: AniListAuthorisation,
- userIdentity: UserIdentity,
- year = new Date().getFullYear(),
- disableLoopingActivityCounter = false
+ anilistAuthorisation: AniListAuthorisation,
+ userIdentity: UserIdentity,
+ year = new Date().getFullYear(),
+ disableLoopingActivityCounter = false,
): Promise<ActivityHistoryEntry[]> => {
- const activities = [];
- let page = 1;
- let currentDatabasePage = await database.activities.get(page);
- let currentPage: ActivitiesPage;
-
- 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']) {
- if (disableLoopingActivityCounter) break;
-
- 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;
+ const activities = [];
+ let page = 1;
+ let currentDatabasePage = await database.activities.get(page);
+ let currentPage: ActivitiesPage;
+
+ 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"]) {
+ if (disableLoopingActivityCounter) break;
+
+ 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: `{
+ 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();
+ }`,
+ }),
+ })
+ ).json();
- return activityResponse['data']['Activity']['likes'];
+ 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: `{
+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) }' : ''} }
+ ... on TextActivity { text(asHtml: true) ${replies ? "replies { text(asHtml: true) }" : ""} }
}
- }`
- })
- })
- ).json();
- let text = activityResponse['data']['Activity']['text'];
+ }`,
+ }),
+ })
+ ).json();
+ let text = activityResponse["data"]["Activity"]["text"];
- if (replies)
- for (const reply of activityResponse['data']['Activity']['replies']) text += reply.text;
+ if (replies)
+ for (const reply of activityResponse["data"]["Activity"]["replies"])
+ text += reply.text;
- return text;
+ return text;
};
diff --git a/src/lib/Data/AniList/character.ts b/src/lib/Data/AniList/character.ts
index a7ade17e..3c53b91b 100644
--- a/src/lib/Data/AniList/character.ts
+++ b/src/lib/Data/AniList/character.ts
@@ -1,88 +1,88 @@
export interface Character {
- name: {
- full: string;
- };
- id: number;
- image: {
- large: string;
- medium: string;
- };
+ name: {
+ full: string;
+ };
+ id: number;
+ image: {
+ large: string;
+ medium: string;
+ };
}
export interface CharactersPage {
- data: {
- Page: {
- characters: Character[];
- pageInfo: {
- hasNextPage: boolean;
- currentPage: number;
- };
- };
- };
+ 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) {
+ 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 medium } }
pageInfo { hasNextPage currentPage }
- } }`
- })
- })
- ).json();
+ } }`,
+ }),
+ })
+ ).json();
export const todaysCharacterBirthdays = async (): Promise<Character[]> => {
- const characters = [];
- let page = 1;
- let currentPage = await charactersPage(page);
+ 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'],
- medium: character['image']['medium']
- }
- });
+ for (const character of currentPage["data"]["Page"]["characters"])
+ characters.push({
+ id: character["id"],
+ name: {
+ full: character["name"]["full"],
+ },
+ image: {
+ large: character["image"]["large"],
+ medium: character["image"]["medium"],
+ },
+ });
- 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'],
- medium: character['image']['medium']
- }
- });
+ 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"],
+ medium: character["image"]["medium"],
+ },
+ });
- page += 1;
- currentPage = await charactersPage(page);
- }
+ 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'],
- medium: character['image']['medium']
- }
- });
+ for (const character of currentPage["data"]["Page"]["characters"])
+ characters.push({
+ id: character["id"],
+ name: {
+ full: character["name"]["full"],
+ },
+ image: {
+ large: character["image"]["large"],
+ medium: character["image"]["medium"],
+ },
+ });
- return characters;
+ return characters;
};
diff --git a/src/lib/Data/AniList/follow.ts b/src/lib/Data/AniList/follow.ts
index d450c57f..6aec8b9a 100644
--- a/src/lib/Data/AniList/follow.ts
+++ b/src/lib/Data/AniList/follow.ts
@@ -1,49 +1,49 @@
-import type { AniListAuthorisation } from './identity';
+import type { AniListAuthorisation } from "./identity";
export interface User {
- id: number;
- name: string;
- isFollowing: boolean;
- isFollower: boolean;
- avatar: {
- large: string;
- medium: string;
- };
+ id: number;
+ name: string;
+ isFollowing: boolean;
+ isFollower: boolean;
+ avatar: {
+ large: string;
+ medium: string;
+ };
}
export const toggleFollow = async (
- anilistAuthorisation: AniListAuthorisation,
- username: string
+ 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();
+ 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'];
+ 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/Data/AniList/following.ts b/src/lib/Data/AniList/following.ts
index bec1f53c..60bb3ccc 100644
--- a/src/lib/Data/AniList/following.ts
+++ b/src/lib/Data/AniList/following.ts
@@ -1,60 +1,66 @@
-import { user, type User } from './user';
+import { user, type User } from "./user";
export interface FollowingPage {
- data: {
- Page: {
- pageInfo: {
- hasNextPage: boolean;
- };
- following: Partial<User>[];
- };
- };
+ 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: `{
+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();
+ }`,
+ }),
+ })
+ ).json();
export const followers = async (name: string): Promise<Partial<User>[]> => {
- const activities = [];
- let page = 1;
- const userData = await user(name);
+ const activities = [];
+ let page = 1;
+ const userData = await user(name);
- if (!userData) throw new Error(`User not found: ${name}`);
+ if (!userData) throw new Error(`User not found: ${name}`);
- const id = userData.id;
- let currentPage = await followingPage(page, id);
+ const id = userData.id;
+ let currentPage = await followingPage(page, id);
- for (const activity of currentPage.data.Page.following) activities.push(activity);
+ 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);
+ while (currentPage["data"]["Page"]["pageInfo"]["hasNextPage"]) {
+ for (const activity of currentPage.data.Page.following)
+ activities.push(activity);
- page += 1;
- currentPage = await followingPage(page, id);
- }
+ page += 1;
+ currentPage = await followingPage(page, id);
+ }
- for (const activity of currentPage.data.Page.following) activities.push(activity);
+ 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]];
- }
+ 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;
+ return activities;
};
diff --git a/src/lib/Data/AniList/forum.ts b/src/lib/Data/AniList/forum.ts
index c8514fe1..05cb508c 100644
--- a/src/lib/Data/AniList/forum.ts
+++ b/src/lib/Data/AniList/forum.ts
@@ -1,83 +1,85 @@
-import type { User } from './follow';
-import { user } from './user';
+import type { User } from "./follow";
+import { user } from "./user";
export interface Thread {
- id: number;
- title: string;
- createdAt: number;
- mediaCategories: {
- coverImage: {
- extraLarge: string;
- medium: string;
- };
- }[];
- categories: {
- name: string;
- }[];
+ id: number;
+ title: string;
+ createdAt: number;
+ mediaCategories: {
+ coverImage: {
+ extraLarge: string;
+ medium: string;
+ };
+ }[];
+ categories: {
+ name: string;
+ }[];
}
export interface ThreadPage {
- data: {
- Page: {
- threads: Thread[];
- pageInfo: {
- hasNextPage: boolean;
- currentPage: number;
- };
- };
- };
+ 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}) {
+ 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 medium } } categories { name } }
pageInfo { hasNextPage }
-} }`
- })
- })
- ).json();
+} }`,
+ }),
+ })
+ ).json();
export const threads = async (username: string): Promise<Thread[]> => {
- const allThreads = [];
- const userData = await user(username);
+ const allThreads = [];
+ const userData = await user(username);
- if (!userData) throw new Error(`User not found: ${username}`);
+ if (!userData) throw new Error(`User not found: ${username}`);
- const userId = userData.id;
- let page = 1;
- let currentPage = await threadPage(page, userId);
+ const userId = userData.id;
+ let page = 1;
+ let currentPage = await threadPage(page, userId);
- for (const thread of currentPage.data.Page.threads) allThreads.push(thread);
+ for (const thread of currentPage.data.Page.threads) allThreads.push(thread);
- while (currentPage.data.Page.pageInfo.hasNextPage) {
- page += 1;
- currentPage = await threadPage(page, userId);
+ while (currentPage.data.Page.pageInfo.hasNextPage) {
+ page += 1;
+ currentPage = await threadPage(page, userId);
- for (const thread of currentPage.data.Page.threads) allThreads.push(thread);
- }
+ for (const thread of currentPage.data.Page.threads) allThreads.push(thread);
+ }
- return allThreads;
+ 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();
+ 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'];
+ return activityResponse["data"]["Thread"]["likes"];
};
diff --git a/src/lib/Data/AniList/identity.ts b/src/lib/Data/AniList/identity.ts
index 23f47a2e..eacc2ae4 100644
--- a/src/lib/Data/AniList/identity.ts
+++ b/src/lib/Data/AniList/identity.ts
@@ -1,34 +1,36 @@
export interface UserIdentity {
- id: number;
- name: string;
- avatar: string;
+ id: number;
+ name: string;
+ avatar: string;
}
export interface AniListAuthorisation {
- tokenType: string;
- accessToken: string;
- expiresIn: number;
- refreshToken: string;
+ tokenType: string;
+ accessToken: string;
+ expiresIn: number;
+ refreshToken: string;
}
export const userIdentity = async (
- anilistAuthorisation: AniListAuthorisation
+ 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();
+ 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']
- };
+ return {
+ id: userIdResponse["data"]["Viewer"]["id"],
+ name: userIdResponse["data"]["Viewer"]["name"],
+ avatar: userIdResponse["data"]["Viewer"]["avatar"]["large"],
+ };
};
diff --git a/src/lib/Data/AniList/media.ts b/src/lib/Data/AniList/media.ts
index 2fbbe3f9..7a9fe810 100644
--- a/src/lib/Data/AniList/media.ts
+++ b/src/lib/Data/AniList/media.ts
@@ -1,152 +1,165 @@
-import type { AniListAuthorisation } from '$lib/Data/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';
-import type { PrequelRelation, PrequelRelations } from './prequels';
+import type { AniListAuthorisation } from "$lib/Data/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";
+import type { PrequelRelation, PrequelRelations } from "./prequels";
export enum Type {
- Anime,
- Manga
+ Anime,
+ Manga,
}
export interface MediaTitle {
- romaji: string;
- english: string;
- native: string;
+ romaji: string;
+ english: string;
+ native: string;
}
-export type MediaStatus = 'FINISHED' | 'RELEASING' | 'NOT_YET_RELEASED' | 'CANCELLED' | 'HIATUS';
+export type MediaStatus =
+ | "FINISHED"
+ | "RELEASING"
+ | "NOT_YET_RELEASED"
+ | "CANCELLED"
+ | "HIATUS";
export type MediaListEntryStatus =
- | 'CURRENT'
- | 'PLANNING'
- | 'COMPLETED'
- | 'DROPPED'
- | 'PAUSED'
- | 'REPEATING';
-export type MediaType = 'ANIME' | 'MANGA';
+ | "CURRENT"
+ | "PLANNING"
+ | "COMPLETED"
+ | "DROPPED"
+ | "PAUSED"
+ | "REPEATING";
+export type MediaType = "ANIME" | "MANGA";
export type MediaFormat =
- | 'TV'
- | 'TV_SHORT'
- | 'MOVIE'
- | 'SPECIAL'
- | 'OVA'
- | 'ONA'
- | 'MUSIC'
- | 'MANGA'
- | 'NOVEL'
- | 'ONE_SHOT';
-export type MediaSeason = 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL';
+ | "TV"
+ | "TV_SHORT"
+ | "MOVIE"
+ | "SPECIAL"
+ | "OVA"
+ | "ONA"
+ | "MUSIC"
+ | "MANGA"
+ | "NOVEL"
+ | "ONE_SHOT";
+export type MediaSeason = "WINTER" | "SPRING" | "SUMMER" | "FALL";
export interface Media {
- id: number;
- idMal: number;
- status: MediaStatus;
- type: MediaType;
- episodes: number;
- chapters: number;
- volumes: number;
- duration: number;
- format: MediaFormat;
- title: MediaTitle;
- nextAiringEpisode?: {
- episode: number;
- airingAt?: number;
- nativeAiringAt?: number;
- nativeEpisode?: number;
- };
- synonyms: string[];
- mediaListEntry?: {
- progress: number;
- progressVolumes: number;
- status: MediaListEntryStatus;
- score: number;
- repeat: number;
- startedAt: {
- year: number;
- month: number;
- day: number;
- };
- completedAt: {
- year: number;
- month: number;
- day: number;
- };
- createdAt: number;
- updatedAt: number;
- customLists: Record<string, boolean>;
- };
- startDate: {
- year: number;
- month: number;
- };
- endDate: {
- year: number;
- month: number;
- };
- coverImage: {
- extraLarge: string;
- medium: string;
- };
- tags: {
- name: string;
- rank: number;
- }[];
- genres: string[];
- season: MediaSeason;
- isAdult: boolean;
- relations: PrequelRelations;
+ id: number;
+ idMal: number;
+ status: MediaStatus;
+ type: MediaType;
+ episodes: number;
+ chapters: number;
+ volumes: number;
+ duration: number;
+ format: MediaFormat;
+ title: MediaTitle;
+ nextAiringEpisode?: {
+ episode: number;
+ airingAt?: number;
+ nativeAiringAt?: number;
+ nativeEpisode?: number;
+ };
+ synonyms: string[];
+ mediaListEntry?: {
+ progress: number;
+ progressVolumes: number;
+ status: MediaListEntryStatus;
+ score: number;
+ repeat: number;
+ startedAt: {
+ year: number;
+ month: number;
+ day: number;
+ };
+ completedAt: {
+ year: number;
+ month: number;
+ day: number;
+ };
+ createdAt: number;
+ updatedAt: number;
+ customLists: Record<string, boolean>;
+ };
+ startDate: {
+ year: number;
+ month: number;
+ };
+ endDate: {
+ year: number;
+ month: number;
+ };
+ coverImage: {
+ extraLarge: string;
+ medium: string;
+ };
+ tags: {
+ name: string;
+ rank: number;
+ }[];
+ genres: string[];
+ season: MediaSeason;
+ isAdult: boolean;
+ relations: PrequelRelations;
}
export const flattenLists = (
- lists: { name: string; entries: { media: Media }[] }[],
- all = false
+ 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);
+ 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, options: CollectionOptions = {}) =>
- `{
+const collectionQueryTemplate = (
+ type: Type,
+ userId: number,
+ options: CollectionOptions = {},
+) =>
+ `{
MediaListCollection(
userId: ${userId},
- type: ${type === Type.Anime ? 'ANIME' : 'MANGA'}
- ${options.includeCompleted ? '' : ', status_not_in: [ COMPLETED ]'}) {
+ type: ${type === Type.Anime ? "ANIME" : "MANGA"}
+ ${options.includeCompleted ? "" : ", status_not_in: [ COMPLETED ]"}) {
lists {
name entries {
media {
@@ -162,8 +175,8 @@ const collectionQueryTemplate = (type: Type, userId: number, options: Collection
endDate { year month }
coverImage { extraLarge medium }
${
- options.includeRelations
- ? `
+ options.includeRelations
+ ? `
relations {
edges {
relationType
@@ -177,8 +190,8 @@ const collectionQueryTemplate = (type: Type, userId: number, options: Collection
}
}
`
- : ''
- }
+ : ""
+ }
}
}
}
@@ -186,190 +199,211 @@ const collectionQueryTemplate = (type: Type, userId: number, options: Collection
}`;
interface CollectionOptions {
- includeCompleted?: boolean;
- forcePrune?: boolean;
- all?: boolean;
- addNotification?: (preferences: Options) => void;
- notificationType?: string;
- includeRelations?: boolean;
+ includeCompleted?: boolean;
+ forcePrune?: boolean;
+ all?: boolean;
+ addNotification?: (preferences: Options) => void;
+ notificationType?: string;
+ includeRelations?: boolean;
}
const assignDefaultOptions = (options: CollectionOptions) => {
- const nonNullOptions: CollectionOptions = {
- includeCompleted: false,
- forcePrune: false,
- all: false,
- addNotification: undefined,
- notificationType: undefined,
- includeRelations: false
- };
-
- 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;
- if (options.includeRelations !== undefined)
- nonNullOptions.includeRelations = options.includeRelations;
-
- return nonNullOptions;
+ const nonNullOptions: CollectionOptions = {
+ includeCompleted: false,
+ forcePrune: false,
+ all: false,
+ addNotification: undefined,
+ notificationType: undefined,
+ includeRelations: false,
+ };
+
+ 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;
+ if (options.includeRelations !== undefined)
+ nonNullOptions.includeRelations = options.includeRelations;
+
+ return nonNullOptions;
};
export const mediaListCollection = async (
- anilistAuthorisation: AniListAuthorisation,
- userIdentity: UserIdentity,
- type: Type,
- mediaCache: string | undefined,
- currentLastPruneAt: string | number,
- inputOptions: CollectionOptions = {}
+ 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 = 0;
-
- 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)
- })
- })
- ).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);
+ if (userIdentity.id === -1 || userIdentity.id === -2) return [];
+
+ const options = assignDefaultOptions(inputOptions);
+
+ let currentCacheMinutes = 0;
+
+ 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),
+ }),
+ })
+ ).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, {})
- })
- })
- ).json()
- )['data']['MediaListCollection']['lists']
- );
+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, {}),
+ }),
+ })
+ ).json()
+ )["data"]["MediaListCollection"]["lists"],
+ );
const countMedian = (guesses: number[]) => {
- guesses.sort((a: number, b: number) => a - b);
+ guesses.sort((a: number, b: number) => a - b);
- const mid = Math.floor(guesses.length / 2);
+ const mid = Math.floor(guesses.length / 2);
- return guesses.length % 2 !== 0 ? guesses[mid] : (guesses[mid - 1] + guesses[mid]) / 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);
+ 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;
+ 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);
+ 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;
+ const frequency: { [key: number]: number } = {};
+ let maximumFrequency = 0;
+ let mode = 0;
- for (const guess of guesses) {
- frequency[guess] = (frequency[guess] || 0) + 1;
+ for (const guess of guesses) {
+ frequency[guess] = (frequency[guess] || 0) + 1;
- if (frequency[guess] > maximumFrequency) {
- maximumFrequency = frequency[guess];
- mode = guess;
- }
- }
+ if (frequency[guess] > maximumFrequency) {
+ maximumFrequency = frequency[guess];
+ mode = guess;
+ }
+ }
- return mode;
+ return mode;
};
export const recentMediaActivities = async (
- userIdentity: UserIdentity,
- media: Media,
- method: 'median' | 'iqr_median' | 'iqr_mode' | 'mode'
+ 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: `{
+ 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 }
@@ -377,137 +411,139 @@ export const recentMediaActivities = async (
}
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: number;
-
- 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;
+ }`,
+ }),
+ })
+ ).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: number;
+
+ 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: `{
+ (
+ 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'];
+ }`,
+ }),
+ })
+ ).json()
+ )["data"]["Media"]["coverImage"]["extraLarge"];
export interface UnwatchedRelationMap {
- media: Media;
- unwatchedRelations: PrequelRelation[];
+ media: Media;
+ unwatchedRelations: PrequelRelation[];
}
export const filterRelations = (media: Media[], includeSideStories = false) => {
- const unwatchedRelationsMap: UnwatchedRelationMap[] = [];
-
- for (const mediaItem of media) {
- const sequels = mediaItem.relations.edges.filter(
- (relation: PrequelRelation) =>
- (relation.relationType === 'SEQUEL' ||
- (relation.relationType === 'SIDE_STORY' && includeSideStories)) &&
- !media.some((mediaItem) => mediaItem.id === relation.node.id) &&
- (relation.node.mediaListEntry
- ? relation.node.mediaListEntry.status !== 'COMPLETED'
- : true) &&
- relation.node.episodes &&
- relation.node.status !== 'NOT_YET_RELEASED' &&
- relation.node.status !== 'CANCELLED'
- );
-
- if (sequels.length > 0) {
- unwatchedRelationsMap.push({
- media: mediaItem,
- unwatchedRelations: sequels
- });
- }
- }
-
- return unwatchedRelationsMap;
+ const unwatchedRelationsMap: UnwatchedRelationMap[] = [];
+
+ for (const mediaItem of media) {
+ const sequels = mediaItem.relations.edges.filter(
+ (relation: PrequelRelation) =>
+ (relation.relationType === "SEQUEL" ||
+ (relation.relationType === "SIDE_STORY" && includeSideStories)) &&
+ !media.some((mediaItem) => mediaItem.id === relation.node.id) &&
+ (relation.node.mediaListEntry
+ ? relation.node.mediaListEntry.status !== "COMPLETED"
+ : true) &&
+ relation.node.episodes &&
+ relation.node.status !== "NOT_YET_RELEASED" &&
+ relation.node.status !== "CANCELLED",
+ );
+
+ if (sequels.length > 0) {
+ unwatchedRelationsMap.push({
+ media: mediaItem,
+ unwatchedRelations: sequels,
+ });
+ }
+ }
+
+ return unwatchedRelationsMap;
};
diff --git a/src/lib/Data/AniList/notifications.ts b/src/lib/Data/AniList/notifications.ts
index 184ffe95..a12fd402 100644
--- a/src/lib/Data/AniList/notifications.ts
+++ b/src/lib/Data/AniList/notifications.ts
@@ -1,94 +1,100 @@
-import { database } from '$lib/Database/IDB/user';
+import { database } from "$lib/Database/IDB/user";
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';
+ 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} {
+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 {
+ 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();
+ }`,
+ )}`;
+ 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;
+ if (data["errors"]) return null;
- return data['data']['Page']['notifications'];
+ return data["data"]["Page"]["notifications"];
};
export const isNotificationQueued = (
- recentNotifications: Notification[] | null,
- lastNotificationID: number | null
+ recentNotifications: Notification[] | null,
+ lastNotificationID: number | null,
) =>
- recentNotifications &&
- recentNotifications.length > 0 &&
- (recentNotifications[0].id > (lastNotificationID as number) ||
- new Date(recentNotifications[0].createdAt * 1000).getTime() + 30000 > new Date().getTime());
+ recentNotifications &&
+ recentNotifications.length > 0 &&
+ (recentNotifications[0].id > (lastNotificationID as number) ||
+ new Date(recentNotifications[0].createdAt * 1000).getTime() + 30000 >
+ new Date().getTime());
export const updateLastNotificationID = async (
- userID: number,
- recentNotifications: Notification[]
-) => await database.users.update(userID, { lastNotificationID: recentNotifications[0].id });
+ userID: number,
+ recentNotifications: Notification[],
+) =>
+ await database.users.update(userID, {
+ lastNotificationID: recentNotifications[0].id,
+ });
diff --git a/src/lib/Data/AniList/prequels.ts b/src/lib/Data/AniList/prequels.ts
index 47e83f0e..b521616f 100644
--- a/src/lib/Data/AniList/prequels.ts
+++ b/src/lib/Data/AniList/prequels.ts
@@ -1,97 +1,102 @@
-import type { AniListAuthorisation } from './identity';
-import type { MediaListEntryStatus, MediaSeason, MediaStatus, MediaTitle } from './media';
+import type { AniListAuthorisation } from "./identity";
+import type {
+ MediaListEntryStatus,
+ MediaSeason,
+ MediaStatus,
+ 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;
- medium: string;
- };
+ id: number;
+ title: MediaTitle;
+ episodes: number;
+ seen: number;
+ nextAiringEpisode?: {
+ episode: number;
+ airingAt?: number;
+ };
+ startDate: {
+ year: number;
+ month: number;
+ day: number;
+ };
+ coverImage: {
+ extraLarge: string;
+ medium: string;
+ };
}
export interface PrequelRelationNode {
- id: number;
- title: MediaTitle;
- episodes: number;
- status: MediaStatus;
- mediaListEntry: {
- status: MediaListEntryStatus;
- progress: number;
- };
- coverImage: {
- extraLarge: string;
- medium: string;
- };
- startDate: {
- year: number;
- };
+ id: number;
+ title: MediaTitle;
+ episodes: number;
+ status: MediaStatus;
+ mediaListEntry: {
+ status: MediaListEntryStatus;
+ progress: number;
+ };
+ coverImage: {
+ extraLarge: string;
+ medium: string;
+ };
+ startDate: {
+ year: number;
+ };
}
export interface PrequelRelation {
- relationType: string;
- node: PrequelRelationNode;
+ relationType: string;
+ node: PrequelRelationNode;
}
export interface PrequelRelations {
- edges: PrequelRelation[];
+ edges: PrequelRelation[];
}
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;
- medium: string;
- };
- }[];
- pageInfo: {
- hasNextPage: boolean;
- };
- };
- };
+ data: {
+ Page: {
+ media: {
+ title: MediaTitle;
+ id: number;
+ relations: PrequelRelations;
+ nextAiringEpisode?: {
+ episode: number;
+ airingAt?: number;
+ };
+ startDate: {
+ year: number;
+ month: number;
+ day: number;
+ };
+ coverImage: {
+ extraLarge: string;
+ medium: string;
+ };
+ }[];
+ pageInfo: {
+ hasNextPage: boolean;
+ };
+ };
+ };
}
const prequelsPage = async (
- page: number,
- anilistAuthorisation: AniListAuthorisation,
- year: number,
- season: MediaSeason
+ page: number,
+ anilistAuthorisation: AniListAuthorisation,
+ year: number,
+ season: MediaSeason,
): 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: `{
+ 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
@@ -124,64 +129,75 @@ const prequelsPage = async (
}
}
}
-}`
- })
- })
- ).json();
+}`,
+ }),
+ })
+ ).json();
export const prequels = async (
- anilistAuthorisation: AniListAuthorisation,
- year: number,
- season: 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'
+ 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;
+ 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/Data/AniList/schedule.ts b/src/lib/Data/AniList/schedule.ts
index 18f02579..eb70a03b 100644
--- a/src/lib/Data/AniList/schedule.ts
+++ b/src/lib/Data/AniList/schedule.ts
@@ -1,44 +1,44 @@
-import type { Media, MediaTitle } from './media';
+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;
- medium: string;
- };
- }[];
- pageInfo: {
- hasNextPage: boolean;
- };
- };
- };
+ data: {
+ Page: {
+ media: {
+ title: MediaTitle;
+ synonyms: string[];
+ id: number;
+ idMal: number;
+ episodes: number;
+ nextAiringEpisode?: {
+ episode: number;
+ airingAt?: number;
+ };
+ coverImage: {
+ extraLarge: string;
+ medium: string;
+ };
+ }[];
+ pageInfo: {
+ hasNextPage: boolean;
+ };
+ };
+ };
}
const schedulePage = async (
- page: number,
- year: number,
- season: 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'
+ 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: `{
+ 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
@@ -50,57 +50,71 @@ const schedulePage = async (
coverImage { extraLarge medium }
}
}
-}`
- })
- })
- ).json();
+}`,
+ }),
+ })
+ ).json();
-type Season = 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL';
+type Season = "WINTER" | "SPRING" | "SUMMER" | "FALL";
export const scheduleMediaListCollection = async (
- year: number,
- season: Season,
- includeLastSeason = false
+ 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[]>;
+ 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/Data/AniList/user.ts b/src/lib/Data/AniList/user.ts
index 19de8c45..d16b8612 100644
--- a/src/lib/Data/AniList/user.ts
+++ b/src/lib/Data/AniList/user.ts
@@ -1,60 +1,63 @@
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;
- medium: string;
- };
- bannerImage: string;
+ 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;
+ medium: 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;
+ 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 | null> => {
- const response = 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}"`}) {
+export const user = async (
+ username: string,
+ id = false,
+): Promise<User | null> => {
+ const response = 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 medium } statistics {
anime {
count meanScore minutesWatched episodesWatched
@@ -63,32 +66,32 @@ export const user = async (username: string, id = false): Promise<User | null> =
count meanScore chaptersRead volumesRead
}
}
- } }`
- })
- })
- ).json();
+ } }`,
+ }),
+ })
+ ).json();
- return response['data']['User'] === null ? null : response['data']['User'];
+ return response["data"]["User"] === null ? null : response["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}") {
+ (
+ 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'];
+ } }`,
+ }),
+ })
+ ).json()
+ )["data"]["User"];
diff --git a/src/lib/Data/AniList/wrapped.ts b/src/lib/Data/AniList/wrapped.ts
index 9db502b2..a7b4f829 100644
--- a/src/lib/Data/AniList/wrapped.ts
+++ b/src/lib/Data/AniList/wrapped.ts
@@ -1,72 +1,72 @@
-import type { AniListAuthorisation, UserIdentity } from './identity';
-import type { Media } from './media';
+import type { AniListAuthorisation, UserIdentity } from "./identity";
+import type { Media } from "./media";
export enum SortOptions {
- SCORE,
- MINUTES_WATCHED,
- COUNT
+ 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[];
- }[];
+ 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;
- };
+ statistics: {
+ anime: WrappedMediaFormat;
+ manga: WrappedMediaFormat;
+ };
+ activities: {
+ statusCount: number;
+ messageCount: number;
+ };
+ avatar: {
+ large: string;
+ };
}
const profileActivities = async (
- user: AniListAuthorisation,
- identity: UserIdentity,
- date = new Date(),
- disableLoopingActivityCounter = false
+ user: AniListAuthorisation,
+ identity: UserIdentity,
+ date = new Date(),
+ disableLoopingActivityCounter = false,
) => {
- 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: `{
+ 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)}) {
+ new Date(date.getFullYear(), 0, 1).getTime() / 1000,
+ )}, createdAt_lesser: ${Math.floor(new Date(date.getFullYear(), 7, 1).getTime() / 1000)}) {
... on TextActivity {
type
createdAt
@@ -80,69 +80,69 @@ const profileActivities = async (
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']) {
- if (disableLoopingActivityCounter) break;
-
- 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
- };
+}`,
+ }),
+ })
+ ).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"]) {
+ if (disableLoopingActivityCounter) break;
+
+ 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,
- disableLoopingActivityCounter = false
+ anilistAuthorisation: AniListAuthorisation | undefined,
+ identity: UserIdentity,
+ year = new Date().getFullYear(),
+ skipActivities = false,
+ disableLoopingActivityCounter = 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: `{
+ 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 {
@@ -158,146 +158,171 @@ export const wrapped = async (
}
}
}
-}`
- })
- })
- ).json();
- let statusCountActivities = 0;
- let messageCountActivities = 0;
-
- if (!skipActivities && anilistAuthorisation) {
- const { statusCount, messageCount } = await profileActivities(
- anilistAuthorisation,
- identity,
- new Date(year, 11, 31),
- disableLoopingActivityCounter
- );
-
- statusCountActivities = statusCount;
- messageCountActivities = messageCount;
- }
-
- return {
- statistics: wrappedResponse['data']['User']['statistics'],
- activities: {
- statusCount: statusCountActivities,
- messageCount: messageCountActivities
- },
- avatar: wrappedResponse['data']['User']['avatar']
- };
+}`,
+ }),
+ })
+ ).json();
+ let statusCountActivities = 0;
+ let messageCountActivities = 0;
+
+ if (!skipActivities && anilistAuthorisation) {
+ const { statusCount, messageCount } = await profileActivities(
+ anilistAuthorisation,
+ identity,
+ new Date(year, 11, 31),
+ disableLoopingActivityCounter,
+ );
+
+ 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;
+ 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[] = []
+ 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: Media;
-
- try {
- topGenreMedia = media.find((m) => m.genres.includes(genres[0].genre)) || media[0];
- } catch {
- topGenreMedia = media[0];
- }
-
- let topTagMedia: Media;
-
- 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
- };
+ 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: Media;
+
+ try {
+ topGenreMedia =
+ media.find((m) => m.genres.includes(genres[0].genre)) || media[0];
+ } catch {
+ topGenreMedia = media[0];
+ }
+
+ let topTagMedia: Media;
+
+ 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,
+ };
};