mirror of
https://github.com/p-stream/providers.git
synced 2026-01-11 20:10:33 +00:00
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:
parent
8b667bd776
commit
a693757598
8 changed files with 561 additions and 88 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
|
|
|
|||
205
src/providers/embeds/vidhide.ts
Normal file
205
src/providers/embeds/vidhide.ts
Normal 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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
192
src/providers/sources/pelisplushd.ts
Normal file
192
src/providers/sources/pelisplushd.ts
Normal 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
|
||||
|
|
@ -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`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue