diff --git a/src/providers/embeds/streamtape.ts b/src/providers/embeds/streamtape.ts index 187f8e7..6e465c3 100644 --- a/src/providers/embeds/streamtape.ts +++ b/src/providers/embeds/streamtape.ts @@ -20,13 +20,7 @@ function embed(provider: { id: string; name: string; rank: number }) { name: provider.name, rank: provider.rank, async scrape(ctx) { - const response = await fetch(ctx.url, { - headers: { - Accept: 'text/html', - }, - }); - - const embedHtml = await response.text(); + const embedHtml = await ctx.proxiedFetcher(ctx.url); const match = embedHtml.match(/robotlink'\).innerHTML = (.*)'/); if (!match) throw new Error('No match found'); diff --git a/src/providers/embeds/streamwish.ts b/src/providers/embeds/streamwish.ts index 425610d..518114e 100644 --- a/src/providers/embeds/streamwish.ts +++ b/src/providers/embeds/streamwish.ts @@ -1,6 +1,101 @@ 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 = [ { @@ -10,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, }, ]; @@ -31,19 +126,57 @@ function embed(provider: { id: string; name: string; rank: number }) { name: provider.name, rank: provider.rank, async scrape(ctx) { - const encodedUrl = encodeURIComponent(ctx.url); - const apiUrl = `https://ws-m3u8.moonpic.qzz.io/m3u8/${encodedUrl}`; + 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 response = await fetch(apiUrl, { - headers: { - Accept: 'application/json', - // 'ngrok-skip-browser-warning': 'true', // this header bypass ngrok warning - }, - }); + 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 data: { m3u8: string } = await response.json(); - const videoUrl = data.m3u8; - if (!videoUrl) throw new NotFoundError('No video URL found'); + if (!obfuscatedScript) { + return { stream: [], embeds: [{ embedId: provider.id, url: ctx.url }] }; + } + + 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: [ @@ -53,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 diff --git a/src/providers/sources/animeflv.ts b/src/providers/sources/animeflv.ts index 9b0562e..59cd66d 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 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