mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-21 14:47:41 +01:00
commit
38fa25da2c
13 changed files with 161 additions and 148 deletions
3
.github/CODEOWNERS
vendored
Normal file
3
.github/CODEOWNERS
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
* @movie-web/core
|
||||
|
||||
.github @binaryoverload
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "movie-web",
|
||||
"version": "3.1.4",
|
||||
"version": "3.2.0",
|
||||
"private": true,
|
||||
"homepage": "https://movie-web.app",
|
||||
"dependencies": {
|
||||
|
|
|
@ -51,27 +51,35 @@ registerEmbedScraper({
|
|||
}
|
||||
);
|
||||
|
||||
let sources:
|
||||
| {
|
||||
file: string;
|
||||
type: string;
|
||||
let sources: { file: string; type: string } | null = null;
|
||||
|
||||
if (!isJSON(streamRes.sources)) {
|
||||
const decryptionKey = JSON.parse(
|
||||
await proxiedFetch<string>(
|
||||
`https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt`
|
||||
)
|
||||
) as [number, number][];
|
||||
|
||||
let extractedKey = "";
|
||||
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] = "";
|
||||
}
|
||||
| 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 decryptedStream = AES.decrypt(sources, decryptionKey).toString(
|
||||
enc.Utf8
|
||||
);
|
||||
}
|
||||
|
||||
const decryptedStream = AES.decrypt(
|
||||
sourcesArray.join(""),
|
||||
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,
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
1
src/backend/providers/flixhq/common.ts
Normal file
1
src/backend/providers/flixhq/common.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const flixHqBase = "https://flixhq.to";
|
36
src/backend/providers/flixhq/index.ts
Normal file
36
src/backend/providers/flixhq/index.ts
Normal 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),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
41
src/backend/providers/flixhq/scrape.ts
Normal file
41
src/backend/providers/flixhq/scrape.ts
Normal 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;
|
||||
}
|
43
src/backend/providers/flixhq/search.ts
Normal file
43
src/backend/providers/flixhq/search.ts
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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} `;
|
||||
|
|
|
@ -18,6 +18,12 @@ import { compareTitle } from "@/utils/titleMatch";
|
|||
|
||||
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 = {
|
||||
"360p": MWStreamQuality.Q360P,
|
||||
"480p": MWStreamQuality.Q480P,
|
||||
|
@ -199,7 +205,7 @@ registerProvider({
|
|||
return {
|
||||
embeds: [],
|
||||
stream: {
|
||||
streamUrl: hdQuality.path,
|
||||
streamUrl: makeFasterUrl(hdQuality.path),
|
||||
quality: qualityMap[hdQuality.quality as QualityInMap],
|
||||
type: MWStreamType.MP4,
|
||||
captions: mappedCaptions,
|
||||
|
@ -248,13 +254,14 @@ registerProvider({
|
|||
const mappedCaptions = subtitleRes.list
|
||||
.map(convertSubtitles)
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
embeds: [],
|
||||
stream: {
|
||||
quality: qualityMap[
|
||||
hdQuality.quality as QualityInMap
|
||||
] as MWStreamQuality,
|
||||
streamUrl: hdQuality.path,
|
||||
streamUrl: makeFasterUrl(hdQuality.path),
|
||||
type: MWStreamType.MP4,
|
||||
captions: mappedCaptions,
|
||||
},
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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")]
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue