diff options
| author | Fuwn <[email protected]> | 2026-03-01 16:20:51 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-03-01 16:21:02 -0800 |
| commit | eae5d24d9e79e59a19d4721caaeaa0ca650ecb33 (patch) | |
| tree | 1b685bb248e051dfa26d2bfdebe6689402dd93c5 /src/lib/Data/AniList | |
| parent | chore(tooling): remove legacy eslint and prettier (diff) | |
| download | due.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.ts | 526 | ||||
| -rw-r--r-- | src/lib/Data/AniList/character.ts | 142 | ||||
| -rw-r--r-- | src/lib/Data/AniList/follow.ts | 82 | ||||
| -rw-r--r-- | src/lib/Data/AniList/following.ts | 88 | ||||
| -rw-r--r-- | src/lib/Data/AniList/forum.ts | 122 | ||||
| -rw-r--r-- | src/lib/Data/AniList/identity.ts | 50 | ||||
| -rw-r--r-- | src/lib/Data/AniList/media.ts | 864 | ||||
| -rw-r--r-- | src/lib/Data/AniList/notifications.ts | 160 | ||||
| -rw-r--r-- | src/lib/Data/AniList/prequels.ts | 282 | ||||
| -rw-r--r-- | src/lib/Data/AniList/schedule.ts | 182 | ||||
| -rw-r--r-- | src/lib/Data/AniList/user.ts | 145 | ||||
| -rw-r--r-- | src/lib/Data/AniList/wrapped.ts | 523 |
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, + }; }; |