From 4a352879759e70f6548e8e4734d53c50d2b23df9 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 18 Feb 2023 22:42:24 +0100 Subject: [PATCH] start on jons providers Co-authored-by: Jip Frijlink Co-authored-by: Jonathan Barrow --- src/backend/embeds/playm4u.ts | 20 +++ src/backend/embeds/streamm4u.ts | 71 +++++++++++ src/backend/helpers/embed.ts | 4 +- src/backend/helpers/scrape.ts | 18 +-- src/backend/index.ts | 4 +- src/backend/providers/m4ufree.ts | 207 +++++++++++++++++++++++++++++++ 6 files changed, 313 insertions(+), 11 deletions(-) create mode 100644 src/backend/embeds/playm4u.ts create mode 100644 src/backend/embeds/streamm4u.ts create mode 100644 src/backend/providers/m4ufree.ts diff --git a/src/backend/embeds/playm4u.ts b/src/backend/embeds/playm4u.ts new file mode 100644 index 00000000..4be4d455 --- /dev/null +++ b/src/backend/embeds/playm4u.ts @@ -0,0 +1,20 @@ +import { MWEmbedType } from "@/backend/helpers/embed"; +import { MWMediaType } from "../metadata/types"; +import { registerEmbedScraper } from "@/backend/helpers/register"; +import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; + +registerEmbedScraper({ + id: "playm4u", + displayName: "playm4u", + for: MWEmbedType.PLAYM4U, + rank: 0, + async getStream(ctx) { + throw new Error("Oh well 2") + return { + streamUrl: '', + quality: MWStreamQuality.Q1080P, + captions: [], + type: MWStreamType.MP4, + }; + }, +}) \ No newline at end of file diff --git a/src/backend/embeds/streamm4u.ts b/src/backend/embeds/streamm4u.ts new file mode 100644 index 00000000..ccbc7d47 --- /dev/null +++ b/src/backend/embeds/streamm4u.ts @@ -0,0 +1,71 @@ +import { MWEmbedType } from "@/backend/helpers/embed"; +import { MWMediaType } from "../metadata/types"; +import { registerEmbedScraper } from "@/backend/helpers/register"; +import { MWStreamQuality, MWStreamType, MWStream } from "@/backend/helpers/streams"; +import { proxiedFetch } from "@/backend/helpers/fetch"; + +const HOST = 'streamm4u.club'; +const URL_BASE = `https://${HOST}`; +const URL_API = `${URL_BASE}/api`; +const URL_API_SOURCE = `${URL_API}/source`; + +// TODO check out 403 / 404 on successfully returned video stream URLs +registerEmbedScraper({ + id: "streamm4u", + displayName: "streamm4u", + for: MWEmbedType.STREAMM4U, + rank: 100, + async getStream({ progress, url }) { + + const scrapingThreads = []; + let streams = []; + + const sources = (await scrape(url)).sort((a, b) => Number(b.quality.replace("p", "")) - Number(a.quality.replace("p", ""))); + let preferredSourceIndex = 0; + let preferredSource; + + while (!preferredSource && sources[preferredSourceIndex]) { + console.log('Testing', preferredSourceIndex) + console.log(sources[preferredSourceIndex]?.streamUrl) + // try { + // await proxiedFetch(sources[preferredSourceIndex]?.streamUrl) + // } catch (err) { } + preferredSource = sources[0] + preferredSourceIndex++ + } + console.log(preferredSource) + + if (!preferredSource) throw new Error("No source found") + + progress(100) + + return preferredSource + }, +}) + +async function scrape(embed: string) { + const sources: MWStream[] = []; + + const embedID = embed.split('/').pop(); + + console.log(`${URL_API_SOURCE}/${embedID}`) + const json = await proxiedFetch(`${URL_API_SOURCE}/${embedID}`, { + method: 'POST', + body: `r=&d=${HOST}` + }); + + if (json.success) { + const streams = json.data; + + for (const stream of streams) { + sources.push({ + streamUrl: stream.file as string, + quality: stream.label as MWStreamQuality, + type: stream.type as MWStreamType, + captions: [] + }); + } + } + + return sources; +} \ No newline at end of file diff --git a/src/backend/helpers/embed.ts b/src/backend/helpers/embed.ts index 9f99b28a..0ccf1419 100644 --- a/src/backend/helpers/embed.ts +++ b/src/backend/helpers/embed.ts @@ -1,7 +1,9 @@ import { MWStream } from "./streams"; export enum MWEmbedType { - OPENLOAD = "openload", + M4UFREE = "m4ufree", + STREAMM4U = "streamm4u", + PLAYM4U = "playm4u" } export type MWEmbed = { diff --git a/src/backend/helpers/scrape.ts b/src/backend/helpers/scrape.ts index 2e9e5e65..7805fa4c 100644 --- a/src/backend/helpers/scrape.ts +++ b/src/backend/helpers/scrape.ts @@ -25,15 +25,15 @@ type MWProviderRunContextBase = { }; type MWProviderRunContextTypeSpecific = | { - type: MWMediaType.MOVIE | MWMediaType.ANIME; - episode: undefined; - season: undefined; - } + type: MWMediaType.MOVIE | MWMediaType.ANIME; + episode: undefined; + season: undefined; + } | { - type: MWMediaType.SERIES; - episode: string; - season: string; - }; + type: MWMediaType.SERIES; + episode: string; + season: string; + }; export type MWProviderRunContext = MWProviderRunContextBase & MWProviderRunContextTypeSpecific; @@ -50,7 +50,7 @@ async function findBestEmbedStream( embedNum += 1; if (!embed.type) continue; const scraper = getEmbedScraperByType(embed.type); - if (!scraper) throw new Error("Type for embed not found"); + if (!scraper) throw new Error("Type for embed not found: " + embed.type); const eventId = [providerId, scraper.id, embedNum].join("|"); diff --git a/src/backend/index.ts b/src/backend/index.ts index bb752003..7261a45d 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -5,8 +5,10 @@ import "./providers/gdriveplayer"; import "./providers/flixhq"; import "./providers/superstream"; import "./providers/netfilm"; +import "./providers/m4ufree"; // embeds -// -- nothing here yet +import "./embeds/streamm4u" +import "./embeds/playm4u" initializeScraperStore(); diff --git a/src/backend/providers/m4ufree.ts b/src/backend/providers/m4ufree.ts new file mode 100644 index 00000000..78cc2d15 --- /dev/null +++ b/src/backend/providers/m4ufree.ts @@ -0,0 +1,207 @@ +import { compareTitle } from "@/utils/titleMatch"; +import { MWEmbedType } from "../helpers/embed"; +import { proxiedFetch } from "../helpers/fetch"; +import { registerProvider } from "../helpers/register"; +import { MWMediaType } from "../metadata/types"; +import { MWEmbed } from "@/backend/helpers/embed"; + +const HOST = 'm4ufree.com'; +const URL_BASE = `https://${HOST}`; +const URL_SEARCH = `${URL_BASE}/search`; +const URL_AJAX = `${URL_BASE}/ajax`; +const URL_AJAX_TV = `${URL_BASE}/ajaxtv`; + +// * Years can be in one of 4 formats: +// * - "startyear" (for movies, EX: 2022) +// * - "startyear-" (for TV series which has not ended, EX: 2022-) +// * - "startyear-endyear" (for TV series which has ended, EX: 2022-2023) +// * - "startyearendyear" (for TV series which has ended, EX: 20222023) +const REGEX_TITLE_AND_YEAR = /(.*) \(?(\d*|\d*-|\d*-\d*)\)?$/; +const REGEX_TYPE = /.*-(movie|tvshow)-online-free-m4ufree\.html/; +const REGEX_COOKIES = /XSRF-TOKEN=(.*?);.*laravel_session=(.*?);/; +const REGEX_SEASON_EPISODE = /S(\d*)-E(\d*)/; + +function toDom(html: string) { + return new DOMParser().parseFromString(html, "text/html") +} + +registerProvider({ + id: "m4ufree", + displayName: "m4ufree", + rank: -1, + type: [MWMediaType.MOVIE, MWMediaType.SERIES], + + async scrape({ media, progress, type, episode: episodeId, season: seasonId }) { + const season = media.meta.seasons?.find(s => s.id === seasonId)?.number || 1 + const episode = media.meta.type === MWMediaType.SERIES ? media.meta.seasonData.episodes.find(ep => ep.id === episodeId)?.number || 1 : undefined + + const embeds: MWEmbed[] = []; + + /* +, { + responseType: "text" as any, + } + */ + let responseText = await proxiedFetch(`${URL_SEARCH}/${encodeURIComponent(media.meta.title)}.html`); + let dom = toDom(responseText); + + const searchResults = [...dom.querySelectorAll('.item')].map(element => { + const tooltipText = element.querySelector('.tiptitle p')?.innerHTML; + if (!tooltipText) return; + + let regexResult = REGEX_TITLE_AND_YEAR.exec(tooltipText); + + if (!regexResult || !regexResult[1] || !regexResult[2]) { + return; + } + + const title = regexResult[1]; + const year = Number(regexResult[2].slice(0, 4)); // * Some media stores the start AND end year. Only need start year + const a = element.querySelector('a'); + if (!a) return; + const href = a.href; + + regexResult = REGEX_TYPE.exec(href); + + if (!regexResult || !regexResult[1]) { + return; + } + + let scraperDeterminedType = regexResult[1]; + + scraperDeterminedType = scraperDeterminedType === 'tvshow' ? 'show' : 'movie'; // * Map to Trakt type + + return { type: scraperDeterminedType, title, year, href }; + }).filter(item => item); + + const mediaInResults = searchResults.find(item => item && item.title === media.meta.title && item.year.toString() === media.meta.year); + + if (!mediaInResults) { + // * Nothing found + return { + embeds, + }; + } + + let cookies: string | null = ''; + const responseTextFromMedia = await proxiedFetch(mediaInResults.href, { + onResponse(context) { + cookies = context.response.headers.get('X-Set-Cookie') + }, + }); + dom = toDom(responseTextFromMedia); + + let regexResult = REGEX_COOKIES.exec(cookies); + + if (!regexResult || !regexResult[1] || !regexResult[2]) { + // * DO SOMETHING? + throw new Error("No regexResults, yikesssssss kinda gross idk") + } + + const cookieHeader = `XSRF-TOKEN=${regexResult[1]}; laravel_session=${regexResult[2]}`; + + const token = dom.querySelector('meta[name="csrf-token"]')?.getAttribute("content"); + if (!token) return { embeds }; + + if (type === MWMediaType.SERIES) { + // * Get the season/episode data + const episodes = [...dom.querySelectorAll('.episode')].map(element => { + regexResult = REGEX_SEASON_EPISODE.exec(element.innerHTML); + + if (!regexResult || !regexResult[1] || !regexResult[2]) { + return; + } + + const episode = Number(regexResult[1]); + const season = Number(regexResult[2]); + + return { + id: element.getAttribute('idepisode'), + episode: episode, + season: season + }; + }).filter(item => item); + + const ep = episodes.find(ep => ep && ep.episode === episode && ep.season === season); + if (!ep) return { embeds } + + const form = `idepisode=${ep.id}&_token=${token}`; + + let response = await proxiedFetch(URL_AJAX_TV, { + method: 'POST', + headers: { + 'Accept': '*/*', + 'Accept-Encoding': 'gzip, deflate, br', + 'Accept-Language': "en-US,en;q=0.9", + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + 'X-Requested-With': 'XMLHttpRequest', + 'Sec-CH-UA': '"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"', + 'Sec-CH-UA-Mobile': '?0', + 'Sec-CH-UA-Platform': '"Linux"', + 'Sec-Fetch-Site': 'same-origin', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Dest': 'empty', + 'X-Cookie': cookieHeader, + 'X-Origin': URL_BASE, + 'X-Referer': mediaInResults.href + }, + body: form + }); + + dom = toDom(response); + } + + const servers = [...dom.querySelectorAll('.singlemv')].map(element => element.getAttribute('data')); + + for (const server of servers) { + const form = `m4u=${server}&_token=${token}`; + + const response = await proxiedFetch(URL_AJAX, { + method: 'POST', + headers: { + 'Accept': '*/*', + 'Accept-Encoding': 'gzip, deflate, br', + 'Accept-Language': "en-US,en;q=0.9", + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + 'X-Requested-With': 'XMLHttpRequest', + 'Sec-CH-UA': '"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"', + 'Sec-CH-UA-Mobile': '?0', + 'Sec-CH-UA-Platform': '"Linux"', + 'Sec-Fetch-Site': 'same-origin', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Dest': 'empty', + 'X-Cookie': cookieHeader, + 'X-Origin': URL_BASE, + 'X-Referer': mediaInResults.href + }, + body: form + }); + + const dom = toDom(response); + + const link = dom.querySelector('iframe')?.src; + + const getEmbedType = (url: string) => { + if (url.startsWith("https://streamm4u.club")) return MWEmbedType.STREAMM4U + if (url.startsWith("https://play.playm4u.xyz")) return MWEmbedType.PLAYM4U + return null; + } + + if (!link) continue; + + const embedType = getEmbedType(link); + if (embedType) { + embeds.push({ + url: link, + type: embedType + }) + }; + } + + console.log(embeds); + return { + embeds, + } + + } +});