mirror of
https://github.com/sussy-code/providers.git
synced 2026-04-18 15:22:01 +00:00
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:
parent
9b6eb8417c
commit
b5a212ab19
8 changed files with 357 additions and 5 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
65
src/providers/embeds/hydrax.ts
Normal file
65
src/providers/embeds/hydrax.ts
Normal 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: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
123
src/providers/embeds/playm4u/nm.ts
Normal file
123
src/providers/embeds/playm4u/nm.ts
Normal 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: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
156
src/providers/sources/m4ufree.ts
Normal file
156
src/providers/sources/m4ufree.ts
Normal 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,
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue