mirror of
https://github.com/p-stream/providers.git
synced 2026-03-11 17:55:36 +00:00
Merge remote-tracking branch 'public/production' into production
This commit is contained in:
commit
dcebd18715
4 changed files with 505 additions and 94 deletions
|
|
@ -20,13 +20,7 @@ function embed(provider: { id: string; name: string; rank: number }) {
|
||||||
name: provider.name,
|
name: provider.name,
|
||||||
rank: provider.rank,
|
rank: provider.rank,
|
||||||
async scrape(ctx) {
|
async scrape(ctx) {
|
||||||
const response = await fetch(ctx.url, {
|
const embedHtml = await ctx.proxiedFetcher<string>(ctx.url);
|
||||||
headers: {
|
|
||||||
Accept: 'text/html',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const embedHtml = await response.text();
|
|
||||||
|
|
||||||
const match = embedHtml.match(/robotlink'\).innerHTML = (.*)'/);
|
const match = embedHtml.match(/robotlink'\).innerHTML = (.*)'/);
|
||||||
if (!match) throw new Error('No match found');
|
if (!match) throw new Error('No match found');
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,101 @@
|
||||||
import { flags } from '@/entrypoint/utils/targets';
|
import { flags } from '@/entrypoint/utils/targets';
|
||||||
import { makeEmbed } from '@/providers/base';
|
import { makeEmbed } from '@/providers/base';
|
||||||
import { NotFoundError } from '@/utils/errors';
|
|
||||||
|
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 > 36 && base < 62) {
|
||||||
|
this.ALPHABET[base] = this.ALPHABET[base] || this.ALPHABET[62].substring(0, base);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = [
|
||||||
|
/}\('(.*)', *(\d+|\[\]), *(\d+), *'(.*)'\.split\('\|'\), *(\d+), *(.*)\)\)/,
|
||||||
|
/}\('(.*)', *(\d+|\[\]), *(\d+), *'(.*)'\.split\('\|'\)/,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const juicer of juicers) {
|
||||||
|
const args = juicer.exec(code);
|
||||||
|
if (args) {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
payload: args[1],
|
||||||
|
symtab: args[4].split('|'),
|
||||||
|
radix: parseInt(args[2], 10),
|
||||||
|
count: parseInt(args[3], 10),
|
||||||
|
};
|
||||||
|
} 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 _replacestrings(str: string): string {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const replaced = payload.replace(/\b\w+\b/g, lookup);
|
||||||
|
return _replacestrings(replaced);
|
||||||
|
}
|
||||||
|
|
||||||
const providers = [
|
const providers = [
|
||||||
{
|
{
|
||||||
|
|
@ -10,17 +105,17 @@ const providers = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'streamwish-latino',
|
id: 'streamwish-latino',
|
||||||
name: 'StreamWish (Latino)',
|
name: 'streamwish (latino)',
|
||||||
rank: 170,
|
rank: 170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'streamwish-spanish',
|
id: 'streamwish-spanish',
|
||||||
name: 'StreamWish (Castellano)',
|
name: 'streamwish (castellano)',
|
||||||
rank: 169,
|
rank: 169,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'streamwish-english',
|
id: 'streamwish-english',
|
||||||
name: 'StreamWish (English)',
|
name: 'streamwish (english)',
|
||||||
rank: 168,
|
rank: 168,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -31,19 +126,57 @@ function embed(provider: { id: string; name: string; rank: number }) {
|
||||||
name: provider.name,
|
name: provider.name,
|
||||||
rank: provider.rank,
|
rank: provider.rank,
|
||||||
async scrape(ctx) {
|
async scrape(ctx) {
|
||||||
const encodedUrl = encodeURIComponent(ctx.url);
|
const headers = {
|
||||||
const apiUrl = `https://ws-m3u8.moonpic.qzz.io/m3u8/${encodedUrl}`;
|
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 response = await fetch(apiUrl, {
|
const html = await ctx.proxiedFetcher<string>(ctx.url, { headers });
|
||||||
headers: {
|
const obfuscatedScript = html.match(/<script[^>]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/);
|
||||||
Accept: 'application/json',
|
|
||||||
// 'ngrok-skip-browser-warning': 'true', // this header bypass ngrok warning
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const data: { m3u8: string } = await response.json();
|
if (!obfuscatedScript) {
|
||||||
const videoUrl = data.m3u8;
|
return { stream: [], embeds: [{ embedId: provider.id, url: ctx.url }] };
|
||||||
if (!videoUrl) throw new NotFoundError('No video URL found');
|
}
|
||||||
|
|
||||||
|
let unpackedScript: string;
|
||||||
|
try {
|
||||||
|
unpackedScript = unpack(obfuscatedScript[1]);
|
||||||
|
} catch {
|
||||||
|
return { stream: [], embeds: [{ embedId: provider.id, url: ctx.url }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
let videoUrl = links[0].url;
|
||||||
|
|
||||||
|
if (!/^https?:\/\//.test(videoUrl)) {
|
||||||
|
videoUrl = `https://swiftplayers.com/${videoUrl.replace(/^\/+/g, '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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]*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];
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Intentionally empty to suppress errors during variant fetching
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stream: [
|
stream: [
|
||||||
|
|
@ -53,12 +186,19 @@ function embed(provider: { id: string; name: string; rank: number }) {
|
||||||
playlist: videoUrl,
|
playlist: videoUrl,
|
||||||
flags: [flags.CORS_ALLOWED],
|
flags: [flags.CORS_ALLOWED],
|
||||||
captions: [],
|
captions: [],
|
||||||
|
headers: {
|
||||||
|
Referer: ctx.url,
|
||||||
|
Origin: ctx.url,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
embeds: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const [streamwishJapaneseScraper, streamwishLatinoScraper, streamwishSpanishScraper, streamwishEnglishScraper] =
|
export const [streamwishLatinoScraper, streamwishSpanishScraper, streamwishEnglishScraper, streamwishJapaneseScraper] =
|
||||||
providers.map(embed);
|
providers.map(embed);
|
||||||
|
|
||||||
|
// made by @moonpic
|
||||||
|
|
|
||||||
|
|
@ -1,79 +1,171 @@
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
import { load } from 'cheerio';
|
||||||
|
|
||||||
import { flags } from '@/entrypoint/utils/targets';
|
import { flags } from '@/entrypoint/utils/targets';
|
||||||
import { SourcererOutput, makeSourcerer } from '@/providers/base';
|
import { SourcererOutput, makeSourcerer } from '@/providers/base';
|
||||||
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
|
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
|
||||||
import { NotFoundError } from '@/utils/errors';
|
import { NotFoundError } from '@/utils/errors';
|
||||||
|
|
||||||
const apiBase = 'https://ws-m3u8.moonpic.qzz.io:3006';
|
const baseUrl = 'https://www3.animeflv.net';
|
||||||
|
|
||||||
async function searchAnimeFlvAPI(title: string): Promise<string> {
|
async function searchAnimeFlv(title: string): Promise<string> {
|
||||||
const res = await fetch(`${apiBase}/search?title=${encodeURIComponent(title)}`);
|
const searchUrl = `${baseUrl}/browse?q=${encodeURIComponent(title)}`;
|
||||||
if (!res.ok) throw new NotFoundError('Anime not found in API');
|
const html = await fetch(searchUrl).then((r) => r.text());
|
||||||
const data = await res.json();
|
const $ = load(html);
|
||||||
if (!data.url) throw new NotFoundError('Anime not found in API');
|
|
||||||
return data.url;
|
const results = $('div.Container ul.ListAnimes li article');
|
||||||
|
|
||||||
|
if (!results.length) throw new NotFoundError('No se encontró el anime en 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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!animeUrl) {
|
||||||
|
animeUrl = results.first().find('div.Description a.Button').attr('href') || '';
|
||||||
|
}
|
||||||
|
if (!animeUrl) throw new NotFoundError('No se encontró el anime en AnimeFLV');
|
||||||
|
|
||||||
|
const fullUrl = animeUrl.startsWith('http') ? animeUrl : `${baseUrl}${animeUrl}`;
|
||||||
|
return fullUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getEpisodesAPI(animeUrl: string): Promise<{ number: number; url: string }[]> {
|
async function getEpisodes(animeUrl: string): Promise<{ number: number; url: string }[]> {
|
||||||
const res = await fetch(`${apiBase}/episodes?url=${encodeURIComponent(animeUrl)}`);
|
const html = await fetch(animeUrl).then((r) => r.text());
|
||||||
if (!res.ok) throw new NotFoundError('Episodes not found in API');
|
const $ = load(html);
|
||||||
const data = await res.json();
|
|
||||||
if (!data.episodes) throw new NotFoundError('Episodes not found in API');
|
let episodes: { number: number; url: string }[] = [];
|
||||||
return data.episodes;
|
$('script').each((_, script) => {
|
||||||
|
const data = $(script).html() || '';
|
||||||
|
if (data.includes('var anime_info =')) {
|
||||||
|
const animeInfo = data.split('var anime_info = [')[1]?.split('];')[0];
|
||||||
|
const animeUri = animeInfo?.split(',')[2]?.replace(/"/g, '').trim();
|
||||||
|
const episodesRaw = data.split('var episodes = [')[1]?.split('];')[0];
|
||||||
|
if (animeUri && episodesRaw) {
|
||||||
|
const arrEpisodes = episodesRaw.split('],[');
|
||||||
|
episodes = arrEpisodes.map((arrEp) => {
|
||||||
|
const noEpisode = arrEp.replace('[', '').replace(']', '').split(',')[0];
|
||||||
|
return {
|
||||||
|
number: parseInt(noEpisode, 10),
|
||||||
|
url: `${baseUrl}/ver/${animeUri}-${noEpisode}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('[AnimeFLV] No se encontró animeUri o lista de episodios en el script');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (episodes.length === 0) {
|
||||||
|
console.log('[AnimeFLV] No se encontraron episodios');
|
||||||
|
}
|
||||||
|
return episodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getEmbedsAPI(episodeUrl: string): Promise<Record<string, string>> {
|
async function getEmbeds(episodeUrl: string): Promise<{ [key: string]: string | undefined }> {
|
||||||
const res = await fetch(`${apiBase}/embeds?episodeUrl=${encodeURIComponent(episodeUrl)}`);
|
const html = await fetch(episodeUrl).then((r) => r.text());
|
||||||
if (!res.ok) throw new NotFoundError('No embed found for this content');
|
const $ = load(html);
|
||||||
const data = await res.json();
|
|
||||||
return data;
|
// Busca el script que contiene la variable videos
|
||||||
|
const script = $('script:contains("var videos =")').html();
|
||||||
|
if (!script) return {};
|
||||||
|
|
||||||
|
// Extrae el objeto videos usando regex
|
||||||
|
const match = script.match(/var videos = (\{[\s\S]*?\});/);
|
||||||
|
if (!match) return {};
|
||||||
|
|
||||||
|
let videos: any = {};
|
||||||
|
try {
|
||||||
|
videos = eval(`(${match[1]})`);
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Busca StreamWish en SUB
|
||||||
|
let streamwishJapanese: string | undefined;
|
||||||
|
if (videos.SUB) {
|
||||||
|
const sw = videos.SUB.find((s: any) => s.title?.toLowerCase() === 'sw');
|
||||||
|
if (sw && (sw.url || sw.code)) {
|
||||||
|
streamwishJapanese = sw.url || sw.code;
|
||||||
|
if (streamwishJapanese && streamwishJapanese.startsWith('/e/')) {
|
||||||
|
streamwishJapanese = `https://streamwish.to${streamwishJapanese}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Busca Streamtape en LAT
|
||||||
|
let streamtapeLatino: string | undefined;
|
||||||
|
if (videos.LAT) {
|
||||||
|
const stape = videos.LAT.find(
|
||||||
|
(s: any) => s.title?.toLowerCase() === 'stape' || s.title?.toLowerCase() === 'streamtape',
|
||||||
|
);
|
||||||
|
if (stape && (stape.url || stape.code)) {
|
||||||
|
streamtapeLatino = stape.url || stape.code;
|
||||||
|
if (streamtapeLatino && streamtapeLatino.startsWith('/e/')) {
|
||||||
|
streamtapeLatino = `https://streamtape.com${streamtapeLatino}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'streamwish-japanese': streamwishJapanese,
|
||||||
|
'streamtape-latino': streamtapeLatino,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
|
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
|
||||||
const title = ctx.media.title;
|
const title = ctx.media.title;
|
||||||
if (!title) throw new NotFoundError('Missing title');
|
if (!title) throw new NotFoundError('Falta el título');
|
||||||
|
console.log(`[AnimeFLV] Iniciando scraping para: ${title}`);
|
||||||
|
|
||||||
// Search anime/movie by title using the API
|
const animeUrl = await searchAnimeFlv(title);
|
||||||
const animeUrl = await searchAnimeFlvAPI(title);
|
|
||||||
|
|
||||||
// Get episodes using the API
|
|
||||||
const episodes = await getEpisodesAPI(animeUrl);
|
|
||||||
|
|
||||||
let episodeUrl = animeUrl;
|
let episodeUrl = animeUrl;
|
||||||
|
|
||||||
if (ctx.media.type === 'show') {
|
if (ctx.media.type === 'show') {
|
||||||
const episode = ctx.media.episode?.number;
|
const episode = ctx.media.episode?.number;
|
||||||
if (!episode) throw new NotFoundError('Missing episode data');
|
if (!episode) throw new NotFoundError('Faltan datos de episodio');
|
||||||
|
|
||||||
|
const episodes = await getEpisodes(animeUrl);
|
||||||
const ep = episodes.find((e) => e.number === episode);
|
const ep = episodes.find((e) => e.number === episode);
|
||||||
if (!ep) throw new NotFoundError('Episode not found');
|
if (!ep) throw new NotFoundError(`No se encontró el episodio ${episode}`);
|
||||||
|
|
||||||
episodeUrl = ep.url;
|
episodeUrl = ep.url;
|
||||||
} else if (ctx.media.type === 'movie') {
|
} else if (ctx.media.type === 'movie') {
|
||||||
// For movies, use the first episode (by convention)
|
const html = await fetch(animeUrl).then((r) => r.text());
|
||||||
const ep = episodes.find((e) => e.number === 1) || episodes[0];
|
const $ = load(html);
|
||||||
if (!ep) throw new NotFoundError('Movie episode not found');
|
|
||||||
episodeUrl = ep.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all embeds using the API
|
let animeUri: string | null = null;
|
||||||
const embedsData = await getEmbedsAPI(episodeUrl);
|
$('script').each((_, script) => {
|
||||||
|
const data = $(script).html() || '';
|
||||||
const embeds = [];
|
if (data.includes('var anime_info =')) {
|
||||||
if (embedsData['streamwish-japanese']) {
|
const animeInfo = data.split('var anime_info = [')[1]?.split('];')[0];
|
||||||
embeds.push({
|
animeUri = animeInfo?.split(',')[2]?.replace(/"/g, '').trim() || null;
|
||||||
embedId: 'streamwish-japanese',
|
}
|
||||||
url: embedsData['streamwish-japanese'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (embedsData['streamtape-latino']) {
|
|
||||||
embeds.push({
|
|
||||||
embedId: 'streamtape-latino',
|
|
||||||
url: embedsData['streamtape-latino'],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!animeUri) throw new NotFoundError('No se pudo obtener el animeUri para la película');
|
||||||
|
|
||||||
|
episodeUrl = `${baseUrl}/ver/${animeUri}-1`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (embeds.length === 0) throw new NotFoundError('No valid embed found for this content');
|
const embedsObj = await getEmbeds(episodeUrl);
|
||||||
|
|
||||||
return { embeds };
|
// Construye el array de embeds válidos
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { embeds: filteredEmbeds };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const animeflvScraper = makeSourcerer({
|
export const animeflvScraper = makeSourcerer({
|
||||||
|
|
@ -85,3 +177,5 @@ export const animeflvScraper = makeSourcerer({
|
||||||
scrapeShow: comboScraper,
|
scrapeShow: comboScraper,
|
||||||
scrapeMovie: comboScraper,
|
scrapeMovie: comboScraper,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// made by @moonpic
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,227 @@
|
||||||
|
import { load } from 'cheerio';
|
||||||
|
|
||||||
import { flags } from '@/entrypoint/utils/targets';
|
import { flags } from '@/entrypoint/utils/targets';
|
||||||
import { SourcererOutput, makeSourcerer } from '@/providers/base';
|
import { SourcererOutput, makeSourcerer } from '@/providers/base';
|
||||||
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
|
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
|
||||||
import { NotFoundError } from '@/utils/errors';
|
import { NotFoundError } from '@/utils/errors';
|
||||||
|
|
||||||
|
const baseUrl = 'https://www.cuevana3.eu';
|
||||||
|
|
||||||
|
interface Video {
|
||||||
|
result: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VideosByLanguage {
|
||||||
|
latino?: Video[];
|
||||||
|
spanish?: Video[];
|
||||||
|
english?: Video[];
|
||||||
|
[key: string]: Video[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MovieData {
|
||||||
|
videos: VideosByLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EpisodeData {
|
||||||
|
videos: VideosByLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTitle(title: string): string {
|
||||||
|
return title
|
||||||
|
.normalize('NFD') // Remove accents
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9\s-]/gi, '') // Remove non-alphanumeric characters
|
||||||
|
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||||
|
.replace(/-+/g, '-'); // Remove multiple hyphens
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStreamUrl(ctx: MovieScrapeContext | ShowScrapeContext, embedUrl: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const html = await ctx.proxiedFetcher(embedUrl);
|
||||||
|
const match = html.match(/var url = '([^']+)'/);
|
||||||
|
if (match) {
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors from dead embeds
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateStream(url: string): boolean {
|
||||||
|
return (
|
||||||
|
url.startsWith('https://') && (url.includes('streamwish') || url.includes('filemoon') || url.includes('vidhide'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractVideos(ctx: MovieScrapeContext | ShowScrapeContext, videos: VideosByLanguage) {
|
||||||
|
const videoList: { embedId: string; url: string }[] = [];
|
||||||
|
|
||||||
|
for (const [lang, videoArray] of Object.entries(videos)) {
|
||||||
|
if (!videoArray) continue;
|
||||||
|
|
||||||
|
for (const video of videoArray) {
|
||||||
|
if (!video.result) continue;
|
||||||
|
|
||||||
|
const realUrl = await getStreamUrl(ctx, video.result);
|
||||||
|
if (!realUrl || !validateStream(realUrl)) continue;
|
||||||
|
|
||||||
|
let embedId = '';
|
||||||
|
if (realUrl.includes('filemoon')) embedId = 'filemoon';
|
||||||
|
else if (realUrl.includes('streamwish')) {
|
||||||
|
if (lang === 'latino') embedId = 'streamwish-latino';
|
||||||
|
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 continue;
|
||||||
|
|
||||||
|
videoList.push({
|
||||||
|
embedId,
|
||||||
|
url: realUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return videoList;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 fetchTitleSubstitutes(): Promise<Record<string, string>> {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://raw.githubusercontent.com/moonpic/fixed-titles/refs/heads/main/main.json');
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch fallback titles');
|
||||||
|
return await response.json();
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
|
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
|
||||||
const { tmdbId, type } = ctx.media;
|
const mediaType = ctx.media.type;
|
||||||
|
const tmdbId = ctx.media.tmdbId;
|
||||||
|
const apiKey = '7604525319adb2db8e7e841cb98e9217';
|
||||||
|
|
||||||
if (!tmdbId) {
|
if (!tmdbId) {
|
||||||
throw new NotFoundError('TMDB ID is required');
|
throw new NotFoundError('TMDB ID is required to fetch the title in Spanish');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Definir baseUrl
|
const translatedTitle = await fetchTmdbTitleInSpanish(Number(tmdbId), apiKey, mediaType);
|
||||||
const baseUrl = `https://ws-m3u8.moonpic.qzz.io:3008/tmdb`;
|
let normalizedTitle = normalizeTitle(translatedTitle);
|
||||||
|
|
||||||
let url = '';
|
let pageUrl =
|
||||||
if (type === 'movie') {
|
mediaType === 'movie'
|
||||||
url = `${baseUrl}/movie/${tmdbId}`;
|
? `${baseUrl}/ver-pelicula/${normalizedTitle}`
|
||||||
} else if (type === 'show') {
|
: `${baseUrl}/episodio/${normalizedTitle}-temporada-${ctx.media.season?.number}-episodio-${ctx.media.episode?.number}`;
|
||||||
const showMedia = ctx.media as Extract<typeof ctx.media, { type: 'show' }>;
|
|
||||||
if (showMedia.season?.number != null && showMedia.episode?.number != null) {
|
ctx.progress(60);
|
||||||
url = `${baseUrl}/tv/${tmdbId}/season/${showMedia.season.number}/episode/${showMedia.episode.number}`;
|
|
||||||
} else {
|
let pageContent = await ctx.proxiedFetcher(pageUrl);
|
||||||
throw new NotFoundError('Missing parameters for TV episode');
|
let $ = load(pageContent);
|
||||||
|
|
||||||
|
let script = $('script')
|
||||||
|
.toArray()
|
||||||
|
.find((scriptEl) => {
|
||||||
|
const content = (scriptEl.children[0] as any)?.data || '';
|
||||||
|
return content.includes('{"props":{"pageProps":');
|
||||||
|
});
|
||||||
|
|
||||||
|
let embeds: { embedId: string; url: string }[] = [];
|
||||||
|
|
||||||
|
if (script) {
|
||||||
|
let jsonData: any;
|
||||||
|
try {
|
||||||
|
const jsonString = (script.children[0] as any).data;
|
||||||
|
const start = jsonString.indexOf('{"props":{"pageProps":');
|
||||||
|
if (start === -1) throw new Error('No valid JSON start found');
|
||||||
|
const partialJson = jsonString.slice(start);
|
||||||
|
jsonData = JSON.parse(partialJson);
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new NotFoundError(`Failed to parse JSON: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaType === 'movie') {
|
||||||
|
const movieData = jsonData.props.pageProps.thisMovie as MovieData;
|
||||||
|
if (movieData?.videos) {
|
||||||
|
embeds = (await extractVideos(ctx, movieData.videos)) ?? [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const episodeData = jsonData.props.pageProps.episode as EpisodeData;
|
||||||
|
if (episodeData?.videos) {
|
||||||
|
embeds = (await extractVideos(ctx, episodeData.videos)) ?? [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
throw new NotFoundError('Missing parameters for TV episode');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Realiza la solicitud
|
if (embeds.length === 0) {
|
||||||
const response = await fetch(url);
|
const fallbacks = await fetchTitleSubstitutes();
|
||||||
if (!response.ok) {
|
const fallbackTitle = fallbacks[tmdbId.toString()];
|
||||||
throw new NotFoundError(`Failed to fetch data from local server: ${response.statusText}`);
|
|
||||||
|
if (!fallbackTitle) {
|
||||||
|
throw new NotFoundError('No embed data found and no fallback title available');
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedTitle = normalizeTitle(fallbackTitle);
|
||||||
|
pageUrl =
|
||||||
|
mediaType === 'movie'
|
||||||
|
? `${baseUrl}/ver-pelicula/${normalizedTitle}`
|
||||||
|
: `${baseUrl}/episodio/${normalizedTitle}-temporada-${ctx.media.season?.number}-episodio-${ctx.media.episode?.number}`;
|
||||||
|
|
||||||
|
pageContent = await ctx.proxiedFetcher(pageUrl);
|
||||||
|
$ = load(pageContent);
|
||||||
|
script = $('script')
|
||||||
|
.toArray()
|
||||||
|
.find((scriptEl) => {
|
||||||
|
const content = (scriptEl.children[0] as any)?.data || '';
|
||||||
|
return content.includes('{"props":{"pageProps":');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (script) {
|
||||||
|
let jsonData: any;
|
||||||
|
try {
|
||||||
|
const jsonString = (script.children[0] as any).data;
|
||||||
|
const start = jsonString.indexOf('{"props":{"pageProps":');
|
||||||
|
if (start === -1) throw new Error('No valid JSON start found');
|
||||||
|
const partialJson = jsonString.slice(start);
|
||||||
|
jsonData = JSON.parse(partialJson);
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new NotFoundError(`Failed to parse JSON: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaType === 'movie') {
|
||||||
|
const movieData = jsonData.props.pageProps.thisMovie as MovieData;
|
||||||
|
if (movieData?.videos) {
|
||||||
|
embeds = (await extractVideos(ctx, movieData.videos)) ?? [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const episodeData = jsonData.props.pageProps.episode as EpisodeData;
|
||||||
|
if (episodeData?.videos) {
|
||||||
|
embeds = (await extractVideos(ctx, episodeData.videos)) ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
if (embeds.length === 0) {
|
||||||
|
|
||||||
// Validar que vengan embeds válidos
|
|
||||||
if (!data.embeds || !Array.isArray(data.embeds) || data.embeds.length === 0) {
|
|
||||||
throw new NotFoundError('No valid streams found');
|
throw new NotFoundError('No valid streams found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return { embeds: data.embeds };
|
return { embeds };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const cuevana3Scraper = makeSourcerer({
|
export const cuevana3Scraper = makeSourcerer({
|
||||||
|
|
@ -52,3 +233,5 @@ export const cuevana3Scraper = makeSourcerer({
|
||||||
scrapeMovie: comboScraper,
|
scrapeMovie: comboScraper,
|
||||||
scrapeShow: comboScraper,
|
scrapeShow: comboScraper,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// made by @moonpic
|
||||||
Loading…
Reference in a new issue