diff --git a/src/providers/all.ts b/src/providers/all.ts index 51324b6..690cbc5 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -1,4 +1,11 @@ import { Embed, Sourcerer } from '@/providers/base'; +import { AnimekaiScraper } from './embeds/animekai'; +import { animekaiScraper } from './sources/animekai'; +import { vidkingEmbedScraper } from './embeds/vidking'; +import { vidkingScraper } from './sources/vidKing'; +import { FedAPIScraper } from './sources/fed/fedapi'; +//import { FedAPIDBScraper } from './sources/fed/fedapidb'; +import { xprimeScraper } from './sources/fed/xprime'; import { doodScraper } from '@/providers/embeds/dood'; import { filemoonScraper } from '@/providers/embeds/filemoon'; import { mixdropScraper } from '@/providers/embeds/mixdrop'; @@ -13,12 +20,11 @@ import { thunderleafScraper } from '@/providers/sources/florence/thunderleaf'; import { fsharetvScraper } from '@/providers/sources/disabled/fsharetv'; import { fsOnlineEmbeds, fsOnlineScraper } from '@/providers/sources/fsonline/index'; import { insertunitScraper } from '@/providers/sources/disabled/insertunit'; -import { movieboxScraper } from '@/providers/sources/Moviebox/moviebox'; +import { movieboxScraper } from '@/providers/sources/disabled/moviebox'; import { flixerScraper } from '@/providers/sources/Moviebox/flixerz'; import { hdmovieScraper } from '@/providers/sources/Moviebox/hdmovie'; import { zyonScraper } from './sources/Moviebox/zyon'; import { smashyScraper } from '@/providers/sources/florence/smashy'; -import { xprimeScraper } from '@/providers/sources/florence/xprime'; import { mp4hydraScraper } from '@/providers/sources/disabled/mp4hydra'; import { pirxcyScraper } from '@/providers/sources/disabled/pirxcy'; import { tugaflixScraper } from '@/providers/sources/tugaflix'; @@ -91,7 +97,7 @@ import { nunflixScraper } from './sources/disabled/nunflix'; import { pelisplushdScraper } from './sources/pelisplushd'; import { primewireScraper } from './sources/disabled/primewire'; import { rgshowsScraper } from './sources/rgshows'; -import { ridooMoviesScraper } from './sources/disabled/ridomovies'; +import { ridooMoviesScraper } from './sources/ridomovies'; import { slidemoviesScraper } from './sources/disabled/slidemovies'; import { soaperTvScraper } from './sources/disabled/soapertv'; import { streamboxScraper } from './sources/disabled/streambox'; @@ -128,12 +134,12 @@ export function gatherAllSources(): Array { slidemoviesScraper, vidapiClickScraper, hdmovieScraper, - xprimeScraper, flixerScraper, coitusScraper, streamboxScraper, nunflixScraper, EightStreamScraper, + xprimeScraper, movieboxScraper, wecimaScraper, animeflvScraper, @@ -151,6 +157,10 @@ export function gatherAllSources(): Array { debridScraper, cinehdplusScraper, fullhdfilmizleScraper, + animekaiScraper, + FedAPIScraper, + //FedAPIDBScraper, + vidkingScraper ]; } @@ -217,5 +227,7 @@ export function gatherAllEmbeds(): Array { droploadScraper, supervideoScraper, voeScraper, + AnimekaiScraper, + vidkingEmbedScraper ]; } diff --git a/src/providers/embeds/animekai.ts b/src/providers/embeds/animekai.ts new file mode 100644 index 0000000..76b3717 --- /dev/null +++ b/src/providers/embeds/animekai.ts @@ -0,0 +1,74 @@ +import { EmbedOutput, makeEmbed } from '@/providers/base'; +import { NotFoundError } from '@/utils/errors'; + +import { Caption, labelToLanguageCode } from '../captions'; + +interface StreamData { + headers: { + Referer: string; + Origin?: string; + }; + sources: Array<{ + url: string; + isM3U8: boolean; + }>; + subtitles?: Array<{ + url: string; + lang?: string; + kind?: string; + }>; +} + +export const AnimekaiScraper = makeEmbed({ + id: 'animekai-embed', + name: 'AnimeKai', + rank: 415, + flags: [], // ← REQUIRED + async scrape(ctx): Promise { + const { episodeId } = JSON.parse(ctx.url); + const data = await ctx.fetcher( + `https://api.1anime.app/anime/animekai/watch/${encodeURIComponent(episodeId)}`, + ); + + if (!data?.sources?.length) throw new NotFoundError('No stream found'); + + ctx.progress(50); + + const captions: Caption[] = (data.subtitles ?? []) + .filter((sub) => sub.lang && sub.kind !== 'thumbnails') + .map((sub) => ({ + type: 'vtt', + id: sub.url, + url: sub.url, + language: labelToLanguageCode(sub.lang!.replace(/_\[.*?\]$/, '').trim()) || 'unknown', + hasCorsRestrictions: true, + })); + + const hlsSource = data.sources.find((s) => s.isM3U8); + if (!hlsSource) throw new NotFoundError('No HLS stream found'); + + ctx.progress(90); + + const headers: Record = {}; + if (data.headers.Referer) { + headers.Referer = data.headers.Referer; + try { + headers.Origin = new URL(data.headers.Referer).origin; + } catch {} + } + if (data.headers.Origin) headers.Origin = data.headers.Origin; + + return { + stream: [ + { + id: 'primary', + captions, + playlist: hlsSource.url, + headers, + type: 'hls', + flags: [], + }, + ], + }; + }, +}); \ No newline at end of file diff --git a/src/providers/embeds/vidking.ts b/src/providers/embeds/vidking.ts new file mode 100644 index 0000000..821c46f --- /dev/null +++ b/src/providers/embeds/vidking.ts @@ -0,0 +1,101 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { makeEmbed } from '@/providers/base'; + +const userAgent = + 'Mozilla/5.0 (Linux; Android 11; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36'; + +export const vidkingEmbedScraper = makeEmbed({ + id: 'vidking', + name: 'VidKing', + rank: 175, + flags: [flags.CORS_ALLOWED], + async scrape(ctx) { + const url = ctx.url; + const parsedUrl = new URL(url); + const origin = parsedUrl.origin; + + const headers: Record = { + '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 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); + + 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]; + } + } + + 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}`; + } + + const isHls = videoUrl.includes('.m3u8') || /\.m3u8[?#]/.test(videoUrl); + + if (isHls) { + return { + stream: [ + { + id: 'primary', + type: 'hls', + playlist: videoUrl, + flags: [flags.CORS_ALLOWED], + captions: [], + headers: { + Referer: origin, + Origin: origin, + 'User-Agent': userAgent, + }, + }, + ], + }; + } + + return { + stream: [ + { + id: 'primary', + type: 'file', + flags: [flags.CORS_ALLOWED], + captions: [], + qualities: { + unknown: { + type: 'mp4', + url: videoUrl, + }, + }, + preferredHeaders: { + Referer: origin, + Origin: origin, + 'User-Agent': userAgent, + }, + }, + ], + }; + }, +}); \ No newline at end of file diff --git a/src/providers/sources/Moviebox/flixerz.ts b/src/providers/sources/Moviebox/flixerz.ts index c705a48..32a2533 100644 --- a/src/providers/sources/Moviebox/flixerz.ts +++ b/src/providers/sources/Moviebox/flixerz.ts @@ -1,56 +1,180 @@ +// src/providers/sources/flixerz/flixerz.ts 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'; +const PROVIDER = 'flixerz'; -async function fetchflixerStream( +// Helper: Detect hex token (64+ hex chars) +function isHexToken(str: string): boolean { + return /^[0-9a-f]{64,}$/i.test(str.trim()); +} + +// Helper: Extract body text from various response shapes +function extractResponseBody(res: unknown): string { + if (typeof res === 'string') return res; + + if (res instanceof Buffer) return res.toString('utf-8'); + + if (res && typeof res === 'object') { + const obj = res as Record; + + // Common response body locations + const bodyFields = ['data', 'body', 'text', 'content', 'response']; + for (const field of bodyFields) { + if (typeof obj[field] === 'string') { + return obj[field] as string; + } + // Nested object with text/data inside + if (obj[field] && typeof obj[field] === 'object') { + const nested = obj[field] as Record; + if (typeof nested.data === 'string') return nested.data; + if (typeof nested.body === 'string') return nested.body; + if (typeof nested.text === 'string') return nested.text; + } + } + + // Try JSON stringify as last resort + return JSON.stringify(obj); + } + + return String(res); +} + +// Helper: Build playback URLs from token +function buildPlaybackUrls(token: string): string[] { + return [ + `${API_BASE}/${PROVIDER}/play?token=${token}`, + `${API_BASE}/${PROVIDER}/stream?token=${token}`, + `${API_BASE}/${PROVIDER}/source?token=${token}`, + `${API_BASE}/${PROVIDER}/play/${token}`, + `${API_BASE}/${PROVIDER}/hls/${token}.m3u8`, + `https://cdn.videasy.net/${PROVIDER}/${token}/index.m3u8`, + `https://cdn.videasy.net/hls/${token}.m3u8?provider=${PROVIDER}`, + ]; +} + +async function fetchFlixerzStream( 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: PROVIDER, + }); + + 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 tokenUrl = `${API_BASE}/${PROVIDER}/sources-with-title?${params.toString()}`; + try { - const params = new URLSearchParams({ - title: ctx.media.title, - mediaType: type, - year: String(ctx.media.releaseYear), - tmdbId: String(ctx.media.tmdbId), - imdbId: ctx.media.imdbId ?? '', - episodeId: - type === 'tv' && 'episode' in ctx.media - ? String(ctx.media.episode.number) - : '1', - seasonId: - type === 'tv' && 'season' in ctx.media - ? String(ctx.media.season.number) - : '1', + // Fetch without generic type to get raw response + const rawResponse = await ctx.proxiedFetcher(tokenUrl, { + headers: { + Accept: '*/*', + 'User-Agent': 'Mozilla/5.0', + Referer: 'https://videasy.net/', + Origin: 'https://videasy.net', + }, }); - const url = `${API_BASE}/myflixerzupcloud/sources-with-title?${params.toString()}`; + // Debug: Log response structure + console.log('[flixerz] Raw response type:', typeof rawResponse); + console.log('[flixerz] Raw response keys:', Object.keys(rawResponse || {})); + + // Extract body text + const token = extractResponseBody(rawResponse).trim(); + + // Debug: Log extracted token + console.log('[flixerz] Extracted token preview:', token.slice(0, 100)); + console.log('[flixerz] Token length:', token.length); + + if (!isHexToken(token)) { + console.error(`[flixerz] Expected hex token, got: ${token.slice(0, 200)}`); + throw new Error('Invalid token response from API'); + } - const stream = { - id: 'flixer', - type: 'hls' as const, - playlist: url, - flags: [flags.CORS_ALLOWED], - captions: [], - }; + console.log(`[flixerz] Got token (${token.length} chars), testing playback URLs...`); + const candidates = buildPlaybackUrls(token); + + for (const candidate of candidates) { + try { + const testRes = await ctx.proxiedFetcher(candidate, { + headers: { + Referer: 'https://videasy.net/', + Origin: 'https://videasy.net', + }, + }); + + if (testRes.startsWith('#EXTM3U') || testRes.includes('#EXTINF:')) { + console.log(`[flixerz] ✅ Found working playlist: ${candidate}`); + return { + embeds: [], + stream: [ + { + id: 'flixerz-primary', + type: 'hls', + playlist: candidate, + headers: { + referer: 'https://videasy.net/', + origin: 'https://videasy.net', + }, + flags: [flags.CORS_ALLOWED], + captions: [], + }, + ], + }; + } + } catch { + continue; + } + } + + // Fallback to first pattern + const fallback = candidates[0]; + console.log(`[flixerz] Using fallback: ${fallback}`); return { embeds: [], - stream: [stream], + stream: [ + { + id: 'flixerz-primary', + type: 'hls', + playlist: fallback, + headers: { + referer: 'https://videasy.net/', + origin: 'https://videasy.net', + }, + flags: [flags.CORS_ALLOWED], + captions: [], + }, + ], }; - } catch { - throw new NotFoundError('flixerz stream not found'); + + } catch (err) { + console.error('[flixerz] Error:', err); + throw new NotFoundError('Flixerz stream not found'); } } export const flixerScraper = makeSourcerer({ id: 'flixerz', name: 'Flixerz', - rank: 17, + rank: 99, disabled: false, flags: [flags.CORS_ALLOWED], - scrapeMovie: (ctx: MovieScrapeContext) => fetchflixerStream(ctx, 'movie'), - scrapeShow: (ctx: ShowScrapeContext) => fetchflixerStream(ctx, 'tv'), + scrapeMovie: (ctx) => fetchFlixerzStream(ctx, 'movie'), + scrapeShow: (ctx) => fetchFlixerzStream(ctx, 'tv'), }); \ No newline at end of file diff --git a/src/providers/sources/Moviebox/moviebox.ts b/src/providers/sources/Moviebox/moviebox.ts deleted file mode 100644 index 06cc6fd..0000000 --- a/src/providers/sources/Moviebox/moviebox.ts +++ /dev/null @@ -1,56 +0,0 @@ -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'; - -async function fetchMovieboxStream( - ctx: MovieScrapeContext | ShowScrapeContext, - type: 'movie' | 'tv', -): Promise { - try { - const params = new URLSearchParams({ - title: ctx.media.title, - mediaType: type, - year: String(ctx.media.releaseYear), - tmdbId: String(ctx.media.tmdbId), - imdbId: ctx.media.imdbId ?? '', - episodeId: - type === 'tv' && 'episode' in ctx.media - ? String(ctx.media.episode.number) - : '1', - seasonId: - type === 'tv' && 'season' in ctx.media - ? String(ctx.media.season.number) - : '1', - }); - - const url = `${API_BASE}/moviebox/sources-with-title?${params.toString()}`; - - const stream = { - id: 'moviebox', - type: 'hls' as const, - playlist: url, - flags: [flags.CORS_ALLOWED], - captions: [], - }; - - return { - embeds: [], - stream: [stream], - }; - } catch { - throw new NotFoundError('Moviebox stream not found'); - } -} - -export const movieboxScraper = makeSourcerer({ - id: 'moviebox', - name: 'Moviebox', - rank: 99, - disabled: false, - flags: [flags.CORS_ALLOWED], - scrapeMovie: (ctx: MovieScrapeContext) => fetchMovieboxStream(ctx, 'movie'), - scrapeShow: (ctx: ShowScrapeContext) => fetchMovieboxStream(ctx, 'tv'), -}); \ No newline at end of file diff --git a/src/providers/sources/animekai.ts b/src/providers/sources/animekai.ts new file mode 100644 index 0000000..1e46cef --- /dev/null +++ b/src/providers/sources/animekai.ts @@ -0,0 +1,63 @@ +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +const consumetBase = 'https://api.1anime.app/anime/animekai'; + +interface SearchResult { + id: string; + title: string; +} + +interface SearchResponse { + results: SearchResult[]; +} + +interface Episode { + id: string; + number: number; +} + +interface InfoResponse { + episodes: Episode[]; +} + +function normalizeTitle(s: string): string { + return s + .normalize('NFD') + .replace(/\p{Diacritic}/gu, '') + .toLowerCase(); +} + +async function searchAnime(ctx: ShowScrapeContext, title: string): Promise { + const data = await ctx.fetcher(`${consumetBase}/${encodeURIComponent(title)}`); + if (!data?.results?.length) throw new NotFoundError('Anime not found on AnimeKai'); + const normalizedTitle = normalizeTitle(title); + const exact = data.results.find((r) => normalizeTitle(r.title) === normalizedTitle); + return (exact ?? data.results[0]).id; +} + +async function scrapeAnimekai(ctx: ShowScrapeContext): Promise { + const title = ctx.media.title; + const episodeNumber = ctx.media.episode.number; + + const animeId = await searchAnime(ctx, title); + + const info = await ctx.fetcher(`${consumetBase}/info?id=${animeId}`); + if (!info?.episodes?.length) throw new NotFoundError('No episodes found on AnimeKai'); + + const ep = info.episodes.find((e) => e.number === episodeNumber); + if (!ep) throw new NotFoundError('Episode not found on AnimeKai'); + + return { + embeds: [{ embedId: 'animekai-embed', url: JSON.stringify({ episodeId: ep.id }) }], + }; +} + +export const animekaiScraper = makeSourcerer({ + id: 'animekai', + name: 'AnimeKai 🔥', + rank: 93, + flags: [], + scrapeShow: scrapeAnimekai, +}); \ No newline at end of file diff --git a/src/providers/sources/coitus.ts b/src/providers/sources/coitus.ts new file mode 100644 index 0000000..9ddf76c --- /dev/null +++ b/src/providers/sources/coitus.ts @@ -0,0 +1,74 @@ +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, +}); \ No newline at end of file diff --git a/src/providers/sources/disabled/moviebox.ts b/src/providers/sources/disabled/moviebox.ts new file mode 100644 index 0000000..bd26949 --- /dev/null +++ b/src/providers/sources/disabled/moviebox.ts @@ -0,0 +1,114 @@ +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'; + +interface MovieboxApiResponse { + success: boolean; + data?: { + streamUrl?: string; + hls?: string; + url?: string; + sources?: Array<{ file: string; type: string }>; + }; +} + +async function fetchMovieboxStream( + 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', + }); + + 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); + + try { + // Fetch as text first to see raw response + const rawResponse = await ctx.proxiedFetcher(apiUrl, { + headers: { + Accept: 'application/json', + 'User-Agent': 'Mozilla/5.0', + }, + }); + + // DEBUG: Log raw response + console.log('[moviebox] Raw API Response:', rawResponse.slice(0, 2000)); + + // 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'); + } + + 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'); + } + + return { + embeds: [], + stream: [ + { + id: 'moviebox-primary', + type: 'hls', + playlist: hlsUrl, + headers: { + referer: 'https://videasy.net/', + origin: 'https://videasy.net', + }, + flags: [flags.CORS_ALLOWED], + captions: [], + }, + ], + }; + } catch (err) { + console.error('[moviebox] Fetch error:', err); + throw new NotFoundError('Moviebox stream not found'); + } +} + +export const movieboxScraper = makeSourcerer({ + id: 'moviebox', + name: 'Moviebox', + rank: 99, + disabled: true, + flags: [flags.CORS_ALLOWED], + scrapeMovie: (ctx) => fetchMovieboxStream(ctx, 'movie'), + scrapeShow: (ctx) => fetchMovieboxStream(ctx, 'tv'), +}); \ No newline at end of file diff --git a/src/providers/sources/ee3/common.ts b/src/providers/sources/ee3/common.ts index e6da8bf..b39bb2a 100644 --- a/src/providers/sources/ee3/common.ts +++ b/src/providers/sources/ee3/common.ts @@ -1,5 +1,5 @@ export const apiBaseUrl = 'https://borg.rips.cc'; -export const username = '_sf_'; // I'd appreciate if you made your own account "_sf_" seems to be removed. Invite codes are: fmhy or mpgh +export const username = '_ps_'; -export const password = 'defonotscraping'; +export const password = 'defonotscraping'; \ No newline at end of file diff --git a/src/providers/sources/ee3/index.ts b/src/providers/sources/ee3/index.ts index 09c76fa..38e7bad 100644 --- a/src/providers/sources/ee3/index.ts +++ b/src/providers/sources/ee3/index.ts @@ -113,8 +113,8 @@ async function comboScraper(ctx: MovieScrapeContext): Promise { export const ee3Scraper = makeSourcerer({ id: 'ee3', name: 'EE3', - rank: 23, + rank: 97, disabled: false, flags: [], scrapeMovie: comboScraper, -}); +}); \ No newline at end of file diff --git a/src/providers/sources/fed/fedapi.ts b/src/providers/sources/fed/fedapi.ts new file mode 100644 index 0000000..edd8398 --- /dev/null +++ b/src/providers/sources/fed/fedapi.ts @@ -0,0 +1,162 @@ +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'; + +const getUserToken = (): string | null => { + try { + if (typeof window === 'undefined') return null; + const prefData = window.localStorage.getItem('__MW::preferences'); + if (!prefData) return null; + const parsedAuth = JSON.parse(prefData); + return parsedAuth?.state?.febboxKey || null; + } catch (e) { + console.warn('Unable to access localStorage or parse auth data:', e); + return null; + } +}; + + +const BASE_URL = 'https://mznxiwqjdiq00239q.space'; + + + +interface StreamEntry { + type: 'hls' | 'mp4'; + url: string; +} + +interface StreamData { + streams: Record; + subtitles: Record; + error?: string; + name?: string; + size?: string; +} + +async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { + const userToken = getUserToken(); + if (!userToken) throw new NotFoundError('Requires a user token!'); + + let turnstileToken: string; + try { + turnstileToken = await getTurnstileToken('0x4AAAAAABgPwhrOT6x6sTjI'); + } catch (error) { + // eslint-disable-next-line no-alert + alert('FED API Turnstile verification failed. Please refresh the page and try again.'); + throw new NotFoundError(`Turnstile verification failed: ${error}`); + } + + ctx.progress(50); + + const name = ctx.media.title; + let apiUrl = `${BASE_URL}/fedapi?name=${encodeURIComponent(name)}&year=${ctx.media.releaseYear}&ui=${encodeURIComponent(userToken)}`; + if (ctx.media.type === 'show') { + apiUrl += `&season=${ctx.media.season.number}&episode=${ctx.media.episode.number}`; + } + + const res = await fetch(apiUrl, { credentials: 'omit' }); + if (!res.ok) throw new NotFoundError('API request failed'); + const data: StreamData = await res.json(); + + if (data?.error && data.error.endsWith('not found in database')) { + throw new NotFoundError('No stream found'); + } + if (!data) throw new NotFoundError('No response from API'); + + ctx.progress(90); + + type StreamInfo = { url: string; type: 'hls' | 'mp4' }; + const streams = Object.entries(data.streams).reduce((acc: Record, [quality, entry]) => { + const url = typeof entry === 'string' ? entry : entry.url; + const type = typeof entry === 'string' ? 'mp4' : entry.type; + + let qualityKey: number; + if (quality === 'ORG') { + const urlPath = url.split('?')[0]; + if (urlPath.toLowerCase().includes('.mp4') || type === 'hls') { + acc.unknown = { url, type }; + } + return acc; + } + if (quality === '4K') { + qualityKey = 2160; + } else { + qualityKey = parseInt(quality.replace('P', ''), 10); + } + if (Number.isNaN(qualityKey) || acc[qualityKey]) return acc; + acc[qualityKey] = { url, type }; + return acc; + }, {}); + + const captions: Caption[] = []; + if (data.subtitles) { + for (const [langKey, subtitleData] of Object.entries(data.subtitles)) { + const languageKeyPart = langKey.split('_')[0]; + const languageName = languageKeyPart.charAt(0).toUpperCase() + languageKeyPart.slice(1); + const languageCode = labelToLanguageCode(languageName)?.toLowerCase() ?? 'unknown'; + + if (subtitleData.subtitle_link) { + const url = subtitleData.subtitle_link; + const isVtt = url.toLowerCase().endsWith('.vtt'); + captions.push({ + type: isVtt ? 'vtt' : 'srt', + id: url, + url, + language: languageCode, + hasCorsRestrictions: false, + }); + } + } + } + + ctx.progress(90); + + const hlsStream = streams[2160] ?? streams[1080] ?? streams[720] ?? streams[480] ?? streams[360] ?? streams.unknown; + if (hlsStream?.type === 'hls') { + 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 FedAPIScraper = makeSourcerer({ + id: 'fedapi', + name: 'FED API 🔥', + rank: 101, + flags: [flags.CORS_ALLOWED], + scrapeMovie: comboScraper, + scrapeShow: comboScraper, +}); \ No newline at end of file diff --git a/src/providers/sources/fed/fedapidb.ts b/src/providers/sources/fed/fedapidb.ts new file mode 100644 index 0000000..cd63609 --- /dev/null +++ b/src/providers/sources/fed/fedapidb.ts @@ -0,0 +1,229 @@ +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'; + +const getUserToken = (): string | null => { + try { + if (typeof window === 'undefined') return null; + const prefData = window.localStorage.getItem('__MW::preferences'); + if (!prefData) return null; + const parsedAuth = JSON.parse(prefData); + return parsedAuth?.state?.febboxKey || null; + } catch (e) { + console.warn('Unable to access localStorage or parse auth data:', e); + return null; + } +}; + +const getRegion = (): string | null => { + try { + if (typeof window === 'undefined') return null; + const regionData = window.localStorage.getItem('__MW::region'); + if (!regionData) return null; + const parsed = JSON.parse(regionData); + return parsed?.state?.region ?? null; + } catch (e) { + console.warn('Unable to access localStorage or parse auth data:', e); + return null; + } +}; + +const BASE_URL = 'https://fed-api-db.pstream.mov'; + +function selectSubdomainByRegion(input: string | null): string | null { + const region = (input || '').toLowerCase(); + + if (/(^|\b)(usa5|usa6|usa7|uk1|de2|hk1|ca1|au1|sg1|in1)(\b|$)/.test(region)) { + const match = region.match(/(usa5|usa6|usa7|uk1|de2|hk1|ca1|au1|sg1|in1)/); + if (match) return match[1]; + } + + if (region.includes('dallas')) return 'usa5'; + if (region.includes('portland')) return 'usa6'; + if (region.includes('new-york')) return 'usa7'; + if (region.includes('paris')) return Math.random() < 0.5 ? 'uk1' : 'de2'; + if (region.includes('hong-kong')) return 'hk1'; + if (region.includes('kansas')) return Math.random() < 0.5 ? 'usa7' : 'usa6'; + if (region.includes('sydney')) return 'au1'; + if (region.includes('singapore')) return 'sg1'; + if (region.includes('mumbai')) return 'in1'; + + if (region === 'east') return 'usa7'; + if (region === 'west') return 'usa6'; + if (region === 'south') return 'usa5'; + if (region === 'europe') return Math.random() < 0.5 ? 'uk1' : 'de2'; + if (region === 'asia') return 'sg1'; + + return null; +} + +function rewriteSheguSubdomain(originalUrl: string, subdomain: string): string { + try { + const parsed = new URL(originalUrl); + if (parsed.hostname.endsWith('.shegu.net')) { + parsed.hostname = `${subdomain}.shegu.net`; + return parsed.toString(); + } + return originalUrl; + } catch { + return originalUrl; + } +} + +interface StreamData { + streams: Record; + subtitles: Record; + error?: string; + name?: string; + size?: string; +} + +async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { + const userToken = getUserToken(); + if (!userToken) throw new NotFoundError('Requires a user token!'); + + const region = getRegion(); + + let turnstileToken: string; + try { + turnstileToken = await getTurnstileToken('0x4AAAAAACuH31Fvud7uaIMf'); + } catch (error) { + // eslint-disable-next-line no-alert + alert('FED DB Turnstile verification failed. Please refresh the page and try again.'); + throw new NotFoundError(`Turnstile verification failed: ${error}`); + } + + ctx.progress(50); + + const apiUrl = + ctx.media.type === 'movie' + ? `${BASE_URL}/movie/${ctx.media.tmdbId}` + : `${BASE_URL}/tv/${ctx.media.tmdbId}/${ctx.media.season.number}/${ctx.media.episode.number}`; + + const data = await ctx.fetcher(apiUrl); + + if (data?.error && data.error.endsWith('not found in database')) { + throw new NotFoundError('No stream found'); + } + if (!data) throw new NotFoundError('No response from API'); + + ctx.progress(90); + + const streams = Object.entries(data.streams).reduce((acc: Record, [quality, url]) => { + let qualityKey: number; + if (quality === 'ORG') { + const urlPath = url.split('?')[0]; + if (urlPath.toLowerCase().includes('.mp4')) { + acc.unknown = url; + } + return acc; + } + if (quality === '4K') { + qualityKey = 2160; + } else { + qualityKey = parseInt(quality.replace('P', ''), 10); + } + if (Number.isNaN(qualityKey) || acc[qualityKey]) return acc; + acc[qualityKey] = url; + return acc; + }, {}); + + const filteredStreams = Object.entries(streams).reduce((acc: Record, [quality, url]) => { + acc[quality] = url; + return acc; + }, {}); + + const selectedSubdomain = selectSubdomainByRegion(region); + if (selectedSubdomain) { + Object.keys(filteredStreams).forEach((q) => { + filteredStreams[q] = rewriteSheguSubdomain(filteredStreams[q], selectedSubdomain as string); + }); + } + + const captions: Caption[] = []; + if (data.subtitles) { + for (const [langKey, subtitleData] of Object.entries(data.subtitles)) { + const languageKeyPart = langKey.split('_')[0]; + const languageName = languageKeyPart.charAt(0).toUpperCase() + languageKeyPart.slice(1); + const languageCode = labelToLanguageCode(languageName)?.toLowerCase() ?? 'unknown'; + + if (subtitleData.subtitle_link) { + const url = subtitleData.subtitle_link; + const isVtt = url.toLowerCase().endsWith('.vtt'); + captions.push({ + type: isVtt ? 'vtt' : 'srt', + id: url, + url, + language: languageCode, + hasCorsRestrictions: false, + }); + } + } + } + + ctx.progress(90); + + return { + embeds: [], + stream: [ + { + id: 'primary', + captions, + qualities: { + ...(filteredStreams[2160] && { + '4k': { + type: 'mp4', + url: filteredStreams[2160], + }, + }), + ...(filteredStreams[1080] && { + 1080: { + type: 'mp4', + url: filteredStreams[1080], + }, + }), + ...(filteredStreams[720] && { + 720: { + type: 'mp4', + url: filteredStreams[720], + }, + }), + ...(filteredStreams[480] && { + 480: { + type: 'mp4', + url: filteredStreams[480], + }, + }), + ...(filteredStreams[360] && { + 360: { + type: 'mp4', + url: filteredStreams[360], + }, + }), + ...(filteredStreams.unknown && { + unknown: { + type: 'mp4', + url: filteredStreams.unknown, + }, + }), + }, + type: 'file', + flags: [flags.CORS_ALLOWED], + }, + ], + }; +} + +export const FedAPIDBScraper = makeSourcerer({ + id: 'fedapidb', + name: 'FED DB 🔥', + rank: 299, + disabled: true, + flags: [flags.CORS_ALLOWED], + scrapeMovie: comboScraper, + scrapeShow: comboScraper, +}); \ No newline at end of file diff --git a/src/providers/sources/florence/xprime.ts b/src/providers/sources/fed/xprime.ts similarity index 51% rename from src/providers/sources/florence/xprime.ts rename to src/providers/sources/fed/xprime.ts index e2dba58..ecb1c39 100644 --- a/src/providers/sources/florence/xprime.ts +++ b/src/providers/sources/fed/xprime.ts @@ -1,63 +1,63 @@ import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; import { NotFoundError } from '@/utils/errors'; import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { flags } from '@/entrypoint/utils/targets'; +import { getTurnstileToken } from '@/utils/turnstile'; const baseUrl = 'mznxiwqjdiq00239q.space'; const UA = "Windows NT 10.0 Very nice person"; -async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { +async function comboScraper( + ctx: ShowScrapeContext | MovieScrapeContext +): Promise { + + let turnstileToken: string; + try { + turnstileToken = await getTurnstileToken('0x4AAAAAACuH31Fvud7uaIMf'); + } catch { + throw new NotFoundError('Turnstile verification failed'); + } + const name = encodeURIComponent(ctx.media.title); const year = ctx.media.releaseYear; const tmdbId = ctx.media.tmdbId; const imdbId = ctx.media.imdbId || ''; - + const season = (ctx as ShowScrapeContext).media.season?.number || 1; const episode = (ctx as ShowScrapeContext).media.episode?.number || 1; - const turnstileToken = localStorage.getItem('turnstile_token'); - - const endpoints = [ - "fed", - "bomber", - "primebox", - "vento", - "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 || '', + 'cf-turnstile-response': turnstileToken, }; for (const ep of endpoints) { try { - let url = `https://${baseUrl}/${ep}?name=${name}&id=${tmdbId}&imdb=${imdbId}&season=${season}&episode=${episode}`; - + 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}`; - } else { - url += `&year=${year}`; + url = + `https://${baseUrl}/${ep}?name=${name}` + + `&fallback_year=${year}&season=${season}&episode=${episode}`; } - const res = await ctx.proxiedFetcher(url, { - method: 'GET', - headers: commonHeaders + const res = await ctx.proxiedFetcher(url, { + headers: commonHeaders, }); let streamUrl = ''; - if (res?.servers && Array.isArray(res.servers) && res.servers.length > 0) { - streamUrl = res.servers[0].url; - } else if (res?.url) { - streamUrl = res.url; - } + if (res?.servers?.length) streamUrl = res.servers[0].url; + else if (res?.url) streamUrl = res.url; - if (streamUrl && streamUrl.includes('.m3u8')) { - ctx.progress(100); - + if (streamUrl?.includes('.m3u8')) { return { embeds: [], stream: [ @@ -67,27 +67,26 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis playlist: streamUrl, headers: { ...commonHeaders, - 'Referer': `https://${baseUrl}/`, + Referer: `https://${baseUrl}/`, }, - flags: [], + flags: [flags.CORS_ALLOWED], captions: [], }, ], }; } - } catch (e) { - continue; - } + + } catch {} } - throw new NotFoundError('No valid Xprime streams found across all endpoints'); + throw new NotFoundError('No valid streams found'); } export const xprimeScraper = makeSourcerer({ id: 'xprime', name: 'Xprime', - rank: 98, - flags: [], + rank: 92, + flags: [flags.CORS_ALLOWED], scrapeMovie: comboScraper, scrapeShow: comboScraper, }); \ No newline at end of file diff --git a/src/providers/sources/florence/smashy.ts b/src/providers/sources/florence/smashy.ts index 2b3d619..37b6ea8 100644 --- a/src/providers/sources/florence/smashy.ts +++ b/src/providers/sources/florence/smashy.ts @@ -127,7 +127,7 @@ async function smashyScrapy( export const smashyScraper = makeSourcerer({ id: 'smashy', name: 'SmashyStream', - rank: 90, + rank: 94, flags: [flags.CORS_ALLOWED], scrapeMovie: smashyScrapy, scrapeShow: smashyScrapy, diff --git a/src/providers/sources/fsonline/doodstream.ts b/src/providers/sources/fsonline/doodstream.ts index bcbbc07..5d01265 100644 --- a/src/providers/sources/fsonline/doodstream.ts +++ b/src/providers/sources/fsonline/doodstream.ts @@ -86,7 +86,7 @@ async function getStream(ctx: ScrapeContext, url: string): Promise<[string, stri } // console.log(LOG_PREFIX, 'Stream URL', streamURL); - return [streamURL, streamHost]; + return [streamURL, new URL(streamURL).hostname]; } export async function scrapeDoodstreamEmbed(ctx: EmbedScrapeContext): Promise { @@ -125,4 +125,4 @@ export async function scrapeDoodstreamEmbed(ctx: EmbedScrapeContext): Promise { + return title + .toLowerCase() + .trim() + .replace(/[^\w\s]/g, '') + .replace(/\s+/g, ' '); +}; + const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) => { const searchResult = await ctx.proxiedFetcher('/search', { baseUrl: ridoMoviesApiBase, @@ -18,14 +24,37 @@ const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) => q: ctx.media.title, }, }); + + if (!searchResult.data?.items || searchResult.data.items.length === 0) { + throw new NotFoundError('No search results found'); + } + const mediaData = searchResult.data.items.map((movieEl) => { const name = movieEl.title; const year = movieEl.contentable.releaseYear; const fullSlug = movieEl.fullSlug; return { name, year, fullSlug }; }); - const targetMedia = mediaData.find((m) => m.name === ctx.media.title && m.year === ctx.media.releaseYear.toString()); - if (!targetMedia?.fullSlug) throw new NotFoundError('No watchable item found'); + + const normalizedSearchTitle = normalizeTitle(ctx.media.title); + const searchYear = ctx.media.releaseYear.toString(); + + let targetMedia = mediaData.find((m) => normalizeTitle(m.name) === normalizedSearchTitle && m.year === searchYear); + + if (!targetMedia) { + targetMedia = mediaData.find((m) => { + const normalizedName = normalizeTitle(m.name); + return ( + m.year === searchYear && + (normalizedName.includes(normalizedSearchTitle) || normalizedSearchTitle.includes(normalizedName)) + ); + }); + } + + if (!targetMedia?.fullSlug) { + throw new NotFoundError('No matching media found'); + } + ctx.progress(40); let iframeSourceUrl = `/${targetMedia.fullSlug}/videos`; @@ -34,14 +63,20 @@ const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) => const showPageResult = await ctx.proxiedFetcher(`/${targetMedia.fullSlug}`, { baseUrl: ridoMoviesBase, }); + const fullEpisodeSlug = `season-${ctx.media.season.number}/episode-${ctx.media.episode.number}`; const regexPattern = new RegExp( - `\\\\"id\\\\":\\\\"(\\d+)\\\\"(?=.*?\\\\\\"fullSlug\\\\\\":\\\\\\"[^"]*${fullEpisodeSlug}[^"]*\\\\\\")`, + `\\\\"id\\\\":\\\\"(\\d+)\\\\"(?=.*?\\\\"fullSlug\\\\":\\\\"[^"]*${fullEpisodeSlug}[^"]*\\\\")`, 'g', ); + const matches = [...showPageResult.matchAll(regexPattern)]; const episodeIds = matches.map((match) => match[1]); - if (episodeIds.length === 0) throw new NotFoundError('No watchable item found'); + + if (episodeIds.length === 0) { + throw new NotFoundError('Episode not found'); + } + const episodeId = episodeIds[episodeIds.length - 1]; iframeSourceUrl = `/episodes/${episodeId}/videos`; } @@ -49,24 +84,38 @@ const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) => const iframeSource = await ctx.proxiedFetcher(iframeSourceUrl, { baseUrl: ridoMoviesApiBase, }); + if (!iframeSource.data || iframeSource.data.length === 0) { + throw new NotFoundError('No video sources found'); + } + const iframeSource$ = load(iframeSource.data[0].url); const iframeUrl = iframeSource$('iframe').attr('data-src'); - if (!iframeUrl) throw new NotFoundError('No watchable item found'); + + if (!iframeUrl) { + throw new NotFoundError('No iframe URL found'); + } + ctx.progress(60); const embeds: SourcererEmbed[] = []; - if (iframeUrl.includes('closeload')) { - embeds.push({ - embedId: closeLoadScraper.id, - url: iframeUrl, - }); - } + + let embedId = 'closeload'; + if (iframeUrl.includes('ridoo')) { - embeds.push({ - embedId: ridooScraper.id, - url: iframeUrl, - }); + embedId = 'ridoo'; } + + embeds.push({ + embedId, + url: iframeUrl, + }); + + ctx.progress(80); + + if (embeds.length === 0) { + throw new NotFoundError('No supported embeds found'); + } + ctx.progress(90); return { @@ -77,9 +126,9 @@ const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) => export const ridooMoviesScraper = makeSourcerer({ id: 'ridomovies', name: 'RidoMovies', - rank: 210, + rank: 96, flags: [], - disabled: true, + disabled: false, scrapeMovie: universalScraper, scrapeShow: universalScraper, -}); +}); \ No newline at end of file diff --git a/src/providers/sources/disabled/ridomovies/types.ts b/src/providers/sources/ridomovies/types.ts similarity index 99% rename from src/providers/sources/disabled/ridomovies/types.ts rename to src/providers/sources/ridomovies/types.ts index a030738..17088ac 100644 --- a/src/providers/sources/disabled/ridomovies/types.ts +++ b/src/providers/sources/ridomovies/types.ts @@ -75,4 +75,4 @@ export type IframeSourceResult = { data: { url: string; }[]; -}; +}; \ No newline at end of file diff --git a/src/providers/sources/vidKing.ts b/src/providers/sources/vidKing.ts new file mode 100644 index 0000000..303912a --- /dev/null +++ b/src/providers/sources/vidKing.ts @@ -0,0 +1,30 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; + +const baseUrl = 'https://www.vidking.net'; + +async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { + const url = + ctx.media.type === 'movie' + ? `${baseUrl}/embed/movie/${ctx.media.tmdbId}` + : `${baseUrl}/embed/tv/${ctx.media.tmdbId}/${ctx.media.season.number}/${ctx.media.episode.number}`; + + return { + embeds: [ + { + embedId: 'vidking', + url, + }, + ], + }; +} + +export const vidkingScraper = makeSourcerer({ + id: 'Vidking', + name: 'VidKing', + rank: 95, + flags: [flags.CORS_ALLOWED], + scrapeMovie: comboScraper, + scrapeShow: comboScraper, +}); \ No newline at end of file diff --git a/src/providers/sources/vidlink.ts b/src/providers/sources/vidlink.ts new file mode 100644 index 0000000..bdd7559 --- /dev/null +++ b/src/providers/sources/vidlink.ts @@ -0,0 +1,113 @@ +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +const API_BASE = 'https://enc-dec.app/api'; +const VIDLINK_BASE = 'https://vidlink.pro/api/b'; + +const headers = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', + Connection: 'keep-alive', + Referer: 'https://vidlink.pro/', + Origin: 'https://vidlink.pro', +}; + +async function encryptTmdbId(ctx: MovieScrapeContext | ShowScrapeContext, tmdbId: string): Promise { + const response = await ctx.proxiedFetcher<{ result: string }>(`${API_BASE}/enc-vidlink`, { + method: 'GET', + query: { text: tmdbId }, + }); + + if (!response?.result) { + throw new NotFoundError('Failed to encrypt TMDB ID'); + } + + return response.result; +} + +async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { + const { tmdbId } = ctx.media; + + ctx.progress(10); + + const encryptedId = await encryptTmdbId(ctx, tmdbId.toString()); + + ctx.progress(30); + + const apiUrl = + ctx.media.type === 'movie' + ? `${VIDLINK_BASE}/movie/${encryptedId}` + : `${VIDLINK_BASE}/tv/${encryptedId}/${ctx.media.season.number}/${ctx.media.episode.number}`; + + const vidlinkRaw = await ctx.proxiedFetcher(apiUrl, { + headers, + }); + + if (!vidlinkRaw) { + throw new NotFoundError('No response from vidlink API'); + } + + ctx.progress(60); + + let vidlinkData: { stream?: any }; + try { + vidlinkData = typeof vidlinkRaw === 'string' ? JSON.parse(vidlinkRaw) : vidlinkRaw; + } catch { + throw new NotFoundError('Invalid JSON from vidlink API'); + } + + ctx.progress(80); + + if (!vidlinkData.stream) { + throw new NotFoundError('No stream data found in vidlink response'); + } + + const { stream } = vidlinkData; + + const captions = []; + if (stream.captions && Array.isArray(stream.captions)) { + for (const caption of stream.captions) { + const captionType = caption.type === 'srt' ? 'srt' : 'vtt'; + captions.push({ + id: caption.id || caption.url, + url: caption.url, + language: caption.language || 'Unknown', + type: captionType as 'srt' | 'vtt', + hasCorsRestrictions: caption.hasCorsRestrictions || false, + }); + } + } + + // const flags = stream.flags || []; + // if (vidlinkData.flags) { + // flags.push(...vidlinkData.flags); + // } + + ctx.progress(90); + + return { + embeds: [], + stream: [ + { + id: stream.id || 'primary', + type: stream.type || 'file', + qualities: stream.qualities || {}, + playlist: stream.playlist, + captions, + flags: [], + headers: stream.headers || headers, + }, + ], + }; +} + +export const vidlinkScraper = makeSourcerer({ + id: 'vidlink', + name: 'VidLink 🔥', + rank: 96, + disabled: false, + flags: [], + scrapeMovie: comboScraper, + scrapeShow: comboScraper, +}); \ No newline at end of file