aboutsummaryrefslogtreecommitdiff
path: root/src/lib/Media/Anime/Airing/Subtitled
diff options
context:
space:
mode:
authorFuwn <[email protected]>2023-12-20 04:50:34 -0800
committerFuwn <[email protected]>2023-12-20 04:50:34 -0800
commit9fa26f6c6f4e22db44578fd26d5c01c96277d45f (patch)
tree5c95a9c8e961e551278df556497e05a5c40016cb /src/lib/Media/Anime/Airing/Subtitled
parentrefactor(airing): move to module (diff)
downloaddue.moe-9fa26f6c6f4e22db44578fd26d5c01c96277d45f.tar.xz
due.moe-9fa26f6c6f4e22db44578fd26d5c01c96277d45f.zip
refactor(airing): subtitled module
Diffstat (limited to 'src/lib/Media/Anime/Airing/Subtitled')
-rw-r--r--src/lib/Media/Anime/Airing/Subtitled/match.ts156
-rw-r--r--src/lib/Media/Anime/Airing/Subtitled/subsPlease.ts13
2 files changed, 169 insertions, 0 deletions
diff --git a/src/lib/Media/Anime/Airing/Subtitled/match.ts b/src/lib/Media/Anime/Airing/Subtitled/match.ts
new file mode 100644
index 00000000..a0ac9573
--- /dev/null
+++ b/src/lib/Media/Anime/Airing/Subtitled/match.ts
@@ -0,0 +1,156 @@
+import { get } from 'svelte/store';
+import type { Media } from '../../../../AniList/media';
+import settings from '../../../../../stores/settings';
+import type { SubsPlease } from '$lib/Media/Anime/Airing/Subtitled/subsPlease';
+import levenshtein from 'fast-levenshtein';
+
+export interface Time {
+ title: string;
+ time: string;
+ day: string;
+}
+
+export const airedUnsubtitled = (anime: Media) =>
+ anime.nextAiringEpisode?.airingAt &&
+ anime.nextAiringEpisode?.nativeAiringAt &&
+ new Date(anime.nextAiringEpisode.airingAt * 1000).getTime() > Date.now() &&
+ new Date(anime.nextAiringEpisode.nativeAiringAt * 1000).getTime() - Date.now() >
+ 6 * 24 * 60 * 60 * 1000;
+
+const secondsUntil = (targetTime: string, targetDay: string) => {
+ const now = new Date();
+ const [targetHour, targetMinute] = targetTime.split(':').map(Number);
+ let dayDifference =
+ ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'].indexOf(
+ targetDay
+ ) - now.getDay();
+
+ if (dayDifference < 0) dayDifference += 7;
+
+ const targetDate = new Date(now);
+
+ targetDate.setDate(now.getDate() + dayDifference);
+ targetDate.setHours(targetHour, targetMinute, 0, 0);
+
+ const secondsDifference = (Number(targetDate) - Number(now)) / 1000;
+
+ return secondsDifference > 0 ? secondsDifference : secondsDifference + 7 * 24 * 60 * 60;
+};
+
+const normalizeTitle = (title: string | null) => {
+ return (title || '')
+ .toLowerCase()
+ .replace(/season \d+|s\d+|\W/g, '')
+ .replace(/\b(\d)(st|nd|rd|th)\b/g, '$1')
+ .replace(/\b(part|pt)\b/gi, '')
+ .trim();
+};
+
+const findClosestMatch = (times: Time[], titles: string[]) => {
+ let closestMatch: Time | null = null;
+ let smallestDistance = Infinity;
+
+ titles.forEach((animeTitle) => {
+ const normalizedAnimeTitle = normalizeTitle(animeTitle);
+
+ times.forEach((item) => {
+ const normalizedItemTitle = normalizeTitle(item.title);
+ const distance = levenshtein.get(normalizedAnimeTitle, normalizedItemTitle);
+
+ if (
+ distance < smallestDistance &&
+ distance < Math.max(3, normalizedAnimeTitle.length * 0.4)
+ ) {
+ smallestDistance = distance;
+ closestMatch = item;
+ }
+ });
+ });
+
+ return closestMatch as Time | null;
+};
+
+export const findClosestMedia = (media: Media[], matchFor: string) => {
+ if (!matchFor) return null;
+
+ let bestFitMedia: Media | null = null;
+ let smallestDistance = Infinity;
+
+ media.forEach((m) => {
+ [m.title.romaji, m.title.english, ...m.synonyms].filter(Boolean).forEach((title) => {
+ const normalizedItemTitle = normalizeTitle(title);
+ const distance = levenshtein.get(normalizeTitle(matchFor), normalizedItemTitle);
+
+ if (distance < smallestDistance && distance < Math.max(3, normalizedItemTitle.length * 0.4)) {
+ 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
+ // ? Math.round((airingAt - Date.now() / 1000) * 100) / 100
+ // : undefined;
+ const nativeTime = new Date(airingAt ? airingAt * 1000 : 0);
+ let untilAiring;
+ let time = new Date(airingAt ? airingAt * 1000 : 0);
+ let nextEpisode = anime.nextAiringEpisode?.episode || 0;
+
+ if (
+ !(
+ (get(settings).displayNativeCountdown || !subsPlease)
+ // || !(nativeUntilAiring !== undefined && nativeUntilAiring < 24 * 60 * 60)
+ )
+ ) {
+ const times: Time[] = [];
+
+ for (const [key, value] of Object.entries(subsPlease.schedule)) {
+ const flattenedValue = Array.isArray(value) ? value.flat() : [];
+
+ for (const time of flattenedValue) {
+ times.push({
+ title: time.title,
+ time: time.time,
+ day: key
+ });
+ }
+ }
+
+ const foundTime: Time | null = findClosestMatch(times, [
+ anime.title.romaji,
+ anime.title.english,
+ ...anime.synonyms
+ ]);
+
+ if (foundTime) {
+ untilAiring = secondsUntil((foundTime as Time).time, (foundTime as Time).day);
+ time = new Date(Date.now() + untilAiring * 1000);
+ }
+ }
+
+ if (
+ // This was an insane debugging session .... What, like eight hours? ...
+ airedUnsubtitled({
+ nextAiringEpisode: {
+ ...anime.nextAiringEpisode,
+ airingAt: time.getTime() / 1000,
+ nativeAiringAt: nativeTime.getTime() / 1000
+ }
+ } as Media)
+ )
+ nextEpisode -= 1;
+
+ return {
+ ...anime,
+ nextAiringEpisode: {
+ episode: nextEpisode,
+ airingAt: time.getTime() / 1000,
+ nativeAiringAt: nativeTime.getTime() / 1000
+ }
+ } as Media;
+};
diff --git a/src/lib/Media/Anime/Airing/Subtitled/subsPlease.ts b/src/lib/Media/Anime/Airing/Subtitled/subsPlease.ts
new file mode 100644
index 00000000..3815259d
--- /dev/null
+++ b/src/lib/Media/Anime/Airing/Subtitled/subsPlease.ts
@@ -0,0 +1,13 @@
+export interface SubsPlease {
+ tz: string;
+ schedule: {
+ [key in string]: SubsPleaseEpisode;
+ }[];
+}
+
+export interface SubsPleaseEpisode {
+ title: string;
+ page: string;
+ image_url: string;
+ time: string;
+}