mirror of
https://github.com/movie-web/movie-web.git
synced 2025-09-13 18:13:24 +00:00
Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
47eba8caa4 | ||
|
1dc957b56a | ||
|
e653c72d87 | ||
|
c39d61cf53 | ||
|
b14a73378f | ||
|
43d1e290fc | ||
|
1f6318360e | ||
|
791299dd43 | ||
|
2c92bbf94e | ||
|
e3569c7ed7 | ||
|
196a805d32 | ||
|
94d6d7b37e | ||
|
fde5f0c82e | ||
|
bb449d6dfb | ||
|
bb8b21324b | ||
|
53fe6031d1 | ||
|
ee9400373d | ||
|
6c8cc63cbc | ||
|
8c105e78b5 | ||
|
28f253c542 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "movie-web",
|
"name": "movie-web",
|
||||||
"version": "3.2.0",
|
"version": "3.2.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://movie-web.app",
|
"homepage": "https://movie-web.app",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@@ -29,6 +29,32 @@ function isJSON(json: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractKey(script: string): [number, number][] | null {
|
||||||
|
const startOfSwitch = script.lastIndexOf("switch");
|
||||||
|
const endOfCases = script.indexOf("partKeyStartPosition");
|
||||||
|
const switchBody = script.slice(startOfSwitch, endOfCases);
|
||||||
|
|
||||||
|
const nums: [number, number][] = [];
|
||||||
|
const matches = switchBody.matchAll(
|
||||||
|
/:[a-zA-Z0-9]+=([a-zA-Z0-9]+),[a-zA-Z0-9]+=([a-zA-Z0-9]+);/g
|
||||||
|
);
|
||||||
|
for (const match of matches) {
|
||||||
|
const innerNumbers: number[] = [];
|
||||||
|
for (const varMatch of [match[1], match[2]]) {
|
||||||
|
const regex = new RegExp(`${varMatch}=0x([a-zA-Z0-9]+)`, "g");
|
||||||
|
const varMatches = [...script.matchAll(regex)];
|
||||||
|
const lastMatch = varMatches[varMatches.length - 1];
|
||||||
|
if (!lastMatch) return null;
|
||||||
|
const number = parseInt(lastMatch[1], 16);
|
||||||
|
innerNumbers.push(number);
|
||||||
|
}
|
||||||
|
|
||||||
|
nums.push([innerNumbers[0], innerNumbers[1]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nums;
|
||||||
|
}
|
||||||
|
|
||||||
registerEmbedScraper({
|
registerEmbedScraper({
|
||||||
id: "upcloud",
|
id: "upcloud",
|
||||||
displayName: "UpCloud",
|
displayName: "UpCloud",
|
||||||
@@ -54,23 +80,31 @@ registerEmbedScraper({
|
|||||||
let sources: { file: string; type: string } | null = null;
|
let sources: { file: string; type: string } | null = null;
|
||||||
|
|
||||||
if (!isJSON(streamRes.sources)) {
|
if (!isJSON(streamRes.sources)) {
|
||||||
const decryptionKey = JSON.parse(
|
const scriptJs = await proxiedFetch<string>(
|
||||||
await proxiedFetch<string>(
|
`https://rabbitstream.net/js/player/prod/e4-player.min.js`,
|
||||||
`https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt`
|
{
|
||||||
)
|
responseType: "text" as any,
|
||||||
) as [number, number][];
|
}
|
||||||
|
);
|
||||||
|
const decryptionKey = extractKey(scriptJs);
|
||||||
|
if (!decryptionKey) throw new Error("Key extraction failed");
|
||||||
|
|
||||||
let extractedKey = "";
|
let extractedKey = "";
|
||||||
const sourcesArray = streamRes.sources.split("");
|
let strippedSources = streamRes.sources;
|
||||||
for (const index of decryptionKey) {
|
let totalledOffset = 0;
|
||||||
for (let i: number = index[0]; i < index[1]; i += 1) {
|
decryptionKey.forEach(([a, b]) => {
|
||||||
extractedKey += streamRes.sources[i];
|
const start = a + totalledOffset;
|
||||||
sourcesArray[i] = "";
|
const end = start + b;
|
||||||
}
|
extractedKey += streamRes.sources.slice(start, end);
|
||||||
}
|
strippedSources = strippedSources.replace(
|
||||||
|
streamRes.sources.substring(start, end),
|
||||||
|
""
|
||||||
|
);
|
||||||
|
totalledOffset += b;
|
||||||
|
});
|
||||||
|
|
||||||
const decryptedStream = AES.decrypt(
|
const decryptedStream = AES.decrypt(
|
||||||
sourcesArray.join(""),
|
strippedSources,
|
||||||
extractedKey
|
extractedKey
|
||||||
).toString(enc.Utf8);
|
).toString(enc.Utf8);
|
||||||
const parsedStream = JSON.parse(decryptedStream)[0];
|
const parsedStream = JSON.parse(decryptedStream)[0];
|
||||||
|
@@ -18,12 +18,6 @@ import { compareTitle } from "@/utils/titleMatch";
|
|||||||
|
|
||||||
const nanoid = customAlphabet("0123456789abcdef", 32);
|
const nanoid = customAlphabet("0123456789abcdef", 32);
|
||||||
|
|
||||||
function makeFasterUrl(url: string) {
|
|
||||||
const fasterUrl = new URL(url);
|
|
||||||
fasterUrl.host = "mp4.shegu.net"; // this domain is faster
|
|
||||||
return fasterUrl.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const qualityMap = {
|
const qualityMap = {
|
||||||
"360p": MWStreamQuality.Q360P,
|
"360p": MWStreamQuality.Q360P,
|
||||||
"480p": MWStreamQuality.Q480P,
|
"480p": MWStreamQuality.Q480P,
|
||||||
@@ -154,13 +148,13 @@ registerProvider({
|
|||||||
async scrape({ media, episode, progress }) {
|
async scrape({ media, episode, progress }) {
|
||||||
// Find Superstream ID for show
|
// Find Superstream ID for show
|
||||||
const searchQuery = {
|
const searchQuery = {
|
||||||
module: "Search3",
|
module: "Search4",
|
||||||
page: "1",
|
page: "1",
|
||||||
type: "all",
|
type: "all",
|
||||||
keyword: media.meta.title,
|
keyword: media.meta.title,
|
||||||
pagelimit: "20",
|
pagelimit: "20",
|
||||||
};
|
};
|
||||||
const searchRes = (await get(searchQuery, true)).data;
|
const searchRes = (await get(searchQuery, true)).data.list;
|
||||||
progress(33);
|
progress(33);
|
||||||
|
|
||||||
const superstreamEntry = searchRes.find(
|
const superstreamEntry = searchRes.find(
|
||||||
@@ -205,7 +199,7 @@ registerProvider({
|
|||||||
return {
|
return {
|
||||||
embeds: [],
|
embeds: [],
|
||||||
stream: {
|
stream: {
|
||||||
streamUrl: makeFasterUrl(hdQuality.path),
|
streamUrl: hdQuality.path,
|
||||||
quality: qualityMap[hdQuality.quality as QualityInMap],
|
quality: qualityMap[hdQuality.quality as QualityInMap],
|
||||||
type: MWStreamType.MP4,
|
type: MWStreamType.MP4,
|
||||||
captions: mappedCaptions,
|
captions: mappedCaptions,
|
||||||
@@ -261,7 +255,7 @@ registerProvider({
|
|||||||
quality: qualityMap[
|
quality: qualityMap[
|
||||||
hdQuality.quality as QualityInMap
|
hdQuality.quality as QualityInMap
|
||||||
] as MWStreamQuality,
|
] as MWStreamQuality,
|
||||||
streamUrl: makeFasterUrl(hdQuality.path),
|
streamUrl: hdQuality.path,
|
||||||
type: MWStreamType.MP4,
|
type: MWStreamType.MP4,
|
||||||
captions: mappedCaptions,
|
captions: mappedCaptions,
|
||||||
},
|
},
|
||||||
|
@@ -51,7 +51,7 @@ function MediaCardContent({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
"relative mb-4 aspect-[2/3] w-full overflow-hidden rounded-xl bg-denim-500 bg-cover bg-center transition-[border-radius] duration-100",
|
"relative mb-4 w-full overflow-hidden rounded-xl bg-denim-500 bg-cover bg-center pb-[150%] transition-[border-radius] duration-100",
|
||||||
closable ? "" : "group-hover:rounded-lg",
|
closable ? "" : "group-hover:rounded-lg",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
style={{
|
style={{
|
||||||
|
@@ -7,6 +7,7 @@ interface Config {
|
|||||||
TMDB_READ_API_KEY: string;
|
TMDB_READ_API_KEY: string;
|
||||||
CORS_PROXY_URL: string;
|
CORS_PROXY_URL: string;
|
||||||
NORMAL_ROUTER: boolean;
|
NORMAL_ROUTER: boolean;
|
||||||
|
DISALLOWED_IDS: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RuntimeConfig {
|
export interface RuntimeConfig {
|
||||||
@@ -16,6 +17,7 @@ export interface RuntimeConfig {
|
|||||||
TMDB_READ_API_KEY: string;
|
TMDB_READ_API_KEY: string;
|
||||||
NORMAL_ROUTER: boolean;
|
NORMAL_ROUTER: boolean;
|
||||||
PROXY_URLS: string[];
|
PROXY_URLS: string[];
|
||||||
|
DISALLOWED_IDS: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const env: Record<keyof Config, undefined | string> = {
|
const env: Record<keyof Config, undefined | string> = {
|
||||||
@@ -25,6 +27,7 @@ const env: Record<keyof Config, undefined | string> = {
|
|||||||
DISCORD_LINK: undefined,
|
DISCORD_LINK: undefined,
|
||||||
CORS_PROXY_URL: import.meta.env.VITE_CORS_PROXY_URL,
|
CORS_PROXY_URL: import.meta.env.VITE_CORS_PROXY_URL,
|
||||||
NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER,
|
NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER,
|
||||||
|
DISALLOWED_IDS: import.meta.env.VITE_DISALLOWED_IDS,
|
||||||
};
|
};
|
||||||
|
|
||||||
// loads from different locations, in order: environment (VITE_{KEY}), window (public/config.js)
|
// loads from different locations, in order: environment (VITE_{KEY}), window (public/config.js)
|
||||||
@@ -61,5 +64,8 @@ export function conf(): RuntimeConfig {
|
|||||||
.split(",")
|
.split(",")
|
||||||
.map((v) => v.trim()),
|
.map((v) => v.trim()),
|
||||||
NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true",
|
NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true",
|
||||||
|
DISALLOWED_IDS: getKey("DISALLOWED_IDS", "")
|
||||||
|
.split(",")
|
||||||
|
.map((v) => v.trim()), // Should be comma-seperated and contain the media type and ID, formatted like so: movie-753342,movie-753342,movie-753342
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -15,10 +15,12 @@ import {
|
|||||||
} from "@/backend/metadata/types/mw";
|
} from "@/backend/metadata/types/mw";
|
||||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
|
import { ErrorMessage } from "@/components/layout/ErrorBoundary";
|
||||||
import { Loading } from "@/components/layout/Loading";
|
import { Loading } from "@/components/layout/Loading";
|
||||||
import { useGoBack } from "@/hooks/useGoBack";
|
import { useGoBack } from "@/hooks/useGoBack";
|
||||||
import { useLoading } from "@/hooks/useLoading";
|
import { useLoading } from "@/hooks/useLoading";
|
||||||
import { SelectedMediaData, useScrape } from "@/hooks/useScrape";
|
import { SelectedMediaData, useScrape } from "@/hooks/useScrape";
|
||||||
|
import { conf } from "@/setup/config";
|
||||||
import { useWatchedItem } from "@/state/watched";
|
import { useWatchedItem } from "@/state/watched";
|
||||||
import { MetaController } from "@/video/components/controllers/MetaController";
|
import { MetaController } from "@/video/components/controllers/MetaController";
|
||||||
import { ProgressListenerController } from "@/video/components/controllers/ProgressListenerController";
|
import { ProgressListenerController } from "@/video/components/controllers/ProgressListenerController";
|
||||||
@@ -53,6 +55,31 @@ function MediaViewLoading(props: { onGoBack(): void }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MediaVIewNotAllowed(props: { onGoBack(): void }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex flex-1 items-center justify-center">
|
||||||
|
<Helmet>
|
||||||
|
<title>{t("videoPlayer.got")}</title>
|
||||||
|
</Helmet>
|
||||||
|
<div className="absolute inset-x-0 top-0 px-8 py-6">
|
||||||
|
<VideoPlayerHeader onClick={props.onGoBack} />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<ErrorMessage
|
||||||
|
error={{
|
||||||
|
name: "Media not allowed",
|
||||||
|
description:
|
||||||
|
"this media is no longer available due to a takedown notice or copyright claim",
|
||||||
|
path: "",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface MediaViewScrapingProps {
|
interface MediaViewScrapingProps {
|
||||||
onStream(stream: MWStream): void;
|
onStream(stream: MWStream): void;
|
||||||
onGoBack(): void;
|
onGoBack(): void;
|
||||||
@@ -240,6 +267,14 @@ export function MediaView() {
|
|||||||
});
|
});
|
||||||
}, [exec, history, params]);
|
}, [exec, history, params]);
|
||||||
|
|
||||||
|
const disallowedEntries = conf().DISALLOWED_IDS.map((id) => id.split("-"));
|
||||||
|
if (
|
||||||
|
disallowedEntries.find(
|
||||||
|
(entry) => meta?.tmdbId === entry[1] && meta?.meta?.type === entry[0]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return <MediaVIewNotAllowed onGoBack={goBack} />;
|
||||||
|
|
||||||
if (loading) return <MediaViewLoading onGoBack={goBack} />;
|
if (loading) return <MediaViewLoading onGoBack={goBack} />;
|
||||||
if (error) return <MediaFetchErrorView />;
|
if (error) return <MediaFetchErrorView />;
|
||||||
if (!meta || !selected)
|
if (!meta || !selected)
|
||||||
|
Reference in New Issue
Block a user