import { findBestMatchingTitle } from "~/libs/findBestMatchingTitle"; import { sleep } from "~/libs/sleep"; import { Episode, type EpisodesResponse } from "~/types/episode"; export async function getEpisodesFromAniwatch( aniListId: number, shouldRetry: boolean = false, ): Promise { try { const animeTitle = await import("~/libs/anilist/getTitle") .then(({ fetchTitleFromAnilist }) => fetchTitleFromAnilist(aniListId, undefined), ) .then((title) => ({ english: title?.title?.english, userPreferred: title?.title?.userPreferred, })); if (!animeTitle.english && !animeTitle.userPreferred) { return null; } const aniwatchId = await getAniwatchId(animeTitle); if (!aniwatchId) { return null; } const episodes: Episode[] | null = await fetchEpisodes( aniwatchId, aniListId, ); if (!episodes || episodes.length === 0) { return null; } // Tower of God S2 if (aniListId == 153406) { const aniwatchId = await getAniwatchId({ english: "Tower of God Season 2: Workshop Battle", }); if (aniwatchId) { const lastEpisodeOfPreviousTitle = episodes.at(-1)!!.number; return { providerId: "aniwatch", episodes: await fetchEpisodes(aniwatchId, aniListId).then( (extraEpisodes) => episodes.concat( extraEpisodes?.map(({ number, ...episode }) => ({ ...episode, number: number + lastEpisodeOfPreviousTitle, })) ?? [], ), ), }; } } return { providerId: "aniwatch", episodes }; } catch (error) { if (shouldRetry && "response" in error && error.response.status === 429) { console.log( "429, retrying in", error.response.headers.get("Retry-After"), ); return sleep( Number(error.response.headers.get("Retry-After")!) * 1000, ).then(() => getEpisodesFromAniwatch(aniListId)); } console.error( new Error( `Error trying to load episodes from aniwatch; aniListId: ${aniListId}`, { cause: error }, ), ); } return null; } async function fetchEpisodes( aniwatchId: string, aniListId: number, ): Promise< | { number: number; id: string; updatedAt: number; description?: string | null | undefined; title?: string | null | undefined; img?: string | null | undefined; rating?: number | null | undefined; }[] | null > { return await fetch( `https://aniwatch.up.railway.app/api/v2/hianime/anime/${aniwatchId}/episodes`, ) .then( (res) => res.json() as Promise<{ success: boolean; data: AniwatchEpisodesResponse; }>, ) .then(({ success, data }) => { if (!success || data.totalEpisodes === 0) { console.error( `Error trying to load episodes from aniwatch; aniListId: ${aniListId}, totalEpisodes: ${totalEpisodes}`, ); return null; } const { totalEpisodes, episodes } = data; return episodes.map(({ episodeId, number, title }) => ({ id: episodeId, number, title, updatedAt: 0, })); }); } function getAniwatchId( animeTitle: Partial<{ english: string; userPreferred: string }>, ): Promise { return fetch( `https://aniwatch.up.railway.app/api/v2/hianime/search?q=${encodeURIComponent(animeTitle.english ?? animeTitle.userPreferred!)}`, ) .then( (res) => res.json() as Promise<{ success: boolean; data: AniwatchSearchResponse; }>, ) .then(({ success, data: { animes } }) => { if (!success) { return; } const bestMatchingTitle = findBestMatchingTitle( animeTitle, animes.map((anime) => ({ english: anime.name, userPreferred: anime.jname, })), ); return animes.find( (anime) => anime.name === bestMatchingTitle || anime.jname === bestMatchingTitle, )?.id; }); } export interface AniwatchEpisodesResponse { totalEpisodes: number; episodes: AniwatchEpisode[]; } export interface AniwatchEpisode { title: string; episodeId: string; number: number; isFiller: boolean; } export interface AniwatchSearchResponse { animes: Anime[]; currentPage: number; hasNextPage: boolean; totalPages: number; } interface Anime { id: string; name: string; jname: string; poster: string; duration: string; type: Type; rating: null | string; episodes: Episodes; } interface Episodes { sub: number | null; dub: number | null; } enum Type { Movie = "Movie", Ona = "ONA", Ova = "OVA", Special = "Special", Tv = "TV", }