diff options
| author | Fuwn <[email protected]> | 2023-09-27 23:33:01 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2023-09-27 23:33:01 -0700 |
| commit | 8562ba4280c575b3f04df598b7954a2d28b19e50 (patch) | |
| tree | eaa43530441522b5104a1fd1e404ce6b663fc9b0 /src/lib | |
| parent | fix(anime): template increment render (diff) | |
| download | due.moe-8562ba4280c575b3f04df598b7954a2d28b19e50.tar.xz due.moe-8562ba4280c575b3f04df598b7954a2d28b19e50.zip | |
feat(wrapped): initial wrapped prototype
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/AniList/activity.ts | 39 | ||||
| -rw-r--r-- | src/lib/AniList/media.ts | 12 | ||||
| -rw-r--r-- | src/lib/AniList/wrapped.ts | 113 | ||||
| -rw-r--r-- | src/lib/Tools/ActivityHistory.svelte | 45 | ||||
| -rw-r--r-- | src/lib/Tools/Wrapped.svelte | 227 |
5 files changed, 396 insertions, 40 deletions
diff --git a/src/lib/AniList/activity.ts b/src/lib/AniList/activity.ts index bd4bafc0..df150c96 100644 --- a/src/lib/AniList/activity.ts +++ b/src/lib/AniList/activity.ts @@ -5,6 +5,45 @@ export interface ActivityHistoryEntry { amount: number; } +export const fillMissingDays = ( + inputActivities: ActivityHistoryEntry[], + startOfYear = false +): ActivityHistoryEntry[] => { + const timezoneOffset = new Date().getTimezoneOffset() * 60 * 1000; + const activities = inputActivities; + const firstDate = startOfYear + ? new Date(new Date().getFullYear(), 0, 1, 0, 0, 0, 0) + : new Date(activities[0].date * 1000 + timezoneOffset); + const lastDate = new Date(activities[activities.length - 1].date * 1000 + timezoneOffset); + const currentDate = firstDate; + + while (currentDate <= lastDate) { + const current_unix_timestamp = currentDate.getTime(); + let found = false; + + for (let i = 0; i < activities.length; i++) { + if (activities[i].date * 1000 + timezoneOffset === current_unix_timestamp) { + found = true; + + break; + } + } + + if (!found) { + activities.push({ + date: current_unix_timestamp / 1000, + amount: 0 + }); + } + + currentDate.setDate(currentDate.getDate() + 1); + } + + // activities.sort((a: { date: number }, b: { date: number }) => a.date - b.date); + + return activities; +}; + export const activityHistory = async ( userIdentity: UserIdentity ): Promise<ActivityHistoryEntry[]> => { diff --git a/src/lib/AniList/media.ts b/src/lib/AniList/media.ts index 6187094d..d9ce5e57 100644 --- a/src/lib/AniList/media.ts +++ b/src/lib/AniList/media.ts @@ -28,10 +28,14 @@ export interface Media { mediaListEntry?: { progress: number; status: string; + score: number; }; startDate: { year: number; }; + coverImage: { + extraLarge: string; + }; } export const flattenLists = (lists: { entries: { media: Media }[] }[]) => { @@ -59,7 +63,8 @@ export const mediaListCollection = async ( type: Type, mediaCache: string | undefined, currentLastPruneAt: string | number, - forcePrune = false + forcePrune = false, + includeCompleted = false ): Promise<Media[]> => { let currentCacheMinutes; @@ -106,7 +111,7 @@ export const mediaListCollection = async ( body: JSON.stringify({ query: `{ MediaListCollection(userId: ${userIdentity.id}, type: ${ type === Type.Anime ? 'ANIME' : 'MANGA' - }, status_not_in: [ COMPLETED ]) { + }${includeCompleted ? '' : ', status_not_in: [ COMPLETED ]'}) { lists { entries { media { id status @@ -115,8 +120,9 @@ export const mediaListCollection = async ( format title { romaji english native } nextAiringEpisode { episode timeUntilAiring } - mediaListEntry { progress status } + mediaListEntry { progress status score(format: POINT_100) } startDate { year } + coverImage { extraLarge } } } } } }` diff --git a/src/lib/AniList/wrapped.ts b/src/lib/AniList/wrapped.ts new file mode 100644 index 00000000..bd700101 --- /dev/null +++ b/src/lib/AniList/wrapped.ts @@ -0,0 +1,113 @@ +import type { AniListAuthorisation, UserIdentity } from './identity'; + +export interface Wrapped { + statistics: { + anime: { + startYears: { + startYear: number; + minutesWatched: number; + count: number; + }[]; + }; + manga: { + startYears: { + startYear: number; + chaptersRead: number; + count: number; + }[]; + }; + }; + activities: { + statusCount: number; + messageCount: number; + }; + avatar: { + large: string; + }; +} + +const profileActivities = async (user: AniListAuthorisation, identity: UserIdentity) => { + 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 ]) { + ... on TextActivity { + type + } + ... on MessageActivity { + type + } + } + pageInfo { + hasNextPage + } + } +}` + }) + }) + ).json(); + + const pages = []; + let page = 1; + let response = await get(page); + + 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').length, + messageCount: pages.flat().filter((activity) => activity.type == 'MESSAGE').length + }; +}; + +export const wrapped = async ( + anilistAuthorisation: AniListAuthorisation, + identity: UserIdentity +): Promise<Wrapped> => { + const wrappedResponse = 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: "${identity.name}") { + avatar { large } + statistics { + anime { startYears { startYear minutesWatched count } } + manga { startYears { startYear chaptersRead count } } + } + } +}` + }) + }) + ).json(); + const { statusCount, messageCount } = await profileActivities(anilistAuthorisation, identity); + + return { + statistics: wrappedResponse['data']['User']['statistics'], + activities: { + statusCount, + messageCount + }, + avatar: wrappedResponse['data']['User']['avatar'] + }; +}; diff --git a/src/lib/Tools/ActivityHistory.svelte b/src/lib/Tools/ActivityHistory.svelte index 58ba4173..a8f3c45b 100644 --- a/src/lib/Tools/ActivityHistory.svelte +++ b/src/lib/Tools/ActivityHistory.svelte @@ -1,5 +1,9 @@ <script lang="ts"> - import { activityHistory, type ActivityHistoryEntry } from '$lib/AniList/activity.js'; + import { + activityHistory, + fillMissingDays, + type ActivityHistoryEntry + } from '$lib/AniList/activity.js'; import { onMount } from 'svelte'; import userIdentity from '../../stores/userIdentity.js'; import { @@ -11,7 +15,6 @@ let activityHistoryData: Promise<ActivityHistoryEntry[]>; let currentUserIdentity = { name: '', id: -1 }; - const timezoneOffset = new Date().getTimezoneOffset() * 60 * 1000; onMount(async () => { if (user !== undefined) { @@ -22,43 +25,9 @@ currentUserIdentity = JSON.parse($userIdentity); currentUserIdentity.name = currentUserIdentity.name; activityHistoryData = activityHistory(currentUserIdentity); - console.log(fillMissingDays(await activityHistory(currentUserIdentity))); } }); - const fillMissingDays = (inputActivities: ActivityHistoryEntry[]): ActivityHistoryEntry[] => { - let activities = inputActivities; - const firstDate = new Date(activities[0].date * 1000 + timezoneOffset); - const lastDate = new Date(activities[activities.length - 1].date * 1000 + timezoneOffset); - const currentDate = firstDate; - - while (currentDate <= lastDate) { - const current_unix_timestamp = currentDate.getTime(); - let found = false; - - for (let i = 0; i < activities.length; i++) { - if (activities[i].date * 1000 + timezoneOffset === current_unix_timestamp) { - found = true; - - break; - } - } - - if (!found) { - activities.push({ - date: current_unix_timestamp / 1000, - amount: 0 - }); - } - - currentDate.setDate(currentDate.getDate() + 1); - } - - // activities.sort((a: { date: number }, b: { date: number }) => a.date - b.date); - - return activities; - }; - // const incrementDate = (date: Date): Date => { // date.setDate(date.getDate() + 1); @@ -83,7 +52,9 @@ {#each fillMissingDays(activities) as activity} {#if activity.amount === 0} <li> - {new Date(activity.date * 1000 + timezoneOffset).toDateString()} + {new Date( + activity.date * 1000 + new Date().getTimezoneOffset() * 60 * 1000 + ).toDateString()} </li> {/if} {/each} diff --git a/src/lib/Tools/Wrapped.svelte b/src/lib/Tools/Wrapped.svelte new file mode 100644 index 00000000..bd40e5ef --- /dev/null +++ b/src/lib/Tools/Wrapped.svelte @@ -0,0 +1,227 @@ +<script lang="ts"> + import userIdentity from '../../stores/userIdentity.js'; + import { + userIdentity as getUserIdentity, + type AniListAuthorisation + } from '$lib/AniList/identity'; + import { onMount } from 'svelte'; + import { wrapped } from '$lib/AniList/wrapped.js'; + import { activityHistory, fillMissingDays } from '$lib/AniList/activity.js'; + import { Type, mediaListCollection, type Media } from '$lib/AniList/media.js'; + import anime from '../../stores/anime.js'; + import lastPruneTimes from '../../stores/lastPruneTimes.js'; + import manga from '../../stores/manga.js'; + + export let user: AniListAuthorisation; + + let currentUserIdentity = { name: '', id: -1 }; + let episodes = 0; + let animeList: Media[] | undefined = undefined; + let mangaList: Media[] | undefined = undefined; + + onMount(async () => { + if (user !== undefined) { + if ($userIdentity === '') { + userIdentity.set(JSON.stringify(await getUserIdentity(user))); + } + + currentUserIdentity = JSON.parse($userIdentity); + currentUserIdentity.name = currentUserIdentity.name; + animeList = await mediaListCollection( + user, + currentUserIdentity, + Type.Anime, + $anime, + $lastPruneTimes.anime, + true, + true + ); + mangaList = await mediaListCollection( + user, + currentUserIdentity, + Type.Manga, + $manga, + $lastPruneTimes.manga, + true, + true + ); + animeList = animeList.sort((a, b) => { + if (a.mediaListEntry?.score === undefined) { + return 1; + } else if (b.mediaListEntry?.score === undefined) { + return -1; + } else { + return b.mediaListEntry?.score - a.mediaListEntry?.score; + } + }); + mangaList = mangaList.sort((a, b) => { + if (a.mediaListEntry?.score === undefined) { + return 1; + } else if (b.mediaListEntry?.score === undefined) { + return -1; + } else { + return b.mediaListEntry?.score - a.mediaListEntry?.score; + } + }); + + for (const media of animeList) { + if (media.startDate.year === new Date(Date.now()).getFullYear()) { + episodes += media.mediaListEntry?.progress || 0; + } + } + } + }); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + const year = (statistic: { startYears: any }) => { + return statistic.startYears.find((y: { startYear: number }) => y.startYear === 2023); + }; +</script> + +{#if currentUserIdentity.id !== -1} + {#await wrapped(user, currentUserIdentity)} + Loading ... + {:then wrapped} + {@const anime = year(wrapped.statistics.anime)} + {@const manga = year(wrapped.statistics.manga)} + + <div class="categories-grid"> + <div class="grid-item image-grid"> + <img src={wrapped.avatar.large} alt="User Avatar" /> + <div> + <div> + <b> + {currentUserIdentity.name} + </b> + </div> + <div> + Status Posts: {wrapped.activities.statusCount} + </div> + <div> + Messages: {wrapped.activities.messageCount} + </div> + <div> + Days Active: {#await activityHistory(currentUserIdentity)} + Loading ... + {:then activities} + {#if activities === undefined} + Loading ... + {:else} + {fillMissingDays(activities, true).filter((a) => a.amount !== 0).length}/365 + {/if} + {/await} + </div> + </div> + </div> + <div class="category-grid"> + <div class="grid-item"> + <b>Anime</b> + </div> + <div class="grid-item"> + Time Watched: {((anime.minutesWatched || 0) / 60 / 24).toFixed(2)} days + </div> + <div class="grid-item"> + Completed: {anime.count} + </div> + <div class="grid-item">Episodes: {episodes}</div> + </div> + <div class="category-grid"> + <div class="grid-item"> + <b>Manga</b> + </div> + <div class="grid-item"> + Time Read: {(((manga.chaptersRead || 0) * 8.58) / 60 / 24).toFixed(2)} days + </div> + <div class="grid-item"> + Completed: {manga.count} + </div> + <div class="grid-item"> + Chapters: {manga.chaptersRead || 0} + </div> + </div> + <div class="category-grid"> + <div class="grid-item image-grid"> + {#if animeList !== undefined} + <img + src={animeList[0].coverImage.extraLarge} + alt="Highest Rated Anime Cover" + class="cover-image" + /> + <div> + <b>Highest Rated Anime</b> + <ol> + {#each animeList?.slice(0, 5) as anime} + <li> + {anime.title.english || anime.title.romaji || anime.title.native} + </li> + {/each} + </ol> + </div> + {:else} + Loading ... + {/if} + </div> + </div> + <div class="category-grid"> + <div class="grid-item image-grid"> + {#if mangaList !== undefined} + <img + src={mangaList[0].coverImage.extraLarge} + alt="Highest Rated Manga Cover" + class="cover-image" + /> + <div> + <b>Highest Rated Manga</b> + <ol> + {#each mangaList?.slice(0, 5) as manga} + <li> + {manga.title.english || manga.title.romaji || manga.title.native} + </li> + {/each} + </ol> + </div> + {:else} + Loading ... + {/if} + </div> + </div> + </div> + {:catch error} + {error} + {/await} +{:else} + Loading ... +{/if} + +<style> + .categories-grid { + display: flex; + flex-wrap: wrap; + row-gap: 1em; + column-gap: 1em; + } + + .category-grid { + display: grid; + } + + .image-grid { + display: grid; + column-gap: 1em; + grid-template-columns: 1fr 2fr; + } + + .image-grid img { + height: 6em; + } + + .cover-image { + height: 8.75em !important; + } + + .grid-item { + min-width: 300px; + flex: 1 1 300px; + /* text-align: center; */ + } +</style> |