From a87fee111ccb501121b5aa3c1e48601f4869198a Mon Sep 17 00:00:00 2001
From: Fuwn
Date: Wed, 20 Dec 2023 02:20:37 -0800
Subject: feat(schedule): match media to anilist
---
src/lib/AniList/schedule.ts | 75 +++++++++++++++++++
src/lib/Media/Anime/airing.ts | 24 +++++-
src/lib/Media/Anime/season.ts | 11 +++
src/routes/schedule/+page.svelte | 153 ++++++++++++++++++++++++---------------
4 files changed, 204 insertions(+), 59 deletions(-)
create mode 100644 src/lib/AniList/schedule.ts
create mode 100644 src/lib/Media/Anime/season.ts
(limited to 'src')
diff --git a/src/lib/AniList/schedule.ts b/src/lib/AniList/schedule.ts
new file mode 100644
index 00000000..33351a09
--- /dev/null
+++ b/src/lib/AniList/schedule.ts
@@ -0,0 +1,75 @@
+import type { Media, MediaTitle } from './media';
+
+interface SchedulePage {
+ data: {
+ Page: {
+ media: {
+ title: MediaTitle;
+ synonyms: string[];
+ id: number;
+ idMal: number;
+ episodes: number;
+ nextAiringEpisode?: {
+ episode: number;
+ airingAt?: number;
+ };
+ coverImage: {
+ extraLarge: string;
+ };
+ }[];
+ pageInfo: {
+ hasNextPage: boolean;
+ };
+ };
+ };
+}
+
+const schedulePage = async (
+ page: number,
+ year: number,
+ season: 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'
+): Promise =>
+ await (
+ await fetch('https://graphql.anilist.co', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json'
+ },
+ body: JSON.stringify({
+ query: `{
+ Page(page: ${page}) {
+ pageInfo {
+ hasNextPage
+ }
+ media(season: ${season}, seasonYear: ${year}) {
+ id idMal episodes synonyms
+ title { english romaji native }
+ nextAiringEpisode { episode airingAt }
+ coverImage { extraLarge }
+ }
+ }
+}`
+ })
+ })
+ ).json();
+
+export const scheduleMediaListCollection = async (
+ year: number,
+ season: 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'
+) => {
+ const scheduledMedia = [];
+ let page = 1;
+ let currentPage = await schedulePage(page, year, season);
+
+ for (const candidate of currentPage.data.Page.media) scheduledMedia.push(candidate);
+
+ while (currentPage['data']['Page']['pageInfo']['hasNextPage']) {
+ for (const candidate of currentPage.data.Page.media) scheduledMedia.push(candidate);
+
+ page += 1;
+ currentPage = await schedulePage(page, year, season);
+ }
+
+ return scheduledMedia as Partial;
+};
diff --git a/src/lib/Media/Anime/airing.ts b/src/lib/Media/Anime/airing.ts
index 15abbf2a..9c13fb5d 100644
--- a/src/lib/Media/Anime/airing.ts
+++ b/src/lib/Media/Anime/airing.ts
@@ -6,7 +6,7 @@ import type { SubsPlease } from '$lib/subsPlease';
import levenshtein from 'fast-levenshtein';
import { totalEpisodes } from './episodes';
-interface Time {
+export interface Time {
title: string;
time: string;
day: string;
@@ -66,6 +66,28 @@ const findClosestMatch = (times: Time[], titles: string[]) => {
return closestMatch as Time | null;
};
+export const findClosestMedia = (media: Media[], matchFor: string) => {
+ if (!matchFor) return null;
+
+ let bestFitMedia: Media | null = null;
+ let smallestDistance = Infinity;
+
+ const normalizedSingleTitle = normalizeTitle(matchFor);
+
+ media.forEach((m) => {
+ [m.title.romaji, m.title.english, ...m.synonyms].filter(Boolean).forEach((title) => {
+ const distance = levenshtein.get(normalizedSingleTitle, normalizeTitle(title));
+
+ if (distance < smallestDistance && distance < normalizedSingleTitle.length / 2) {
+ smallestDistance = distance;
+ bestFitMedia = m;
+ }
+ });
+ });
+
+ return bestFitMedia as Media | null;
+};
+
export const injectAiringTime = (anime: Media, subsPlease: SubsPlease | null) => {
const airingAt = anime.nextAiringEpisode?.airingAt;
// const nativeUntilAiring = airingAt
diff --git a/src/lib/Media/Anime/season.ts b/src/lib/Media/Anime/season.ts
new file mode 100644
index 00000000..d7922e2b
--- /dev/null
+++ b/src/lib/Media/Anime/season.ts
@@ -0,0 +1,11 @@
+export const season = () => {
+ if (new Date().getMonth() >= 0 && new Date().getMonth() <= 2)
+ return 'WINTER' as 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL';
+ else if (new Date().getMonth() >= 3 && new Date().getMonth() <= 5)
+ return 'SPRING' as 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL';
+ else if (new Date().getMonth() >= 6 && new Date().getMonth() <= 8)
+ return 'SUMMER' as 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL';
+ else if (new Date().getMonth() >= 9 && new Date().getMonth() <= 11)
+ return 'FALL' as 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL';
+ else return 'WINTER' as 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL';
+};
diff --git a/src/routes/schedule/+page.svelte b/src/routes/schedule/+page.svelte
index c629f820..2a975c46 100644
--- a/src/routes/schedule/+page.svelte
+++ b/src/routes/schedule/+page.svelte
@@ -5,8 +5,14 @@
import settings from '../../stores/settings';
import { parseOrDefault } from '$lib/Tools/tool';
import { browser } from '$app/environment';
+ import type { Media } from '$lib/AniList/media';
+ import { scheduleMediaListCollection } from '$lib/AniList/schedule';
+ import { season } from '$lib/Media/Anime/season';
+ import { findClosestMedia } from '$lib/Media/Anime/airing';
+ import MediaTitleDisplay from '$lib/List/MediaTitleDisplay.svelte';
let subsPleasePromise: Promise;
+ let scheduledMediaPromise: Promise>;
const urlParameters = browser ? new URLSearchParams(window.location.search) : null;
let timeZone = parseOrDefault(
urlParameters,
@@ -14,11 +20,13 @@
Intl.DateTimeFormat().resolvedOptions().timeZone
);
- onMount(
- async () => (subsPleasePromise = fetch(`/api/subsplease?tz=${timeZone}`).then((r) => r.json()))
- );
+ onMount(async () => {
+ subsPleasePromise = fetch(`/api/subsplease?tz=${timeZone}`).then((r) => r.json());
+ scheduledMediaPromise = scheduleMediaListCollection(new Date().getFullYear(), season());
+ });
let hoveredItem: SubsPleaseEpisode | null = null;
+ let hoveredMedia: Media | null = null;
let imageStyle = '';
let imageHeight = 0;
@@ -58,67 +66,94 @@
return shiftedSchedule;
};
+
+ const associateMedia = (media: (Media | undefined)[], title: string) =>
+ findClosestMedia(media as Media[], title);
{#await subsPleasePromise}
Loading ...
{:then subsPlease}
{#if subsPlease}
-
- {timeZone.split('/').reverse().join(', ').replace(/_/g, ' ')}
-
-
-
-
-
-
-
- {#each Object.entries(shiftSubsPleaseSchedule(subsPlease.schedule)) as [day, scheduleEntry]}
-
- {day}
-
-
- {#each Object.values(scheduleEntry) as entry}
- - (hoveredItem = entry)}
- on:mouseleave={() => (hoveredItem = null)}
- on:mousemove={onMouseMove}
- >
-
- {entry.title}
-
- {#if !$settings.displayCountdownRightAligned}
- |
- {/if}
-
- {entry.time}
-
-
+ {#await scheduledMediaPromise}
+ Loading ...
+ {:then scheduledMedia}
+ {#if scheduledMedia}
+
+ {timeZone.split('/').reverse().join(', ').replace(/_/g, ' ')}
+
+
+
+
-
-
-
- {/each}
-
+
+
+
+
+ {#each Object.entries(shiftSubsPleaseSchedule(subsPlease.schedule)) as [day, scheduleEntry]}
+
+ {day}
+
+
+ {#each Object.values(scheduleEntry) as entry}
+ {@const media = associateMedia(scheduledMedia, entry.title)}
+
+ - {
+ hoveredItem = entry;
+ hoveredMedia = media;
+ }}
+ on:mouseleave={() => {
+ hoveredItem = null;
+ hoveredMedia = null;
+ }}
+ on:mousemove={onMouseMove}
+ >
+
+ {#if media}
+
+ {:else}
+ {entry.title}
+ {/if}
+
+ {#if !$settings.displayCountdownRightAligned}
+ |
+ {/if}
+
+ {entry.time}
+
+
+ {/each}
+
+
+
+
+ {/each}
+
+ {:else}
+ Loading ...
+ {/if}
+ {:catch}
+
+ {/await}
{:else}
Loading ...
{/if}
@@ -129,7 +164,9 @@
{#if hoveredItem}
--
cgit v1.2.3