add pelisplushd and vidhide embed

Co-Authored-By: MoonPic <99568458+moonpic@users.noreply.github.com>
This commit is contained in:
Pas 2025-10-26 12:54:28 -06:00
parent a2e362a8f6
commit 44630a0ace
3 changed files with 425 additions and 0 deletions

View file

@ -40,6 +40,7 @@ import {
streamwishSpanishScraper,
} from './embeds/streamwish';
import { vidCloudScraper } from './embeds/vidcloud';
import { vidhideEnglishScraper, vidhideLatinoScraper, vidhideSpanishScraper } from './embeds/vidhide';
import { vidifyEmbeds } from './embeds/vidify';
import {
vidnestAllmoviesEmbed,
@ -81,6 +82,7 @@ import { lookmovieScraper } from './sources/lookmovie';
import { madplayScraper } from './sources/madplay';
import { myanimeScraper } from './sources/myanime';
import { nunflixScraper } from './sources/nunflix';
import { pelisplushdScraper } from './sources/pelisplushd';
import { rgshowsScraper } from './sources/rgshows';
import { ridooMoviesScraper } from './sources/ridomovies';
import { slidemoviesScraper } from './sources/slidemovies';
@ -134,6 +136,7 @@ export function gatherAllSources(): Array<Sourcerer> {
animetsuScraper,
lookmovieScraper,
turbovidSourceScraper,
pelisplushdScraper,
];
}
@ -197,5 +200,8 @@ export function gatherAllEmbeds(): Array<Embed> {
myanimesubScraper,
myanimedubScraper,
filemoonScraper,
vidhideLatinoScraper,
vidhideSpanishScraper,
vidhideEnglishScraper,
];
}

View file

@ -0,0 +1,234 @@
import { flags } from '@/entrypoint/utils/targets';
import { makeEmbed } from '@/providers/base';
import type { EmbedScrapeContext } from '@/utils/context';
class Unbaser {
private ALPHABET: Record<number, string> = {
62: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
95: ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~',
};
private dictionary: Record<string, number> = {};
private base: number;
public unbase: (value: string) => number;
constructor(base: number) {
this.base = base;
// If base is between 36 and 62, set ALPHABET accordingly
if (base > 36 && base < 62) {
this.ALPHABET[base] = this.ALPHABET[base] || this.ALPHABET[62].substring(0, base);
}
// If base is between 2 and 36, use parseInt
if (base >= 2 && base <= 36) {
this.unbase = (value: string) => parseInt(value, base);
} else {
try {
[...this.ALPHABET[base]].forEach((cipher, index) => {
this.dictionary[cipher] = index;
});
} catch {
throw new Error('Unsupported base encoding.');
}
this.unbase = this._dictunbaser.bind(this);
}
}
private _dictunbaser(value: string): number {
let ret = 0;
[...value].reverse().forEach((cipher, index) => {
ret += this.base ** index * this.dictionary[cipher];
});
return ret;
}
}
function _filterargs(code: string) {
const juicers = [/}\s*\('(.*)',\s*(\d+|\[\]),\s*(\d+),\s*'(.*)'\.split\('\|'\)/];
for (const juicer of juicers) {
const args = juicer.exec(code);
if (args) {
try {
return {
payload: args[1],
radix: parseInt(args[2], 10),
count: parseInt(args[3], 10),
symtab: args[4].split('|'),
};
} catch {
throw new Error('Corrupted p.a.c.k.e.r. data.');
}
}
}
throw new Error('Could not make sense of p.a.c.k.e.r data (unexpected code structure)');
}
function unpack(packedCode: string): string {
const { payload, symtab, radix, count } = _filterargs(packedCode);
if (count !== symtab.length) throw new Error('Malformed p.a.c.k.e.r. symtab.');
let unbase: Unbaser;
try {
unbase = new Unbaser(radix);
} catch {
throw new Error('Unknown p.a.c.k.e.r. encoding.');
}
const lookup = (match: string): string => {
const word = match;
const word2 = radix === 1 ? symtab[parseInt(word, 10)] : symtab[unbase.unbase(word)];
return word2 || word;
};
return payload.replace(/\b\w+\b/g, lookup);
}
// Official VidHide domains list
const VIDHIDE_DOMAINS = ['https://vidhidepro.com', 'https://vidhidefast.com', 'https://dinisglows.com'];
function buildOfficialUrl(originalUrl: string, officialDomain: string): string {
try {
const u = new URL(originalUrl);
return `${officialDomain}${u.pathname}${u.search}${u.hash}`;
} catch {
return originalUrl;
}
}
async function fetchWithOfficialDomains(
ctx: EmbedScrapeContext,
headers: Record<string, string>,
): Promise<{ html: string; usedUrl: string }> {
for (const domain of VIDHIDE_DOMAINS) {
const testUrl = buildOfficialUrl(ctx.url, domain);
try {
const html = await ctx.proxiedFetcher<string>(testUrl, { headers });
if (html && html.includes('eval(function(p,a,c,k,e,d')) {
return { html, usedUrl: testUrl };
}
if (html) {
return { html, usedUrl: testUrl };
}
} catch (err) {
// Silence errors
}
}
throw new Error('Could not get valid HTML from any official domain');
}
const providers = [
{
id: 'vidhide-latino',
name: 'VidHide (Latino)',
rank: 13,
},
{
id: 'vidhide-spanish',
name: 'VidHide (Castellano)',
rank: 14,
},
{
id: 'vidhide-english',
name: 'VidHide (English)',
rank: 15,
},
];
function extractSubtitles(unpackedScript: string): { file: string; label: string }[] {
const subtitleRegex = /{file:"([^"]+)",label:"([^"]+)"}/g;
const results: { file: string; label: string }[] = [];
const matches = unpackedScript.matchAll(subtitleRegex);
for (const match of matches) {
results.push({ file: match[1], label: match[2] });
}
return results;
}
function makeVidhideScraper(provider: { id: string; name: string; rank: number }) {
return makeEmbed({
id: provider.id,
name: provider.name,
rank: provider.rank,
async scrape(ctx) {
const headers = {
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'Accept-Encoding': '*',
'Accept-Language': 'en-US,en;q=0.9',
'User-Agent': 'Mozilla/5.0',
};
const { html, usedUrl } = await fetchWithOfficialDomains(ctx, headers);
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
if (!obfuscatedScript) {
return { stream: [], embeds: [{ embedId: provider.id, url: ctx.url }] };
}
let unpackedScript: string;
try {
unpackedScript = unpack(obfuscatedScript[1]);
} catch (e) {
return { stream: [], embeds: [{ embedId: provider.id, url: ctx.url }] };
}
const m3u8Links = Array.from(unpackedScript.matchAll(/"(http[^"]*?\.m3u8[^"]*?)"/g)).map((m) => m[1]);
// Only look for links containing master.m3u8
const masterUrl = m3u8Links.find((url) => url.includes('master.m3u8'));
if (!masterUrl) {
return { stream: [], embeds: [{ embedId: provider.id, url: ctx.url }] };
}
let videoUrl = masterUrl;
const subtitles = extractSubtitles(unpackedScript);
try {
const m3u8Content = await ctx.proxiedFetcher<string>(videoUrl, {
headers: { Referer: ctx.url },
});
const variants = Array.from(
m3u8Content.matchAll(/#EXT-X-STREAM-INF:[^\n]+\n(?!iframe)([^\n]*master\.m3u8[^\n]*)/gi),
);
if (variants.length > 0) {
const best = variants[0];
const base = videoUrl.substring(0, videoUrl.lastIndexOf('/') + 1);
videoUrl = base + best[1];
}
// No else, no index, no fallback
} catch (e) {
// Silence variant errors
}
const directHeaders = {
Referer: usedUrl,
Origin: new URL(usedUrl).origin,
};
return {
stream: [
{
id: 'primary',
type: 'hls',
playlist: videoUrl,
headers: directHeaders,
flags: [flags.IP_LOCKED],
captions: subtitles.map((s: { file: string; label: string }, idx: number) => {
const ext = s.file.split('.').pop()?.toLowerCase();
const type: 'srt' | 'vtt' = ext === 'srt' ? 'srt' : 'vtt';
return {
type,
id: `caption-${idx}`,
url: s.file,
hasCorsRestrictions: false,
language: s.label || 'unknown',
};
}),
},
],
};
},
});
}
export const [vidhideLatinoScraper, vidhideSpanishScraper, vidhideEnglishScraper] = providers.map(makeVidhideScraper);
// made by @moonpic

View file

@ -0,0 +1,185 @@
import { load } from 'cheerio';
import type { Element as CheerioElement } from 'cheerio';
import { flags } from '@/entrypoint/utils/targets';
import { SourcererOutput, makeSourcerer } from '@/providers/base';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
const baseUrl = 'https://ww3.pelisplus.to';
function normalizeTitle(title: string): string {
return title
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/[^a-z0-9\s-]/gi, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
}
function decodeBase64(str: string): string {
try {
return atob(str);
} catch {
return '';
}
}
function fetchUrls(text?: string): string[] {
if (!text) return [];
const linkRegex = /(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])/g;
return Array.from(text.matchAll(linkRegex)).map((m) => m[0].replace(/^"+|"+$/g, ''));
}
async function resolvePlayerUrl(ctx: MovieScrapeContext | ShowScrapeContext, url: string): Promise<string> {
try {
const html = await ctx.proxiedFetcher(url);
const $ = load(html);
const script = $('script:contains("window.onload")').html() || '';
return fetchUrls(script)[0] || '';
} catch {
return '';
}
}
async function extractVidhideEmbed(ctx: MovieScrapeContext | ShowScrapeContext, $: ReturnType<typeof load>) {
const regIsUrl = /^https?:\/\/([\w.-]+\.[a-z]{2,})(\/.*)?$/i;
const playerLinks: { idx: number; langBtn: string; url: string }[] = [];
$('.bg-tabs ul li').each((idx: number, el: CheerioElement) => {
const li = $(el);
const langBtn = li.parent()?.parent()?.find('button').first().text().trim().toLowerCase();
const dataServer = li.attr('data-server') || '';
const decoded = decodeBase64(dataServer);
const url = regIsUrl.test(decoded) ? decoded : `${baseUrl}/player/${btoa(dataServer)}`;
playerLinks.push({ idx, langBtn, url });
});
const results: { embedId: string; url: string }[] = [];
for (const link of playerLinks) {
let realUrl = link.url;
if (realUrl.includes('/player/')) {
realUrl = await resolvePlayerUrl(ctx, realUrl);
}
if (/vidhide/i.test(realUrl)) {
let embedId = 'vidhide';
if (link.langBtn.includes('latino')) embedId = 'vidhide-latino';
else if (link.langBtn.includes('castellano') || link.langBtn.includes('español')) embedId = 'vidhide-spanish';
else if (link.langBtn.includes('ingles') || link.langBtn.includes('english')) embedId = 'vidhide-english';
results.push({ embedId, url: realUrl });
}
}
return results;
}
async function fetchTmdbTitleInSpanish(tmdbId: number, apiKey: string, mediaType: 'movie' | 'show'): Promise<string> {
const endpoint =
mediaType === 'movie'
? `https://api.themoviedb.org/3/movie/${tmdbId}?api_key=${apiKey}&language=es-ES`
: `https://api.themoviedb.org/3/tv/${tmdbId}?api_key=${apiKey}&language=es-ES`;
const response = await fetch(endpoint);
if (!response.ok) {
throw new Error(`Error fetching TMDB data: ${response.statusText}`);
}
const tmdbData = await response.json();
return mediaType === 'movie' ? tmdbData.title : tmdbData.name;
}
async function fallbackSearchByGithub(
ctx: MovieScrapeContext | ShowScrapeContext,
): Promise<{ embedId: string; url: string }[]> {
const tmdbId = ctx.media.tmdbId;
const mediaType = ctx.media.type;
if (!tmdbId) return [];
const jsonFile = mediaType === 'movie' ? 'pelisplushd_movies.json' : 'pelisplushd_series.json';
let fallbacks: Record<string, string> = {};
try {
const url = `https://raw.githubusercontent.com/moonpic/fixed-titles/main/${jsonFile}`;
const response = await fetch(url);
if (!response.ok) throw new Error();
fallbacks = await response.json();
} catch {
return [];
}
const fallbackTitle = fallbacks[tmdbId.toString()];
if (!fallbackTitle) return [];
const normalizedTitle = normalizeTitle(fallbackTitle);
const pageUrl =
mediaType === 'movie'
? `${baseUrl}/pelicula/${normalizedTitle}`
: `${baseUrl}/serie/${normalizedTitle}/season/${ctx.media.season?.number}/episode/${ctx.media.episode?.number}`;
let html = '';
try {
html = await ctx.proxiedFetcher(pageUrl);
} catch {
return [];
}
const $ = load(html);
return extractVidhideEmbed(ctx, $);
}
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
const mediaType = ctx.media.type;
const tmdbId = ctx.media.tmdbId;
const apiKey = '7604525319adb2db8e7e841cb98e9217'; // API key for TMDB
if (!tmdbId) throw new NotFoundError('TMDB ID is required to fetch the title in Spanish');
let translatedTitle = '';
try {
translatedTitle = await fetchTmdbTitleInSpanish(Number(tmdbId), apiKey, mediaType);
} catch {
throw new NotFoundError('Could not get the title from TMDB');
}
const normalizedTitle = normalizeTitle(translatedTitle);
const pageUrl =
mediaType === 'movie'
? `${baseUrl}/pelicula/${normalizedTitle}`
: `${baseUrl}/serie/${normalizedTitle}/season/${ctx.media.season?.number}/episode/${ctx.media.episode?.number}`;
ctx.progress(60);
let html = '';
try {
html = await ctx.proxiedFetcher(pageUrl);
} catch {
html = '';
}
let embeds: { embedId: string; url: string }[] = [];
if (html) {
const $ = load(html);
try {
embeds = await extractVidhideEmbed(ctx, $);
} catch {
embeds = [];
}
}
if (!embeds.length) {
embeds = await fallbackSearchByGithub(ctx);
}
if (!embeds.length) {
throw new NotFoundError('No vidhide embed found in PelisPlusHD');
}
return { embeds };
}
export const pelisplushdScraper = makeSourcerer({
id: 'pelisplushd',
name: 'PelisPlusHD',
rank: 75,
flags: [flags.IP_LOCKED], // Vidhide embeds are IP locked
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
});
// made by @moonpic