diff options
| author | Fuwn <[email protected]> | 2023-12-20 04:50:34 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2023-12-20 04:50:34 -0800 |
| commit | 9fa26f6c6f4e22db44578fd26d5c01c96277d45f (patch) | |
| tree | 5c95a9c8e961e551278df556497e05a5c40016cb /src/lib/Media/Anime/Airing/Subtitled | |
| parent | refactor(airing): move to module (diff) | |
| download | due.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.ts | 156 | ||||
| -rw-r--r-- | src/lib/Media/Anime/Airing/Subtitled/subsPlease.ts | 13 |
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; +} |