merge dev

Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
mrjvs 2023-08-20 14:57:32 +02:00
commit e93644b688
10 changed files with 158 additions and 145 deletions

3
.github/CODEOWNERS vendored Normal file
View File

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

View File

@ -51,27 +51,35 @@ registerEmbedScraper({
} }
); );
let sources: let sources: { file: string; type: string } | null = null;
| {
file: string;
type: string;
}
| string = streamRes.sources;
if (!isJSON(sources) || typeof sources === "string") { if (!isJSON(streamRes.sources)) {
const decryptionKey = await proxiedFetch<string>( const decryptionKey = JSON.parse(
await proxiedFetch<string>(
`https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt` `https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt`
); )
) as [number, number][];
const decryptedStream = AES.decrypt(sources, decryptionKey).toString( let extractedKey = "";
enc.Utf8 const sourcesArray = streamRes.sources.split("");
); for (const index of decryptionKey) {
for (let i: number = index[0]; i < index[1]; i += 1) {
extractedKey += streamRes.sources[i];
sourcesArray[i] = "";
}
}
const decryptedStream = AES.decrypt(
sourcesArray.join(""),
extractedKey
).toString(enc.Utf8);
const parsedStream = JSON.parse(decryptedStream)[0]; const parsedStream = JSON.parse(decryptedStream)[0];
if (!parsedStream) throw new Error("No stream found"); 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 { return {
embedId: MWEmbedType.UPCLOUD, embedId: MWEmbedType.UPCLOUD,
streamUrl: sources.file, 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://consumet-api-clone.vercel.app/meta/tmdb"; // instance stolen from streaminal :)
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", id: "hdwatched",
displayName: "HDwatched", displayName: "HDwatched",
rank: 150, rank: 150,
disabled: true, // very slow, haven't seen it work for a while
type: [MWMediaType.MOVIE, MWMediaType.SERIES], type: [MWMediaType.MOVIE, MWMediaType.SERIES],
async scrape(options) { async scrape(options) {
const { media, progress } = options; const { media, progress } = options;

View File

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

View File

@ -18,6 +18,12 @@ 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,
@ -199,7 +205,7 @@ registerProvider({
return { return {
embeds: [], embeds: [],
stream: { stream: {
streamUrl: hdQuality.path, streamUrl: makeFasterUrl(hdQuality.path),
quality: qualityMap[hdQuality.quality as QualityInMap], quality: qualityMap[hdQuality.quality as QualityInMap],
type: MWStreamType.MP4, type: MWStreamType.MP4,
captions: mappedCaptions, captions: mappedCaptions,
@ -248,13 +254,14 @@ registerProvider({
const mappedCaptions = subtitleRes.list const mappedCaptions = subtitleRes.list
.map(convertSubtitles) .map(convertSubtitles)
.filter(Boolean); .filter(Boolean);
return { return {
embeds: [], embeds: [],
stream: { stream: {
quality: qualityMap[ quality: qualityMap[
hdQuality.quality as QualityInMap hdQuality.quality as QualityInMap
] as MWStreamQuality, ] as MWStreamQuality,
streamUrl: hdQuality.path, streamUrl: makeFasterUrl(hdQuality.path),
type: MWStreamType.MP4, type: MWStreamType.MP4,
captions: mappedCaptions, captions: mappedCaptions,
}, },