feat: add Consumet as provider for stream URL
Summary: Test Plan:
This commit is contained in:
5
src/consumet.ts
Normal file
5
src/consumet.ts
Normal 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);
|
||||
@@ -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<EpisodesResponse | null> {
|
||||
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())
|
||||
|
||||
@@ -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<boolean>(c.env, "ENABLE_ANIFY");
|
||||
return getEpisodesFromAnify(isAnifyEnabled, aniListId);
|
||||
},
|
||||
() =>
|
||||
import("./consumet").then(({ getEpisodesFromConsumet }) =>
|
||||
getEpisodesFromConsumet(aniListId),
|
||||
|
||||
36
src/controllers/episodes/getEpisodeUrl/consumet.ts
Normal file
36
src/controllers/episodes/getEpisodeUrl/consumet.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<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") {
|
||||
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;
|
||||
|
||||
@@ -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<ISource> {
|
||||
if (episodeId === "unknown") {
|
||||
return Promise.resolve({ sources: [] });
|
||||
}
|
||||
|
||||
return { ANIME: { Gogoanime }, META: { Anilist } };
|
||||
});
|
||||
return Promise.resolve({
|
||||
sources: [{ url: "https://consumet.com" }],
|
||||
subtitles: [],
|
||||
});
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user