11 changed files with 156 additions and 146 deletions
@ -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), |
|
||||||
}, |
|
||||||
}; |
|
||||||
}, |
|
||||||
}); |
|
@ -0,0 +1 @@ |
|||||||
|
export const flixHqBase = "https://flixhq.to"; |
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
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