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",
|
"name": "movie-web",
|
||||||
"version": "3.1.4",
|
"version": "3.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://movie-web.app",
|
"homepage": "https://movie-web.app",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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",
|
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;
|
||||||
|
|
|
@ -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} `;
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -117,7 +117,7 @@ function MediaCardContent({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<span>{media.title}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<DotList className="text-xs" content={dotListContent} />
|
<DotList className="text-xs" content={dotListContent} />
|
||||||
|
|
|
@ -42,5 +42,5 @@ module.exports = {
|
||||||
animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" }
|
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