feat: add Amvstrm as provider for stream URL
Summary: Test Plan:
This commit is contained in:
75
src/controllers/episodes/getEpisodeUrl/amvstrm.ts
Normal file
75
src/controllers/episodes/getEpisodeUrl/amvstrm.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import type { FetchUrlResponse } from "./responseType";
|
||||||
|
|
||||||
|
export async function getSourcesFromAmvstrm(
|
||||||
|
watchId: string,
|
||||||
|
): Promise<FetchUrlResponse | null> {
|
||||||
|
const source = await fetch(
|
||||||
|
`https://api-amvstrm.nyt92.eu.org/api/v2/stream/${watchId}`,
|
||||||
|
)
|
||||||
|
.then((res) => res.json<AmvstrmStreamResponse>())
|
||||||
|
.then(({ stream }) => {
|
||||||
|
const streamObj = stream?.multi;
|
||||||
|
if (!!streamObj) {
|
||||||
|
return streamObj.main ?? streamObj.backup;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((streamObj) => streamObj?.url);
|
||||||
|
|
||||||
|
if (!source) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
source,
|
||||||
|
subtitles: [],
|
||||||
|
audio: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AmvstrmStreamResponse {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
info: Info;
|
||||||
|
stream: Stream;
|
||||||
|
iframe: Iframe[];
|
||||||
|
plyr: Nspl;
|
||||||
|
nspl: Nspl;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Iframe {
|
||||||
|
name: string;
|
||||||
|
iframe: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Info {
|
||||||
|
title: string;
|
||||||
|
id: string;
|
||||||
|
episode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Nspl {
|
||||||
|
main: string;
|
||||||
|
backup: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Stream {
|
||||||
|
multi: Multi;
|
||||||
|
tracks: Tracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Multi {
|
||||||
|
main: Backup;
|
||||||
|
backup: Backup;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Backup {
|
||||||
|
url: string;
|
||||||
|
label: string;
|
||||||
|
isM3U8: boolean;
|
||||||
|
quality: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Tracks {
|
||||||
|
file: string;
|
||||||
|
kind: string;
|
||||||
|
}
|
||||||
@@ -90,4 +90,45 @@ 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 Amvstrm", async () => {
|
||||||
|
const response = await app.request(
|
||||||
|
"/episodes/1/url",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
provider: "amvstrm",
|
||||||
|
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://www032.vipanicdn.net/streamhls/aa804a2400535d84dd59454b28d329fb/ep.1.1712504065.m3u8",
|
||||||
|
subtitles: [],
|
||||||
|
audio: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("with no URL from Amvstrm source", async () => {
|
||||||
|
const response = await app.request("/episodes/-1/url", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
provider: "amvstrm",
|
||||||
|
id: "unknown",
|
||||||
|
}),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.json()).resolves.toEqual({ success: false });
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -112,6 +112,26 @@ app.openapi(route, async (c) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (provider === "amvstrm") {
|
||||||
|
try {
|
||||||
|
const result = await import("./amvstrm").then(
|
||||||
|
({ getSourcesFromAmvstrm }) => getSourcesFromAmvstrm(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 Amvstrm", e);
|
||||||
|
|
||||||
|
return c.json(ErrorResponse, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return c.json(ErrorResponse, { status: 400 });
|
return c.json(ErrorResponse, { status: 400 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
77
src/mocks/amvstrm/sources.ts
Normal file
77
src/mocks/amvstrm/sources.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { HttpResponse, http } from "msw";
|
||||||
|
|
||||||
|
export function getAmvstrmSources() {
|
||||||
|
return http.get(
|
||||||
|
"https://api-amvstrm.nyt92.eu.org/api/v2/stream/:id",
|
||||||
|
({ params }) => {
|
||||||
|
const { id } = params;
|
||||||
|
|
||||||
|
if (id === "unknown") {
|
||||||
|
return HttpResponse.json(
|
||||||
|
{
|
||||||
|
code: 404,
|
||||||
|
message:
|
||||||
|
"The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.",
|
||||||
|
},
|
||||||
|
{ status: 404 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return HttpResponse.json({
|
||||||
|
code: 200,
|
||||||
|
message: "success",
|
||||||
|
info: {
|
||||||
|
title: "Mushoku Tensei II: Isekai Ittara Honki Dasu Part 2",
|
||||||
|
id: "mushoku-tensei-ii-isekai-ittara-honki-dasu-part-2",
|
||||||
|
episode: "1",
|
||||||
|
},
|
||||||
|
stream: {
|
||||||
|
multi: {
|
||||||
|
main: {
|
||||||
|
url: "https://www032.vipanicdn.net/streamhls/aa804a2400535d84dd59454b28d329fb/ep.1.1712504065.m3u8",
|
||||||
|
label: "hls P",
|
||||||
|
isM3U8: true,
|
||||||
|
quality: "default",
|
||||||
|
},
|
||||||
|
backup: {
|
||||||
|
url: "https://www032.anicdnstream.info/videos/hls/6Ogzt4UOJPbzciJM8EJvgg/1717137410/223419/aa804a2400535d84dd59454b28d329fb/ep.1.1712504065.m3u8",
|
||||||
|
label: "hls P",
|
||||||
|
isM3U8: true,
|
||||||
|
quality: "backup",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tracks: "",
|
||||||
|
},
|
||||||
|
iframe: [
|
||||||
|
{
|
||||||
|
name: "Multiquality Server",
|
||||||
|
iframe:
|
||||||
|
"https://embtaku.com/embedplus?id=MjIzNDE5&token=dvjcF3MKtKBIeAe7rQhIpw&expires=1717137409",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Streamwish",
|
||||||
|
iframe: "https://awish.pro/e/nr6ogony8osz",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Doodstream",
|
||||||
|
iframe: "https://dood.wf/e/4g6gt8yygdnz",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Mp4upload",
|
||||||
|
iframe: "https://www.mp4upload.com/embed-3dshuf4wf6md.html",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plyr: {
|
||||||
|
main: "https://plyr.link/p/player.html#aHR0cHM6Ly93d3cwMzIudmlwYW5pY2RuLm5ldC9zdHJlYW1obHMvYWE4MDRhMjQwMDUzNWQ4NGRkNTk0NTRiMjhkMzI5ZmIvZXAuMS4xNzEyNTA0MDY1Lm0zdTg=",
|
||||||
|
backup:
|
||||||
|
"https://plyr.link/p/player.html#aHR0cHM6Ly93d3cwMzIuYW5pY2Ruc3RyZWFtLmluZm8vdmlkZW9zL2hscy82T2d6dDRVT0pQYnpjaUpNOEVKdmdnLzE3MTcxMzc0MTAvMjIzNDE5L2FhODA0YTI0MDA1MzVkODRkZDU5NDU0YjI4ZDMyOWZiL2VwLjEuMTcxMjUwNDA2NS5tM3U4",
|
||||||
|
},
|
||||||
|
nspl: {
|
||||||
|
main: "https://nspl.nyt92.eu.org/player?p=JnRpdGxlPW11c2hva3UtdGVuc2VpLWlpLWlzZWthaS1pdHRhcmEtaG9ua2ktZGFzdS1wYXJ0LTItZXBpc29kZS0xJmZpbGU9aHR0cHM6Ly93d3cwMzIudmlwYW5pY2RuLm5ldC9zdHJlYW1obHMvYWE4MDRhMjQwMDUzNWQ4NGRkNTk0NTRiMjhkMzI5ZmIvZXAuMS4xNzEyNTA0MDY1Lm0zdTgmdGh1bWJuYWlscz11bmRlZmluZWQ=",
|
||||||
|
backup:
|
||||||
|
"https://nspl.nyt92.eu.org/player?p=JnRpdGxlPW11c2hva3UtdGVuc2VpLWlpLWlzZWthaS1pdHRhcmEtaG9ua2ktZGFzdS1wYXJ0LTItZXBpc29kZS0xJmZpbGU9aHR0cHM6Ly93d3cwMzIuYW5pY2Ruc3RyZWFtLmluZm8vdmlkZW9zL2hscy82T2d6dDRVT0pQYnpjaUpNOEVKdmdnLzE3MTcxMzc0MTAvMjIzNDE5L2FhODA0YTI0MDA1MzVkODRkZDU5NDU0YjI4ZDMyOWZiL2VwLjEuMTcxMjUwNDA2NS5tM3U4JnRodW1ibmFpbHM9dW5kZWZpbmVk",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getAmvstrmEpisodes } from "./amvstrm/episodes";
|
import { getAmvstrmEpisodes } from "./amvstrm/episodes";
|
||||||
import { getAmvstrmSearchResults } from "./amvstrm/search";
|
import { getAmvstrmSearchResults } from "./amvstrm/search";
|
||||||
|
import { getAmvstrmSources } from "./amvstrm/sources";
|
||||||
import { getAmvstrmTitle } from "./amvstrm/title";
|
import { getAmvstrmTitle } from "./amvstrm/title";
|
||||||
import { getAnifyEpisodes } from "./anify/episodes";
|
import { getAnifyEpisodes } from "./anify/episodes";
|
||||||
import { getAnifySources } from "./anify/sources";
|
import { getAnifySources } from "./anify/sources";
|
||||||
@@ -11,6 +12,7 @@ export const handlers = [
|
|||||||
getAnilistSearchResults(),
|
getAnilistSearchResults(),
|
||||||
getAnilistTitle(),
|
getAnilistTitle(),
|
||||||
getAmvstrmEpisodes(),
|
getAmvstrmEpisodes(),
|
||||||
|
getAmvstrmSources(),
|
||||||
getAmvstrmSearchResults(),
|
getAmvstrmSearchResults(),
|
||||||
getAmvstrmTitle(),
|
getAmvstrmTitle(),
|
||||||
getAnifyEpisodes(),
|
getAnifyEpisodes(),
|
||||||
|
|||||||
8
src/types/anilist-graphql.d.ts
vendored
8
src/types/anilist-graphql.d.ts
vendored
@@ -1,5 +1,3 @@
|
|||||||
import * as gqlTada from "gql.tada";
|
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
|
|
||||||
@@ -205,8 +203,10 @@ export type introspection = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
declare module "gql.tada" {
|
import * as gqlTada from 'gql.tada';
|
||||||
|
|
||||||
|
declare module 'gql.tada' {
|
||||||
interface setupSchema {
|
interface setupSchema {
|
||||||
introspection: introspection;
|
introspection: introspection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user