diff --git a/src/providers/all.ts b/src/providers/all.ts index 97fc582..176b0d9 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -1,6 +1,20 @@ import { Embed, Sourcerer } from '@/providers/base'; import { doodScraper } from '@/providers/embeds/dood'; import { mixdropScraper } from '@/providers/embeds/mixdrop'; +import { + riveAsiacloudScraper, + riveEe3Scraper, + riveFastxScraper, + riveFilmechoScraper, + riveG1Scraper, + riveG2Scraper, + riveGhostScraper, + riveGuruScraper, + riveHydraxScraper, + riveKageScraper, + riveNovaScraper, + rivePutafilmeScraper, +} from '@/providers/embeds/rive'; import { turbovidScraper } from '@/providers/embeds/turbovid'; import { upcloudScraper } from '@/providers/embeds/upcloud'; import { autoembedScraper } from '@/providers/sources/autoembed'; @@ -9,6 +23,7 @@ import { ee3Scraper } from '@/providers/sources/ee3'; import { fsharetvScraper } from '@/providers/sources/fsharetv'; import { insertunitScraper } from '@/providers/sources/insertunit'; import { mp4hydraScraper } from '@/providers/sources/mp4hydra'; +import { riveScraper } from '@/providers/sources/rive'; import { tugaflixScraper } from '@/providers/sources/tugaflix'; import { vidsrcsuScraper } from '@/providers/sources/vidsrcsu'; @@ -88,6 +103,7 @@ export function gatherAllSources(): Array { coitusScraper, streamboxScraper, nunflixScraper, + riveScraper, ]; } @@ -134,5 +150,17 @@ export function gatherAllEmbeds(): Array { FedAPIPrivateScraper, FedAPISharedScraper, FedDBScraper, + riveAsiacloudScraper, + riveEe3Scraper, + riveFastxScraper, + riveFilmechoScraper, + riveG1Scraper, + riveG2Scraper, + riveGhostScraper, + riveGuruScraper, + riveHydraxScraper, + riveKageScraper, + riveNovaScraper, + rivePutafilmeScraper, ]; } diff --git a/src/providers/embeds/rive.ts b/src/providers/embeds/rive.ts new file mode 100644 index 0000000..fd0dffa --- /dev/null +++ b/src/providers/embeds/rive.ts @@ -0,0 +1,113 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { makeEmbed } from '@/providers/base'; + +const providers = [ + { + id: 'rive-hydrax', + rank: 270, + }, + { + id: 'rive-fastx', + rank: 269, + }, + { + id: 'rive-filmecho', + rank: 268, + }, + { + id: 'rive-nova', + rank: 267, + }, + { + id: 'rive-guru', + rank: 266, + }, + { + id: 'rive-g1', + rank: 265, + }, + { + id: 'rive-g2', + rank: 264, + }, + { + id: 'rive-ee3', + rank: 263, + }, + { + id: 'rive-ghost', + rank: 262, + }, + { + id: 'rive-putafilme', + rank: 261, + }, + { + id: 'rive-asiacloud', + rank: 260, + }, + { + id: 'rive-kage', + rank: 259, + }, +]; + +function embed(provider: { id: string; rank: number; disabled?: boolean }) { + return makeEmbed({ + id: provider.id, + name: provider.id + .split('-') + .map((word) => word[0].toUpperCase() + word.slice(1)) + .join(' '), + disabled: provider.disabled, + rank: provider.rank, + async scrape(ctx) { + const isHLS = ctx.url.includes('.m3u8') || ctx.url.includes('hls') || ctx.url.includes('playlist'); + + if (isHLS) { + return { + stream: [ + { + id: 'primary', + type: 'hls', + playlist: ctx.url, + flags: [flags.CORS_ALLOWED], + captions: [], + }, + ], + }; + } + return { + stream: [ + { + id: 'primary', + type: 'file', + qualities: { + unknown: { + type: 'mp4', + url: ctx.url, + }, + }, + flags: [flags.CORS_ALLOWED], + captions: [], + }, + ], + }; + }, + }); +} + +export const [ + riveHydraxScraper, + riveFastxScraper, + riveFilmechoScraper, + riveNovaScraper, + riveGuruScraper, + riveG1Scraper, + riveG2Scraper, + riveEe3Scraper, + riveGhostScraper, + rivePutafilmeScraper, + riveAsiacloudScraper, + riveKageScraper, +] = providers.map(embed); diff --git a/src/providers/sources/rive.ts b/src/providers/sources/rive.ts new file mode 100644 index 0000000..b2bd212 --- /dev/null +++ b/src/providers/sources/rive.ts @@ -0,0 +1,196 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererEmbed, SourcererOutput, makeSourcerer } from '@/providers/base'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +const baseUrl = 'https://rivestream.org'; + +function generateSecretKey(id: number | string) { + const keyArray = [ + 'I', + '3LZu', + 'M2V3', + '4EXX', + 's4', + 'yRy', + 'oqMz', + 'ysE', + 'RT', + 'iSI', + 'zlc', + 'H', + 'YNp', + '5vR6', + 'h9S', + 'R', + 'jo', + 'F', + 'h2', + 'W8', + 'i', + 'sz09', + 'Xom', + 'gpU', + 'q', + '6Qvg', + 'Cu', + '5Zaz', + 'VK', + 'od', + 'FGY4', + 'eu', + 'D5Q', + 'smH', + '11eq', + 'QrXs', + '3', + 'L3', + 'YhlP', + 'c', + 'Z', + 'YT', + 'bnsy', + '5', + 'fcL', + 'L22G', + 'r8', + 'J', + '4', + 'gnK', + ]; + + // Handle undefined/null input + if (typeof id === 'undefined' || id === null) { + return 'rive'; + } + + // Convert to number and calculate array index + const numericId = typeof id === 'string' ? parseInt(id, 10) : Number(id); + const index = numericId % keyArray.length; + + // Handle NaN cases (invalid number conversion) + if (Number.isNaN(index)) { + return 'rive'; + } + + return keyArray[index]; +} + +function processProxiedURL(url: string): string { + if (url.includes('orbitproxy')) { + try { + const urlParts = url.split(/orbitproxy\.[^/]+\//); + if (urlParts.length >= 2) { + const encryptedPart = urlParts[1].split('.m3u8')[0]; + try { + const decodedData = Buffer.from(encryptedPart, 'base64').toString('utf-8'); + + const jsonData = JSON.parse(decodedData); + const originalUrl = jsonData.u; + const referer = jsonData.r || ''; + + const encodedUrl = encodeURIComponent(originalUrl); + const encodedHeaders = encodeURIComponent( + JSON.stringify({ + referer, + }), + ); + + const proxyUrl = `https://m3u8.wafflehacker.io/m3u8-proxy?url=${encodedUrl}&headers=${encodedHeaders}`; + return proxyUrl; + } catch (jsonError) { + console.error('Error decoding/parsing orbitproxy data:', jsonError); + } + } + } catch (error) { + console.error('Error processing orbitproxy URL:', error); + } + } + return url; +} + +async function comboScraper(ctx: MovieScrapeContext | ShowScrapeContext): Promise { + const tmdbId = ctx.media.tmdbId; + const secret = generateSecretKey(tmdbId); + + const servers = [ + 'hydrax', + 'fastx', + 'filmecho', + 'nova', + 'guru', + 'g1', + 'g2', + 'ee3', + 'ghost', + 'putafilme', + 'asiacloud', + 'kage', + ]; + + const route = + ctx.media.type === 'show' + ? `/api/backendfetch?requestID=tvVideoProvider&id=${tmdbId}&season=${ctx.media.season.number}&episode=${ctx.media.episode.number}&secretKey=${secret}&service=` + : `/api/backendfetch?requestID=movieVideoProvider&id=${tmdbId}&secretKey=${secret}&service=`; + + ctx.progress(20); + + const embeds: SourcererEmbed[] = []; + const processedServers = new Set(); + + await Promise.all( + servers.map(async (server) => { + try { + const url = baseUrl + route + server; + + const response = await ctx.proxiedFetcher(url, { + headers: { + Referer: baseUrl, + }, + }); + + const data = typeof response === 'string' ? JSON.parse(response) : response; + + if (!data?.data || !data?.data?.sources?.length) { + return; + } + + data.data.sources.forEach((source: any) => { + const processedUrl = processProxiedURL(source.url); + + const serverKey = `rive-${server}`; + + if (!processedServers.has(serverKey)) { + processedServers.add(serverKey); + + embeds.push({ + embedId: serverKey, + url: processedUrl, + }); + } + }); + } catch (error) { + console.error(`Error fetching Rive ${server}:`, error); + } + }), + ); + + ctx.progress(90); + + if (!embeds.length) { + throw new NotFoundError('No embeds found from Rive'); + } + + return { + embeds, + }; +} + +export const riveScraper = makeSourcerer({ + id: 'rive', + name: 'Rive', + rank: 130, + flags: [flags.CORS_ALLOWED], + scrapeMovie: comboScraper, + scrapeShow: comboScraper, +});