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; } 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; let nativeTime = new Date(airingAt ? airingAt * 1000 : 0); let untilAiring; let time = new Date(airingAt ? airingAt * 1000 : 0); const 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 (airingAt && nativeTime > time) { [nativeTime, time] = [time, nativeTime]; } return { ...anime, nextAiringEpisode: { episode: nextEpisode, airingAt: time.getTime() / 1000, nativeAiringAt: nativeTime.getTime() / 1000 } } as Media; };