Compare commits

...

21 Commits
3.1.4 ... 3.2.3

Author SHA1 Message Date
mrjvs
e3569c7ed7 Merge pull request #479 from movie-web/dev
v3.2.3 - fixing upcloud
2023-11-07 21:14:16 +01:00
mrjvs
196a805d32 Merge branch 'master' into dev 2023-11-07 21:13:07 +01:00
William Oldham
94d6d7b37e Merge pull request #478 from movie-web/fix-upcloud
Fix upcloud
2023-11-07 20:10:22 +00:00
mrjvs
fde5f0c82e version bump 2023-11-07 21:07:39 +01:00
mrjvs
bb449d6dfb Fix upcloud 2023-11-07 21:04:43 +01:00
William Oldham
bb8b21324b Merge pull request #475 from movie-web/dev
Fix superstream
2023-11-01 14:07:45 +00:00
Jip Fr
53fe6031d1 Fix superstream 2023-11-01 15:04:52 +01:00
William Oldham
ee9400373d Merge pull request #409 from movie-web/dev
Release 3.2.1
2023-08-16 20:09:03 +01:00
William Oldham
6c8cc63cbc Merge pull request #408 from movie-web/gone-is-fast-cdn
Goodbye faster cdn
2023-08-16 19:33:22 +01:00
mrjvs
8c105e78b5 bump version 2023-08-16 19:26:43 +02:00
mrjvs
28f253c542 Remove faster cdn, as its broken 2023-08-16 19:26:29 +02:00
William Oldham
38fa25da2c Merge pull request #405 from movie-web/dev
Version 3.2.0
2023-08-15 22:33:15 +01:00
mrjvs
efb9a7a076 bump version 2023-08-15 23:30:41 +02:00
William Oldham
5eab635f19 Merge pull request #404 from movie-web/more-providers
Updated providers
2023-08-15 22:25:29 +01:00
mrjvs
c1dceab8eb Fix class sorting 2023-08-15 23:17:57 +02:00
mrjvs
e202229766 add todo 2023-08-15 23:04:01 +02:00
mrjvs
2e3684eaad rip out consumet and fix upcloud 2023-08-15 22:46:48 +02:00
mrjvs
31fcd22822 Make superstream a fast boi 2023-08-15 20:19:25 +02:00
mrjvs
1524a3af39 faster superstream url 2023-08-15 20:13:35 +02:00
mrjvs
072b2d134b Disabled broken providers 2023-08-15 20:10:51 +02:00
William Oldham
606e55d552 Create CODEOWNERS 2023-08-14 23:28:30 +01:00
13 changed files with 187 additions and 146 deletions

3
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,3 @@
* @movie-web/core
.github @binaryoverload

View File

@@ -1,6 +1,6 @@
{
"name": "movie-web",
"version": "3.1.4",
"version": "3.2.3",
"private": true,
"homepage": "https://movie-web.app",
"dependencies": {

View File

@@ -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({
id: "upcloud",
displayName: "UpCloud",
@@ -51,27 +77,43 @@ registerEmbedScraper({
}
);
let sources:
| {
file: string;
type: string;
let sources: { file: string; type: string } | null = null;
if (!isJSON(streamRes.sources)) {
const scriptJs = await proxiedFetch<string>(
`https://rabbitstream.net/js/player/prod/e4-player.min.js`,
{
responseType: "text" as any,
}
| string = streamRes.sources;
if (!isJSON(sources) || typeof sources === "string") {
const decryptionKey = await proxiedFetch<string>(
`https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt`
);
const decryptionKey = extractKey(scriptJs);
if (!decryptionKey) throw new Error("Key extraction failed");
const decryptedStream = AES.decrypt(sources, decryptionKey).toString(
enc.Utf8
);
let extractedKey = "";
let strippedSources = streamRes.sources;
let totalledOffset = 0;
decryptionKey.forEach(([a, b]) => {
const start = a + totalledOffset;
const end = start + b;
extractedKey += streamRes.sources.slice(start, end);
strippedSources = strippedSources.replace(
streamRes.sources.substring(start, end),
""
);
totalledOffset += b;
});
const decryptedStream = AES.decrypt(
strippedSources,
extractedKey
).toString(enc.Utf8);
const parsedStream = JSON.parse(decryptedStream)[0];
if (!parsedStream) throw new Error("No stream found");
sources = parsedStream as { file: string; type: string };
sources = parsedStream;
}
if (!sources) throw new Error("upcloud source not found");
return {
embedId: MWEmbedType.UPCLOUD,
streamUrl: sources.file,

View File

@@ -1,128 +0,0 @@
import { compareTitle } from "@/utils/titleMatch";
import {
getMWCaptionTypeFromUrl,
isSupportedSubtitle,
} from "../helpers/captions";
import { mwFetch } from "../helpers/fetch";
import { registerProvider } from "../helpers/register";
import { MWCaption, MWStreamQuality, MWStreamType } from "../helpers/streams";
import { MWMediaType } from "../metadata/types/mw";
const flixHqBase = "https://api.consumet.org/meta/tmdb";
type FlixHQMediaType = "Movie" | "TV Series";
interface FLIXMediaBase {
id: number;
title: string;
url: string;
image: string;
type: FlixHQMediaType;
releaseDate: string;
}
interface FLIXSubType {
url: string;
lang: string;
}
function convertSubtitles({ url, lang }: FLIXSubType): MWCaption | null {
if (lang.includes("(maybe)")) return null;
const supported = isSupportedSubtitle(url);
if (!supported) return null;
const type = getMWCaptionTypeFromUrl(url);
return {
url,
langIso: lang,
type,
};
}
const qualityMap: Record<string, MWStreamQuality> = {
"360": MWStreamQuality.Q360P,
"540": MWStreamQuality.Q540P,
"480": MWStreamQuality.Q480P,
"720": MWStreamQuality.Q720P,
"1080": MWStreamQuality.Q1080P,
};
function flixTypeToMWType(type: FlixHQMediaType) {
if (type === "Movie") return MWMediaType.MOVIE;
return MWMediaType.SERIES;
}
registerProvider({
id: "flixhq",
displayName: "FlixHQ",
rank: 100,
type: [MWMediaType.MOVIE, MWMediaType.SERIES],
async scrape({ media, episode, progress }) {
if (!this.type.includes(media.meta.type)) {
throw new Error("Unsupported type");
}
// search for relevant item
const searchResults = await mwFetch<any>(
`/${encodeURIComponent(media.meta.title)}`,
{
baseURL: flixHqBase,
}
);
const foundItem = searchResults.results.find((v: FLIXMediaBase) => {
if (v.type !== "Movie" && v.type !== "TV Series") return false;
return (
compareTitle(v.title, media.meta.title) &&
flixTypeToMWType(v.type) === media.meta.type &&
v.releaseDate === media.meta.year
);
});
if (!foundItem) throw new Error("No watchable item found");
// get media info
progress(25);
const mediaInfo = await mwFetch<any>(`/info/${foundItem.id}`, {
baseURL: flixHqBase,
params: {
type: flixTypeToMWType(foundItem.type),
},
});
if (!mediaInfo.id) throw new Error("No watchable item found");
// get stream info from media
progress(50);
let episodeId: string | undefined;
if (media.meta.type === MWMediaType.MOVIE) {
episodeId = mediaInfo.episodeId;
} else if (media.meta.type === MWMediaType.SERIES) {
const seasonNo = media.meta.seasonData.number;
const episodeNo = media.meta.seasonData.episodes.find(
(e) => e.id === episode
)?.number;
const season = mediaInfo.seasons.find((o: any) => o.season === seasonNo);
episodeId = season.episodes.find((o: any) => o.episode === episodeNo).id;
}
if (!episodeId) throw new Error("No watchable item found");
progress(75);
const watchInfo = await mwFetch<any>(`/watch/${episodeId}`, {
baseURL: flixHqBase,
params: {
id: mediaInfo.id,
},
});
if (!watchInfo.sources) throw new Error("No watchable item found");
// get best quality source
// comes sorted by quality in descending order
const source = watchInfo.sources[0];
return {
embeds: [],
stream: {
streamUrl: source.url,
quality: qualityMap[source.quality],
type: source.isM3U8 ? MWStreamType.HLS : MWStreamType.MP4,
captions: watchInfo.subtitles.map(convertSubtitles).filter(Boolean),
},
};
},
});

View File

@@ -0,0 +1 @@
export const flixHqBase = "https://flixhq.to";

View File

@@ -0,0 +1,36 @@
import { MWEmbedType } from "@/backend/helpers/embed";
import { registerProvider } from "@/backend/helpers/register";
import { MWMediaType } from "@/backend/metadata/types/mw";
import {
getFlixhqSourceDetails,
getFlixhqSources,
} from "@/backend/providers/flixhq/scrape";
import { getFlixhqId } from "@/backend/providers/flixhq/search";
registerProvider({
id: "flixhq",
displayName: "FlixHQ",
rank: 100,
type: [MWMediaType.MOVIE, MWMediaType.SERIES],
async scrape({ media }) {
const id = await getFlixhqId(media.meta);
if (!id) throw new Error("flixhq no matching item found");
// TODO tv shows not supported. just need to scrape the specific episode sources
const sources = await getFlixhqSources(id);
const upcloudStream = sources.find(
(v) => v.embed.toLowerCase() === "upcloud"
);
if (!upcloudStream) throw new Error("upcloud stream not found for flixhq");
return {
embeds: [
{
type: MWEmbedType.UPCLOUD,
url: await getFlixhqSourceDetails(upcloudStream.episodeId),
},
],
};
},
});

View File

@@ -0,0 +1,41 @@
import { proxiedFetch } from "@/backend/helpers/fetch";
import { flixHqBase } from "@/backend/providers/flixhq/common";
export async function getFlixhqSources(id: string) {
const type = id.split("/")[0];
const episodeParts = id.split("-");
const episodeId = episodeParts[episodeParts.length - 1];
const data = await proxiedFetch<string>(
`/ajax/${type}/episodes/${episodeId}`,
{
baseURL: flixHqBase,
}
);
const doc = new DOMParser().parseFromString(data, "text/html");
const sourceLinks = [...doc.querySelectorAll(".nav-item > a")].map((el) => {
const embedTitle = el.getAttribute("title");
const linkId = el.getAttribute("data-linkid");
if (!embedTitle || !linkId) throw new Error("invalid sources");
return {
embed: embedTitle,
episodeId: linkId,
};
});
return sourceLinks;
}
export async function getFlixhqSourceDetails(
sourceId: string
): Promise<string> {
const jsonData = await proxiedFetch<Record<string, any>>(
`/ajax/sources/${sourceId}`,
{
baseURL: flixHqBase,
}
);
return jsonData.link;
}

View File

@@ -0,0 +1,43 @@
import { proxiedFetch } from "@/backend/helpers/fetch";
import { MWMediaMeta } from "@/backend/metadata/types/mw";
import { flixHqBase } from "@/backend/providers/flixhq/common";
import { compareTitle } from "@/utils/titleMatch";
export async function getFlixhqId(meta: MWMediaMeta): Promise<string | null> {
const searchResults = await proxiedFetch<string>(
`/search/${meta.title.replaceAll(/[^a-z0-9A-Z]/g, "-")}`,
{
baseURL: flixHqBase,
}
);
const doc = new DOMParser().parseFromString(searchResults, "text/html");
const items = [...doc.querySelectorAll(".film_list-wrap > div.flw-item")].map(
(el) => {
const id = el
.querySelector("div.film-poster > a")
?.getAttribute("href")
?.slice(1);
const title = el
.querySelector("div.film-detail > h2 > a")
?.getAttribute("title");
const year = el.querySelector(
"div.film-detail > div.fd-infor > span:nth-child(1)"
)?.textContent;
if (!id || !title || !year) return null;
return {
id,
title,
year,
};
}
);
const matchingItem = items.find(
(v) => v && compareTitle(meta.title, v.title) && meta.year === v.year
);
if (!matchingItem) return null;
return matchingItem.id;
}

View File

@@ -120,6 +120,7 @@ registerProvider({
id: "hdwatched",
displayName: "HDwatched",
rank: 150,
disabled: true, // very slow, haven't seen it work for a while
type: [MWMediaType.MOVIE, MWMediaType.SERIES],
async scrape(options) {
const { media, progress } = options;

View File

@@ -9,6 +9,7 @@ registerProvider({
id: "sflix",
displayName: "Sflix",
rank: 50,
disabled: true, // domain dead
type: [MWMediaType.MOVIE, MWMediaType.SERIES],
async scrape({ media, episode, progress }) {
let searchQuery = `${media.meta.title} `;

View File

@@ -148,13 +148,13 @@ registerProvider({
async scrape({ media, episode, progress }) {
// Find Superstream ID for show
const searchQuery = {
module: "Search3",
module: "Search4",
page: "1",
type: "all",
keyword: media.meta.title,
pagelimit: "20",
};
const searchRes = (await get(searchQuery, true)).data;
const searchRes = (await get(searchQuery, true)).data.list;
progress(33);
const superstreamEntry = searchRes.find(
@@ -248,6 +248,7 @@ registerProvider({
const mappedCaptions = subtitleRes.list
.map(convertSubtitles)
.filter(Boolean);
return {
embeds: [],
stream: {

View File

@@ -117,7 +117,7 @@ function MediaCardContent({
/>
</div>
</div>
<h1 className="mb-1 max-h-[4.5rem] text-ellipsis break-words font-bold text-white line-clamp-3">
<h1 className="mb-1 line-clamp-3 max-h-[4.5rem] text-ellipsis break-words font-bold text-white">
<span>{media.title}</span>
</h1>
<DotList className="text-xs" content={dotListContent} />

View File

@@ -42,5 +42,5 @@ module.exports = {
animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" }
}
},
plugins: [require("tailwind-scrollbar"), require("@tailwindcss/line-clamp")]
plugins: [require("tailwind-scrollbar")]
};