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 { 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())
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
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.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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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: [],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user