aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFuwn <[email protected]>2023-09-07 21:05:20 -0700
committerFuwn <[email protected]>2023-09-07 21:05:20 -0700
commit724e1d1b642059784e70894fa10647cd1c2b5206 (patch)
tree52fe2b02c6c4087192bbd495accf9f2e4f0291d9 /src
parentfeat(routes): profile page (diff)
downloaddue.moe-724e1d1b642059784e70894fa10647cd1c2b5206.tar.xz
due.moe-724e1d1b642059784e70894fa10647cd1c2b5206.zip
feat(media): public media list collection
Diffstat (limited to 'src')
-rw-r--r--src/lib/AniList/media.ts32
-rw-r--r--src/lib/List/User/AnimeList.svelte186
2 files changed, 218 insertions, 0 deletions
diff --git a/src/lib/AniList/media.ts b/src/lib/AniList/media.ts
index 1fddbbe5..341e09a4 100644
--- a/src/lib/AniList/media.ts
+++ b/src/lib/AniList/media.ts
@@ -138,3 +138,35 @@ export const mediaListCollection = async (
return flattenLists(userIdResponse['data']['MediaListCollection']['lists']);
};
+
+export const publicMediaListCollection = async (userId: number, type: Type): Promise<Media[]> => {
+ const userIdResponse = await (
+ await fetch('https://graphql.anilist.co', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json'
+ },
+ body: JSON.stringify({
+ query: `{ MediaListCollection(userId: ${userId}, type: ${
+ type === Type.Anime ? 'ANIME' : 'MANGA'
+ }, status_not_in: [ COMPLETED ]) {
+ lists { entries { media {
+ id
+ status
+ type
+ episodes
+ format
+ title { romaji english native }
+ nextAiringEpisode { episode timeUntilAiring }
+ mediaListEntry { progress status }
+ startDate { year }
+ } } }
+ }
+ }`
+ })
+ })
+ ).json();
+
+ return flattenLists(userIdResponse['data']['MediaListCollection']['lists']);
+};
diff --git a/src/lib/List/User/AnimeList.svelte b/src/lib/List/User/AnimeList.svelte
new file mode 100644
index 00000000..43608fc9
--- /dev/null
+++ b/src/lib/List/User/AnimeList.svelte
@@ -0,0 +1,186 @@
+<script lang="ts">
+ /* eslint svelte/no-at-html-tags: "off" */
+
+ import { Type, type Media, publicMediaListCollection } from '$lib/AniList/media';
+ import { onDestroy, onMount } from 'svelte';
+ import settings from '../../../stores/settings';
+
+ export let userId: number;
+ export let displayUnresolved: boolean;
+
+ let animeLists: Promise<Media[]>;
+ let startTime: number;
+ let endTime: number;
+
+ const keyCacher = setInterval(() => {
+ startTime = performance.now();
+ endTime = -1;
+ animeLists = publicMediaListCollection(userId, Type.Anime);
+ }, $settings.cacheMinutes * 1000 * 60);
+
+ onMount(async () => {
+ startTime = performance.now();
+ animeLists = publicMediaListCollection(userId, Type.Anime);
+ });
+
+ onDestroy(() => {
+ clearInterval(keyCacher);
+ });
+
+ const cleanMedia = (media: Media[], displayUnresolved: boolean) => {
+ if (media === undefined) {
+ return [];
+ }
+
+ const releasingMedia = media.filter((media: Media) => media.status == 'RELEASING');
+ const outdatedMedia = releasingMedia.filter((media: Media) => {
+ return (
+ (media.nextAiringEpisode || { episode: 0 }).episode - 1 !=
+ (media.mediaListEntry || { progress: 0 }).progress
+ );
+ });
+ let finalMedia = outdatedMedia.map((media: Media) => {
+ if ((media.nextAiringEpisode || { episode: 0 }).episode - 1 <= 0) {
+ media.nextAiringEpisode = { episode: -1 };
+ }
+
+ return media;
+ });
+
+ if (!displayUnresolved) {
+ finalMedia = finalMedia.filter((media: Media) => media.nextAiringEpisode?.episode !== -1);
+ }
+
+ finalMedia.sort((a: Media, b: Media) => {
+ if ($settings.sortByDifference === true) {
+ const difference = (anime: Media) => {
+ return (
+ (anime.nextAiringEpisode?.episode === -1
+ ? 99999
+ : anime.nextAiringEpisode?.episode || -1) -
+ (anime.mediaListEntry || { progress: 0 }).progress
+ );
+ };
+
+ return difference(a) - difference(b);
+ } else {
+ return (
+ (a.nextAiringEpisode?.timeUntilAiring || 9999) -
+ (b.nextAiringEpisode?.timeUntilAiring || 9999)
+ );
+ }
+ });
+
+ finalMedia = finalMedia.filter((item, index, array) => {
+ return (
+ array.findIndex((i) => {
+ return i.id === item.id;
+ }) === index
+ );
+ });
+
+ if (!endTime || endTime === -1) {
+ endTime = performance.now() - startTime;
+ }
+
+ return finalMedia;
+ };
+
+ const airingTime = (anime: Media) => {
+ const untilAiring = anime.nextAiringEpisode?.timeUntilAiring;
+ let timeFrame;
+
+ if (untilAiring !== undefined) {
+ let hours = untilAiring / 3600;
+
+ if (hours >= 24) {
+ let weeks = Math.floor(Math.floor(hours / 24) / 7);
+
+ if (weeks >= 1) {
+ weeks = Math.round(weeks);
+
+ timeFrame = `${weeks} week${weeks === 1 ? '' : 's'}`;
+ } else {
+ const days = Math.round(Math.floor(hours / 24));
+
+ timeFrame = `${days} day${days === 1 ? '' : 's'}`;
+ }
+ } else {
+ hours = Math.round(hours);
+
+ timeFrame = `${hours} hour${hours === 1 ? '' : 's'}`;
+ }
+
+ return `<span style="opacity: 50%">${anime.nextAiringEpisode?.episode} in ${timeFrame}</span>`;
+ }
+
+ return '';
+ };
+
+ const totalEpisodes = (anime: Media) => {
+ return anime.episodes === null ? '' : `<span style="opacity: 50%">/${anime.episodes}</span>`;
+ };
+
+ const cleanCache = () => {
+ animeLists = publicMediaListCollection(userId, Type.Anime);
+ };
+</script>
+
+{#await animeLists}
+ <summary>Anime [...] <small style="opacity: 50%">...s</small></summary>
+
+ <ul><li>Loading ...</li></ul>
+{:then media}
+ {@const cleanedMedia = cleanMedia(media, displayUnresolved)}
+
+ <summary
+ >Anime [{cleanedMedia.length}]
+ <small style="opacity: 50%">{endTime / 1000}s</small></summary
+ >
+
+ {#if cleanedMedia.length === 0}
+ <ul>
+ <li>No anime to display. <a href={'#'} on:click={cleanCache}>Force refresh</a></li>
+ </ul>
+ {/if}
+
+ <ul>
+ {#each cleanedMedia as anime}
+ <li>
+ <a
+ href={$settings.linkToAniList
+ ? `https://anilist.co/anime/${anime.id}`
+ : `https://www.livechart.me/search?q=${
+ anime.title.native || anime.title.english || anime.title.romaji
+ }`}
+ target="_blank"
+ >
+ {anime.title.english || anime.title.romaji || anime.title.native}
+ </a>
+ <span style="opacity: 50%;">|</span>
+ {(anime.mediaListEntry || { progress: 0 }).progress}{@html totalEpisodes(anime)}
+ [{anime.nextAiringEpisode?.episode === -1
+ ? '?'
+ : (anime.nextAiringEpisode?.episode || 1) - 1}]
+ {@html airingTime(anime)}
+ </li>
+ {/each}
+ </ul>
+{:catch}
+ <summary>Anime [?] <small style="opacity: 50%">0s</small></summary>
+
+ <ul>
+ <li>
+ <p>
+ Media could not be loaded. You might have been <a
+ href="https://en.wikipedia.org/wiki/Rate_limiting"
+ target="_blank">rate limited</a
+ >.
+ </p>
+ <p>
+ Try again in a few minutes. If the problem persists, please contact
+ <a href="https://anilist.co/user/fuwn" target="_blank">@fuwn</a> on AniList.
+ </p>
+ </li>
+ </ul>
+{/await}