feat: add Amvstrm as provider for stream URL

Summary:

Test Plan:
This commit is contained in:
2024-05-30 22:48:02 -04:00
parent 7e3c818828
commit c0ef6838fc
6 changed files with 220 additions and 5 deletions

View 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;
}

View File

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

View File

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

View 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",
},
});
},
);
}

View File

@@ -1,5 +1,6 @@
import { getAmvstrmEpisodes } from "./amvstrm/episodes";
import { getAmvstrmSearchResults } from "./amvstrm/search";
import { getAmvstrmSources } from "./amvstrm/sources";
import { getAmvstrmTitle } from "./amvstrm/title";
import { getAnifyEpisodes } from "./anify/episodes";
import { getAnifySources } from "./anify/sources";
@@ -11,6 +12,7 @@ export const handlers = [
getAnilistSearchResults(),
getAnilistTitle(),
getAmvstrmEpisodes(),
getAmvstrmSources(),
getAmvstrmSearchResults(),
getAmvstrmTitle(),
getAnifyEpisodes(),

View File

@@ -1,5 +1,3 @@
import * as gqlTada from "gql.tada";
/* eslint-disable */
/* 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 {
introspection: introspection;
introspection: introspection
}
}
}