From 1a06eb51eb9e5c7d3e08ec0d4bc50b6c1bdc8b22 Mon Sep 17 00:00:00 2001 From: Rushil Perera Date: Sun, 18 Aug 2024 21:37:13 -0400 Subject: [PATCH] refactor: replace amvstrm source with aniwatch --- .../episodes/getByAniListId/amvstrm.ts | 60 ---------- .../episodes/getByAniListId/aniwatch.ts | 107 ++++++++++++++++++ .../episodes/getByAniListId/index.ts | 4 +- .../episodes/getEpisodeUrl/amvstrm.ts | 75 ------------ .../episodes/getEpisodeUrl/anify.ts | 16 +-- .../episodes/getEpisodeUrl/aniwatch.ts | 54 +++++++++ .../episodes/getEpisodeUrl/convertSkipTime.ts | 11 ++ .../episodes/getEpisodeUrl/index.ts | 4 +- src/controllers/search/amvstrm.ts | 31 ----- src/controllers/search/index.spec.ts | 6 - src/controllers/search/index.ts | 2 - src/controllers/title/amvstrm.ts | 62 ---------- src/controllers/title/index.spec.ts | 7 -- src/controllers/title/index.ts | 2 - 14 files changed, 179 insertions(+), 262 deletions(-) delete mode 100644 src/controllers/episodes/getByAniListId/amvstrm.ts create mode 100644 src/controllers/episodes/getByAniListId/aniwatch.ts delete mode 100644 src/controllers/episodes/getEpisodeUrl/amvstrm.ts create mode 100644 src/controllers/episodes/getEpisodeUrl/aniwatch.ts create mode 100644 src/controllers/episodes/getEpisodeUrl/convertSkipTime.ts delete mode 100644 src/controllers/search/amvstrm.ts delete mode 100644 src/controllers/title/amvstrm.ts diff --git a/src/controllers/episodes/getByAniListId/amvstrm.ts b/src/controllers/episodes/getByAniListId/amvstrm.ts deleted file mode 100644 index 63f63b7..0000000 --- a/src/controllers/episodes/getByAniListId/amvstrm.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Episode, type EpisodesResponse } from "./episode"; - -export async function getEpisodesFromAmvstrm( - aniListId: number, -): Promise { - try { - const episodes: Episode[] | null = await fetch( - `https://amvstrm.up.railway.app/api/v2/episode/${aniListId}`, - ) - .then((res) => res.json()) - .then(({ code, message, episodes }) => { - if (code >= 400) { - console.error( - `Error trying to load episodes from amvstrm; aniListId: ${aniListId}, code: ${code}, message: ${message}`, - ); - return null; - } - - return episodes.map( - ({ id, description, image, title, episode, airDate }) => ({ - id, - number: episode, - description, - img: image, - title, - updatedAt: airDate ?? 0, - }), - ); - }); - if (!episodes || episodes.length === 0) { - return null; - } - - return { providerId: "amvstrm", episodes }; - } catch (error) { - console.error( - new Error( - `Error trying to load episodes from amvstrm; aniListId: ${aniListId}`, - { cause: error }, - ), - ); - } - - return null; -} - -interface AmvstrmEpisodesResponse { - code: number; - message: string; - episodes: AmvstrmEpisode[]; -} - -interface AmvstrmEpisode { - id: string; - title: string; - description: string | null; - episode: number; - image: string; - airDate: null; -} diff --git a/src/controllers/episodes/getByAniListId/aniwatch.ts b/src/controllers/episodes/getByAniListId/aniwatch.ts new file mode 100644 index 0000000..c1c9934 --- /dev/null +++ b/src/controllers/episodes/getByAniListId/aniwatch.ts @@ -0,0 +1,107 @@ +import { Episode, type EpisodesResponse } from "./episode"; + +export async function getEpisodesFromAniwatch( + aniListId: number, +): Promise { + try { + const animeTitle = await import("~/controllers/title/anilist") + .then(({ fetchTitleFromAnilist }) => + fetchTitleFromAnilist(aniListId, undefined), + ) + .then((title) => title?.title?.userPreferred ?? title?.title?.english); + + if (!animeTitle) { + return null; + } + + const aniwatchId = await getAniwatchId(animeTitle); + if (!aniwatchId) { + return null; + } + + const episodes: Episode[] | null = await fetch( + `https://aniwatch.up.railway.app/anime/episodes/${aniwatchId}`, + ) + .then((res) => res.json()) + .then(({ totalEpisodes, episodes }) => { + if (totalEpisodes === 0) { + console.error( + `Error trying to load episodes from aniwatch; aniListId: ${aniListId}, totalEpisodes: ${totalEpisodes}`, + ); + return null; + } + + return episodes.map(({ episodeId, number, title }) => ({ + id: episodeId, + number, + title, + updatedAt: 0, + })); + }); + if (!episodes || episodes.length === 0) { + return null; + } + + return { providerId: "aniwatch", episodes }; + } catch (error) { + console.error( + new Error( + `Error trying to load episodes from aniwatch; aniListId: ${aniListId}`, + { cause: error }, + ), + ); + } + + return null; +} + +function getAniwatchId(animeTitle: string): Promise { + return fetch( + `https://aniwatch.up.railway.app/anime/search?q=${encodeURIComponent(animeTitle)}`, + ) + .then((res) => res.json()) + .then(({ animes }) => animes[0]?.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", +} diff --git a/src/controllers/episodes/getByAniListId/index.ts b/src/controllers/episodes/getByAniListId/index.ts index 1542f21..fa954ae 100644 --- a/src/controllers/episodes/getByAniListId/index.ts +++ b/src/controllers/episodes/getByAniListId/index.ts @@ -59,8 +59,8 @@ app.openapi(route, async (c) => { getEpisodesFromConsumet(aniListId), ), () => - import("./amvstrm").then(({ getEpisodesFromAmvstrm }) => - getEpisodesFromAmvstrm(aniListId), + import("./aniwatch").then(({ getEpisodesFromAniwatch }) => + getEpisodesFromAniwatch(aniListId), ), ]); diff --git a/src/controllers/episodes/getEpisodeUrl/amvstrm.ts b/src/controllers/episodes/getEpisodeUrl/amvstrm.ts deleted file mode 100644 index 7d1bc6b..0000000 --- a/src/controllers/episodes/getEpisodeUrl/amvstrm.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { FetchUrlResponse } from "./responseType"; - -export async function getSourcesFromAmvstrm( - watchId: string, -): Promise { - const source = await fetch( - `https://amvstrm.up.railway.app/api/v2/stream/${watchId}`, - ) - .then((res) => res.json()) - .then(({ stream }) => { - const streamObj = stream?.multi; - if (!!streamObj) { - return streamObj.main ?? streamObj.backup; - } - }) - .then((streamObj) => streamObj?.url); - - if (!source) { - return null; - } - - return { - source, - subtitles: [], - audio: [], - }; -} - -interface AmvstrmStreamResponse { - code: number; - message: string; - info: Info; - stream: Stream; - iframe: Iframe[]; - plyr: Nspl; - nspl: Nspl; -} - -interface Iframe { - name: string; - iframe: string; -} - -interface Info { - title: string; - id: string; - episode: string; -} - -interface Nspl { - main: string; - backup: string; -} - -interface Stream { - multi: Multi; - tracks: Tracks; -} - -interface Multi { - main: Backup; - backup: Backup; -} - -interface Backup { - url: string; - label: string; - isM3U8: boolean; - quality: string; -} - -interface Tracks { - file: string; - kind: string; -} diff --git a/src/controllers/episodes/getEpisodeUrl/anify.ts b/src/controllers/episodes/getEpisodeUrl/anify.ts index 2e24797..9a7b83d 100644 --- a/src/controllers/episodes/getEpisodeUrl/anify.ts +++ b/src/controllers/episodes/getEpisodeUrl/anify.ts @@ -1,5 +1,6 @@ import { sortByProperty } from "~/libs/sortByProperty"; +import { type SkipTime, convertSkipTime } from "./convertSkipTime"; import { audioPriority, qualityPriority, @@ -37,14 +38,8 @@ export async function getSourcesFromAnify( source, audio, subtitles, - intro: - typeof intro?.start === "number" && typeof intro?.end === "number" - ? [intro.start, intro.end].map((seconds) => Math.floor(seconds)) - : undefined, - outro: - typeof outro?.start === "number" && typeof outro?.end === "number" - ? [outro.start, outro.end].map((seconds) => Math.floor(seconds)) - : undefined, + intro: convertSkipTime(intro), + outro: convertSkipTime(outro), headers: Object.keys(headers ?? {}).length > 0 ? headers : undefined, }; } @@ -58,11 +53,6 @@ interface AnifySourcesResponse { headers?: Record; } -interface SkipTime { - start: number; - end: number; -} - interface VideoSource { url: string; quality: string; diff --git a/src/controllers/episodes/getEpisodeUrl/aniwatch.ts b/src/controllers/episodes/getEpisodeUrl/aniwatch.ts new file mode 100644 index 0000000..31dc774 --- /dev/null +++ b/src/controllers/episodes/getEpisodeUrl/aniwatch.ts @@ -0,0 +1,54 @@ +import { type SkipTime, convertSkipTime } from "./convertSkipTime"; +import type { FetchUrlResponse } from "./responseType"; + +export async function getSourcesFromAmvstrm( + watchId: string, +): Promise { + const { source, intro, outro, subtitles } = await fetch( + `https://aniwatch.up.railway.app/anime/episode-srcs?id=${encodeURIComponent(watchId)}`, + ) + .then((res) => res.json()) + .then(({ intro, outro, sources, tracks }) => { + return { + intro: convertSkipTime(intro), + outro: convertSkipTime(outro), + source: sources[0].url, + subtitles: tracks + .filter(({ kind }) => kind === "captions") + .map(({ file, label }) => ({ url: file, lang: label ?? "" })), + }; + }); + + if (!source) { + return null; + } + + return { + source, + intro, + outro, + subtitles, + audio: [], + }; +} + +interface AniwatchEpisodeUrlResponse { + tracks: Track[]; + intro: SkipTime; + outro: SkipTime; + sources: Source[]; + anilistID: number; + malID: number; +} + +interface Source { + url: string; + type: string; +} + +interface Track { + file: string; + label?: string; + kind: string; + default?: boolean; +} diff --git a/src/controllers/episodes/getEpisodeUrl/convertSkipTime.ts b/src/controllers/episodes/getEpisodeUrl/convertSkipTime.ts new file mode 100644 index 0000000..5a7d964 --- /dev/null +++ b/src/controllers/episodes/getEpisodeUrl/convertSkipTime.ts @@ -0,0 +1,11 @@ +export interface SkipTime { + start: number; + end: number; +} + +export function convertSkipTime(skipTime: SkipTime): number[] | undefined { + return typeof skipTime?.start === "number" && + typeof skipTime?.end === "number" + ? [skipTime.start, skipTime.end].map((seconds) => Math.floor(seconds)) + : undefined; +} diff --git a/src/controllers/episodes/getEpisodeUrl/index.ts b/src/controllers/episodes/getEpisodeUrl/index.ts index 303db94..47f050a 100644 --- a/src/controllers/episodes/getEpisodeUrl/index.ts +++ b/src/controllers/episodes/getEpisodeUrl/index.ts @@ -94,9 +94,9 @@ app.openapi(route, async (c) => { } } - if (provider === "amvstrm") { + if (provider === "aniwatch") { try { - const result = await import("./amvstrm").then( + const result = await import("./aniwatch").then( ({ getSourcesFromAmvstrm }) => getSourcesFromAmvstrm(id), ); if (!result) { diff --git a/src/controllers/search/amvstrm.ts b/src/controllers/search/amvstrm.ts deleted file mode 100644 index fcd6a31..0000000 --- a/src/controllers/search/amvstrm.ts +++ /dev/null @@ -1,31 +0,0 @@ -export async function fetchSearchResultsFromAmvstrm( - query: string, - page: number, - limit: number, -) { - return fetch( - `https://amvstrm.up.railway.app/api/v2/search?q=${query}&p=${page}&limit=${limit}`, - ) - .then((res) => res.json()) - .then(({ pageInfo: { hasNextPage }, results }) => ({ - hasNextPage, - results: results.map( - ({ - id, - title: { userPreferred, english }, - coverImage: { extraLarge, large, medium }, - }: any) => ({ - id, - title: { userPreferred, english }, - coverImage: { extraLarge, large, medium }, - }), - ), - })) - .then((searchResults) => { - if (searchResults.results.length === 0) { - return undefined; - } - - return searchResults; - }); -} diff --git a/src/controllers/search/index.spec.ts b/src/controllers/search/index.spec.ts index 5de9336..78af1ca 100644 --- a/src/controllers/search/index.spec.ts +++ b/src/controllers/search/index.spec.ts @@ -12,12 +12,6 @@ describe('requests the "/search" route', () => { expect(response.json()).resolves.toMatchSnapshot(); }); - it("valid query that returns amvstrm results", async () => { - const response = await app.request("/search?query=amvstrm"); - - expect(response.json()).resolves.toMatchSnapshot(); - }); - it("query that returns no results", async () => { const response = await app.request("/search?query=a"); diff --git a/src/controllers/search/index.ts b/src/controllers/search/index.ts index 4dd23a6..d26d677 100644 --- a/src/controllers/search/index.ts +++ b/src/controllers/search/index.ts @@ -3,7 +3,6 @@ import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; import { fetchFromMultipleSources } from "~/libs/fetchFromMultipleSources"; import { PaginatedResponseSchema } from "~/types/schema"; -import { fetchSearchResultsFromAmvstrm } from "./amvstrm"; import { fetchSearchResultsFromAnilist } from "./anilist"; import { SearchResult } from "./searchResult"; @@ -41,7 +40,6 @@ app.openapi(route, async (c) => { const { result: response, errorOccurred } = await fetchFromMultipleSources([ () => fetchSearchResultsFromAnilist(query, page, limit), - () => fetchSearchResultsFromAmvstrm(query, page, limit), ]); if (!response) { diff --git a/src/controllers/title/amvstrm.ts b/src/controllers/title/amvstrm.ts deleted file mode 100644 index f483b52..0000000 --- a/src/controllers/title/amvstrm.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Title } from "~/types/title"; - -export async function fetchTitleFromAmvstrm( - aniListId: number, -): Promise { - return Promise.all([ - fetch(`https://amvstrm.up.railway.app/api/v2/info/${aniListId}`).then( - (res) => res.json<any>(), - ), - fetchMissingInformationFromAnify(aniListId).catch((err) => { - console.error("Failed to get missing information from Anify", err); - return null; - }), - ]).then(async ([amvstrmInfo, anifyInfo]) => { - if (amvstrmInfo.code >= 400) { - console.error( - `Error trying to load title from amvstrm; aniListId: ${aniListId}, code: ${amvstrmInfo.code}, message: ${amvstrmInfo.message}`, - ); - return undefined; - } - - return { - id: amvstrmInfo.id, - idMal: amvstrmInfo.idMal, - title: { - userPreferred: amvstrmInfo.title.userPreferred, - english: amvstrmInfo.title.english, - }, - description: amvstrmInfo.description, - episodes: amvstrmInfo.episodes, - genres: amvstrmInfo.genres, - status: amvstrmInfo.status, - averageScore: amvstrmInfo.score.averageScore, - bannerImage: amvstrmInfo.bannerImage ?? anifyInfo?.bannerImage, - coverImage: { - extraLarge: amvstrmInfo.coverImage.extraLarge, - large: amvstrmInfo.coverImage.large, - medium: amvstrmInfo.coverImage.medium, - }, - countryOfOrigin: - amvstrmInfo.countryOfOrigin ?? anifyInfo?.countryOfOrigin, - nextAiringEpisode: amvstrmInfo.nextair, - mediaListEntry: null, - }; - }); -} - -type AnifyInformation = { - bannerImage: string | null; - countryOfOrigin: string; -}; - -function fetchMissingInformationFromAnify( - aniListId: number, -): Promise<AnifyInformation> { - return fetch(`https://anify.eltik.cc/info?id=${aniListId}`) - .then((res) => res.json() as Promise<AnifyInformation>) - .then(({ bannerImage, countryOfOrigin }) => ({ - bannerImage, - countryOfOrigin, - })); -} diff --git a/src/controllers/title/index.spec.ts b/src/controllers/title/index.spec.ts index bee1a9e..cb72790 100644 --- a/src/controllers/title/index.spec.ts +++ b/src/controllers/title/index.spec.ts @@ -22,13 +22,6 @@ describe('requests the "/title" route', () => { expect(response.status).toBe(200); }); - it("with an unknown title from anilist but valid title from amvstrm", async () => { - const response = await app.request("/title?id=50"); - - expect(response.json()).resolves.toMatchSnapshot(); - expect(response.status).toBe(200); - }); - it("with an unknown title from all sources", async () => { const response = await app.request("/title?id=-1"); diff --git a/src/controllers/title/index.ts b/src/controllers/title/index.ts index 332ca07..cf2975d 100644 --- a/src/controllers/title/index.ts +++ b/src/controllers/title/index.ts @@ -9,7 +9,6 @@ import { } from "~/types/schema"; import { Title } from "~/types/title"; -import { fetchTitleFromAmvstrm } from "./amvstrm"; import { fetchTitleFromAnilist } from "./anilist"; const app = new OpenAPIHono(); @@ -50,7 +49,6 @@ app.openapi(route, async (c) => { const { result: title, errorOccurred } = await fetchFromMultipleSources([ () => fetchTitleFromAnilist(aniListId, aniListToken ?? undefined), - () => fetchTitleFromAmvstrm(aniListId), ]); if (errorOccurred) {