diff --git a/src/consumet.ts b/src/consumet.ts new file mode 100644 index 0000000..cdb0e7d --- /dev/null +++ b/src/consumet.ts @@ -0,0 +1,5 @@ +import { ANIME, META } from "@consumet/extensions"; +import fetchAdapter from "@haverstack/axios-fetch-adapter"; + +const gogoAnime = new ANIME.Gogoanime(undefined, undefined, fetchAdapter); +export const aniList = new META.Anilist(gogoAnime, undefined, fetchAdapter); diff --git a/src/controllers/episodes/getByAniListId/consumet.ts b/src/controllers/episodes/getByAniListId/consumet.ts index 53dde83..569d8c0 100644 --- a/src/controllers/episodes/getByAniListId/consumet.ts +++ b/src/controllers/episodes/getByAniListId/consumet.ts @@ -1,14 +1,10 @@ -import { ANIME, META } from "@consumet/extensions"; -import fetchAdapter from "@haverstack/axios-fetch-adapter"; +import { aniList } from "~/consumet"; import { Episode, EpisodesResponse } from "./episode"; export async function getEpisodesFromConsumet( aniListId: number, ): Promise { - const gogoAnime = new ANIME.Gogoanime(undefined, undefined, fetchAdapter); - const aniList = new META.Anilist(gogoAnime, undefined, fetchAdapter); - try { const episodes: Episode[] = await aniList .fetchEpisodesListById(aniListId.toString()) diff --git a/src/controllers/episodes/getByAniListId/index.ts b/src/controllers/episodes/getByAniListId/index.ts index 4244dd5..38efc1e 100644 --- a/src/controllers/episodes/getByAniListId/index.ts +++ b/src/controllers/episodes/getByAniListId/index.ts @@ -1,6 +1,7 @@ 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, @@ -49,11 +50,10 @@ app.openapi(route, async (c) => { const aniListId = Number(c.req.param("aniListId")); const episodes = await fetchFromMultipleSources([ - () => - getEpisodesFromAnify( - JSON.parse((c.env?.["ENABLE_ANIFY"] ?? "true") as string), - aniListId, - ), + () => { + const isAnifyEnabled = readEnvVariable(c.env, "ENABLE_ANIFY"); + return getEpisodesFromAnify(isAnifyEnabled, aniListId); + }, () => import("./consumet").then(({ getEpisodesFromConsumet }) => getEpisodesFromConsumet(aniListId), diff --git a/src/controllers/episodes/getEpisodeUrl/consumet.ts b/src/controllers/episodes/getEpisodeUrl/consumet.ts new file mode 100644 index 0000000..6790382 --- /dev/null +++ b/src/controllers/episodes/getEpisodeUrl/consumet.ts @@ -0,0 +1,36 @@ +import { aniList } from "~/consumet"; +import { sortByProperty } from "~/libs/sortByProperty"; + +import { qualityPriority, subtitlesPriority } from "./priorities"; +import type { FetchUrlResponse } from "./responseType"; + +export async function getSourcesFromConsumet( + watchId: string, +): Promise { + try { + const { sources, subtitles, intro, outro } = + await aniList.fetchEpisodeSources(watchId); + + if (sources.length === 0) { + return null; + } + + const source = sources.sort(sortByProperty(qualityPriority, "quality"))[0] + ?.url; + subtitles?.sort(sortByProperty(subtitlesPriority, "lang")); + + return { + source, + subtitles: subtitles ?? [], + audio: [], + intro: intro ? [intro.start, intro.end] : undefined, + outro: outro ? [outro.start, outro.end] : undefined, + }; + } catch (error) { + if (error.message === "Episode not found.") { + return null; + } + + throw error; + } +} diff --git a/src/controllers/episodes/getEpisodeUrl/index.spec.ts b/src/controllers/episodes/getEpisodeUrl/index.spec.ts index ab5d18f..645b8fb 100644 --- a/src/controllers/episodes/getEpisodeUrl/index.spec.ts +++ b/src/controllers/episodes/getEpisodeUrl/index.spec.ts @@ -50,4 +50,44 @@ describe('requests the "/episodes/:id/url" route', () => { expect(response.json()).resolves.toEqual({ success: false }); expect(response.status).toBe(404); }); + + it("with sources from Consumet", async () => { + const response = await app.request( + "/episodes/1/url", + { + method: "POST", + body: JSON.stringify({ + provider: "consumet", + id: "/ore-dake-level-up-na-ken-episode-2", + }), + headers: { "Content-Type": "application/json" }, + }, + { + ENABLE_ANIFY: "true", + }, + ); + + expect(response.json()).resolves.toEqual({ + success: true, + result: { + source: "https://consumet.com", + subtitles: [], + audio: [], + }, + }); + }); + + it("with no URL from Consumet source", async () => { + const response = await app.request("/episodes/-1/url", { + method: "POST", + body: JSON.stringify({ + provider: "consumet", + id: "unknown", + }), + headers: { "Content-Type": "application/json" }, + }); + + expect(response.json()).resolves.toEqual({ success: false }); + expect(response.status).toBe(404); + }); }); diff --git a/src/controllers/episodes/getEpisodeUrl/index.ts b/src/controllers/episodes/getEpisodeUrl/index.ts index 97a3364..5066f27 100644 --- a/src/controllers/episodes/getEpisodeUrl/index.ts +++ b/src/controllers/episodes/getEpisodeUrl/index.ts @@ -1,5 +1,6 @@ import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; +import { readEnvVariable } from "~/libs/readEnvVariable"; import type { Env } from "~/types/env"; import { ErrorResponse, @@ -7,7 +8,6 @@ import { SuccessResponseSchema, } from "~/types/schema"; -import { getSourcesFromAnify } from "./anify"; import { FetchUrlResponse as FetchUrlResponseSchema } from "./responseType"; const FetchUrlRequest = z.object({ id: z.string(), provider: z.string() }); @@ -38,6 +38,14 @@ const route = createRoute({ }, description: "Returns a stream URL", }, + 400: { + content: { + "application/json": { + schema: ErrorResponseSchema, + }, + }, + description: "Unknown provider", + }, 404: { content: { "application/json": { @@ -63,9 +71,32 @@ app.openapi(route, async (c) => { const aniListId = Number(c.req.param("aniListId")); const { provider, id } = await c.req.json(); + const isAnifyEnabled = readEnvVariable(c.env, "ENABLE_ANIFY"); + if (provider === "consumet" || !isAnifyEnabled) { + try { + const result = await import("./consumet").then( + ({ getSourcesFromConsumet }) => getSourcesFromConsumet(id), + ); + if (!result) { + return c.json({ success: false }, { status: 404 }); + } + + return c.json({ + success: true, + result, + }); + } catch (e) { + console.error("Failed to fetch download URL from Consumet", e); + + return c.json(ErrorResponse, { status: 500 }); + } + } + if (provider === "anify") { try { - const result = await getSourcesFromAnify(provider, id, aniListId); + const result = await import("./anify").then(({ getSourcesFromAnify }) => + getSourcesFromAnify(provider, id, aniListId), + ); if (!result) { return c.json({ success: false }, { status: 404 }); } @@ -80,6 +111,8 @@ app.openapi(route, async (c) => { return c.json(ErrorResponse, { status: 500 }); } } + + return c.json(ErrorResponse, { status: 400 }); }); export default app; diff --git a/src/mocks/consumet.ts b/src/mocks/consumet.ts index 676949f..c2d1010 100644 --- a/src/mocks/consumet.ts +++ b/src/mocks/consumet.ts @@ -1,12 +1,10 @@ -import type { IAnimeEpisode } from "@consumet/extensions"; +import type { IAnimeEpisode, ISource } from "@consumet/extensions"; import { mock } from "bun:test"; export function mockConsumet() { - mock.module("@consumet/extensions", () => { - class Gogoanime {} - - class Anilist { + mock.module("src/consumet", () => ({ + aniList: { fetchEpisodesListById( id: string, dub?: boolean | undefined, @@ -23,9 +21,17 @@ export function mockConsumet() { } return Promise.resolve([]); - } - } + }, + fetchEpisodeSources(episodeId: string, ...args: any): Promise { + if (episodeId === "unknown") { + return Promise.resolve({ sources: [] }); + } - return { ANIME: { Gogoanime }, META: { Anilist } }; - }); + return Promise.resolve({ + sources: [{ url: "https://consumet.com" }], + subtitles: [], + }); + }, + }, + })); }