some commits

some fixes - removed logs

some changes to streamwish

final version

remove m3u8 proxy

[Fix] Cuevana3
[Fix] Streamwish Embed
[Add] Pelisplushd
[Add]vidhide Embed

.
This commit is contained in:
MoonPic 2025-09-03 23:24:28 +00:00
parent 8b667bd776
commit a693757598
8 changed files with 561 additions and 88 deletions

View file

@ -3,6 +3,7 @@ import { doodScraper } from '@/providers/embeds/dood';
import { mixdropScraper } from '@/providers/embeds/mixdrop';
import { turbovidScraper } from '@/providers/embeds/turbovid';
import { upcloudScraper } from '@/providers/embeds/upcloud';
import { vidhideEnglishScraper, vidhideLatinoScraper, vidhideSpanishScraper } from '@/providers/embeds/vidhide';
import { autoembedScraper } from '@/providers/sources/autoembed';
import { ee3Scraper } from '@/providers/sources/ee3';
import { fsharetvScraper } from '@/providers/sources/fsharetv';
@ -77,6 +78,7 @@ import { iosmirrorPVScraper } from './sources/iosmirrorpv';
import { lookmovieScraper } from './sources/lookmovie';
import { madplayScraper } from './sources/madplay';
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';
@ -92,6 +94,7 @@ import { zunimeScraper } from './sources/zunime';
export function gatherAllSources(): Array<Sourcerer> {
// all sources are gathered here
return [
pelisplushdScraper,
cuevana3Scraper,
ridooMoviesScraper,
hdRezkaScraper,
@ -133,6 +136,9 @@ export function gatherAllSources(): Array<Sourcerer> {
export function gatherAllEmbeds(): Array<Embed> {
// all embeds are gathered here
return [
vidhideLatinoScraper,
vidhideSpanishScraper,
vidhideEnglishScraper,
upcloudScraper,
vidCloudScraper,
mixdropScraper,

View file

@ -1,14 +1,7 @@
/* eslint-disable no-console */
// --------
// READ
// There will be an ssl certificate error, you must use Node.js fetcher or browser fetcher.
// $ NODE_TLS_REJECT_UNAUTHORIZED=0 npm run cli -- --fetcher node-fetch --source-id streamwish-japanese --url "https://streamwish.to/e/abcdefc123"
// --------
import { flags } from '@/entrypoint/utils/targets';
import { makeEmbed } from '@/providers/base';
import { createM3U8ProxyUrl } from '@/utils/proxy';
class Unbaser {
private ALPHABET: Record<number, string> = {
@ -129,6 +122,69 @@ const providers = [
},
];
// Official StreamWish domains list
const STREAMWISH_DOMAINS = ['https://xenolyzb.com', 'https://hgplaycdn.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;
}
}
type ScraperContext = {
url: string;
proxiedFetcher: <T = string>(url: string, options?: { headers?: Record<string, string> }) => Promise<T>;
};
async function fetchWithOfficialDomains(
ctx: ScraperContext,
headers: Record<string, string>,
): Promise<{ html: string; usedUrl: string }> {
for (const domain of STREAMWISH_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');
}
function extractSubtitles(script: string): Array<{ file: string; label: string }> {
try {
const tracksMatch = script.match(/tracks\s*:\s*\[([^\]]*)\]/);
if (!tracksMatch || !tracksMatch[1].trim()) {
return [];
}
const items = tracksMatch[1].split('},').map((s) => (s.endsWith('}') ? s : `${s}}`));
const subtitles: Array<{ file: string; label: string }> = [];
for (const item of items) {
const fileMatch = item.match(/file:\s*"([^"]+)"/) || item.match(/file:\s*'([^']+)'/);
const labelMatch = item.match(/label:\s*"([^"]+)"/) || item.match(/label:\s*'([^']+)'/);
const kindMatch = item.match(/kind:\s*"([^"]+)"/) || item.match(/kind:\s*'([^']+)'/);
if (fileMatch && kindMatch && kindMatch[1].toLowerCase() === 'captions') {
subtitles.push({
file: fileMatch[1],
label: labelMatch ? labelMatch[1] : '',
});
}
}
return subtitles;
} catch (e) {
return [];
}
}
function embed(provider: { id: string; name: string; rank: number }) {
return makeEmbed({
id: provider.id,
@ -142,81 +198,53 @@ function embed(provider: { id: string; name: string; rank: number }) {
'User-Agent': 'Mozilla/5.0',
};
// console.log(`Fetching initial HTML from:`, ctx.url);
// console.log(`Request headers:`, headers);
let html: string;
try {
html = await ctx.proxiedFetcher<string>(ctx.url, { headers });
// console.log(`Successfully fetched HTML (${html.length} chars)`);
} catch (error) {
// console.error(`Failed to fetch initial HTML:`, error);
console.error(`Error details:`, {
message: error instanceof Error ? error.message : 'Unknown error',
cause: (error as any).cause || undefined,
url: ctx.url,
});
throw error;
}
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 }] };
return { stream: [], embeds: [{ embedId: provider.id, url: usedUrl }] };
}
let unpackedScript: string;
try {
unpackedScript = unpack(obfuscatedScript[1]);
} catch {
return { stream: [], embeds: [{ embedId: provider.id, url: ctx.url }] };
} catch (err) {
return { stream: [], embeds: [{ embedId: provider.id, url: usedUrl }] };
}
const linkMatches = Array.from(unpackedScript.matchAll(/"(hls2|hls4)"\s*:\s*"([^"]*\.m3u8[^"]*)"/g));
const links = linkMatches.map((match) => ({ key: match[1], url: match[2] }));
if (!links.length) {
return { stream: [], embeds: [{ embedId: provider.id, url: ctx.url }] };
return { stream: [], embeds: [{ embedId: provider.id, url: usedUrl }] };
}
let videoUrl = links[0].url;
// Always prefer master.m3u8 and never index.m3u8
let videoUrl = '';
const masterLink = links.find((l) => l.url.includes('master.m3u8'));
if (masterLink) {
videoUrl = masterLink.url;
} else {
// If there is no master, do not use index or any other
return { stream: [], embeds: [{ embedId: provider.id, url: usedUrl }] };
}
// If relative, convert to absolute
if (!/^https?:\/\//.test(videoUrl)) {
videoUrl = `https://swiftplayers.com/${videoUrl.replace(/^\/+/g, '')}`;
}
// console.log(`Attempting to fetch m3u8 from:`, videoUrl);
try {
const m3u8Content = await ctx.proxiedFetcher<string>(videoUrl, {
headers: { Referer: ctx.url },
});
// console.log(`Successfully fetched m3u8 content (${m3u8Content.length} chars)`);
const variants = Array.from(
m3u8Content.matchAll(/#EXT-X-STREAM-INF:[^\n]+\n(?!iframe)([^\n]*index[^\n]*\.m3u8[^\n]*)/gi),
);
if (variants.length > 0) {
const best = variants.find((v) => /#EXT-X-STREAM-INF/.test(v.input || '')) || variants[0];
const base = videoUrl.substring(0, videoUrl.lastIndexOf('/') + 1);
videoUrl = base + best[1];
try {
const base = new URL(usedUrl).origin;
videoUrl = base + videoUrl;
} catch (e) {
// Silence error
}
} catch (error) {
// console.error(`Failed to fetch m3u8 content:`, error);
// console.error(`m3u8 fetch error details:`, {
// message: error instanceof Error ? error.message : 'Unknown error',
// cause: (error as any).cause || undefined,
// url: videoUrl,
// referer: ctx.url,
// });
//
// Intentionally empty to suppress errors during variant fetching
}
// Extract subtitles
const subtitles = extractSubtitles(unpackedScript);
const videoHeaders = {
Referer: ctx.url,
Origin: ctx.url,
Referer: usedUrl,
Origin: usedUrl,
};
return {
@ -224,9 +252,20 @@ function embed(provider: { id: string; name: string; rank: number }) {
{
id: 'primary',
type: 'hls',
playlist: createM3U8ProxyUrl(videoUrl, videoHeaders),
flags: [flags.CORS_ALLOWED],
captions: [],
playlist: videoUrl,
headers: videoHeaders,
flags: [flags.IP_LOCKED],
captions: subtitles.map((s, idx) => {
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 || 'und',
};
}),
},
],
embeds: [],

View file

@ -0,0 +1,205 @@
import { flags } from '@/entrypoint/utils/targets';
import { makeEmbed } from '@/providers/base';
// --- Unpacker
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);
}
const providers = [
{
id: 'vidhide-latino',
name: 'VidHide (Latino)',
rank: 391,
language: 'latino',
},
{
id: 'vidhide-spanish',
name: 'VidHide (Castellano)',
rank: 390,
language: 'castellano',
},
{
id: 'vidhide-english',
name: 'VidHide (English)',
rank: 389,
language: 'english',
},
];
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; language: string }) {
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 = await ctx.proxiedFetcher<string>(ctx.url, { 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 videoHeaders = {
Referer: ctx.url,
Origin: ctx.url,
};
return {
stream: [
{
id: 'primary',
type: 'hls',
playlist: videoUrl,
headers: videoHeaders,
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 || 'und',
};
}),
language: provider.language,
},
],
embeds: [],
};
},
});
}
export const [vidhideLatinoScraper, vidhideSpanishScraper, vidhideEnglishScraper] = providers.map(makeVidhideScraper);
// made by @moonpic

View file

@ -8,6 +8,7 @@ import { NotFoundError } from '@/utils/errors';
const baseUrl = 'https://www3.animeflv.net';
// Search for the anime URL in AnimeFLV
async function searchAnimeFlv(ctx: ShowScrapeContext | MovieScrapeContext, title: string): Promise<string> {
const searchUrl = `${baseUrl}/browse?q=${encodeURIComponent(title)}`;
const html = await ctx.proxiedFetcher(searchUrl);
@ -15,26 +16,27 @@ async function searchAnimeFlv(ctx: ShowScrapeContext | MovieScrapeContext, title
const results = $('div.Container ul.ListAnimes li article');
if (!results.length) throw new NotFoundError('No se encontró el anime en AnimeFLV');
if (!results.length) throw new NotFoundError('Anime not found in AnimeFLV');
let animeUrl = '';
results.each((_, el) => {
const resultTitle = $(el).find('a h3').text().trim().toLowerCase();
if (resultTitle === title.trim().toLowerCase()) {
animeUrl = $(el).find('div.Description a.Button').attr('href') || '';
return false; // salir del each
return false; // exit each
}
});
if (!animeUrl) {
animeUrl = results.first().find('div.Description a.Button').attr('href') || '';
}
if (!animeUrl) throw new NotFoundError('No se encontró el anime en AnimeFLV');
if (!animeUrl) throw new NotFoundError('Anime not found in AnimeFLV');
const fullUrl = animeUrl.startsWith('http') ? animeUrl : `${baseUrl}${animeUrl}`;
return fullUrl;
}
// Get all episodes for a show
async function getEpisodes(
ctx: ShowScrapeContext | MovieScrapeContext,
animeUrl: string,
@ -59,17 +61,18 @@ async function getEpisodes(
};
});
} else {
console.log('[AnimeFLV] No se encontró animeUri o lista de episodios en el script');
console.log('[AnimeFLV] animeUri or episodes list not found in script');
}
}
});
if (episodes.length === 0) {
console.log('[AnimeFLV] No se encontraron episodios');
console.log('[AnimeFLV] No episodes found');
}
return episodes;
}
// Get all embed URLs for an episode
async function getEmbeds(
ctx: ShowScrapeContext | MovieScrapeContext,
episodeUrl: string,
@ -77,11 +80,11 @@ async function getEmbeds(
const html = await ctx.proxiedFetcher(episodeUrl);
const $ = load(html);
// Busca el script que contiene la variable videos
// Find the script containing the videos variable
const script = $('script:contains("var videos =")').html();
if (!script) return {};
// Extrae el objeto videos usando regex
// Extract the videos object using regex
const match = script.match(/var videos = (\{[\s\S]*?\});/);
if (!match) return {};
@ -92,7 +95,7 @@ async function getEmbeds(
return {};
}
// Busca StreamWish en SUB
// Find StreamWish in SUB
let streamwishJapanese: string | undefined;
if (videos.SUB) {
const sw = videos.SUB.find((s: any) => s.title?.toLowerCase() === 'sw');
@ -104,7 +107,7 @@ async function getEmbeds(
}
}
// Busca Streamtape en LAT
// Find Streamtape in LAT
let streamtapeLatino: string | undefined;
if (videos.LAT) {
const stape = videos.LAT.find(
@ -124,10 +127,11 @@ async function getEmbeds(
};
}
// Main scraper logic for both shows and movies
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
const title = ctx.media.title;
if (!title) throw new NotFoundError('Falta el título');
console.log(`[AnimeFLV] Iniciando scraping para: ${title}`);
if (!title) throw new NotFoundError('Missing title');
console.log(`[AnimeFLV] Starting scraping for: ${title}`);
const animeUrl = await searchAnimeFlv(ctx, title);
@ -135,11 +139,11 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis
if (ctx.media.type === 'show') {
const episode = ctx.media.episode?.number;
if (!episode) throw new NotFoundError('Faltan datos de episodio');
if (!episode) throw new NotFoundError('Missing episode data');
const episodes = await getEpisodes(ctx, animeUrl);
const ep = episodes.find((e) => e.number === episode);
if (!ep) throw new NotFoundError(`No se encontró el episodio ${episode}`);
if (!ep) throw new NotFoundError(`Episode ${episode} not found`);
episodeUrl = ep.url;
} else if (ctx.media.type === 'movie') {
@ -155,20 +159,20 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis
}
});
if (!animeUri) throw new NotFoundError('No se pudo obtener el animeUri para la película');
if (!animeUri) throw new NotFoundError('Could not get animeUri for movie');
episodeUrl = `${baseUrl}/ver/${animeUri}-1`;
}
const embedsObj = await getEmbeds(ctx, episodeUrl);
// Construye el array de embeds válidos
// Build the array of valid embeds
const filteredEmbeds = Object.entries(embedsObj)
.filter(([, url]) => typeof url === 'string' && !!url)
.map(([embedId, url]) => ({ embedId, url: url as string }));
if (filteredEmbeds.length === 0) {
throw new NotFoundError('No se encontraron streams válidos');
throw new NotFoundError('No valid streams found');
}
return { embeds: filteredEmbeds };
@ -178,7 +182,7 @@ export const animeflvScraper = makeSourcerer({
id: 'animeflv',
name: 'AnimeFLV',
rank: 90,
disabled: true,
disabled: false,
flags: [flags.CORS_ALLOWED],
scrapeShow: comboScraper,
scrapeMovie: comboScraper,

View file

@ -26,6 +26,7 @@ interface EpisodeData {
videos: VideosByLanguage;
}
// Normalize the title for URL usage
function normalizeTitle(title: string): string {
return title
.normalize('NFD') // Remove accents
@ -36,6 +37,7 @@ function normalizeTitle(title: string): string {
.replace(/-+/g, '-'); // Remove multiple hyphens
}
// Get the real stream URL from the embed URL
async function getStreamUrl(ctx: MovieScrapeContext | ShowScrapeContext, embedUrl: string): Promise<string | null> {
try {
const html = await ctx.proxiedFetcher(embedUrl);
@ -49,12 +51,14 @@ async function getStreamUrl(ctx: MovieScrapeContext | ShowScrapeContext, embedUr
return null;
}
// Validate the stream URL
function validateStream(url: string): boolean {
return (
url.startsWith('https://') && (url.includes('streamwish') || url.includes('filemoon') || url.includes('vidhide'))
);
}
// Extract video information from the page
async function extractVideos(ctx: MovieScrapeContext | ShowScrapeContext, videos: VideosByLanguage) {
const videoList: { embedId: string; url: string }[] = [];
@ -74,8 +78,12 @@ async function extractVideos(ctx: MovieScrapeContext | ShowScrapeContext, videos
else if (lang === 'spanish') embedId = 'streamwish-spanish';
else if (lang === 'english') embedId = 'streamwish-english';
else embedId = 'streamwish-latino';
} else if (realUrl.includes('vidhide')) embedId = 'vidhide';
else if (realUrl.includes('voe')) embedId = 'voe';
} else if (realUrl.includes('vidhide')) {
if (lang === 'latino') embedId = 'vidhide-latino';
else if (lang === 'spanish') embedId = 'vidhide-spanish';
else if (lang === 'english') embedId = 'vidhide-english';
else embedId = 'vidhide-latino';
} else if (realUrl.includes('voe')) embedId = 'voe';
else continue;
videoList.push({
@ -88,6 +96,7 @@ async function extractVideos(ctx: MovieScrapeContext | ShowScrapeContext, videos
return videoList;
}
// Fetch the movie or show title in Spanish from TMDB
async function fetchTmdbTitleInSpanish(tmdbId: number, apiKey: string, mediaType: 'movie' | 'show'): Promise<string> {
const endpoint =
mediaType === 'movie'
@ -102,9 +111,12 @@ async function fetchTmdbTitleInSpanish(tmdbId: number, apiKey: string, mediaType
return mediaType === 'movie' ? tmdbData.title : tmdbData.name;
}
async function fetchTitleSubstitutes(): Promise<Record<string, string>> {
// Fetch fallback title substitutes from a JSON file
async function fetchTitleSubstitutes(mediaType: 'movie' | 'show'): Promise<Record<string, string>> {
const jsonFile = mediaType === 'movie' ? 'cuevana3_movies.json' : 'cuevana3_series.json';
try {
const response = await fetch('https://raw.githubusercontent.com/moonpic/fixed-titles/refs/heads/main/main.json');
const url = `https://raw.githubusercontent.com/moonpic/fixed-titles/main/${jsonFile}`;
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to fetch fallback titles');
return await response.json();
} catch {
@ -112,6 +124,7 @@ async function fetchTitleSubstitutes(): Promise<Record<string, string>> {
}
}
// The main scraper function for movies and shows
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
const mediaType = ctx.media.type;
const tmdbId = ctx.media.tmdbId;
@ -169,7 +182,7 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis
}
if (embeds.length === 0) {
const fallbacks = await fetchTitleSubstitutes();
const fallbacks = await fetchTitleSubstitutes(mediaType);
const fallbackTitle = fallbacks[tmdbId.toString()];
if (!fallbackTitle) {
@ -227,8 +240,8 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis
export const cuevana3Scraper = makeSourcerer({
id: 'cuevana3',
name: 'Cuevana3',
rank: 80,
disabled: true,
rank: 85,
disabled: false,
flags: [flags.CORS_ALLOWED],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,

View file

@ -0,0 +1,192 @@
import { Buffer } from 'buffer';
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 Buffer.from(str, 'base64').toString('utf-8');
} 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/${Buffer.from(dataServer).toString('base64')}`;
playerLinks.push({ idx, langBtn, url });
});
const results: { embedId: string; url: string; language?: 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 language: string | undefined;
if (link.langBtn.includes('latino')) language = 'latino';
else if (link.langBtn.includes('castellano') || link.langBtn.includes('español')) language = 'castellano';
else if (link.langBtn.includes('ingles') || link.langBtn.includes('english')) language = 'english';
let embedId = 'vidhide';
if (language === 'latino') embedId = 'vidhide-latino';
else if (language === 'castellano') embedId = 'vidhide-spanish';
else if (language === 'english') embedId = 'vidhide-english';
results.push({ embedId, url: realUrl, language });
}
}
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';
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: 80,
flags: [flags.CORS_ALLOWED],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
});
// made by @moonpic

View file

@ -42,7 +42,7 @@ const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) =>
const matches = [...showPageResult.matchAll(regexPattern)];
const episodeIds = matches.map((match) => match[1]);
if (episodeIds.length === 0) throw new NotFoundError('No watchable item found');
const episodeId = episodeIds.at(-1);
const episodeId = episodeIds[episodeIds.length - 1]; // replaces .at(-1)
iframeSourceUrl = `/episodes/${episodeId}/videos`;
}

View file

@ -1,12 +1,26 @@
// import { alphaScraper, deltaScraper } from '@/providers/embeds/nsbx';
// import { astraScraper, novaScraper, orionScraper } from '@/providers/embeds/whvx';
import { bombtheirishScraper } from '@/providers/archive/sources/bombtheirish';
import {
streamwishEnglishScraper,
streamwishJapaneseScraper,
streamwishLatinoScraper,
streamwishSpanishScraper,
} from '@/providers/embeds/streamwish';
import { vidhideEnglishScraper, vidhideLatinoScraper, vidhideSpanishScraper } from '@/providers/embeds/vidhide';
import { warezcdnembedMp4Scraper } from '@/providers/embeds/warezcdn/mp4';
import { Stream } from '@/providers/streams';
import { IndividualEmbedRunnerOptions } from '@/runners/individualRunner';
import { ProviderRunnerOptions } from '@/runners/runner';
const SKIP_VALIDATION_CHECK_IDS = [
streamwishEnglishScraper.id,
streamwishLatinoScraper.id,
streamwishJapaneseScraper.id,
streamwishSpanishScraper.id,
vidhideLatinoScraper.id,
vidhideSpanishScraper.id,
vidhideEnglishScraper.id,
warezcdnembedMp4Scraper.id,
// deltaScraper.id,
// alphaScraper.id,