Added M4UFree, made changes to allow cookies to be read, and also fixed embeds not displaying error states properly. Thanks ztpn <@761442124720766997>

This commit is contained in:
Exodus-MW 2024-05-26 02:01:33 +05:30
parent 9b6eb8417c
commit b5a212ab19
8 changed files with 357 additions and 5 deletions

View file

@ -25,7 +25,7 @@ export function makeSimpleProxyFetcher(proxyUrl: string, f: FetchLike): Fetcher
Object.entries(responseHeaderMap).forEach((entry) => {
const value = res.headers.get(entry[0]);
if (!value) return;
res.extraHeaders?.set(entry[0].toLowerCase(), value);
res.extraHeaders?.set(entry[1].toLowerCase(), value);
});
// set correct final url

View file

@ -7,10 +7,11 @@ function getHeaders(list: string[], res: FetchReply): Headers {
const output = new Headers();
list.forEach((header) => {
const realHeader = header.toLowerCase();
const value = res.headers.get(realHeader);
const realValue = res.headers.get(realHeader);
const extraValue = res.extraHeaders?.get(realHeader);
const value = extraValue ?? realValue;
if (!value) return;
output.set(realHeader, extraValue ?? value);
output.set(realHeader, value);
});
return output;
}

View file

@ -28,7 +28,9 @@ import { bflixScraper } from './embeds/bflix';
import { closeLoadScraper } from './embeds/closeload';
import { fileMoonScraper } from './embeds/filemoon';
import { fileMoonMp4Scraper } from './embeds/filemoon/mp4';
import { hydraxScraper } from './embeds/hydrax';
import { alphaScraper, deltaScraper } from './embeds/nsbx';
import { playm4uNMScraper } from './embeds/playm4u/nm';
import { ridooScraper } from './embeds/ridoo';
import { smashyStreamOScraper } from './embeds/smashystream/opstream';
import { smashyStreamFScraper } from './embeds/smashystream/video1';
@ -42,6 +44,7 @@ import { warezcdnembedMp4Scraper } from './embeds/warezcdn/mp4';
import { wootlyScraper } from './embeds/wootly';
import { goojaraScraper } from './sources/goojara';
import { hdRezkaScraper } from './sources/hdrezka';
import { m4uScraper } from './sources/m4ufree';
import { nepuScraper } from './sources/nepu';
import { nitesScraper } from './sources/nites';
import { primewireScraper } from './sources/primewire';
@ -69,6 +72,7 @@ export function gatherAllSources(): Array<Sourcerer> {
nepuScraper,
goojaraScraper,
hdRezkaScraper,
m4uScraper,
primewireScraper,
warezcdnScraper,
insertunitScraper,
@ -111,5 +115,7 @@ export function gatherAllEmbeds(): Array<Embed> {
warezcdnembedHlsScraper,
warezcdnembedMp4Scraper,
bflixScraper,
playm4uNMScraper,
hydraxScraper,
];
}

View file

@ -0,0 +1,65 @@
import { makeEmbed } from '@/providers/base';
export const hydraxScraper = makeEmbed({
id: 'hydrax',
name: 'Hydrax',
rank: 250,
async scrape(ctx) {
// ex-url: https://hihihaha1.xyz/?v=Lgd2uuuTS7
const embed = await ctx.proxiedFetcher<string>(ctx.url);
const match = embed.match(/PLAYER\(atob\("(.*?)"/);
if (!match?.[1]) throw new Error('No Data Found');
ctx.progress(50);
const qualityMatch = embed.match(/({"pieceLength.+?})/);
let qualityData: { pieceLength?: string; sd?: string[]; mHd?: string[]; hd?: string[]; fullHd?: string[] } = {};
if (qualityMatch?.[1]) qualityData = JSON.parse(qualityMatch[1]);
const data: { id: string; domain: string } = JSON.parse(atob(match[1]));
if (!data.id || !data.domain) throw new Error('Required values missing');
const domain = new URL((await ctx.proxiedFetcher.full(`https://${data.domain}`)).finalUrl).hostname;
ctx.progress(100);
return {
stream: [
{
id: 'primary',
type: 'file',
qualities: {
...(qualityData?.fullHd && {
1080: {
type: 'mp4',
url: `https://${domain}/whw${data.id}`,
},
}),
...(qualityData?.hd && {
720: {
type: 'mp4',
url: `https://${domain}/www${data.id}`,
},
}),
...(qualityData?.mHd && {
480: {
type: 'mp4',
url: `https://${domain}/${data.id}`,
},
}),
360: {
type: 'mp4',
url: `https://${domain}/${data.id}`,
},
},
headers: {
Referer: ctx.url.replace(new URL(ctx.url).hostname, 'abysscdn.com'),
},
captions: [],
flags: [],
},
],
};
},
});

View file

@ -0,0 +1,123 @@
import { load } from 'cheerio';
import crypto from 'crypto-js';
import { makeEmbed } from '@/providers/base';
const { AES, MD5 } = crypto;
// I didn't even care to take a look at the code
// it poabably could be better,
// i don't care
// Thanks Paradox_77
function mahoaData(input: string, key: string) {
const a = AES.encrypt(input, key).toString();
const b = a
.replace('U2FsdGVkX1', '')
.replace(/\//g, '|a')
.replace(/\+/g, '|b')
.replace(/\\=/g, '|c')
.replace(/\|/g, '-z');
return b;
}
function caesarShift(str: string, amount: number) {
if (amount < 0) {
return caesarShift(str, amount + 26);
}
let output = '';
for (let i = 0; i < str.length; i++) {
let c = str[i];
if (c.match(/[a-z]/i)) {
const code = str.charCodeAt(i);
if (code >= 65 && code <= 90) {
c = String.fromCharCode(((code - 65 + amount) % 26) + 65);
} else if (code >= 97 && code <= 122) {
c = String.fromCharCode(((code - 97 + amount) % 26) + 97);
}
}
output += c;
}
return output;
}
function stringToHex(tmp: string) {
let str = '';
for (let i = 0; i < tmp.length; i++) {
str += tmp[i].charCodeAt(0).toString(16);
}
return str;
}
function generateResourceToken(idUser: string, idFile: string, domainRef: string) {
const dataToken = stringToHex(
caesarShift(mahoaData(`Win32|${idUser}|${idFile}|${domainRef}`, MD5('plhq@@@2022').toString()), 22),
);
const resourceToken = `${dataToken}|${MD5(`${dataToken}plhq@@@22`).toString()}`;
return resourceToken;
}
const apiUrl = 'https://api-post-iframe-rd.playm4u.xyz/api/playiframe';
type apiRes = {
status: number;
// i only came across url-m3u8
type: 'url-m3u8';
data: string;
cache: boolean;
sub?: string | undefined;
subs?: string | undefined;
};
export const playm4uNMScraper = makeEmbed({
id: 'playm4u-nm',
name: 'PlayM4U',
rank: 240,
scrape: async (ctx) => {
// ex: https://play9str.playm4u.xyz/play/648f159ba3115a6f00744a16
const mainPage$ = load(await ctx.proxiedFetcher<string>(ctx.url));
const script = mainPage$(`script:contains("${apiUrl}")`).text();
if (!script) throw new Error('Failed to get script');
ctx.progress(50);
const domainRef = 'https://ww2.m4ufree.tv';
const idFile = script.match(/var\s?idfile\s?=\s?"(.*)";/im)?.[1];
const idUser = script.match(/var\s?iduser\s?=\s?"(.*)";/im)?.[1];
if (!idFile || !idUser) throw new Error('Failed to get ids');
const charecters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789=+';
const apiRes: apiRes = await ctx.proxiedFetcher<apiRes>(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
namekey: 'playm4u03',
token: Array.from({ length: 100 }, () => charecters.charAt(Math.floor(Math.random() * charecters.length))).join(
'',
),
referrer: domainRef,
data: generateResourceToken(idUser, idFile, domainRef),
}),
});
if (!apiRes.data || apiRes.type !== 'url-m3u8') throw new Error('Failed to get the stream');
ctx.progress(100);
return {
stream: [
{
id: 'primary',
type: 'hls',
playlist: apiRes.data,
captions: [],
flags: [],
},
],
};
},
});

View file

@ -0,0 +1,156 @@
// kinda based on m4uscraper by Paradox_77
// thanks Paradox_77
import { load } from 'cheerio';
import { SourcererEmbed, makeSourcerer } from '@/providers/base';
import { compareMedia } from '@/utils/compare';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { makeCookieHeader, parseSetCookie } from '@/utils/cookie';
import { NotFoundError } from '@/utils/errors';
let baseUrl = 'https://m4ufree.tv';
const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) => {
// this redirects to ww1.m4ufree.tv or ww2.m4ufree.tv
// if i explicitly keep the base ww1 while the load balancers thinks ww2 is optimal
// it will keep redirecting all the requests
// not only that but the last iframe request will fail
const homePage = await ctx.proxiedFetcher.full(baseUrl);
baseUrl = new URL(homePage.finalUrl).origin;
const searchSlug = ctx.media.title
.replace(/'/g, '')
.replace(/!|@|%|\^|\*|\(|\)|\+|=|<|>|\?|\/|,|\.|:|;|'| |"|&|#|\[|\]|~|$|_/g, '-')
.replace(/-+-/g, '-')
.replace(/^-+|-+$/g, '')
.replace(/Ă¢â‚¬â€œ/g, '');
const searchPage$ = load(
await ctx.proxiedFetcher<string>(`/search/${searchSlug}.html`, {
baseUrl,
query: {
type: ctx.media.type === 'movie' ? 'movie' : 'tvs',
},
}),
);
const searchResults: { title: string; year: number | undefined; url: string }[] = [];
searchPage$('.item').each((_, element) => {
const [, title, year] =
searchPage$(element)
// the title emement on their page is broken
// it just breaks when the titles are too big
.find('.imagecover a')
.attr('title')
// ex-titles: Home Alone 1990, Avengers Endgame (2019), The Curse (2023-)
?.match(/^(.*?)\s*(?:\(?\s*(\d{4})(?:\s*-\s*\d{0,4})?\s*\)?)?\s*$/) || [];
const url = searchPage$(element).find('a').attr('href');
if (!title || !url) return;
searchResults.push({ title, year: year ? parseInt(year, 10) : undefined, url });
});
const watchPageUrl = searchResults.find((x) => x && compareMedia(ctx.media, x.title, x.year))?.url;
if (!watchPageUrl) throw new NotFoundError('No watchable item found');
ctx.progress(25);
const watchPage = await ctx.proxiedFetcher.full(watchPageUrl, {
baseUrl,
readHeaders: ['Set-Cookie'],
});
ctx.progress(50);
let watchPage$ = load(watchPage.body);
const csrfToken = watchPage$('script:contains("_token:")')
.html()
?.match(/_token:\s?'(.*)'/m)?.[1];
if (!csrfToken) throw new Error('Failed to find csrfToken');
const laravelSession = parseSetCookie(watchPage.headers.get('Set-Cookie') ?? '').laravel_session;
if (!laravelSession?.value) throw new Error('Failed to find cookie');
const cookie = makeCookieHeader({ [laravelSession.name]: laravelSession.value });
if (ctx.media.type === 'show') {
const s = ctx.media.season.number < 10 ? `0${ctx.media.season.number}` : ctx.media.season.number.toString();
const e = ctx.media.episode.number < 10 ? `0${ctx.media.episode.number}` : ctx.media.episode.number.toString();
const episodeToken = watchPage$(`button:contains("S${s}-E${e}")`).attr('idepisode');
if (!episodeToken) throw new Error('Failed to find episodeToken');
watchPage$ = load(
await ctx.proxiedFetcher<string>('/ajaxtv', {
baseUrl,
method: 'POST',
body: new URLSearchParams({
idepisode: episodeToken,
_token: csrfToken,
}),
headers: {
cookie,
},
}),
);
}
ctx.progress(75);
const embeds: SourcererEmbed[] = [];
const sources: { name: string; data: string }[] = watchPage$('div.row.justify-content-md-center div.le-server')
.map((_, element) => {
const name = watchPage$(element).find('span').text().toLowerCase().replace('#', '');
const data = watchPage$(element).find('span').attr('data');
if (!data || !name) return null;
return { name, data };
})
.get();
for (const source of sources) {
let embedId;
if (source.name === 'm')
embedId = 'playm4u-m'; // TODO
else if (source.name === 'nm') embedId = 'playm4u-nm';
else if (source.name === 'h') embedId = 'hydrax';
else continue;
const iframePage$ = load(
await ctx.proxiedFetcher<string>('/ajax', {
baseUrl,
method: 'POST',
body: new URLSearchParams({
m4u: source.data,
_token: csrfToken,
}),
headers: {
cookie,
},
}),
);
const url = iframePage$('iframe').attr('src');
if (!url) continue;
ctx.progress(100);
embeds.push({ embedId, url });
}
return {
embeds,
};
};
export const m4uScraper = makeSourcerer({
id: 'm4ufree',
name: 'M4UFree',
rank: 125,
flags: [],
scrapeMovie: universalScraper,
scrapeShow: universalScraper,
});

View file

@ -180,7 +180,7 @@ export async function runAllProviders(list: ProviderList, ops: ProviderRunnerOpt
embedOutput.stream = [playableStream];
} catch (error) {
const updateParams: UpdateEvent = {
id: source.id,
id,
percentage: 100,
status: error instanceof NotFoundError ? 'notfound' : 'failure',
reason: error instanceof NotFoundError ? error.message : undefined,

View file

@ -13,7 +13,8 @@ export function makeCookieHeader(cookies: Record<string, string>): string {
}
export function parseSetCookie(headerValue: string): Record<string, Cookie> {
const parsedCookies = setCookieParser.parse(headerValue, {
const splitHeaderValue = setCookieParser.splitCookiesString(headerValue);
const parsedCookies = setCookieParser.parse(splitHeaderValue, {
map: true,
});
return parsedCookies;