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?.substring(0, 100), userPreferred: title?.title?.userPreferred?.substring(0, 100), })); 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( `Error trying to load episodes from aniwatch; aniListId: ${aniListId}`, ); console.error(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<{ status: number; data: AniwatchEpisodesResponse; }>, ) .then(({ status, data }) => { if (status >= 300 || data.totalEpisodes === 0) { console.error( `Error trying to load episodes from aniwatch; aniListId: ${aniListId}, totalEpisodes: ${data.totalEpisodes}`, ); return null; } const { episodes } = data; return episodes.map(({ episodeId, number, title }) => ({ id: episodeId, number, title, updatedAt: 0, })); }); } // function updateTitles(title: Partial<{ english: string; userPreferred: string }>) { // const english = title.english?.toLowerCase(); // const userPreferred = title.userPreferred?.toLowerCase(); // if (english?.match(/my hero academia.+[0-9]$/)) { // } // } function getAniwatchId( animeTitle: Partial<{ english: string; userPreferred: string }>, ): Promise { animeTitle = { english: animeTitle?.english?.toLowerCase(), userPreferred: animeTitle?.userPreferred?.toLowerCase(), }; const promises = []; if (animeTitle.userPreferred) { promises.push( fetch( `https://aniwatch.up.railway.app/api/v2/hianime/search?q=${encodeURIComponent( animeTitle.userPreferred, )}`, ), ); } if (animeTitle.english && animeTitle.english !== animeTitle.userPreferred) { promises.push( fetch( `https://aniwatch.up.railway.app/api/v2/hianime/search?q=${encodeURIComponent( animeTitle.english, )}`, ), ); } return Promise.allSettled(promises) .then((responses) => { return responses.reduce( async (current, res) => { if (res.status === "rejected") { return current; } const json = (await res.value.json()) as { status: number; data: AniwatchSearchResponse; }; const currentValue = await current; return { success: currentValue.success || json.status === 200, data: { ...currentValue.data, animes: [ ...currentValue.data.animes, ...(json.data?.animes ?? []), ], }, }; }, Promise.resolve({ success: false, data: { animes: [] }, }), ); }) .then(({ success, data: { animes } }) => { if (!success) { return; } const { title: bestMatchingTitle, score } = findBestMatchingTitle( animeTitle, animes.map((anime) => ({ english: anime.name, userPreferred: anime.jname, })), ); if (score < 0.8) { return; } return animes.find( (anime) => anime.name?.toLowerCase() === bestMatchingTitle || anime.jname?.toLowerCase() === 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", }