aboutsummaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
authorFuwn <[email protected]>2023-09-27 23:33:01 -0700
committerFuwn <[email protected]>2023-09-27 23:33:01 -0700
commit8562ba4280c575b3f04df598b7954a2d28b19e50 (patch)
treeeaa43530441522b5104a1fd1e404ce6b663fc9b0 /src/lib
parentfix(anime): template increment render (diff)
downloaddue.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.ts39
-rw-r--r--src/lib/AniList/media.ts12
-rw-r--r--src/lib/AniList/wrapped.ts113
-rw-r--r--src/lib/Tools/ActivityHistory.svelte45
-rw-r--r--src/lib/Tools/Wrapped.svelte227
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>