From d3cccd8db128052669893ee9fcb9da80803acdbc Mon Sep 17 00:00:00 2001 From: MoonPic <99568458+moonpic@users.noreply.github.com> Date: Sun, 18 May 2025 01:38:20 +0000 Subject: [PATCH 1/4] [add] animeflv --- src/providers/all.ts | 25 +++++---- src/providers/embeds/streamtape.ts | 77 +++++++++++++++----------- src/providers/embeds/streamwish.ts | 10 +++- src/providers/sources/animeflv.ts | 87 ++++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 43 deletions(-) create mode 100644 src/providers/sources/animeflv.ts diff --git a/src/providers/all.ts b/src/providers/all.ts index 1908d5a..3b6b383 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -47,9 +47,14 @@ import { import { FedAPIPrivateScraper, FedDBScraper } from './embeds/fedapi'; import { mp4hydraServer1Scraper, mp4hydraServer2Scraper } from './embeds/mp4hydra'; import { ridooScraper } from './embeds/ridoo'; -import { streamtapeScraper } from './embeds/streamtape'; +import { streamtapeLatinoScraper, streamtapeScraper } from './embeds/streamtape'; import { streamvidScraper } from './embeds/streamvid'; -import { streamwishEnglishScraper, streamwishLatinoScraper, streamwishSpanishScraper } from './embeds/streamwish'; +import { + streamwishEnglishScraper, + streamwishJapaneseScraper, + streamwishLatinoScraper, + streamwishSpanishScraper, +} from './embeds/streamwish'; import { vidCloudScraper } from './embeds/vidcloud'; import { VidsrcsuServer10Scraper, @@ -74,14 +79,13 @@ import { webtor1080Scraper, webtor480Scraper, webtor4kScraper, webtor720Scraper import { xprimeApolloEmbed, xprimeFoxEmbed, - xprimeHarbourEmbed, xprimeMarantEmbed, xprimePrimenetEmbed, xprimeStreamboxEmbed, - xprimeVolkswagenEmbed, } from './embeds/xprime'; import { oneServerScraper } from './sources/1server'; import { EightStreamScraper } from './sources/8stream'; +import { animeflvScraper } from './sources/animeflv'; import { coitusScraper } from './sources/coitus'; import { ConsumetScraper } from './sources/consumet'; import { cuevana3Scraper } from './sources/cuevana3'; @@ -99,12 +103,12 @@ import { uiraliveScraper } from './sources/uiralive'; import { vidapiClickScraper } from './sources/vidapiclick'; import { warezcdnScraper } from './sources/warezcdn'; import { webtorScraper } from './sources/webtor'; -import { wecimaScraper } from './sources/wecima'; import { xprimeScraper } from './sources/xprime'; export function gatherAllSources(): Array { // all sources are gathered here return [ + animeflvScraper, cuevana3Scraper, catflixScraper, ridooMoviesScraper, @@ -134,7 +138,6 @@ export function gatherAllSources(): Array { ConsumetScraper, hianimeScraper, oneServerScraper, - wecimaScraper, ]; } @@ -147,8 +150,13 @@ export function gatherAllEmbeds(): Array { ridooScraper, closeLoadScraper, doodScraper, + streamwishJapaneseScraper, + streamwishLatinoScraper, + streamwishSpanishScraper, + streamwishEnglishScraper, streamvidScraper, streamtapeScraper, + streamtapeLatinoScraper, warezcdnembedHlsScraper, warezcdnembedMp4Scraper, warezPlayerScraper, @@ -185,8 +193,6 @@ export function gatherAllEmbeds(): Array { xprimeStreamboxEmbed, xprimeMarantEmbed, xprimePrimenetEmbed, - xprimeVolkswagenEmbed, - xprimeHarbourEmbed, ConsumetVidCloudScraper, ConsumetStreamSBScraper, ConsumetVidStreamingScraper, @@ -204,8 +210,5 @@ export function gatherAllEmbeds(): Array { oneServerHianimeEmbed, oneServerAnimepaheEmbed, oneServerAnizoneEmbed, - streamwishLatinoScraper, - streamwishSpanishScraper, - streamwishEnglishScraper, ]; } diff --git a/src/providers/embeds/streamtape.ts b/src/providers/embeds/streamtape.ts index db345b0..6e465c3 100644 --- a/src/providers/embeds/streamtape.ts +++ b/src/providers/embeds/streamtape.ts @@ -1,39 +1,56 @@ import { flags } from '@/entrypoint/utils/targets'; import { makeEmbed } from '@/providers/base'; -export const streamtapeScraper = makeEmbed({ - id: 'streamtape', - name: 'Streamtape', - rank: 160, - async scrape(ctx) { - const embed = await ctx.proxiedFetcher(ctx.url); +const providers = [ + { + id: 'streamtape', + name: 'Streamtape', + rank: 160, + }, + { + id: 'streamtape-latino', + name: 'Streamtape (Latino)', + rank: 159, + }, +]; - const match = embed.match(/robotlink'\).innerHTML = (.*)'/); - if (!match) throw new Error('No match found'); +function embed(provider: { id: string; name: string; rank: number }) { + return makeEmbed({ + id: provider.id, + name: provider.name, + rank: provider.rank, + async scrape(ctx) { + const embedHtml = await ctx.proxiedFetcher(ctx.url); - const [fh, sh] = match?.[1]?.split("+ ('") ?? []; - if (!fh || !sh) throw new Error('No match found'); + const match = embedHtml.match(/robotlink'\).innerHTML = (.*)'/); + if (!match) throw new Error('No match found'); - const url = `https:${fh?.replace(/'/g, '').trim()}${sh?.substring(3).trim()}`; + const [fh, sh] = match?.[1]?.split("+ ('") ?? []; + if (!fh || !sh) throw new Error('No match found'); - return { - stream: [ - { - id: 'primary', - type: 'file', - flags: [flags.CORS_ALLOWED, flags.IP_LOCKED], - captions: [], - qualities: { - unknown: { - type: 'mp4', - url, + const url = `https:${fh?.replace(/'/g, '').trim()}${sh?.substring(3).trim()}`; + + return { + stream: [ + { + id: 'primary', + type: 'file', + flags: [flags.CORS_ALLOWED, flags.IP_LOCKED], + captions: [], + qualities: { + unknown: { + type: 'mp4', + url, + }, + }, + headers: { + Referer: 'https://streamtape.com', }, }, - headers: { - Referer: 'https://streamtape.com', - }, - }, - ], - }; - }, -}); + ], + }; + }, + }); +} + +export const [streamtapeScraper, streamtapeLatinoScraper] = providers.map(embed); diff --git a/src/providers/embeds/streamwish.ts b/src/providers/embeds/streamwish.ts index 5b34de0..af980eb 100644 --- a/src/providers/embeds/streamwish.ts +++ b/src/providers/embeds/streamwish.ts @@ -4,6 +4,11 @@ import { makeEmbed } from '@/providers/base'; import { NotFoundError } from '@/utils/errors'; const providers = [ + { + id: 'streamwish-japanese', + name: 'StreamWish (Japones Sub Español)', + rank: 171, + }, { id: 'streamwish-latino', name: 'StreamWish (Latino)', @@ -30,7 +35,7 @@ function embed(provider: { id: string; name: string; rank: number }) { const encodedUrl = encodeURIComponent(ctx.url); const apiUrl = `https://ws-m3u8.moonpic.qzz.io/m3u8/${encodedUrl}`; - const response = await fetcher(apiUrl, { + const response = await fetch(apiUrl, { headers: { Accept: 'application/json', // 'ngrok-skip-browser-warning': 'true', // this header bypass ngrok warning @@ -58,4 +63,5 @@ function embed(provider: { id: string; name: string; rank: number }) { }); } -export const [streamwishLatinoScraper, streamwishSpanishScraper, streamwishEnglishScraper] = providers.map(embed); +export const [streamwishJapaneseScraper, streamwishLatinoScraper, streamwishSpanishScraper, streamwishEnglishScraper] = + providers.map(embed); diff --git a/src/providers/sources/animeflv.ts b/src/providers/sources/animeflv.ts new file mode 100644 index 0000000..9b0562e --- /dev/null +++ b/src/providers/sources/animeflv.ts @@ -0,0 +1,87 @@ +/* eslint-disable no-console */ +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +const apiBase = 'https://ws-m3u8.moonpic.qzz.io:3006'; + +async function searchAnimeFlvAPI(title: string): Promise { + const res = await fetch(`${apiBase}/search?title=${encodeURIComponent(title)}`); + if (!res.ok) throw new NotFoundError('Anime not found in API'); + const data = await res.json(); + if (!data.url) throw new NotFoundError('Anime not found in API'); + return data.url; +} + +async function getEpisodesAPI(animeUrl: string): Promise<{ number: number; url: string }[]> { + const res = await fetch(`${apiBase}/episodes?url=${encodeURIComponent(animeUrl)}`); + if (!res.ok) throw new NotFoundError('Episodes not found in API'); + const data = await res.json(); + if (!data.episodes) throw new NotFoundError('Episodes not found in API'); + return data.episodes; +} + +async function getEmbedsAPI(episodeUrl: string): Promise> { + const res = await fetch(`${apiBase}/embeds?episodeUrl=${encodeURIComponent(episodeUrl)}`); + if (!res.ok) throw new NotFoundError('No embed found for this content'); + const data = await res.json(); + return data; +} + +async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { + const title = ctx.media.title; + if (!title) throw new NotFoundError('Missing title'); + + // Search anime/movie by title using the API + const animeUrl = await searchAnimeFlvAPI(title); + + // Get episodes using the API + const episodes = await getEpisodesAPI(animeUrl); + + let episodeUrl = animeUrl; + + if (ctx.media.type === 'show') { + const episode = ctx.media.episode?.number; + if (!episode) throw new NotFoundError('Missing episode data'); + const ep = episodes.find((e) => e.number === episode); + if (!ep) throw new NotFoundError('Episode not found'); + episodeUrl = ep.url; + } else if (ctx.media.type === 'movie') { + // For movies, use the first episode (by convention) + const ep = episodes.find((e) => e.number === 1) || episodes[0]; + if (!ep) throw new NotFoundError('Movie episode not found'); + episodeUrl = ep.url; + } + + // Get all embeds using the API + const embedsData = await getEmbedsAPI(episodeUrl); + + const embeds = []; + if (embedsData['streamwish-japanese']) { + embeds.push({ + embedId: 'streamwish-japanese', + url: embedsData['streamwish-japanese'], + }); + } + if (embedsData['streamtape-latino']) { + embeds.push({ + embedId: 'streamtape-latino', + url: embedsData['streamtape-latino'], + }); + } + + if (embeds.length === 0) throw new NotFoundError('No valid embed found for this content'); + + return { embeds }; +} + +export const animeflvScraper = makeSourcerer({ + id: 'animeflv', + name: 'AnimeFLV', + rank: 90, + disabled: false, + flags: [flags.CORS_ALLOWED], + scrapeShow: comboScraper, + scrapeMovie: comboScraper, +}); From db3ecc6625b3438f04708472d9fecc0c27548d0a Mon Sep 17 00:00:00 2001 From: MoonPic <99568458+moonpic@users.noreply.github.com> Date: Sat, 17 May 2025 22:35:24 -0400 Subject: [PATCH 2/4] Update streamwish.ts --- src/providers/embeds/streamwish.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/providers/embeds/streamwish.ts b/src/providers/embeds/streamwish.ts index af980eb..b3f5700 100644 --- a/src/providers/embeds/streamwish.ts +++ b/src/providers/embeds/streamwish.ts @@ -1,4 +1,5 @@ /* eslint-disable no-console */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars import { flags } from '@/entrypoint/utils/targets'; import { makeEmbed } from '@/providers/base'; import { NotFoundError } from '@/utils/errors'; @@ -32,6 +33,8 @@ function embed(provider: { id: string; name: string; rank: number }) { name: provider.name, rank: provider.rank, async scrape(ctx) { + console.log('Starting scrape for StreamWish with URL:', ctx.url); + const encodedUrl = encodeURIComponent(ctx.url); const apiUrl = `https://ws-m3u8.moonpic.qzz.io/m3u8/${encodedUrl}`; @@ -42,9 +45,7 @@ function embed(provider: { id: string; name: string; rank: number }) { }, }); - if (!response.ok) throw new NotFoundError('No video URL found'); - const data = await response.json(); - + const data: { m3u8: string } = await response.json(); const videoUrl = data.m3u8; if (!videoUrl) throw new NotFoundError('No video URL found'); From 5fe7c86f4b04870d7db4a7d1eb0ed03545ffc163 Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Sun, 18 May 2025 09:45:39 -0600 Subject: [PATCH 3/4] Update all.ts --- src/providers/all.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/providers/all.ts b/src/providers/all.ts index 3b6b383..a3eb05e 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -79,9 +79,11 @@ import { webtor1080Scraper, webtor480Scraper, webtor4kScraper, webtor720Scraper import { xprimeApolloEmbed, xprimeFoxEmbed, + xprimeHarbourEmbed, xprimeMarantEmbed, xprimePrimenetEmbed, xprimeStreamboxEmbed, + xprimeVolkswagenEmbed, } from './embeds/xprime'; import { oneServerScraper } from './sources/1server'; import { EightStreamScraper } from './sources/8stream'; @@ -103,12 +105,12 @@ import { uiraliveScraper } from './sources/uiralive'; import { vidapiClickScraper } from './sources/vidapiclick'; import { warezcdnScraper } from './sources/warezcdn'; import { webtorScraper } from './sources/webtor'; +import { wecimaScraper } from './sources/wecima'; import { xprimeScraper } from './sources/xprime'; export function gatherAllSources(): Array { // all sources are gathered here return [ - animeflvScraper, cuevana3Scraper, catflixScraper, ridooMoviesScraper, @@ -138,6 +140,8 @@ export function gatherAllSources(): Array { ConsumetScraper, hianimeScraper, oneServerScraper, + wecimaScraper, + animeflvScraper, ]; } @@ -150,13 +154,8 @@ export function gatherAllEmbeds(): Array { ridooScraper, closeLoadScraper, doodScraper, - streamwishJapaneseScraper, - streamwishLatinoScraper, - streamwishSpanishScraper, - streamwishEnglishScraper, streamvidScraper, streamtapeScraper, - streamtapeLatinoScraper, warezcdnembedHlsScraper, warezcdnembedMp4Scraper, warezPlayerScraper, @@ -193,6 +192,8 @@ export function gatherAllEmbeds(): Array { xprimeStreamboxEmbed, xprimeMarantEmbed, xprimePrimenetEmbed, + xprimeVolkswagenEmbed, + xprimeHarbourEmbed, ConsumetVidCloudScraper, ConsumetStreamSBScraper, ConsumetVidStreamingScraper, @@ -210,5 +211,10 @@ export function gatherAllEmbeds(): Array { oneServerHianimeEmbed, oneServerAnimepaheEmbed, oneServerAnizoneEmbed, + streamwishJapaneseScraper, + streamwishLatinoScraper, + streamwishSpanishScraper, + streamwishEnglishScraper, + streamtapeLatinoScraper, ]; } From 1e3b9c296b2656d2b439b8ae3782b639e56e2db2 Mon Sep 17 00:00:00 2001 From: MoonPic <99568458+moonpic@users.noreply.github.com> Date: Wed, 9 Jul 2025 01:48:02 +0000 Subject: [PATCH 4/4] remove the use of api, providers (api less) --- src/providers/embeds/streamwish.ts | 174 +++++++++++++++++++--- src/providers/sources/animeflv.ts | 188 +++++++++++++++++------ src/providers/sources/cuevana3.ts | 231 ++++++++++++++++++++++++++--- 3 files changed, 503 insertions(+), 90 deletions(-) diff --git a/src/providers/embeds/streamwish.ts b/src/providers/embeds/streamwish.ts index b3f5700..dc3eda3 100644 --- a/src/providers/embeds/streamwish.ts +++ b/src/providers/embeds/streamwish.ts @@ -1,8 +1,101 @@ -/* eslint-disable no-console */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars import { flags } from '@/entrypoint/utils/targets'; import { makeEmbed } from '@/providers/base'; -import { NotFoundError } from '@/utils/errors'; + +class Unbaser { + private ALPHABET: Record = { + 62: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', + 95: "' !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'", + }; + + private dictionary: Record = {}; + + 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 = [ { @@ -12,17 +105,17 @@ const providers = [ }, { id: 'streamwish-latino', - name: 'StreamWish (Latino)', + name: 'streamwish (latino)', rank: 170, }, { id: 'streamwish-spanish', - name: 'StreamWish (Castellano)', + name: 'streamwish (castellano)', rank: 169, }, { id: 'streamwish-english', - name: 'StreamWish (English)', + name: 'streamwish (english)', rank: 168, }, ]; @@ -33,21 +126,57 @@ function embed(provider: { id: string; name: string; rank: number }) { name: provider.name, rank: provider.rank, async scrape(ctx) { - console.log('Starting scrape for StreamWish with URL:', ctx.url); + 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 encodedUrl = encodeURIComponent(ctx.url); - const apiUrl = `https://ws-m3u8.moonpic.qzz.io/m3u8/${encodedUrl}`; + const html = await ctx.proxiedFetcher(ctx.url, { headers }); + const obfuscatedScript = html.match(/]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/); - const response = await fetch(apiUrl, { - headers: { - Accept: 'application/json', - // 'ngrok-skip-browser-warning': 'true', // this header bypass ngrok warning - }, - }); + if (!obfuscatedScript) { + return { stream: [], embeds: [{ embedId: provider.id, url: ctx.url }] }; + } - const data: { m3u8: string } = await response.json(); - const videoUrl = data.m3u8; - 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(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 { stream: [ @@ -57,12 +186,19 @@ function embed(provider: { id: string; name: string; rank: number }) { playlist: videoUrl, flags: [flags.CORS_ALLOWED], captions: [], + headers: { + Referer: ctx.url, + Origin: ctx.url, + }, }, ], + embeds: [], }; }, }); } -export const [streamwishJapaneseScraper, streamwishLatinoScraper, streamwishSpanishScraper, streamwishEnglishScraper] = +export const [streamwishLatinoScraper, streamwishSpanishScraper, streamwishEnglishScraper, streamwishJapaneseScraper] = providers.map(embed); + +// made by @moonpic \ No newline at end of file diff --git a/src/providers/sources/animeflv.ts b/src/providers/sources/animeflv.ts index 9b0562e..d9ff56f 100644 --- a/src/providers/sources/animeflv.ts +++ b/src/providers/sources/animeflv.ts @@ -1,79 +1,171 @@ /* eslint-disable no-console */ +import { load } 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 apiBase = 'https://ws-m3u8.moonpic.qzz.io:3006'; +const baseUrl = 'https://www3.animeflv.net'; -async function searchAnimeFlvAPI(title: string): Promise { - const res = await fetch(`${apiBase}/search?title=${encodeURIComponent(title)}`); - if (!res.ok) throw new NotFoundError('Anime not found in API'); - const data = await res.json(); - if (!data.url) throw new NotFoundError('Anime not found in API'); - return data.url; +async function searchAnimeFlv(title: string): Promise { + const searchUrl = `${baseUrl}/browse?q=${encodeURIComponent(title)}`; + const html = await fetch(searchUrl).then((r) => r.text()); + const $ = load(html); + + 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 }[]> { - const res = await fetch(`${apiBase}/episodes?url=${encodeURIComponent(animeUrl)}`); - if (!res.ok) throw new NotFoundError('Episodes not found in API'); - const data = await res.json(); - if (!data.episodes) throw new NotFoundError('Episodes not found in API'); - return data.episodes; +async function getEpisodes(animeUrl: string): Promise<{ number: number; url: string }[]> { + const html = await fetch(animeUrl).then((r) => r.text()); + const $ = load(html); + + let episodes: { number: number; url: string }[] = []; + $('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> { - const res = await fetch(`${apiBase}/embeds?episodeUrl=${encodeURIComponent(episodeUrl)}`); - if (!res.ok) throw new NotFoundError('No embed found for this content'); - const data = await res.json(); - return data; +async function getEmbeds(episodeUrl: string): Promise<{ [key: string]: string | undefined }> { + const html = await fetch(episodeUrl).then((r) => r.text()); + const $ = load(html); + + // 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 { 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 searchAnimeFlvAPI(title); - - // Get episodes using the API - const episodes = await getEpisodesAPI(animeUrl); + const animeUrl = await searchAnimeFlv(title); let episodeUrl = animeUrl; if (ctx.media.type === 'show') { 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); - if (!ep) throw new NotFoundError('Episode not found'); + if (!ep) throw new NotFoundError(`No se encontró el episodio ${episode}`); + episodeUrl = ep.url; } else if (ctx.media.type === 'movie') { - // For movies, use the first episode (by convention) - const ep = episodes.find((e) => e.number === 1) || episodes[0]; - if (!ep) throw new NotFoundError('Movie episode not found'); - episodeUrl = ep.url; - } + const html = await fetch(animeUrl).then((r) => r.text()); + const $ = load(html); - // Get all embeds using the API - const embedsData = await getEmbedsAPI(episodeUrl); - - const embeds = []; - if (embedsData['streamwish-japanese']) { - embeds.push({ - embedId: 'streamwish-japanese', - url: embedsData['streamwish-japanese'], - }); - } - if (embedsData['streamtape-latino']) { - embeds.push({ - embedId: 'streamtape-latino', - url: embedsData['streamtape-latino'], + let animeUri: string | null = null; + $('script').each((_, script) => { + const data = $(script).html() || ''; + if (data.includes('var anime_info =')) { + const animeInfo = data.split('var anime_info = [')[1]?.split('];')[0]; + animeUri = animeInfo?.split(',')[2]?.replace(/"/g, '').trim() || null; + } }); + + 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({ @@ -85,3 +177,5 @@ export const animeflvScraper = makeSourcerer({ scrapeShow: comboScraper, scrapeMovie: comboScraper, }); + +// made by @moonpic \ No newline at end of file diff --git a/src/providers/sources/cuevana3.ts b/src/providers/sources/cuevana3.ts index 877895a..ce224a8 100644 --- a/src/providers/sources/cuevana3.ts +++ b/src/providers/sources/cuevana3.ts @@ -1,46 +1,227 @@ +import { load } 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://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 { + 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 { + 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> { + 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 { - const { tmdbId, type } = ctx.media; + const mediaType = ctx.media.type; + const tmdbId = ctx.media.tmdbId; + const apiKey = '7604525319adb2db8e7e841cb98e9217'; 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 baseUrl = `https://ws-m3u8.moonpic.qzz.io:3008/tmdb`; + const translatedTitle = await fetchTmdbTitleInSpanish(Number(tmdbId), apiKey, mediaType); + let normalizedTitle = normalizeTitle(translatedTitle); - let url = ''; - if (type === 'movie') { - url = `${baseUrl}/movie/${tmdbId}`; - } else if (type === 'show') { - const showMedia = ctx.media as Extract; - if (showMedia.season?.number != null && showMedia.episode?.number != null) { - url = `${baseUrl}/tv/${tmdbId}/season/${showMedia.season.number}/episode/${showMedia.episode.number}`; - } else { - throw new NotFoundError('Missing parameters for TV episode'); + let pageUrl = + mediaType === 'movie' + ? `${baseUrl}/ver-pelicula/${normalizedTitle}` + : `${baseUrl}/episodio/${normalizedTitle}-temporada-${ctx.media.season?.number}-episodio-${ctx.media.episode?.number}`; + + ctx.progress(60); + + let pageContent = await ctx.proxiedFetcher(pageUrl); + 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 - const response = await fetch(url); - if (!response.ok) { - throw new NotFoundError(`Failed to fetch data from local server: ${response.statusText}`); + if (embeds.length === 0) { + const fallbacks = await fetchTitleSubstitutes(); + const fallbackTitle = fallbacks[tmdbId.toString()]; + + 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(); - - // Validar que vengan embeds válidos - if (!data.embeds || !Array.isArray(data.embeds) || data.embeds.length === 0) { + if (embeds.length === 0) { throw new NotFoundError('No valid streams found'); } - return { embeds: data.embeds }; + return { embeds }; } export const cuevana3Scraper = makeSourcerer({ @@ -52,3 +233,5 @@ export const cuevana3Scraper = makeSourcerer({ scrapeMovie: comboScraper, scrapeShow: comboScraper, }); + +// made by @moonpic \ No newline at end of file