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