10 changed files with 157 additions and 144 deletions
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
* @movie-web/core |
||||
|
||||
.github @binaryoverload |
@ -1,128 +0,0 @@
@@ -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), |
||||
}, |
||||
}; |
||||
}, |
||||
}); |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
export const flixHqBase = "https://flixhq.to"; |
@ -0,0 +1,36 @@
@@ -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), |
||||
}, |
||||
], |
||||
}; |
||||
}, |
||||
}); |
@ -0,0 +1,41 @@
@@ -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; |
||||
} |
@ -0,0 +1,43 @@
@@ -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; |
||||
} |
Loading…
Reference in new issue