From 172597e4c19f983fb0a2f31012ca634851845a14 Mon Sep 17 00:00:00 2001 From: XSWY <134489068+0pt1onz@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:09:56 +0000 Subject: [PATCH] modified: src/providers/all.ts modified: src/providers/embeds/vidking.ts deleted: src/providers/sources/coitus.ts modified: src/providers/sources/disabled/coitus.ts modified: src/providers/sources/disabled/moviebox.ts modified: src/providers/sources/fed/xprime.ts new file: src/providers/sources/florence/movielair.ts new file: src/providers/sources/test.ts --- src/providers/all.ts | 6 +- src/providers/embeds/vidking.ts | 67 +++--- src/providers/sources/coitus.ts | 74 ------- src/providers/sources/disabled/coitus.ts | 2 +- src/providers/sources/disabled/moviebox.ts | 168 +++++++-------- src/providers/sources/fed/xprime.ts | 121 ++++++++--- src/providers/sources/florence/movielair.ts | 124 +++++++++++ src/providers/sources/test.ts | 224 ++++++++++++++++++++ 8 files changed, 553 insertions(+), 233 deletions(-) delete mode 100644 src/providers/sources/coitus.ts create mode 100644 src/providers/sources/florence/movielair.ts create mode 100644 src/providers/sources/test.ts diff --git a/src/providers/all.ts b/src/providers/all.ts index b45b60f..693fcb2 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -5,7 +5,10 @@ import { vidkingEmbedScraper } from './embeds/vidking'; import { vidkingScraper } from './sources/vidKing'; import { FedAPIScraper } from './sources/fed/fedapi'; //import { FedAPIDBScraper } from './sources/fed/fedapidb'; +import { FEDIAPIScraper } from './sources/test'; +import { coitusScraper } from './sources/disabled/coitus' import { xprimeScraper } from './sources/fed/xprime'; +import { movielairScrape } from './sources/florence/movielair' import { doodScraper } from '@/providers/embeds/dood'; import { filemoonScraper } from '@/providers/embeds/filemoon'; import { mixdropScraper } from '@/providers/embeds/mixdrop'; @@ -85,7 +88,6 @@ import { EightStreamScraper } from './sources/disabled/8stream'; import { animeflvScraper } from './sources/animeflv'; import { animetsuScraper } from './sources/animetsu'; import { cinehdplusScraper } from './sources/cinehdplus-es'; -import { coitusScraper } from './sources/disabled/coitus'; import { cuevana3Scraper } from './sources/cuevana3'; import { debridScraper } from './sources/debrid'; import { embedsuScraper } from './sources/disabled/embedsu'; @@ -121,6 +123,7 @@ export function gatherAllSources(): Array { insertunitScraper, zyonScraper, soaperTvScraper, + FEDIAPIScraper, smashyScraper, autoembedScraper, myanimeScraper, @@ -129,6 +132,7 @@ export function gatherAllSources(): Array { fsharetvScraper, zoechipScraper, thunderleafScraper, + movielairScrape, mp4hydraScraper, embedsuScraper, slidemoviesScraper, diff --git a/src/providers/embeds/vidking.ts b/src/providers/embeds/vidking.ts index 96e0263..73480fb 100644 --- a/src/providers/embeds/vidking.ts +++ b/src/providers/embeds/vidking.ts @@ -18,57 +18,43 @@ export const vidkingEmbedScraper = makeEmbed({ 'User-Agent': userAgent, Referer: origin, Origin: origin, - Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.9', }; const html = await ctx.proxiedFetcher(url, { headers }); + const videoTagMatch = html.match(/]+src=["']([^"']+)["']/i); + const m3u8Match = html.match(/["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i); - const sourceMatch = html.match(/src["']?\s*[:=]\s*["'](https?:\/\/[^"']+)["']/i); - const manifestMatch = html.match(/(https?:\/\/[^"']+\.m3u8[^"']*)/i); - const hlsMatch = html.match(/hls["']?\s*[:=]\s*["']([^"']+)["']/i); + const scriptMatch = html.match(/var\s+(?:source|video|url|hlsUrl|manifest|stream)\s*=\s*["']([^"']+)["']/i); - let videoUrl = m3u8Match?.[1] || manifestMatch?.[1] || hlsMatch?.[1] || sourceMatch?.[1]; - - if (!videoUrl) { - const scriptMatch = html.match(/var\s+(?:source|video|url|hlsUrl|manifest|stream)\s*=\s*["']([^"']+)["']/i); - if (scriptMatch) { - videoUrl = scriptMatch[1]; - } - } - - if (!videoUrl) { - const jsonMatch = html.match(/\{[^}]*"(?:src|source|url|hls|manifest)"\s*:\s*"([^"]+)"/i); - if (jsonMatch) { - videoUrl = jsonMatch[1]; - } - } + let videoUrl = videoTagMatch?.[1] || m3u8Match?.[1] || scriptMatch?.[1]; if (!videoUrl) { throw new Error('No video URL found in VidKing embed'); } - if (videoUrl.startsWith('//')) { - videoUrl = `https:${videoUrl}`; - } else if (videoUrl.startsWith('/')) { - videoUrl = `${origin}${videoUrl}`; - } + if (videoUrl.startsWith('//')) videoUrl = `https:${videoUrl}`; + if (videoUrl.startsWith('/')) videoUrl = `${origin}${videoUrl}`; - const isHls = videoUrl.includes('.m3u8') || /\.m3u8[?#]/.test(videoUrl); + const isMp4Worker = videoUrl.includes('workers.dev') || videoUrl.includes('/mp4/'); - if (isHls) { + if (isMp4Worker) { return { stream: [ { id: 'primary', - type: 'hls', - playlist: videoUrl, + type: 'file', flags: [flags.CORS_ALLOWED], captions: [], - headers: { - Referer: origin, - Origin: origin, + qualities: { + unknown: { + type: 'mp4', + url: videoUrl, + }, + }, + preferredHeaders: { + Referer: 'https://vidking.net/', + Origin: 'https://vidking.net/', 'User-Agent': userAgent, }, }, @@ -80,22 +66,17 @@ export const vidkingEmbedScraper = makeEmbed({ stream: [ { id: 'primary', - type: 'file', + type: 'hls', + playlist: videoUrl, flags: [flags.CORS_ALLOWED], captions: [], - qualities: { - unknown: { - type: 'mp4', - url: videoUrl, - }, - }, - preferredHeaders: { - Referer: origin, - Origin: origin, + headers: { + Referer: 'https://vidking.net/', + Origin: 'https://vidking.net/', 'User-Agent': userAgent, }, }, ], }; }, -}); +}); \ No newline at end of file diff --git a/src/providers/sources/coitus.ts b/src/providers/sources/coitus.ts deleted file mode 100644 index 4bc5bba..0000000 --- a/src/providers/sources/coitus.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { flags } from '@/entrypoint/utils/targets'; -import { SourcererOutput, makeSourcerer } from '@/providers/base'; -import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; -import { NotFoundError } from '@/utils/errors'; -import { createM3U8ProxyUrl } from '@/utils/proxy'; - -const baseUrl = 'https://api.coitus.ca'; - -async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { - const apiUrl = - ctx.media.type === 'movie' - ? `${baseUrl}/movie/${ctx.media.tmdbId}` - : `${baseUrl}/tv/${ctx.media.tmdbId}/${ctx.media.season.number}/${ctx.media.episode.number}`; - - const apiRes = await ctx.proxiedFetcher(apiUrl); - - if (!apiRes.videoSource) throw new NotFoundError('No watchable item found'); - - let processedUrl = apiRes.videoSource; - let streamHeaders: Record = {}; - - if (processedUrl.includes('orbitproxy')) { - try { - const urlParts = processedUrl.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 || ''; - - streamHeaders = { referer }; - processedUrl = createM3U8ProxyUrl(originalUrl, ctx.features, streamHeaders); - } catch (jsonError) { - console.error('Error decoding/parsing orbitproxy data:', jsonError); - } - } - } catch (error) { - console.error('Error processing orbitproxy URL:', error); - } - } - - // eslint-disable-next-line no-console - console.log(apiRes); - ctx.progress(90); - - return { - embeds: [], - stream: [ - { - id: 'primary', - captions: [], - playlist: processedUrl, - type: 'hls', - headers: streamHeaders, - flags: [flags.CORS_ALLOWED], - }, - ], - }; -} - -export const coitusScraper = makeSourcerer({ - id: 'coitus', - name: 'Autoembed+', - rank: 94, - disabled: true, - flags: [flags.CORS_ALLOWED], - scrapeMovie: comboScraper, - scrapeShow: comboScraper, -}); diff --git a/src/providers/sources/disabled/coitus.ts b/src/providers/sources/disabled/coitus.ts index b7988e3..7a60b50 100644 --- a/src/providers/sources/disabled/coitus.ts +++ b/src/providers/sources/disabled/coitus.ts @@ -65,7 +65,7 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis export const coitusScraper = makeSourcerer({ id: 'coitus', - name: 'Autoembed+', + name: 'Coitus', rank: 91, disabled: true, flags: [flags.CORS_ALLOWED], diff --git a/src/providers/sources/disabled/moviebox.ts b/src/providers/sources/disabled/moviebox.ts index 8b09be2..7bad670 100644 --- a/src/providers/sources/disabled/moviebox.ts +++ b/src/providers/sources/disabled/moviebox.ts @@ -1,114 +1,116 @@ +import { AES, enc, mode, pad } from 'crypto-js'; import { flags } from '@/entrypoint/utils/targets'; import { makeSourcerer, SourcererOutput } from '@/providers/base'; import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; import { NotFoundError } from '@/utils/errors'; const API_BASE = 'https://api.videasy.net'; +// This salt must match the one currently in users.videasy.net/api/script.js +const XOR_SALT = '8c465aa8af6cbfd4c1f91bf0c8d678ba'; -interface MovieboxApiResponse { - success: boolean; - data?: { - streamUrl?: string; - hls?: string; - url?: string; - sources?: Array<{ file: string; type: string }>; - }; +/** + * Reverses the XOR shift. + * Using WordArray.create allows crypto-js to handle the raw bytes more reliably. + */ +function decodeVideasyHex(hex: string): any { + const words: number[] = []; + for (let i = 0; i < hex.length; i += 8) { + let word = 0; + for (let j = 0; j < 8; j += 2) { + const index = (i + j) / 2; + let byte = parseInt(hex.substring(i + j, i + j + 2), 16); + byte ^= XOR_SALT.charCodeAt(index % XOR_SALT.length); + word = (word << 8) | byte; + } + words.push(word); + } + return enc.Hex.parse(words.map(w => (w >>> 0).toString(16).padStart(8, '0')).join('')); } -async function fetchMovieboxStream( +async function fetchVideasyStream( ctx: MovieScrapeContext | ShowScrapeContext, type: 'movie' | 'tv', ): Promise { - const params = new URLSearchParams({ - title: ctx.media.title, - mediaType: type, - year: String(ctx.media.releaseYear), - tmdbId: String(ctx.media.tmdbId), - imdbId: ctx.media.imdbId ?? '', - providerId: 'moviebox', + const tmdbId = String(ctx.media.tmdbId); + + // Grey's Anatomy -> Grey%27s+Anatomy (matching your browser log) + const encodedTitle = encodeURIComponent(ctx.media.title).replace(/%20/g, '+'); + + const params = [ + `title=${encodedTitle}`, + `mediaType=${type}`, + `year=${ctx.media.releaseYear}`, + `episodeId=${type === 'tv' ? (ctx as ShowScrapeContext).media.episode.number : ''}`, + `seasonId=${type === 'tv' ? (ctx as ShowScrapeContext).media.season.number : ''}`, + `tmdbId=${tmdbId}`, + `imdbId=${ctx.media.imdbId ?? ''}` + ].join('&'); + + const url = `${API_BASE}/moviebox/sources-with-title?${params}`; + console.log(`[MovieBox] Sending Request: ${url}`); + + const response: any = await ctx.proxiedFetcher(url, { + headers: { + 'Origin': 'https://player.videasy.net', + 'Referer': 'https://player.videasy.net/', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0', + 'Accept': '*/*', + 'Cache-Control': 'no-cache', + } }); - if (type === 'tv') { - const showCtx = ctx as ShowScrapeContext; - if (showCtx.media?.season?.number && showCtx.media?.episode?.number) { - params.append('seasonId', String(showCtx.media.season.number)); - params.append('episodeId', String(showCtx.media.episode.number)); - } - } - - const apiUrl = `${API_BASE}/moviebox/sources-with-title?${params.toString()}`; - - // DEBUG: Log the request URL - console.log('[moviebox] Request URL:', apiUrl); + const encryptedData = typeof response === 'string' ? response : response?.sources; + if (!encryptedData || encryptedData.length < 100) throw new NotFoundError('No sources'); try { - // Fetch as text first to see raw response - const rawResponse = await ctx.proxiedFetcher(apiUrl, { - headers: { - Accept: 'application/json', - 'User-Agent': 'Mozilla/5.0', - }, - }); + // Videasy uses the raw string bytes of the salt as key + const key = enc.Utf8.parse(XOR_SALT); + // IV is the first 16 bytes (128 bits) of the salt + const iv = enc.Utf8.parse(XOR_SALT.substring(0, 16)); - // DEBUG: Log raw response - console.log('[moviebox] Raw API Response:', rawResponse.slice(0, 2000)); + const ciphertext = decodeVideasyHex(encryptedData); + + const decrypted = AES.decrypt( + { ciphertext } as any, + key, + { iv, mode: mode.CBC, padding: pad.Pkcs7 } + ); - // Try to parse as JSON - let response: MovieboxApiResponse; - try { - response = JSON.parse(rawResponse); - } catch (parseErr) { - console.error('[moviebox] JSON parse error:', parseErr); - console.error('[moviebox] Response was not valid JSON'); - throw new Error('API response is not valid JSON'); + const decryptedText = decrypted.toString(enc.Utf8); + + // If this is empty, the XOR_SALT provided doesn't match the server's current salt + if (!decryptedText) { + console.log(`[MovieBox] Decryption failed. Raw Start: ${encryptedData.substring(0, 20)}`); + throw new Error("Invalid Decryption Result"); } - console.log('[moviebox] Parsed response:', JSON.stringify(response, null, 2)); - - if (!response?.success || !response?.data) { - console.error('[moviebox] API error:', response?.error || 'No data field'); - throw new Error('API returned no data'); - } - - const hlsUrl = - response.data.streamUrl || - response.data.hls || - response.data.url || - response.data.sources?.find((s) => s.type === 'hls')?.file; - - if (!hlsUrl || typeof hlsUrl !== 'string' || !hlsUrl.toLowerCase().includes('.m3u8')) { - console.error('[moviebox] No HLS URL found in:', response.data); - throw new Error('No valid HLS URL found'); - } + const sources = JSON.parse(decryptedText); + console.log(`[MovieBox] Found ${sources.length} sources.`); return { embeds: [], - stream: [ - { - id: 'moviebox-primary', - type: 'hls', - playlist: hlsUrl, - headers: { - referer: 'https://videasy.net/', - origin: 'https://videasy.net', - }, - flags: [flags.CORS_ALLOWED], - captions: [], - }, - ], + stream: sources.map((s: any) => ({ + id: 'videasy', + type: s.file.includes('m3u8') ? 'hls' : 'file', + playlist: s.file, + quality: s.label || 'Unknown', + flags: [flags.CORS_ALLOWED], + captions: [], + ...(s.file.includes('m3u8') ? {} : { qualities: { "unknown": { type: "mp4", url: s.file } } }) + })) }; - } catch (err) { - console.error('[moviebox] Fetch error:', err); - throw new NotFoundError('Moviebox stream not found'); + } catch (err: any) { + console.error(`[MovieBox] Critical Decryption Failure: ${err.message}`); + throw new NotFoundError('Failed to process stream data'); } } export const movieboxScraper = makeSourcerer({ id: 'moviebox', - name: 'Moviebox', - rank: 99, + name: 'MovieBox', + rank: 110, disabled: true, flags: [flags.CORS_ALLOWED], - scrapeMovie: (ctx) => fetchMovieboxStream(ctx, 'movie'), - scrapeShow: (ctx) => fetchMovieboxStream(ctx, 'tv'), -}); + scrapeMovie: (ctx) => fetchVideasyStream(ctx, 'movie'), + scrapeShow: (ctx) => fetchVideasyStream(ctx, 'tv'), +}); \ No newline at end of file diff --git a/src/providers/sources/fed/xprime.ts b/src/providers/sources/fed/xprime.ts index 5b326be..dce886a 100644 --- a/src/providers/sources/fed/xprime.ts +++ b/src/providers/sources/fed/xprime.ts @@ -5,15 +5,14 @@ import { flags } from '@/entrypoint/utils/targets'; import { getTurnstileToken } from '@/utils/turnstile'; const baseUrl = 'mznxiwqjdiq00239q.space'; -const UA = 'Windows NT 10.0 Very nice person'; +const SITEKEY = '0x4AAAAAACuH31Fvud7uaIMf'; -async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { - let turnstileToken: string; - try { - turnstileToken = await getTurnstileToken('0x4AAAAAACuH31Fvud7uaIMf'); - } catch { - throw new NotFoundError('Turnstile verification failed'); - } +const UA = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36"; + +async function comboScraper( + ctx: ShowScrapeContext | MovieScrapeContext +): Promise { const name = encodeURIComponent(ctx.media.title); const year = ctx.media.releaseYear; @@ -23,36 +22,68 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis const season = (ctx as ShowScrapeContext).media.season?.number || 1; const episode = (ctx as ShowScrapeContext).media.episode?.number || 1; - const endpoints = ['primebox', 'fed', 'vento', 'bomber', 'blackout']; + const endpoints = [ + "primebox", + "fed", + "vento", + "bomber", + "blackout" + ]; - const commonHeaders = { - 'User-Agent': UA, - Referer: 'https://xprime.today/', - Origin: 'https://xprime.today', - Accept: 'application/json', - 'cf-turnstile-response': turnstileToken, - }; + console.log(`[XPRIME] Starting scrape for "${ctx.media.title}" (${year})`); for (const ep of endpoints) { try { - let url = - `https://${baseUrl}/${ep}?name=${name}&id=${tmdbId}&imdb=${imdbId}` + - `&season=${season}&episode=${episode}&year=${year}`; - - if (ep === 'primebox') { - url = `https://${baseUrl}/${ep}?name=${name}` + `&fallback_year=${year}&season=${season}&episode=${episode}`; + console.log(`[XPRIME] Generating Turnstile token for endpoint "${ep}"...`); + let turnstileToken: string; + try { + turnstileToken = await getTurnstileToken(SITEKEY); + console.log(`[XPRIME] Token generated successfully.`); + } catch (err) { + console.warn(`[XPRIME] Turnstile token generation failed for "${ep}", skipping endpoint.`); + continue; } + await new Promise(r => setTimeout(r, 350)); + + let url = + `https://${baseUrl}/${ep}?name=${name}` + + `&id=${tmdbId}&imdb=${imdbId}` + + `&season=${season}&episode=${episode}&year=${year}`; + + if (ep === "primebox") { + url = + `https://${baseUrl}/${ep}?name=${name}` + + `&fallback_year=${year}` + + `&season=${season}&episode=${episode}`; + } + + console.log(`[XPRIME] Fetching from endpoint "${ep}" -> ${url}`); const res = await ctx.proxiedFetcher(url, { - headers: commonHeaders, + headers: { + 'User-Agent': UA, + 'Referer': 'https://xprime.today/', + 'Origin': 'https://xprime.today', + 'Accept': 'application/json', + 'cf-turnstile-response': turnstileToken, + }, }); let streamUrl = ''; + if (res?.servers?.length) { + streamUrl = res.servers[0].url; + console.log(`[XPRIME] Found server stream URL: ${streamUrl}`); + } else if (res?.url) { + streamUrl = res.url; + console.log(`[XPRIME] Found direct stream URL: ${streamUrl}`); + } else { + console.log(`[XPRIME] No stream URL found at this endpoint, continuing...`); + continue; + } - if (res?.servers?.length) streamUrl = res.servers[0].url; - else if (res?.url) streamUrl = res.url; - - if (streamUrl?.includes('.m3u8')) { + // HLS stream + if (streamUrl.includes('.m3u8')) { + console.log(`[XPRIME] Returning HLS stream.`); return { embeds: [], stream: [ @@ -61,8 +92,9 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis type: 'hls', playlist: streamUrl, headers: { - ...commonHeaders, + 'User-Agent': UA, Referer: `https://${baseUrl}/`, + Origin: `https://${baseUrl}`, }, flags: [flags.CORS_ALLOWED], captions: [], @@ -70,10 +102,37 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis ], }; } - } catch {} + + // MP4 fallback + if (streamUrl.includes('.mp4')) { + console.log(`[XPRIME] Returning MP4 stream.`); + return { + embeds: [], + stream: [ + { + id: 'primary', + type: 'file', + qualities: { + 1080: { + type: 'mp4', + url: streamUrl, + }, + }, + flags: [flags.CORS_ALLOWED], + captions: [], + }, + ], + }; + } + + } catch (err) { + console.warn(`[XPRIME] Error scraping endpoint "${ep}":`, err); + continue; + } } - throw new NotFoundError('No valid streams found'); + console.error(`[XPRIME] No valid streams found for "${ctx.media.title}"`); + throw new NotFoundError('No valid Xprime streams found'); } export const xprimeScraper = makeSourcerer({ @@ -83,4 +142,4 @@ export const xprimeScraper = makeSourcerer({ flags: [flags.CORS_ALLOWED], scrapeMovie: comboScraper, scrapeShow: comboScraper, -}); +}); \ No newline at end of file diff --git a/src/providers/sources/florence/movielair.ts b/src/providers/sources/florence/movielair.ts new file mode 100644 index 0000000..a61bf3f --- /dev/null +++ b/src/providers/sources/florence/movielair.ts @@ -0,0 +1,124 @@ +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 BASE = "https://movielair.cc"; + +const HEADERS = { + "user-agent": + "Mozilla/5.0 (X11; Linux x86_64) Gecko/20100101 Firefox/145.0", + referer: BASE, +}; + +async function searchFirst( + ctx: ShowScrapeContext | MovieScrapeContext, + query: string +): Promise { + // IMPORTANT: /1 is required + const url = `${BASE}/search/${encodeURIComponent(query)}/1`; + + const html = await ctx.proxiedFetcher(url, { headers: HEADERS }); + const $ = load(html); + + const first = $(".film-poster a").first().attr("href"); + if (!first) return null; + + return first; +} + +function buildWatchUrl( + ctx: ShowScrapeContext | MovieScrapeContext, + path: string +) { + const id = path.split("/").pop(); + + if (ctx.media.type === "movie") { + return `${BASE}/watch-movie/${id}`; + } + + const season = (ctx as ShowScrapeContext).media.season.number; + const episode = (ctx as ShowScrapeContext).media.episode.number; + + return `${BASE}/watch-tv/${id}?season=${season}&episode=${episode}`; +} + +async function getIframe( + ctx: ShowScrapeContext | MovieScrapeContext, + watchUrl: string +) { + const html = await ctx.proxiedFetcher(watchUrl, { headers: HEADERS }); + const $ = load(html); + + const iframe = $("iframe").attr("src"); + if (!iframe) return null; + + return iframe.startsWith("http") ? iframe : `https:${iframe}`; +} + +async function extractStream( + ctx: ShowScrapeContext | MovieScrapeContext, + iframeUrl: string +) { + const html = await ctx.proxiedFetcher(iframeUrl, { headers: HEADERS }); + + const match = html.match(/file:\s*"(https?:\/\/[^"]+\.m3u8[^"]*)"/); + if (match) return match[1]; + + const match2 = html.match(/sources:\s*\[\{file:"([^"]+)"/); + if (match2) return match2[1]; + + return null; +} + +async function movielairScraper( + ctx: ShowScrapeContext | MovieScrapeContext +): Promise { + ctx.progress(10); + + const title = ctx.media.title; + + const path = await searchFirst(ctx, title); + if (!path) throw new NotFoundError("MovieLair search failed"); + + ctx.progress(30); + + const watchUrl = buildWatchUrl(ctx, path); + + ctx.progress(50); + + const iframe = await getIframe(ctx, watchUrl); + if (!iframe) throw new NotFoundError("MovieLair iframe not found"); + + ctx.progress(70); + + const stream = await extractStream(ctx, iframe); + if (!stream) throw new NotFoundError("MovieLair stream not found"); + + ctx.progress(90); + + return { + embeds: [], + stream: [ + { + id: "primary", + type: "hls" as const, + playlist: stream, + flags: [flags.CORS_ALLOWED], + captions: [], + }, + ], + }; +} + +export const movielairScrape = makeSourcerer({ + id: "movielair", + name: "MovieLair", + rank: 30, + disabled: true, + flags: [flags.CORS_ALLOWED], + scrapeMovie: movielairScraper, + scrapeShow: movielairScraper, +}); \ No newline at end of file diff --git a/src/providers/sources/test.ts b/src/providers/sources/test.ts new file mode 100644 index 0000000..477e000 --- /dev/null +++ b/src/providers/sources/test.ts @@ -0,0 +1,224 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; +import { getTurnstileToken } from '@/utils/turnstile'; + +import { Caption, labelToLanguageCode } from '../captions'; + +// ── Config ──────────────────────────────────────────────────────────────────── +const API_BASE = 'https://z6mgd9v3-8787.euw.devtunnels.ms'; +const TURNSTILE_SITE_KEY = '0x4AAAAAACuH31Fvud7uaIMf'; + +let FebToken = + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NzIyMTQxODAsIm5iZiI6MTc3MjIxNDE4MCwiZXhwIjoxODAzMzE4MjAwLCJkYXRhIjp7InVpZCI6OTMxMzA0LCJ0b2tlbiI6Ijc5NjhmYTc2ZDYzNTFjZTJjMDAxMjE4NzYyNjg4M2VkIn19.zeGmJN0gdC4ObOGIBMiHSqM0M6JtdJeT0dQELGUvVhA"; + +// ── Turnstile Cache ─────────────────────────────────────────────────────────── +let cachedTurnstile: { token: string; ts: number } | null = null; + +async function getCachedTurnstile(): Promise { + const now = Date.now(); + + if (cachedTurnstile && now - cachedTurnstile.ts < 120000) { + return cachedTurnstile.token; + } + + const token = await getTurnstileToken(TURNSTILE_SITE_KEY); + cachedTurnstile = { token, ts: now }; + return token; +} + +// ── Types ───────────────────────────────────────────────────────────────────── +interface StreamEntry { + type: 'hls' | 'mp4'; + url: string; + download?: string; +} + +interface ApiResponse { + streams: Record; + subtitles: Record< + string, + { + subtitle_link: string; + language: string; + type: 'vtt' | 'srt'; + } + >; + name?: string; + error?: string; +} + +// Quality key → numeric for player +const QUALITY_MAP: Record = { + '4K': 2160, + '2160P': 2160, + '1080P': 1080, + '720P': 720, + '480P': 480, + '360P': 360, + 'ORG': 'unknown', +}; + +// ── Main scraper ────────────────────────────────────────────────────────────── +async function comboScraper( + ctx: ShowScrapeContext | MovieScrapeContext, +): Promise { + const userToken = FebToken; + if (!userToken) + throw new NotFoundError('Febbox token not set'); + + // 1. Turnstile + let turnstileToken: string; + try { + turnstileToken = await getCachedTurnstile(); + } catch (err) { + console.log('Turnstile verification failed'); + throw new NotFoundError(`Turnstile failed: ${err}`); + } + + ctx.progress(25); + + // 2. Require IMDB ID + const imdbId = ctx.media.imdbId; + if (!imdbId) throw new NotFoundError('No IMDB ID available'); + + // 3. Build params + const params = new URLSearchParams({ + name: ctx.media.title, + year: String(ctx.media.releaseYear ?? ''), + ui: userToken, + imdb_id: imdbId, + type: ctx.media.type === 'show' ? 'show' : 'movie', + }); + + if (ctx.media.type === 'show') { + params.set('season', String(ctx.media.season.number)); + params.set('episode', String(ctx.media.episode.number)); + } + + ctx.progress(40); + + // 4. Fetch + const res = await fetch(`${API_BASE}/fedapi?${params}`, { + headers: { + 'cf-turnstile-response': turnstileToken, + }, + credentials: 'omit', + }); + + if (res.status === 401) + throw new NotFoundError('Invalid Febbox token'); + if (res.status === 403) + throw new NotFoundError('Turnstile rejected'); + if (res.status === 404) + throw new NotFoundError('Content not found'); + if (!res.ok) + throw new NotFoundError(`API error ${res.status}`); + + const data: ApiResponse = await res.json(); + + if (data?.error) + throw new NotFoundError(data.error); + if (!data?.streams) + throw new NotFoundError('No streams'); + + ctx.progress(80); + + // 5. Normalize streams + type StreamInfo = { url: string; type: 'hls' | 'mp4' }; + const streams: Record = {}; + + for (const [qualityKey, entry] of Object.entries(data.streams)) { + const normKey = QUALITY_MAP[qualityKey.toUpperCase()]; + if (normKey === undefined) continue; + if (streams[normKey]) continue; + streams[normKey] = { url: entry.url, type: entry.type }; + } + + // 6. Captions + const captions: Caption[] = []; + for (const sub of Object.values(data.subtitles ?? {})) { + const url = sub.subtitle_link; + if (!url) continue; + + const langCode = + labelToLanguageCode(sub.language)?.toLowerCase() ?? + 'unknown'; + + captions.push({ + type: sub.type, + id: url, + url, + language: langCode, + hasCorsRestrictions: false, + }); + } + + ctx.progress(95); + + // 7. Return + const orderedKeys = [2160, 1080, 720, 480, 360, 'unknown'] as const; + const hlsStream = orderedKeys + .map((k) => streams[k]) + .find((s) => s?.type === 'hls'); + + if (hlsStream) { + return { + embeds: [], + stream: [ + { + id: 'primary', + captions, + playlist: hlsStream.url, + type: 'hls', + flags: [flags.CORS_ALLOWED], + }, + ], + }; + } + + return { + embeds: [], + stream: [ + { + id: 'primary', + captions, + qualities: { + ...(streams[2160] && { + '4k': { type: 'mp4', url: streams[2160].url }, + }), + ...(streams[1080] && { + 1080: { type: 'mp4', url: streams[1080].url }, + }), + ...(streams[720] && { + 720: { type: 'mp4', url: streams[720].url }, + }), + ...(streams[480] && { + 480: { type: 'mp4', url: streams[480].url }, + }), + ...(streams[360] && { + 360: { type: 'mp4', url: streams[360].url }, + }), + ...(streams['unknown'] && { + unknown: { + type: 'mp4', + url: streams['unknown'].url, + }, + }), + }, + type: 'file', + flags: [flags.CORS_ALLOWED], + }, + ], + }; +} + +export const FEDIAPIScraper = makeSourcerer({ + id: 'fediapi', + name: 'FEDI API 🔥', + rank: 105, + flags: [flags.CORS_ALLOWED], + scrapeMovie: comboScraper, + scrapeShow: comboScraper, +}); \ No newline at end of file