diff --git a/src/controllers/episodes/downloadLegacy.ts b/src/controllers/episodes/downloadLegacy.ts new file mode 100644 index 0000000..79b42ae --- /dev/null +++ b/src/controllers/episodes/downloadLegacy.ts @@ -0,0 +1,117 @@ +import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; +import { env } from "hono/adapter"; + +import { Env } from "~/types/env"; +import { + AniListIdSchema, + ErrorResponse, + ErrorResponseSchema, + SkippableSchema, + SuccessResponseSchema, +} from "~/types/schema"; + +import { getSourcesFromAmvstrm } from "./getEpisodeUrl/amvstrm"; +import { getSourcesFromAnify } from "./getEpisodeUrl/anify"; +import { getSourcesFromConsumet } from "./getEpisodeUrl/consumet"; + +const DownloadRequest = z.object({ + provider: z.string(), + watchId: z.string(), + aniListId: AniListIdSchema, +}); + +const DownloadResponse = SuccessResponseSchema( + z.object({ + source: z.string(), + subtitles: z.array(z.object({ url: z.string(), lang: z.string() })), + audio: z.array(z.object({ url: z.string(), lang: z.string() })), + intro: SkippableSchema, + outro: SkippableSchema, + headers: z.record(z.string()).optional(), + }), +); + +const route = createRoute({ + tags: ["aniplay", "episodes"], + summary: "Fetch stream URL (usually an .m3u8 stream)", + method: "post", + path: "/", + request: { + body: { + content: { + "application/json": { + schema: DownloadRequest, + }, + }, + }, + }, + responses: { + 200: { + description: "Returns a stream URL", + content: { + "application/json": { + schema: DownloadResponse, + }, + }, + }, + 400: { + description: "Invalid request body", + content: { + "application/json": { + schema: ErrorResponseSchema, + }, + }, + }, + }, +}); + +const app = new OpenAPIHono(); + +app.openapi(route, async (c) => { + const { provider, watchId, aniListId } = + await c.req.json(); + + if (provider === "amvstrm") { + try { + return { + success: true, + result: await getSourcesFromAmvstrm(watchId), + }; + } catch (e) { + console.error("Failed to fetch download URL from Amvstrm", e); + + return c.json(ErrorResponse, { status: 500 }); + } + } + + if (provider === "consumet" || !env(c).ENABLE_ANIFY) { + try { + return c.json({ + success: true, + result: await getSourcesFromConsumet(watchId), + }); + } catch (e) { + console.error("Failed to fetch download URL from Consumet", e); + + return c.json(ErrorResponse, { status: 500 }); + } + } + + // provider is not consumet + if (!aniListId) { + return c.json(ErrorResponse, { status: 400 }); + } + + try { + return c.json({ + success: true, + result: await getSourcesFromAnify(provider, watchId, aniListId), + }); + } catch (e) { + console.error("Failed to fetch download URL from Anify", e); + + return c.json(ErrorResponse, { status: 500 }); + } +}); + +export default app; diff --git a/src/controllers/episodes/getEpisodesLegacy.ts b/src/controllers/episodes/getEpisodesLegacy.ts new file mode 100644 index 0000000..ecd1212 --- /dev/null +++ b/src/controllers/episodes/getEpisodesLegacy.ts @@ -0,0 +1,77 @@ +import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; + +import { fetchFromMultipleSources } from "~/libs/fetchFromMultipleSources"; +import { readEnvVariable } from "~/libs/readEnvVariable"; +import type { Env } from "~/types/env"; +import { + AniListIdQuerySchema, + ErrorResponse, + ErrorResponseSchema, + SuccessResponseSchema, +} from "~/types/schema"; + +import { getEpisodesFromAnify } from "./getByAniListId/anify"; +import { EpisodesResponse } from "./getByAniListId/episode"; + +const EpisodesResponseSchema = SuccessResponseSchema(EpisodesResponse); + +const route = createRoute({ + tags: ["aniplay", "episodes"], + summary: "Fetch episodes for a title", + operationId: "fetchEpisodes", + method: "get", + path: "/", + request: { + query: z.object({ aniListId: AniListIdQuerySchema }), + }, + responses: { + 200: { + content: { + "application/json": { + schema: EpisodesResponseSchema, + }, + }, + description: "Returns a list of episodes", + }, + 404: { + content: { + "application/json": { + schema: ErrorResponseSchema, + }, + }, + description: "Returns an empty list because episodes not found", + }, + }, +}); + +const app = new OpenAPIHono(); + +app.openapi(route, async (c) => { + const aniListId = Number(c.req.query("aniListId")); + + const episodes = await fetchFromMultipleSources([ + () => { + const isAnifyEnabled = readEnvVariable(c.env, "ENABLE_ANIFY"); + return getEpisodesFromAnify(isAnifyEnabled, aniListId); + }, + () => + import("./getByAniListId/consumet").then(({ getEpisodesFromConsumet }) => + getEpisodesFromConsumet(aniListId), + ), + () => + import("./getByAniListId/amvstrm").then(({ getEpisodesFromAmvstrm }) => + getEpisodesFromAmvstrm(aniListId), + ), + ]); + + if (!episodes) { + return c.json(ErrorResponse, { status: 404 }); + } + + return c.json({ + success: true, + result: episodes, + }); +}); + +export default app; diff --git a/src/controllers/episodes/index.ts b/src/controllers/episodes/index.ts index ce33e0f..3163b08 100644 --- a/src/controllers/episodes/index.ts +++ b/src/controllers/episodes/index.ts @@ -11,4 +11,10 @@ app.route( await import("./getEpisodeUrl").then((controller) => controller.default), ); +// TODO: Remove this route once v2 is ready +app.route( + "/", + await import("./getEpisodesLegacy").then((controller) => controller.default), +); + export default app; diff --git a/src/index.ts b/src/index.ts index 4e561d1..7f28896 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,13 @@ app.route( "/search", await import("~/controllers/search").then((controller) => controller.default), ); +// TODO: Remove this route once v2 is ready +app.route( + "/download", + await import("~/controllers/episodes/downloadLegacy").then( + (controller) => controller.default, + ), +); // The OpenAPI documentation will be available at /doc app.doc("/openapi.json", {