diff options
| author | Fuwn <[email protected]> | 2024-10-09 00:41:20 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2024-10-09 00:41:43 -0700 |
| commit | 998b63a35256ac985a5a2714dd1ca451af4dfd8a (patch) | |
| tree | 50796121a9d5ab0330fdc5d7e098bda2860d9726 /src/lib/Data/AniList | |
| parent | feat(graphql): add badgeCount field (diff) | |
| download | due.moe-998b63a35256ac985a5a2714dd1ca451af4dfd8a.tar.xz due.moe-998b63a35256ac985a5a2714dd1ca451af4dfd8a.zip | |
chore(prettier): use spaces instead of tabs
Diffstat (limited to 'src/lib/Data/AniList')
| -rw-r--r-- | src/lib/Data/AniList/activity.ts | 450 | ||||
| -rw-r--r-- | src/lib/Data/AniList/character.ts | 140 | ||||
| -rw-r--r-- | src/lib/Data/AniList/follow.ts | 80 | ||||
| -rw-r--r-- | src/lib/Data/AniList/following.ts | 72 | ||||
| -rw-r--r-- | src/lib/Data/AniList/forum.ts | 104 | ||||
| -rw-r--r-- | src/lib/Data/AniList/identity.ts | 48 | ||||
| -rw-r--r-- | src/lib/Data/AniList/media.ts | 778 | ||||
| -rw-r--r-- | src/lib/Data/AniList/notifications.ts | 104 | ||||
| -rw-r--r-- | src/lib/Data/AniList/prequels.ts | 260 | ||||
| -rw-r--r-- | src/lib/Data/AniList/schedule.ts | 138 | ||||
| -rw-r--r-- | src/lib/Data/AniList/user.ts | 138 | ||||
| -rw-r--r-- | src/lib/Data/AniList/wrapped.ts | 486 |
12 files changed, 1399 insertions, 1399 deletions
diff --git a/src/lib/Data/AniList/activity.ts b/src/lib/Data/AniList/activity.ts index c50ffa62..d10a5da7 100644 --- a/src/lib/Data/AniList/activity.ts +++ b/src/lib/Data/AniList/activity.ts @@ -3,269 +3,269 @@ 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']; + }) + }) + ).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: '' }; + 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) - ); + 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); + date.setDate(date.getDate() + 1); - return { date, timezone: history.options.timezone }; + 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() + 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; + 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: `{ + 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 } } } @@ -273,34 +273,34 @@ export const activityLikes = async (id: number): Promise<Partial<User>[]> => { ... 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: `{ + 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']; + }) + }) + ).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 94b2a717..a7ade17e 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 f03193cb..d450c57f 100644 --- a/src/lib/Data/AniList/follow.ts +++ b/src/lib/Data/AniList/follow.ts @@ -1,49 +1,49 @@ 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 1bba66b9..b4772afa 100644 --- a/src/lib/Data/AniList/following.ts +++ b/src/lib/Data/AniList/following.ts @@ -1,56 +1,56 @@ 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: `{ + 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 id = (await user(name)).id; - let currentPage = await followingPage(page, id); + 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); + 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 b7b838cd..a96b74ca 100644 --- a/src/lib/Data/AniList/forum.ts +++ b/src/lib/Data/AniList/forum.ts @@ -2,75 +2,75 @@ import type { User } from './follow'; import { user } from './user'; export interface Thread { - id: number; - title: string; - createdAt: number; - mediaCategories: { - coverImage: { - extraLarge: string; - medium: string; - }; - }[]; + id: number; + title: string; + createdAt: number; + mediaCategories: { + coverImage: { + extraLarge: string; + medium: 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 } } } pageInfo { hasNextPage } } }` - }) - }) - ).json(); + }) + }) + ).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); + 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); + 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 7214ffa4..23f47a2e 100644 --- a/src/lib/Data/AniList/identity.ts +++ b/src/lib/Data/AniList/identity.ts @@ -1,34 +1,34 @@ 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 d3b38005..f8aeb401 100644 --- a/src/lib/Data/AniList/media.ts +++ b/src/lib/Data/AniList/media.ts @@ -8,133 +8,133 @@ 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 MediaListEntryStatus = - | 'CURRENT' - | 'PLANNING' - | 'COMPLETED' - | 'DROPPED' - | 'PAUSED' - | 'REPEATING'; + | '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'; + | '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; - }; - synonyms: string[]; - mediaListEntry?: { - progress: number; - progressVolumes: number; - status: MediaListEntryStatus; - score: number; - repeat: number; - startedAt: { - year: number; - }; - completedAt: { - year: number; - }; - createdAt: number; - updatedAt: number; - }; - startDate: { - year: number; - }; - endDate: { - year: 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; + }; + synonyms: string[]; + mediaListEntry?: { + progress: number; + progressVolumes: number; + status: MediaListEntryStatus; + score: number; + repeat: number; + startedAt: { + year: number; + }; + completedAt: { + year: number; + }; + createdAt: number; + updatedAt: number; + }; + startDate: { + year: number; + }; + endDate: { + year: 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 = {}) => - `{ + `{ MediaListCollection( userId: ${userId}, type: ${type === Type.Anime ? 'ANIME' : 'MANGA'} @@ -154,8 +154,8 @@ const collectionQueryTemplate = (type: Type, userId: number, options: Collection endDate { year } coverImage { extraLarge medium } ${ - options.includeRelations - ? ` + options.includeRelations + ? ` relations { edges { relationType @@ -169,8 +169,8 @@ const collectionQueryTemplate = (type: Type, userId: number, options: Collection } } ` - : '' - } + : '' + } } } } @@ -178,190 +178,190 @@ 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; - - 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; + + 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'] - ); + 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 } @@ -370,136 +370,136 @@ 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; - - 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; + + 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 bebf1b42..b21a71e4 100644 --- a/src/lib/Data/AniList/notifications.ts +++ b/src/lib/Data/AniList/notifications.ts @@ -1,60 +1,60 @@ 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} { + 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 { + )}`; + 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')} @@ -68,11 +68,11 @@ export const notifications = async (accessToken: string): Promise<Notification[] ${threadNotification('ThreadCommentLikeNotification')} ${threadNotification('ThreadLikeNotification')} } } }` - }) - }) - ).json(); + }) + }) + ).json(); - if (data['errors']) return null; + if (data['errors']) return null; - return data['data']['Page']['notifications']; + return data['data']['Page']['notifications']; }; diff --git a/src/lib/Data/AniList/prequels.ts b/src/lib/Data/AniList/prequels.ts index f995d727..47e83f0e 100644 --- a/src/lib/Data/AniList/prequels.ts +++ b/src/lib/Data/AniList/prequels.ts @@ -2,96 +2,96 @@ 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 @@ -125,63 +125,63 @@ 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 0477e4d8..18f02579 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'; 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 @@ -51,56 +51,56 @@ const schedulePage = async ( } } }` - }) - }) - ).json(); + }) + }) + ).json(); 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); + const scheduledMedia = []; + let page = 1; + let currentPage = await schedulePage(page, year, season); - for (const candidate of currentPage.data.Page.media) scheduledMedia.push(candidate); + 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); + 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); - } + page += 1; + currentPage = await schedulePage(page, year, season); + } - for (const candidate of currentPage.data.Page.media) scheduledMedia.push(candidate); + for (const candidate of currentPage.data.Page.media) scheduledMedia.push(candidate); - if (includeLastSeason) { - const lastSeason = { - WINTER: 'FALL', - SPRING: 'WINTER', - SUMMER: 'SPRING', - FALL: 'SUMMER' - }[season]; + if (includeLastSeason) { + const lastSeason = { + WINTER: 'FALL', + SPRING: 'WINTER', + SUMMER: 'SPRING', + FALL: 'SUMMER' + }[season]; - const lastSeasonYear = season === 'WINTER' ? year - 1 : year; + const lastSeasonYear = season === 'WINTER' ? year - 1 : year; - let page = 1; - let currentPage = await schedulePage(page, lastSeasonYear, lastSeason as Season); + let page = 1; + let currentPage = await schedulePage(page, lastSeasonYear, lastSeason as Season); - for (const candidate of currentPage.data.Page.media) scheduledMedia.push(candidate); + 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); + 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); - } + page += 1; + currentPage = await schedulePage(page, lastSeasonYear, lastSeason as Season); + } - for (const candidate of currentPage.data.Page.media) scheduledMedia.push(candidate); - } + for (const candidate of currentPage.data.Page.media) scheduledMedia.push(candidate); + } - return scheduledMedia as Partial<Media[]>; + return scheduledMedia as Partial<Media[]>; }; diff --git a/src/lib/Data/AniList/user.ts b/src/lib/Data/AniList/user.ts index 40097c13..5b9390db 100644 --- a/src/lib/Data/AniList/user.ts +++ b/src/lib/Data/AniList/user.ts @@ -1,61 +1,61 @@ 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> => - ( - 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}"`}) { + ( + 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 @@ -65,29 +65,29 @@ export const user = async (username: string, id = false): Promise<User> => } } } }` - }) - }) - ).json() - )['data']['User']; + }) + }) + ).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}") { + ( + 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 72c81cd8..f9302b90 100644 --- a/src/lib/Data/AniList/wrapped.ts +++ b/src/lib/Data/AniList/wrapped.ts @@ -2,72 +2,72 @@ 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() + 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: `{ + 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 @@ -82,66 +82,66 @@ const profileActivities = async ( } } }` - }) - }) - ).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 - }; + }) + }) + ).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 + 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: `{ + 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,144 +158,144 @@ 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) - ); - - 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) + ); + + 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; - - 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 - }; + 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 + }; }; |