import { DateTime } from "luxon"; import { PromiseTimedOutError, promiseTimeout } from "~/libs/promiseTimeout"; import { readEnvVariable } from "~/libs/readEnvVariable"; import { sortByProperty } from "~/libs/sortByProperty"; import { getValue, setValue } from "~/models/kv"; import type { EpisodesResponse } from "~/types/episode"; export async function getEpisodesFromAnify( aniListId: number, ): Promise { if (await shouldSkipAnify(aniListId)) { console.log("Skipping Anify for title", aniListId); return null; } let response: AnifyEpisodesResponse[] | null = null; const abortController = new AbortController(); try { response = await promiseTimeout( fetch(`https://anify.eltik.cc/episodes/${aniListId}`, { signal: abortController.signal, }).then((res) => res.json() as Promise), 30 * 1000, ); if ("error" in response) { const error = response.error; if (error === "Too many requests") { console.log( "Sending too many requests to Anify, setting killswitch until", DateTime.now().plus({ minutes: 1 }).toISO(), ); setValue( "anify_killswitch_till", DateTime.now().plus({ minutes: 1 }).toISO(), ); } return null; } } catch (e) { if (e instanceof PromiseTimedOutError) { abortController.abort("Loading episodes from Anify timed out"); } console.error( `Error trying to load episodes from anify; aniListId: ${aniListId}`, ); console.error(e); } if (!response || response.length === 0) { return null; } const sourcePriority = { zoro: 1, gogoanime: 2, }; const filteredEpisodesData = response .filter(({ providerId }) => { if (providerId === "9anime" || providerId === "animepahe") { return false; } if (aniListId == 166873 && providerId === "zoro") { // Mushoku Tensei: Job Reincarnation S2 Part 2 returns incorrect mapping for Zoro only return false; } return true; }) .sort(sortByProperty(sourcePriority, "providerId")); if (filteredEpisodesData.length === 0) { return null; } const selectedEpisodeData = filteredEpisodesData[0]; return { providerId: selectedEpisodeData.providerId, episodes: selectedEpisodeData.episodes.map( ({ id, number, description, img, rating, title, updatedAt }) => ({ id, number, description, img, rating, title, updatedAt: updatedAt ?? 0, }), ), }; } export async function shouldSkipAnify(aniListId: number): Promise { if (!readEnvVariable("ENABLE_ANIFY")) { return true; } // Some mappings on Anify are incorrect so they return episodes from a similar title if ( [ 153406, // Tower of God S2 158927, // Spy x Family S2 166873, // Mushoku Tensei: Jobless Reincarnation S2 part 2 163134, // Re:ZERO -Starting Life in Another World- Season 3 163146, // Blue Lock S2 ].includes(aniListId) ) { return true; } return await getValue("anify_killswitch_till").then((dateTime) => { if (!dateTime) { return false; } return DateTime.fromISO(dateTime).diffNow().as("minutes") > 0; }); } interface AnifyEpisodesResponse { providerId: string; episodes: { id: string; isFiller: boolean | undefined; number: number; title: string; img: string | null; hasDub: boolean; description: string | null; rating: number | null; updatedAt: number | undefined; }[]; }