feat: add Consumet as provider for stream URL

Summary:

Test Plan:
This commit is contained in:
2024-05-30 09:47:07 -04:00
parent 40daf70209
commit 7e3c818828
7 changed files with 137 additions and 21 deletions

5
src/consumet.ts Normal file
View File

@@ -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);

View File

@@ -1,14 +1,10 @@
import { ANIME, META } from "@consumet/extensions"; import { aniList } from "~/consumet";
import fetchAdapter from "@haverstack/axios-fetch-adapter";
import { Episode, EpisodesResponse } from "./episode"; import { Episode, EpisodesResponse } from "./episode";
export async function getEpisodesFromConsumet( export async function getEpisodesFromConsumet(
aniListId: number, aniListId: number,
): Promise<EpisodesResponse | null> { ): Promise<EpisodesResponse | null> {
const gogoAnime = new ANIME.Gogoanime(undefined, undefined, fetchAdapter);
const aniList = new META.Anilist(gogoAnime, undefined, fetchAdapter);
try { try {
const episodes: Episode[] = await aniList const episodes: Episode[] = await aniList
.fetchEpisodesListById(aniListId.toString()) .fetchEpisodesListById(aniListId.toString())

View File

@@ -1,6 +1,7 @@
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
import { fetchFromMultipleSources } from "~/libs/fetchFromMultipleSources"; import { fetchFromMultipleSources } from "~/libs/fetchFromMultipleSources";
import { readEnvVariable } from "~/libs/readEnvVariable";
import type { Env } from "~/types/env"; import type { Env } from "~/types/env";
import { import {
AniListIdQuerySchema, AniListIdQuerySchema,
@@ -49,11 +50,10 @@ app.openapi(route, async (c) => {
const aniListId = Number(c.req.param("aniListId")); const aniListId = Number(c.req.param("aniListId"));
const episodes = await fetchFromMultipleSources([ const episodes = await fetchFromMultipleSources([
() => () => {
getEpisodesFromAnify( const isAnifyEnabled = readEnvVariable<boolean>(c.env, "ENABLE_ANIFY");
JSON.parse((c.env?.["ENABLE_ANIFY"] ?? "true") as string), return getEpisodesFromAnify(isAnifyEnabled, aniListId);
aniListId, },
),
() => () =>
import("./consumet").then(({ getEpisodesFromConsumet }) => import("./consumet").then(({ getEpisodesFromConsumet }) =>
getEpisodesFromConsumet(aniListId), getEpisodesFromConsumet(aniListId),

View File

@@ -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<FetchUrlResponse | null> {
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;
}
}

View File

@@ -50,4 +50,44 @@ describe('requests the "/episodes/:id/url" route', () => {
expect(response.json()).resolves.toEqual({ success: false }); expect(response.json()).resolves.toEqual({ success: false });
expect(response.status).toBe(404); 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);
});
}); });

View File

@@ -1,5 +1,6 @@
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
import { readEnvVariable } from "~/libs/readEnvVariable";
import type { Env } from "~/types/env"; import type { Env } from "~/types/env";
import { import {
ErrorResponse, ErrorResponse,
@@ -7,7 +8,6 @@ import {
SuccessResponseSchema, SuccessResponseSchema,
} from "~/types/schema"; } from "~/types/schema";
import { getSourcesFromAnify } from "./anify";
import { FetchUrlResponse as FetchUrlResponseSchema } from "./responseType"; import { FetchUrlResponse as FetchUrlResponseSchema } from "./responseType";
const FetchUrlRequest = z.object({ id: z.string(), provider: z.string() }); const FetchUrlRequest = z.object({ id: z.string(), provider: z.string() });
@@ -38,6 +38,14 @@ const route = createRoute({
}, },
description: "Returns a stream URL", description: "Returns a stream URL",
}, },
400: {
content: {
"application/json": {
schema: ErrorResponseSchema,
},
},
description: "Unknown provider",
},
404: { 404: {
content: { content: {
"application/json": { "application/json": {
@@ -63,9 +71,32 @@ app.openapi(route, async (c) => {
const aniListId = Number(c.req.param("aniListId")); const aniListId = Number(c.req.param("aniListId"));
const { provider, id } = await c.req.json<typeof FetchUrlRequest._type>(); const { provider, id } = await c.req.json<typeof FetchUrlRequest._type>();
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") { if (provider === "anify") {
try { try {
const result = await getSourcesFromAnify(provider, id, aniListId); const result = await import("./anify").then(({ getSourcesFromAnify }) =>
getSourcesFromAnify(provider, id, aniListId),
);
if (!result) { if (!result) {
return c.json({ success: false }, { status: 404 }); 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: 500 });
} }
} }
return c.json(ErrorResponse, { status: 400 });
}); });
export default app; export default app;

View File

@@ -1,12 +1,10 @@
import type { IAnimeEpisode } from "@consumet/extensions"; import type { IAnimeEpisode, ISource } from "@consumet/extensions";
import { mock } from "bun:test"; import { mock } from "bun:test";
export function mockConsumet() { export function mockConsumet() {
mock.module("@consumet/extensions", () => { mock.module("src/consumet", () => ({
class Gogoanime {} aniList: {
class Anilist {
fetchEpisodesListById( fetchEpisodesListById(
id: string, id: string,
dub?: boolean | undefined, dub?: boolean | undefined,
@@ -23,9 +21,17 @@ export function mockConsumet() {
} }
return Promise.resolve([]); return Promise.resolve([]);
} },
} fetchEpisodeSources(episodeId: string, ...args: any): Promise<ISource> {
if (episodeId === "unknown") {
return Promise.resolve({ sources: [] });
}
return { ANIME: { Gogoanime }, META: { Anilist } }; return Promise.resolve({
}); sources: [{ url: "https://consumet.com" }],
subtitles: [],
});
},
},
}));
} }