From 5815ab8cbc367f8ba1d0362c9413c52986f0f708 Mon Sep 17 00:00:00 2001 From: Fuwn Date: Wed, 20 Dec 2023 04:46:23 -0800 Subject: refactor(airing): move to module --- src/lib/List/Anime/CleanAnimeList.svelte | 2 +- src/lib/List/Anime/DueAnimeList.svelte | 2 +- src/lib/Media/Anime/Airing/subtitled.ts | 156 ++++++++++++++++++++ src/lib/Media/Anime/Airing/time.ts | 83 +++++++++++ src/lib/Media/Anime/airing.ts | 235 ------------------------------- src/lib/Tools/SequelSpy.svelte | 2 +- src/routes/schedule/+page.svelte | 2 +- 7 files changed, 243 insertions(+), 239 deletions(-) create mode 100644 src/lib/Media/Anime/Airing/subtitled.ts create mode 100644 src/lib/Media/Anime/Airing/time.ts delete mode 100644 src/lib/Media/Anime/airing.ts (limited to 'src') diff --git a/src/lib/List/Anime/CleanAnimeList.svelte b/src/lib/List/Anime/CleanAnimeList.svelte index 3da3556e..64af9ddd 100644 --- a/src/lib/List/Anime/CleanAnimeList.svelte +++ b/src/lib/List/Anime/CleanAnimeList.svelte @@ -5,7 +5,7 @@ import type { Media } from '$lib/AniList/media'; import { cleanCache, updateMedia } from '$lib/Media/Anime/cache'; import { totalEpisodes } from '$lib/Media/Anime/episodes'; - import { airingTime } from '$lib/Media/Anime/airing'; + import { airingTime } from '$lib/Media/Anime/Airing/time'; import type { AniListAuthorisation, UserIdentity } from '$lib/AniList/identity'; import ListTitle from '../ListTitle.svelte'; import MediaTitle from '../MediaTitleDisplay.svelte'; diff --git a/src/lib/List/Anime/DueAnimeList.svelte b/src/lib/List/Anime/DueAnimeList.svelte index ac370892..98cc43c0 100644 --- a/src/lib/List/Anime/DueAnimeList.svelte +++ b/src/lib/List/Anime/DueAnimeList.svelte @@ -7,7 +7,7 @@ import lastPruneTimes from '../../../stores/lastPruneTimes'; import AnimeList from './AnimeListTemplate.svelte'; import type { SubsPlease } from '$lib/subsPlease'; - import { injectAiringTime } from '$lib/Media/Anime/airing'; + import { injectAiringTime } from '$lib/Media/Anime/Airing/subtitled'; export let user: AniListAuthorisation; export let identity: UserIdentity; diff --git a/src/lib/Media/Anime/Airing/subtitled.ts b/src/lib/Media/Anime/Airing/subtitled.ts new file mode 100644 index 00000000..73e4bddd --- /dev/null +++ b/src/lib/Media/Anime/Airing/subtitled.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/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/time.ts b/src/lib/Media/Anime/Airing/time.ts new file mode 100644 index 00000000..59e9e073 --- /dev/null +++ b/src/lib/Media/Anime/Airing/time.ts @@ -0,0 +1,83 @@ +import type { Media } from '$lib/AniList/media'; +import type { MediaPrequel } from '$lib/AniList/prequels'; +import type { SubsPlease } from '$lib/subsPlease'; +import settings from '../../../../stores/settings'; +import { injectAiringTime } from './subtitled'; +import { totalEpisodes } from '../episodes'; + +export const airingTime = ( + originalAnime: Media, + subsPlease: SubsPlease | null, + upcoming = false +) => { + const anime = injectAiringTime(originalAnime, subsPlease); + const airingAt = anime.nextAiringEpisode?.airingAt; + const untilAiring = airingAt ? Math.round((airingAt - Date.now() / 1000) * 100) / 100 : undefined; + const time = new Date(airingAt ? airingAt * 1000 : 0).toLocaleTimeString([], { + hour12: !settings.get().display24HourTime, + hour: 'numeric', + minute: '2-digit' + }); + let timeFrame; + let hours = null; + + if ( + (anime as unknown as MediaPrequel).startDate && + new Date( + anime.startDate.year, + (anime as unknown as MediaPrequel).startDate.month, + (anime as unknown as MediaPrequel).startDate.day + ) < new Date() + ) + return `on ${new Date( + anime.startDate.year, + (anime as unknown as MediaPrequel).startDate.month, + (anime as unknown as MediaPrequel).startDate.day + ).toLocaleDateString()}`; + + if (untilAiring !== undefined) { + let minutes = untilAiring / 60; + let few = true; + + if (minutes > 60) { + hours = minutes / 60; + + if (hours >= 24) { + // let weeks = Math.floor(Math.floor(hours / 24) / 7); + + few = false; + + // if (weeks >= 1) { + // weeks = Math.round(weeks); + + // timeFrame = `${weeks} week${weeks === 1 ? '' : 's'}`; + // } else { + const days = Math.round(Math.floor(hours / 24)); + + timeFrame = `${days.toFixed(0)} day${days === 1 ? '' : 's'}`; + // } + } else timeFrame = `${hours.toFixed(1)} hour${hours === 1 ? '' : 's'}`; + } else { + minutes = Math.round(minutes); + + timeFrame = `${minutes} minute${minutes === 1 ? '' : 's'}`; + } + + const opacity = Math.max(50, 100 - (untilAiring / 60 / 60 / 24 / 7) * 50); + + if (upcoming) + return `${anime.nextAiringEpisode?.episode}${totalEpisodes( + anime + )} in ${timeFrame} ${few ? `(${time})` : ''}`; + else + return `${anime.nextAiringEpisode?.episode} in ${ + few ? '' : '' + }${timeFrame}${few ? '' : ''} ${few ? `(${time})` : ''}`; + } + + return ''; +}; diff --git a/src/lib/Media/Anime/airing.ts b/src/lib/Media/Anime/airing.ts deleted file mode 100644 index 5d51386b..00000000 --- a/src/lib/Media/Anime/airing.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { get } from 'svelte/store'; -import type { Media } from '../../AniList/media'; -import settings from '../../../stores/settings'; -import type { MediaPrequel } from '$lib/AniList/prequels'; -import type { SubsPlease } from '$lib/subsPlease'; -import levenshtein from 'fast-levenshtein'; -import { totalEpisodes } from './episodes'; - -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; -}; - -export const airingTime = ( - originalAnime: Media, - subsPlease: SubsPlease | null, - upcoming = false -) => { - const anime = injectAiringTime(originalAnime, subsPlease); - const airingAt = anime.nextAiringEpisode?.airingAt; - const untilAiring = airingAt ? Math.round((airingAt - Date.now() / 1000) * 100) / 100 : undefined; - const time = new Date(airingAt ? airingAt * 1000 : 0).toLocaleTimeString([], { - hour12: !settings.get().display24HourTime, - hour: 'numeric', - minute: '2-digit' - }); - let timeFrame; - let hours = null; - - if ( - (anime as unknown as MediaPrequel).startDate && - new Date( - anime.startDate.year, - (anime as unknown as MediaPrequel).startDate.month, - (anime as unknown as MediaPrequel).startDate.day - ) < new Date() - ) - return `on ${new Date( - anime.startDate.year, - (anime as unknown as MediaPrequel).startDate.month, - (anime as unknown as MediaPrequel).startDate.day - ).toLocaleDateString()}`; - - if (untilAiring !== undefined) { - let minutes = untilAiring / 60; - let few = true; - - if (minutes > 60) { - hours = minutes / 60; - - if (hours >= 24) { - // let weeks = Math.floor(Math.floor(hours / 24) / 7); - - few = false; - - // if (weeks >= 1) { - // weeks = Math.round(weeks); - - // timeFrame = `${weeks} week${weeks === 1 ? '' : 's'}`; - // } else { - const days = Math.round(Math.floor(hours / 24)); - - timeFrame = `${days.toFixed(0)} day${days === 1 ? '' : 's'}`; - // } - } else timeFrame = `${hours.toFixed(1)} hour${hours === 1 ? '' : 's'}`; - } else { - minutes = Math.round(minutes); - - timeFrame = `${minutes} minute${minutes === 1 ? '' : 's'}`; - } - - const opacity = Math.max(50, 100 - (untilAiring / 60 / 60 / 24 / 7) * 50); - - if (upcoming) - return `${anime.nextAiringEpisode?.episode}${totalEpisodes( - anime - )} in ${timeFrame} ${few ? `(${time})` : ''}`; - else - return `${anime.nextAiringEpisode?.episode} in ${ - few ? '' : '' - }${timeFrame}${few ? '' : ''} ${few ? `(${time})` : ''}`; - } - - return ''; -}; diff --git a/src/lib/Tools/SequelSpy.svelte b/src/lib/Tools/SequelSpy.svelte index 2ff7832b..44e453fe 100644 --- a/src/lib/Tools/SequelSpy.svelte +++ b/src/lib/Tools/SequelSpy.svelte @@ -4,7 +4,7 @@ import MediaTitle from '$lib/List/MediaTitleDisplay.svelte'; import { onMount } from 'svelte'; import { clearAllParameters, parseOrDefault } from './tool'; - import { airingTime } from '$lib/Media/Anime/airing'; + import { airingTime } from '$lib/Media/Anime/Airing/time'; import type { Media } from '$lib/AniList/media'; import { page } from '$app/stores'; import { browser } from '$app/environment'; diff --git a/src/routes/schedule/+page.svelte b/src/routes/schedule/+page.svelte index d88535cb..fd3ac442 100644 --- a/src/routes/schedule/+page.svelte +++ b/src/routes/schedule/+page.svelte @@ -8,7 +8,7 @@ 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 { findClosestMedia } from '$lib/Media/Anime/Airing/subtitled'; import MediaTitleDisplay from '$lib/List/MediaTitleDisplay.svelte'; import { outboundLink } from '$lib/Media/links'; -- cgit v1.2.3