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 { Env } from "~/types/env"; import type { EpisodesResponse } from "~/types/episode"; export async function getEpisodesFromAnify( env: Env, aniListId: number, ): Promise { if (await shouldSkipAnify(env, 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( env, "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( new Error( `Error trying to load episodes from anify; aniListId: ${aniListId}`, { cause: e }, ), ); } if (!response || response.length === 0) { return null; } const sourcePriority = { zoro: 1, gogoanime: 2, }; const filteredEpisodesData = response .filter(({ providerId }) => { if (providerId === "9anime") { 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")); 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( env: Env, aniListId: number, ): Promise { if (!readEnvVariable(env, "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 ].includes(aniListId) ) { return true; } return await getValue(env, "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; }[]; }