diff --git a/package.json b/package.json index c051ca2..f523a7e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,8 @@ "name": "@vyla-entertainment/sdk", "version": "1.20.0", "scripts": { - "test": "node test/index.js" + "test": "node test/index.js", + "test:all": "node test/index.js all" }, "author": "vyla-entertainment", "contributors": [ diff --git a/src/config.js b/src/config.js index d9fe3b6..012ed58 100644 --- a/src/config.js +++ b/src/config.js @@ -94,24 +94,6 @@ export const SOURCES = [ }], }, - { - key: 'cinezo', - label: 'Cinezo', - sourceFile: 'cinezo', - proxyParam: 'czo', - timeout: 25000, - jitter: 500, - retries: 2, - multiUrl: true, - cdnHeaders: [{ - pattern: /yenime\.net|flikhub\.net|hakunaymatata\.com/i, - headers: { - Referer: 'https://player.cinezo.live/', - Origin: 'https://player.cinezo.live', - }, - }], - }, - { key: 'cno', label: 'Cno', @@ -355,7 +337,7 @@ export const SOURCES = [ retries: 1, multiUrl: true, skipProxy: true, - disabled: false, // Enabled, but only returns streams when a manifest is provided + disabled: false, }, { @@ -499,26 +481,6 @@ export const SOURCES = [ multiUrl: true, }, - { - key: 'vidrock', - label: 'VidRock', - sourceFile: 'vidrock', - proxyParam: 'vr', - timeout: 20000, - jitter: 800, - retries: 3, - multiUrl: true, - cdnHeaders: [{ - pattern: /./, - headers: { - Accept: '/', - 'Accept-Language': 'en-US,en;q=0.9', - Referer: 'https://vidrock.ru/', - Origin: 'https://vidrock.ru', - }, - },], - }, - { key: 'vidup', label: 'VidUp', diff --git a/src/sdk.d.ts b/src/sdk.d.ts index 0f27998..a45a330 100644 --- a/src/sdk.d.ts +++ b/src/sdk.d.ts @@ -20,10 +20,12 @@ export interface SourceConfig { disabled?: boolean; sourcesTimeout?: number; } + export interface ProbeResult { ok: boolean; ms: number | null; } + export interface StreamResult { url: string; headers: { @@ -46,6 +48,7 @@ export interface DownloadResult { type: "mkv"; active: boolean; } + export interface SubtitleResult { label: string; file: string; @@ -64,30 +67,12 @@ export default class VylaSDK { constructor(options?: VylaSDKOptions); - /** - * Get the list of configured sources. - * @param excludeDisabled If true, filters out disabled sources. - */ getSources(excludeDisabled?: boolean): SourceConfig[]; - /** - * Probe all active (non-disabled) sources for availability. - */ probeAllSources(): Promise; - /** - * Probe a single source by key. - */ probeSource(key: string): Promise; - /** - * Get a playable stream for a given source/media id. - * @param key Source key - * @param id Media id (e.g. TMDB id) - * @param s Season number (for TV), or null for movies - * @param e Episode number (for TV), or null for movies - * @param clientIP Optional client IP for source-specific logic - */ getStream( key: string, id: string | number, @@ -96,24 +81,12 @@ export default class VylaSDK { clientIP?: string | null ): Promise; - /** - * Get subtitles for a movie or TV episode. - * @param id Media id - * @param s Season number (for TV), or null for movies - * @param e Episode number (for TV), or null for movies - */ getSubtitles( id: string | number, s?: number | null, e?: number | null ): Promise; - /** - * Get download links for a movie or TV episode. - * @param id Media id - * @param s Season number (for TV), or null for movies - * @param e Episode number (for TV), or null for movies - */ getDownloads( id: string | number, s?: number | null, diff --git a/src/sdk.js b/src/sdk.js index 48268d5..93abced 100644 --- a/src/sdk.js +++ b/src/sdk.js @@ -1,78 +1,82 @@ -import SOURCES, { SOURCE_MAP } from "./config.js"; -import { fetchDownloads } from "./downloads/downloads.js"; -import { probeSource } from "./health.js"; -import { getChapterPages, getMangaDetails, searchManga } from "./manga/atsu.js"; -import { fetchSubtitles, getSubPathsMovie, getSubPathsTv } from "./subtitles.js"; -import { createStreamArgs } from "./utils/helpers.js"; - -const ALL_SOURCE_MODULES = Object.fromEntries( - await Promise.all(SOURCES.map(async cfg => [cfg.key, await import(`./sources/${cfg.sourceFile}.js`)])) -); - -const SOURCE_MODULES = Object.fromEntries( - Object.entries(ALL_SOURCE_MODULES).filter(([key]) => !SOURCE_MAP[key]?.disabled) -); - -export default class VylaSDK { - tmdbApiKey = null; - penguManifest = null; - constructor({ tmdbApiKey, penguManifest } = {}) { - this.tmdbApiKey = tmdbApiKey || null; - this.penguManifest = penguManifest || null; - } - getSources(excludeDisabled = false) { - if (excludeDisabled) { - return SOURCES.filter(cfg => !cfg.disabled); - } - return SOURCES; - } - async probeAllSources() { - const active = SOURCES.filter(cfg => !cfg.disabled); - - const results = await Promise.allSettled( - active.map(async cfg => await probeSource(cfg, SOURCE_MODULES[cfg.key])) - ); - - const sources = Object.fromEntries( - active.map((cfg, i) => [ - cfg.key, - results[i].status === 'fulfilled' ? results[i].value : { ok: false, ms: null }, - ]) - ); - - return sources; - } - async probeSource(key) { - const cfg = SOURCES.find(cfg => cfg.key === key); - if (!cfg) throw new Error(`Source with key "${key}" not found`); - const mod = SOURCE_MODULES[key]; - if (!mod) throw new Error(`Source module for key "${key}" not found`); - return await probeSource(this, cfg, mod); - } - async getStream(key, id, s = null, e = null, clientIP = null) { - const cfg = SOURCES.find(cfg => cfg.key === key); - if (!cfg) throw new Error(`Source with key "${key}" not found`); - const mod = SOURCE_MODULES[key]; - if (!mod) throw new Error(`Source module for key "${key}" not found`); - const streamArgs = await createStreamArgs(cfg, this, id, s, e, clientIP); - return await mod.getStream(streamArgs); - } - async getSubtitles(id, s = null, e = null) { - const paths = s != null && e != null - ? await getSubPathsTv(id, s, e) - : await getSubPathsMovie(id); - return await fetchSubtitles(paths); - } - async getDownloads(id, s = null, e = null) { - return await fetchDownloads(this, id, s, e); - } - async searchManga(query, page = 1, perPage = 12) { - return await searchManga(query, page, perPage); - } - async getManga(id) { - return await getMangaDetails(id); - } - async getMangaChapter(mangaId, chapterId) { - return await getChapterPages(mangaId, chapterId); - } -} \ No newline at end of file +import SOURCES, { SOURCE_MAP } from "./config.js"; +import { fetchDownloads } from "./downloads/downloads.js"; +import { probeSource } from "./health.js"; +import { getChapterPages, getMangaDetails, searchManga } from "./manga/atsu.js"; +import { fetchSubtitles, getSubPathsMovie, getSubPathsTv } from "./subtitles.js"; +import { createStreamArgs } from "./utils/helpers.js"; + +const ALL_SOURCE_MODULES = Object.fromEntries( + await Promise.all(SOURCES.map(async cfg => [cfg.key, await import(`./sources/${cfg.sourceFile}.js`)])) +); + +const SOURCE_MODULES = Object.fromEntries( + Object.entries(ALL_SOURCE_MODULES).filter(([key]) => !SOURCE_MAP[key]?.disabled) +); + +export default class VylaSDK { + tmdbApiKey = null; + penguManifest = null; + constructor({ tmdbApiKey, penguManifest } = {}) { + this.tmdbApiKey = tmdbApiKey || null; + this.penguManifest = penguManifest || null; + } + getSources(excludeDisabled = false) { + if (excludeDisabled) { + return SOURCES.filter(cfg => !cfg.disabled); + } + return SOURCES; + } + async probeAllSources() { + const active = SOURCES.filter(cfg => !cfg.disabled); + + const results = await Promise.allSettled( + active.map(async cfg => await probeSource(this, cfg, SOURCE_MODULES[cfg.key])) + ); + + const sources = Object.fromEntries( + active.map((cfg, i) => [ + cfg.key, + results[i].status === 'fulfilled' ? results[i].value : { ok: false, ms: null }, + ]) + ); + + return sources; + } + async probeSource(key) { + const cfg = SOURCES.find(cfg => cfg.key === key); + if (!cfg) throw new Error(`Source with key "${key}" not found`); + const mod = SOURCE_MODULES[key]; + if (!mod) throw new Error(`Source module for key "${key}" not found`); + return await probeSource(this, cfg, mod); + } + async getStream(key, id, s = null, e = null, clientIP = null) { + const cfg = SOURCES.find(cfg => cfg.key === key); + if (!cfg) throw new Error(`Source with key "${key}" not found`); + const mod = SOURCE_MODULES[key]; + if (!mod) throw new Error(`Source module for key "${key}" not found`); + const streamArgs = await createStreamArgs(cfg, this, id, s, e, clientIP); + const result = await mod.getStream(streamArgs); + if (result && typeof result === 'object') { + delete result.subtitles; + } + return result; + } + async getSubtitles(id, s = null, e = null) { + const paths = s != null && e != null + ? await getSubPathsTv(id, s, e) + : await getSubPathsMovie(id); + return await fetchSubtitles(paths); + } + async getDownloads(id, s = null, e = null) { + return await fetchDownloads(this, id, s, e); + } + async searchManga(query, page = 1, perPage = 12) { + return await searchManga(query, page, perPage); + } + async getManga(id) { + return await getMangaDetails(id); + } + async getMangaChapter(mangaId, chapterId) { + return await getChapterPages(mangaId, chapterId); + } +} diff --git a/src/sources/111477.js b/src/sources/111477.js index ca88abd..4101c51 100644 --- a/src/sources/111477.js +++ b/src/sources/111477.js @@ -1,11 +1,11 @@ -import { getUA } from '../utils/helpers.js'; +import { getUA, resolveImdbId, resolveTmdbKey } from '../utils/helpers.js'; const SERVICE_ORIGIN = 'https://st.111477.xyz'; -function generateManifestBaseUrl(tmdbKey) { +function generateManifestBaseUrl(apiKey) { let config = 'https://a.111477.xyz/::sort=file-desc::limit=3'; - if (tmdbKey) { - config += `::tmdb=${tmdbKey}`; + if (apiKey) { + config += `::tmdb=${apiKey}`; } const b64 = Buffer.from(config, 'utf8') .toString('base64') @@ -15,28 +15,13 @@ function generateManifestBaseUrl(tmdbKey) { return `${SERVICE_ORIGIN}/config/${b64}`; } -async function resolveImdbId(sdk, id, isTv) { - const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY; - if (!apiKey) return null; - const type = isTv ? 'tv' : 'movie'; - try { - const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`, { - signal: AbortSignal.timeout(6000), - }); - if (!res.ok) return null; - const data = await res.json(); - return data.imdb_id || data.external_ids?.imdb_id || null; - } catch { - return null; - } -} - -export async function getStream({ id, s, e, sdk }) { - const isTv = Boolean(s && e); +export async function getStream(args) { + const { id, s, e, sdk } = args || {}; + const isTv = s != null && e != null; + const apiKey = resolveTmdbKey(args, sdk); const ua = getUA(); - const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY; - const imdbId = await resolveImdbId(sdk, id, isTv); + const imdbId = await resolveImdbId(id, isTv, apiKey); const targetIds = []; if (imdbId && imdbId.startsWith('tt')) { @@ -48,7 +33,7 @@ export async function getStream({ id, s, e, sdk }) { const seenUrls = new Set(); const allUrls = []; - const defaultHeaders = { + const headers = { 'User-Agent': ua, Accept: 'application/json, text/plain, */*', }; @@ -60,7 +45,7 @@ export async function getStream({ id, s, e, sdk }) { try { const res = await fetch(endpoint, { - headers: defaultHeaders, + headers, signal: AbortSignal.timeout(12000), }); @@ -70,19 +55,22 @@ export async function getStream({ id, s, e, sdk }) { if (Array.isArray(data?.streams) && data.streams.length > 0) { for (const item of data.streams) { if (!item || typeof item !== 'object') continue; - const url = String(item.url || '').trim(); - if (!url || !url.startsWith('http') || seenUrls.has(url)) continue; + const streamUrl = String(item.url || '').trim(); + if (!streamUrl || !streamUrl.startsWith('http') || seenUrls.has(streamUrl)) continue; - seenUrls.add(url); + seenUrls.add(streamUrl); const rawTitle = item.title ? String(item.title).trim() : ''; const rawName = item.name ? String(item.name).trim() : '111477'; const label = rawTitle || rawName; allUrls.push({ - url, + url: streamUrl, label, - headers: defaultHeaders, + server: label, + quality: 'Auto', + type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', + headers, }); } @@ -95,7 +83,7 @@ export async function getStream({ id, s, e, sdk }) { return { url: allUrls[0].url, - allUrls, headers: allUrls[0].headers, + allUrls, }; } \ No newline at end of file diff --git a/src/sources/123anime.js b/src/sources/123anime.js index 6bafdb8..b357805 100644 --- a/src/sources/123anime.js +++ b/src/sources/123anime.js @@ -1,57 +1,121 @@ -import { getTmdbInfo, fetchJson, USER_AGENT } from '../utils/helpers.js'; - -async function resolveSlug(id, s, e, info) { - const titles = info?.titles || []; - const keywords = []; - for (const title of titles) { - const t = title.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim(); - if (!keywords.includes(t)) keywords.push(t); - const words = title.split(/[\s:]+/).filter(w => w.length > 2); - if (words.length >= 2) { - const short = words.slice(0, 3).join(' ').toLowerCase(); - if (!keywords.includes(short)) keywords.push(short); - } - } - const slugCandidates = []; - for (const keyword of keywords) { - try { - const data = await fetchJson(`https://123animehub.cc/ajax/film/search?keyword=${encodeURIComponent(keyword)}&_=${Date.now()}`, { headers: { 'X-Requested-With': 'XMLHttpRequest', 'Referer': 'https://123animehub.cc', 'Accept': 'application/json', 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(8000) }); - let html = data.html || '', idx = 0; - while ((idx = html.indexOf('href="/anime/', idx)) !== -1) { - const start = idx + 13; - const end = html.indexOf('"', start); - const slug = html.slice(start, end); - if (!slugCandidates.includes(slug)) slugCandidates.push(slug); - idx = end; - } - } catch { } - if (slugCandidates.length) break; - } - for (const slug of slugCandidates) { - try { - const data = await fetchJson(`https://123animehub.cc/ajax/episode/info?epr=${encodeURIComponent(s ? `${slug}/${s}/${e}` : `${slug}/1`)}&ts=1&_=${Date.now()}`, { headers: { 'Referer': `https://123animehub.cc/anime/${slug}`, 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/javascript, */*; q=0.01', 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(10000) }); - if (data?.target) return data.target; - } catch { } - } - return null; -} - -export async function getStream({ id, s, e }) { - if (!s) return null; - const info = await getTmdbInfo(id, 'tv', s); - if (!info.isAnime) return null; - const target = await resolveSlug(id, s, e, info); - if (!target) return null; - const embedMatch = target.match(/\/embed-[^/]+\/([A-Za-z0-9+/=]+)$/); - if (!embedMatch) return null; - const origin = new URL(target).origin; - try { - const embedRes = await fetch(target, { headers: { 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(10000) }); - if (!embedRes.ok) return null; - const setCookie = embedRes.headers.get('set-cookie'); - const data = await fetchJson(`${origin}/hs/getSources?id=${embedMatch[1]}`, { headers: { 'Referer': target, 'Accept': '*/*', 'User-Agent': USER_AGENT, ...(setCookie ? { 'Cookie': setCookie.split(';')[0].trim() } : {}) }, signal: AbortSignal.timeout(10000) }); - const src = typeof data.sources === 'string' && data.sources.length ? data.sources : (Array.isArray(data.sources) && data.sources.length ? (data.sources[0].file || data.sources[0].src || data.sources[0].url) : null); - if (src) return { url: src, headers: { 'Referer': 'https://play2.echovideo.ru/', 'Origin': 'https://play2.echovideo.ru' } }; - } catch { } - return null; +import { fetchJson, getTmdbMetadata, resolveTmdbKey, USER_AGENT } from '../utils/helpers.js'; + +const BASE_URL = 'https://123animehub.cc'; +const CDN_ORIGIN = 'https://play2.echovideo.ru'; + +async function resolveSlug(id, s, e, titles) { + const keywords = []; + for (const title of titles) { + const t = title.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim(); + if (!keywords.includes(t)) keywords.push(t); + const words = title.split(/[\s:]+/).filter(w => w.length > 2); + if (words.length >= 2) { + const short = words.slice(0, 3).join(' ').toLowerCase(); + if (!keywords.includes(short)) keywords.push(short); + } + } + + const slugCandidates = []; + for (const keyword of keywords) { + try { + const data = await fetchJson(`${BASE_URL}/ajax/film/search?keyword=${encodeURIComponent(keyword)}&_=${Date.now()}`, { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + Referer: `${BASE_URL}/`, + Accept: 'application/json', + 'User-Agent': USER_AGENT, + }, + signal: AbortSignal.timeout(8000), + }); + let html = data.html || ''; + let idx = 0; + while ((idx = html.indexOf('href="/anime/', idx)) !== -1) { + const start = idx + 13; + const end = html.indexOf('"', start); + const slug = html.slice(start, end); + if (!slugCandidates.includes(slug)) slugCandidates.push(slug); + idx = end; + } + } catch { } + if (slugCandidates.length) break; + } + + for (const slug of slugCandidates) { + try { + const data = await fetchJson(`${BASE_URL}/ajax/episode/info?epr=${encodeURIComponent(s ? `${slug}/${s}/${e}` : `${slug}/1`)}&ts=1&_=${Date.now()}`, { + headers: { + Referer: `${BASE_URL}/anime/${slug}`, + 'X-Requested-With': 'XMLHttpRequest', + Accept: 'application/json, text/javascript, */*; q=0.01', + 'User-Agent': USER_AGENT, + }, + signal: AbortSignal.timeout(10000), + }); + if (data?.target) return data.target; + } catch { } + } + return null; +} + +export async function getStream(args) { + const { id, s, e, sdk } = args || {}; + const isTv = s != null && e != null; + if (!s) return null; + + const apiKey = resolveTmdbKey(args, sdk); + const meta = await getTmdbMetadata(id, isTv, apiKey); + if (!meta?.isAnime) return null; + + const target = await resolveSlug(id, s, e, meta.titles || []); + if (!target) return null; + + const embedMatch = target.match(/\/embed-[^/]+\/([A-Za-z0-9+/=]+)$/); + if (!embedMatch) return null; + + const origin = new URL(target).origin; + try { + const embedRes = await fetch(target, { + headers: { 'User-Agent': USER_AGENT }, + signal: AbortSignal.timeout(10000), + }); + if (!embedRes.ok) return null; + + const setCookie = embedRes.headers.get('set-cookie'); + const data = await fetchJson(`${origin}/hs/getSources?id=${embedMatch[1]}`, { + headers: { + Referer: target, + Accept: '*/*', + 'User-Agent': USER_AGENT, + ...(setCookie ? { Cookie: setCookie.split(';')[0].trim() } : {}), + }, + signal: AbortSignal.timeout(10000), + }); + + const streamUrl = typeof data.sources === 'string' && data.sources.length + ? data.sources + : (Array.isArray(data.sources) && data.sources.length ? (data.sources[0].file || data.sources[0].src || data.sources[0].url) : null); + + if (!streamUrl) return null; + + const headers = { + Referer: `${CDN_ORIGIN}/`, + Origin: CDN_ORIGIN, + }; + + const allUrls = [{ + url: streamUrl, + server: '123Anime', + quality: 'Auto', + type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', + headers, + }]; + + return { + url: streamUrl, + headers, + allUrls, + }; + } catch { + return null; + } } \ No newline at end of file diff --git a/src/sources/3eyedraven.js b/src/sources/3eyedraven.js index 1c6f08a..568757e 100644 --- a/src/sources/3eyedraven.js +++ b/src/sources/3eyedraven.js @@ -1,101 +1,45 @@ -import { getUA } from "../utils/helpers.js"; +import { getTmdbMetadata, getUA, resolveTmdbKey, slugify } from '../utils/helpers.js'; -const BASE_URL = "https://3eyedraven.watch"; -const API_BASE = "https://3eyedraven.watch/api"; - -function getHeaders() { - return { - "User-Agent": getUA(), - Accept: "application/json", - Referer: `${BASE_URL}/`, - Origin: BASE_URL, - }; -} - -function resolveApiKey(args) { - return ( - args?.tmdbApiKey || - args?.sdk?.tmdbApiKey || - process.env.TMDB_API_KEY || - null - ); -} - -async function getTitle(id, isTv, apiKey) { - if (!apiKey) return null; - const type = isTv ? "tv" : "movie"; - try { - const res = await fetch( - `https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, - { signal: AbortSignal.timeout(5000) } - ); - if (!res.ok) return null; - const data = await res.json(); - return isTv ? data.name : data.title; - } catch { - return null; - } -} - -function slugify(text) { - return text - .toLowerCase() - .normalize("NFKD") - .replace(/[\u0300-\u036f]/g, "") - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-|-$/g, ""); -} +const BASE_URL = 'https://3eyedraven.watch'; +const API_BASE = 'https://3eyedraven.watch/api'; function absolute(url) { - if (!url) return ""; + if (!url) return ''; if (/^https?:\/\//.test(url)) return url; - return `${BASE_URL}${url.startsWith("/") ? "" : "/"}${url}`; + return `${BASE_URL}${url.startsWith('/') ? '' : '/'}${url}`; } export async function getStream(args) { - const { id, s = null, e = null } = args; + const { id, s, e, sdk } = args || {}; const isTv = s != null && e != null; + const apiKey = resolveTmdbKey(args, sdk); - const apiKey = resolveApiKey(args); - const title = await getTitle(id, isTv, apiKey); - - if (!title) { - if (process.env.DEBUG_CAUSE) { - console.error( - `[3eyedraven] Could not resolve title for ${isTv ? "tv" : "movie"} ${id} ` + - `(tmdbApiKey ${apiKey ? "present" : "MISSING"})` - ); - } - return null; - } + const meta = await getTmdbMetadata(id, isTv, apiKey); + const title = meta?.title; + if (!title) return null; const identifier = `${slugify(title)}-${id}`; - const url = isTv ? `${API_BASE}/stream/${identifier}?season=${s}&episode=${e}` : `${API_BASE}/stream/${identifier}`; + const headers = { + 'User-Agent': getUA(), + Accept: 'application/json', + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + }; + try { const res = await fetch(url, { - headers: getHeaders(), + headers, signal: AbortSignal.timeout(15000), }); - - if (!res.ok) { - if (process.env.DEBUG_CAUSE) { - console.error(`[3eyedraven] HTTP ${res.status} for ${url}`); - } - return null; - } + if (!res.ok) return null; const data = await res.json(); const stream = data?.stream; - if (!stream?.url) { - if (process.env.DEBUG_CAUSE) { - console.error(`[3eyedraven] No stream.url in response for ${url}`); - } - return null; - } + if (!stream?.url) return null; const streamUrl = absolute(stream.url); if (!/^https?:\/\//.test(streamUrl)) return null; @@ -105,32 +49,35 @@ export async function getStream(args) { const subUrl = absolute(sub.url); if (!subUrl) return null; return { - label: sub.label || sub.language || "Subtitle", + label: sub.label || sub.language || 'Subtitle', file: subUrl, - type: "vtt", - source: "3eyedraven", + type: 'vtt', + source: '3eyedraven', }; }) .filter(Boolean); - return { + const allUrls = [{ url: streamUrl, - headers: { - "User-Agent": getUA(), - Referer: `${BASE_URL}/`, - Origin: BASE_URL, - }, - server: "3eyedraven", - quality: "1080p", - type: stream.format === "hls" ? "hls" : "mp4", + headers, + server: '3eyedraven', + quality: '1080p', + type: stream.format === 'hls' ? 'hls' : 'mp4', subtitles, skipProxy: true, + }]; + + return { + url: streamUrl, + headers, + server: '3eyedraven', + quality: '1080p', + type: stream.format === 'hls' ? 'hls' : 'mp4', + subtitles, + skipProxy: true, + allUrls, }; - } catch (err) { - if (process.env.DEBUG_CAUSE) { - console.error(`[3eyedraven] fetch error for ${url}:`, err.message); - if (err.cause) console.error(" cause:", err.cause.code, err.cause.message); - } + } catch { return null; } } \ No newline at end of file diff --git a/src/sources/cinecat.js b/src/sources/cinecat.js index 29b48b8..1a2f1a9 100644 --- a/src/sources/cinecat.js +++ b/src/sources/cinecat.js @@ -1,19 +1,38 @@ import { fetchJson } from '../utils/helpers.js'; -export async function getStream({ id, s, e }) { +const BASE_URL = 'https://zcdn.bluevelvet.space'; + +export async function getStream(args) { + const { id, s, e } = args || {}; const isTv = s != null && e != null; + const url = isTv - ? `https://zcdn.bluevelvet.space/cdn?tmdb=${id}&type=tv&season=${s}&episode=${e}` - : `https://zcdn.bluevelvet.space/cdn?tmdb=${id}&type=movie`; + ? `${BASE_URL}/cdn?tmdb=${id}&type=tv&season=${s}&episode=${e}` + : `${BASE_URL}/cdn?tmdb=${id}&type=movie`; - const res = await fetchJson(url); - if (!res || !res.ready || !res.url) return null; + try { + const res = await fetchJson(url); + if (!res || !res.ready || !res.url) return null; - return { - url: res.url, - headers: { - Referer: 'https://zcdn.bluevelvet.space/', - Origin: 'https://zcdn.bluevelvet.space', - } - }; + const headers = { + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + }; + + const allUrls = [{ + url: res.url, + server: 'CineCat', + quality: 'Auto', + type: res.url.includes('.m3u8') ? 'hls' : 'mp4', + headers, + }]; + + return { + url: res.url, + headers, + allUrls, + }; + } catch { + return null; + } } \ No newline at end of file diff --git a/src/sources/cinemaarmy.js b/src/sources/cinemaarmy.js index b50f621..b99414b 100644 --- a/src/sources/cinemaarmy.js +++ b/src/sources/cinemaarmy.js @@ -1,19 +1,18 @@ -import crypto from "crypto"; -import { Session, ClientIdentifier, initTLS, destroyTLS } from "node-tls-client"; +import crypto from 'node:crypto'; +import { ClientIdentifier, destroyTLS, initTLS, Session } from 'node-tls-client'; -const BASE_URL = "https://cinema.army"; -const API_BASE = "https://cinema.army/api"; - -const XOR_KEY = "f16a6b92fcd6eed3e9476297f893f2505c32f79bc6efef9b6c95496aa5914548"; -const HMAC_KEY = "a2cfc9eaa2a3690b5d4f6b5f958dbdeae52a097283d375bd253ee7c7062524f5"; - -const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"; +const BASE_URL = 'https://cinema.army'; +const API_BASE = `${BASE_URL}/api`; +const HMAC_KEY = 'a2cfc9eaa2a3690b5d4f6b5f958dbdeae52a097283d375bd253ee7c7062524f5'; +const XOR_KEY = 'f16a6b92fcd6eed3e9476297f893f2505c32f79bc6efef9b6c95496aa5914548'; +const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'; const SERVERS = [ - { key: "cookiebakers", method: "POST", label: "CookieBakers" }, - { key: "tcloud", method: "GET", label: "TCloud" }, - { key: "ipcloud", method: "GET", label: "IPcloud" }, - { key: "dcloud", method: "GET", label: "Dcloud" }, + { key: 'cookiebakers', label: 'CookieBakers', method: 'POST' }, + { key: 'tcloud', label: 'TCloud', method: 'POST' }, + { key: 'ipcloud', label: 'IPCloud', method: 'POST' }, + { key: 'dcloud', label: 'DCloud', method: 'POST' }, + { key: 'tidb', label: 'TIDB', method: 'POST' }, ]; let tlsInitialized = false; @@ -40,22 +39,22 @@ function getHeaders(token, id, isTv, s, e) { : `${BASE_URL}/watch?id=${id}&type=movie`; return { - "User-Agent": UA, - "Accept": "application/json", - "Accept-Language": "en-US,en;q=0.9", - "Accept-Encoding": "gzip, deflate, br", - "Cache-Control": "no-cache", - "Pragma": "no-cache", - "Referer": referer, - "Origin": BASE_URL, - "Sec-Ch-Ua": '"Chromium";v="131", "Not_A Brand";v="24"', - "Sec-Ch-Ua-Mobile": "?0", - "Sec-Ch-Ua-Platform": '"Windows"', - "Sec-Fetch-Dest": "empty", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Site": "same-origin", - "Priority": "u=1, i", - "X-Stream-Token": token, + 'User-Agent': UA, + Accept: 'application/json', + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + 'Cache-Control': 'no-cache', + Pragma: 'no-cache', + Referer: referer, + Origin: BASE_URL, + 'Sec-Ch-Ua': '"Chromium";v="131", "Not_A Brand";v="24"', + 'Sec-Ch-Ua-Mobile': '?0', + 'Sec-Ch-Ua-Platform': '"Windows"', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + Priority: 'u=1, i', + 'X-Stream-Token': token, }; } @@ -63,20 +62,20 @@ function generateToken(id) { try { const minute = Math.floor(Date.now() / 1000 / 60); return crypto - .createHmac("sha256", HMAC_KEY) + .createHmac('sha256', HMAC_KEY) .update(`${id}:${minute}`) - .digest("hex"); + .digest('hex'); } catch { - return ""; + return ''; } } function decryptStreamUrl(encoded) { - if (!encoded) return ""; - if (encoded.startsWith("http://") || encoded.startsWith("https://")) return encoded; + if (!encoded) return ''; + if (encoded.startsWith('http://') || encoded.startsWith('https://')) return encoded; try { - const decoded = Buffer.from(encoded, "base64").toString("binary"); - let out = ""; + const decoded = Buffer.from(encoded, 'base64').toString('binary'); + let out = ''; for (let i = 0; i < decoded.length; i++) { out += String.fromCharCode( decoded.charCodeAt(i) ^ XOR_KEY.charCodeAt(i % XOR_KEY.length) @@ -89,7 +88,7 @@ function decryptStreamUrl(encoded) { } export async function getStream(args) { - const { id, s = null, e = null } = args; + const { id, s, e } = args || {}; const isTv = s != null && e != null; const token = generateToken(id); @@ -107,62 +106,62 @@ export async function getStream(args) { const results = await Promise.allSettled( requests.map(async ({ url, server }) => { try { - const res = server.method === "POST" - ? await sess.post(url, { headers }) + const res = server.method === 'POST' + ? await sess.post(url, { headers, body: '' }) : await sess.get(url, { headers }); - if (res.status !== 200) { - if (process.env.DEBUG_CAUSE) { - console.error(`[CinemaArmy ${server.key}] HTTP ${res.status}`); - } - return null; - } + if (res?.status !== 200) return null; + + const rawBody = typeof res.text === 'function' ? await res.text() : res.body; - const text = await res.text(); let data; try { - data = JSON.parse(text); - } catch { + data = typeof rawBody === 'object' && rawBody !== null ? rawBody : JSON.parse(rawBody); + } catch (err) { return null; } const encrypted = data?.stream?.url; - if (!encrypted) return null; + if (!encrypted) { + return null; + } const streamUrl = decryptStreamUrl(encrypted); - if (!streamUrl || !/^https?:\/\//.test(streamUrl)) return null; + + if (!streamUrl || !/^https?:\/\//.test(streamUrl)) { + return null; + } return { url: streamUrl, headers: { - "User-Agent": UA, + 'User-Agent': UA, Referer: `${BASE_URL}/`, Origin: BASE_URL, }, server: `CinemaArmy (${server.label})`, - quality: "1080p", - type: streamUrl.includes(".m3u8") ? "hls" : "mp4", + quality: '1080p', + type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', }; } catch (err) { - if (process.env.DEBUG_CAUSE) { - console.error(`[CinemaArmy ${server.key}] ${err.message}`); - if (err.cause) { - console.error(` cause.code: ${err.cause.code || "n/a"}`); - console.error(` cause.message: ${err.cause.message || err.cause}`); - } - } return null; } }) ); const allUrls = results - .filter(r => r.status === "fulfilled" && r.value) + .filter(r => r.status === 'fulfilled' && r.value) .map(r => r.value); - if (!allUrls.length) return null; + if (!allUrls.length) { + return null; + } - return { allUrls }; + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; } export async function cleanup() { diff --git a/src/sources/cinesrc.js b/src/sources/cinesrc.js index 8b0ad5a..63c8f86 100644 --- a/src/sources/cinesrc.js +++ b/src/sources/cinesrc.js @@ -1,6 +1,6 @@ import { fetchJson, fetchText } from '../utils/helpers.js'; -const API = "https://enc-dec.app/api"; +const API = 'https://enc-dec.app/api'; async function solveStage1(challenge1, seed) { const K = [ @@ -210,91 +210,93 @@ async function solveStage2(data) { throw new Error('no solution found'); } -export async function getStream({ id, s, e }) { +export async function getStream(args) { + const { id, s, e } = args || {}; const isTv = s != null && e != null; + const mediaType = isTv ? 'tv' : 'movie'; const type = isTv ? 'tv' : 'movie'; const url = isTv ? `https://cinesrc.st/embed/tv/${id}?s=${s}&e=${e}` : `https://cinesrc.st/embed/movie/${id}`; const headers = { - "Origin": "https://cinesrc.st", - "Referer": "https://cinesrc.st/", - "Content-Type": "text/plain;charset=UTF-8", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.37 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.37" + Origin: 'https://cinesrc.st', + Referer: 'https://cinesrc.st/', + 'Content-Type': 'text/plain;charset=UTF-8', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.37 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.37', }; const fields = [type, id, s ? String(s) : null, e ? String(e) : null]; const encoded = btoa(JSON.stringify(fields).replace(/ /g, '')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); - const bootstrapHeaders = { ...headers, "x-cs-q": encoded }; - const bootstrap = await fetchJson("https://cinesrc.st/api/c/bootstrap", { - method: "POST", + const bootstrapHeaders = { ...headers, 'x-cs-q': encoded }; + const bootstrap = await fetchJson('https://cinesrc.st/api/c/bootstrap', { + method: 'POST', headers: bootstrapHeaders, - body: "{}" + body: '{}', }); const cookies = { - "x-cs-b": "20261014-a", - "x-cs-q": encoded, - "x-cs-r": bootstrap.r, - "x-cs-p": bootstrap.p, + 'x-cs-b': '20261014-a', + 'x-cs-q': encoded, + 'x-cs-r': bootstrap.r, + 'x-cs-p': bootstrap.p, }; - const challenge1 = await fetchJson("https://cinesrc.st/api/c/issue", { - headers: { ...headers, ...cookies } + const challenge1 = await fetchJson('https://cinesrc.st/api/c/issue', { + headers: { ...headers, ...cookies }, }); const payload1 = { data: { url, - user_agent: headers["User-Agent"], - challenge_data: challenge1 + user_agent: headers['User-Agent'], + challenge_data: challenge1, }, - type: "seed" + type: 'seed', }; const seedRes = await fetchJson(`${API}/enc-cinesrc`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload1) + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload1), }); const seedData = seedRes.result; const solution1 = await solveStage1(challenge1, seedData.seed); - const challenge2 = await fetchJson("https://cinesrc.st/api/c/stage2/issue", { - headers: { ...headers, ...cookies } + const challenge2 = await fetchJson('https://cinesrc.st/api/c/stage2/issue', { + headers: { ...headers, ...cookies }, }); const solution2 = await solveStage2(challenge2); const payload2 = { data: { url, - user_agent: headers["User-Agent"], + user_agent: headers['User-Agent'], challenge_data: { stage1: { challenge: challenge1, solution: solution1 }, - stage2: { challenge: challenge2, solution: solution2 } + stage2: { challenge: challenge2, solution: solution2 }, }, - state: seedData.state + state: seedData.state, }, - type: "encrypt" + type: 'encrypt', }; const encryptRes = await fetchJson(`${API}/enc-cinesrc`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload2) + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload2), }); const encData = encryptRes.result; - const token = `${encData.token}::c3::${cookies["x-cs-r"]}`; + const token = `${encData.token}::c3::${cookies['x-cs-r']}`; const nextActions = encData.headers; const providersText = await fetchText(url, { - method: "POST", - headers: { ...headers, ...cookies, "Next-Action": nextActions.getProviderList }, - body: JSON.stringify([]) + method: 'POST', + headers: { ...headers, ...cookies, 'Next-Action': nextActions.getProviderList }, + body: JSON.stringify([]), }); const lines = providersText.split('\n'); @@ -320,19 +322,19 @@ export async function getStream({ id, s, e }) { const provider = providerObj.id; const streamPayload = [ id, - isTv ? "show" : type, - isTv ? String(s) : "$undefined", - isTv ? String(e) : "$undefined", + isTv ? 'show' : type, + isTv ? String(s) : '$undefined', + isTv ? String(e) : '$undefined', token, - provider + provider, ]; let streamRes; try { streamRes = await fetchText(url, { - method: "POST", - headers: { ...headers, ...cookies, "Next-Action": nextActions.getStream }, - body: JSON.stringify(streamPayload) + method: 'POST', + headers: { ...headers, ...cookies, 'Next-Action': nextActions.getStream }, + body: JSON.stringify(streamPayload), }); } catch { continue; @@ -343,9 +345,9 @@ export async function getStream({ id, s, e }) { let decryptRes; try { decryptRes = await fetchJson(`${API}/dec-cinesrc`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ text: streamRes, state: encData.state }) + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text: streamRes, state: encData.state }), }); } catch { continue; @@ -378,10 +380,12 @@ export async function getStream({ id, s, e }) { allUrls.push({ url: streamUrl, server: `CineSrc - ${providerObj.name || provider}`, + quality: 'Auto', + type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', headers: resultData.headers || { - Referer: "https://cinesrc.st/", - Origin: "https://cinesrc.st" - } + Referer: 'https://cinesrc.st/', + Origin: 'https://cinesrc.st', + }, }); break; } @@ -393,6 +397,6 @@ export async function getStream({ id, s, e }) { return { url: allUrls[0].url, headers: allUrls[0].headers, - allUrls + allUrls, }; } \ No newline at end of file diff --git a/src/sources/cinezo.js b/src/sources/cinezo.js deleted file mode 100644 index f67ee83..0000000 --- a/src/sources/cinezo.js +++ /dev/null @@ -1,55 +0,0 @@ -import { fetchJson } from '../utils/helpers.js'; - -export async function getStream({ id, s, e }) { - const isTv = s != null && e != null; - const referer = isTv - ? `https://player.cinezo.live/embed/tv/${id}?s=${s}&e=${e}` - : `https://player.cinezo.live/embed/movie/${id}`; - - const headers = { - 'Accept': '*/*', - 'Origin': 'https://player.cinezo.live', - 'Referer': referer, - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36 Edg/155.0.0.0' - }; - - const hosts = [ - 'https://proxy.yenime.net', - 'https://proxy1.flikhub.net' - ]; - - const sources = ['zendaya', 'berlin', 'jennifer', 'scarlet']; - const allUrls = []; - - for (const host of hosts) { - for (const source of sources) { - const url = isTv - ? `${host}/tv?id=${id}&season=${s}&episode=${e}&mode=json&sources=${source}&hevc=1` - : `${host}/movie?id=${id}&mode=json&sources=${source}&hevc=1`; - - try { - const res = await fetchJson(url, { headers }); - const streamUrl = res?.source?.url; - if (streamUrl) { - allUrls.push({ - url: streamUrl, - server: `Cinezo - ${res.source.label || source}`, - headers: { - Referer: 'https://player.cinezo.live/', - Origin: 'https://player.cinezo.live' - } - }); - } - } catch { } - } - if (allUrls.length) break; - } - - if (!allUrls.length) return null; - - return { - url: allUrls[0].url, - headers: allUrls[0].headers, - allUrls - }; -} \ No newline at end of file diff --git a/src/sources/cno.js b/src/sources/cno.js index 22b6cb4..c3d8307 100644 --- a/src/sources/cno.js +++ b/src/sources/cno.js @@ -1,20 +1,23 @@ import { fetchText } from '../utils/helpers.js'; -export async function getStream({ id, s, e }) { +const BASE_URL = 'https://cno.to'; + +export async function getStream(args) { + const { id, s, e } = args || {}; const isTv = s != null && e != null; const serverIndices = Array.from({ length: 50 }, (_, i) => i + 1); const headers = { - 'Accept': 'application/json, text/plain, */*', - 'Referer': 'https://cno.to/', - 'Origin': 'https://cno.to', - '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' + Accept: 'application/json, text/plain, */*', + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + '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', }; const promises = serverIndices.map(async (serverNum) => { const url = isTv - ? `https://cno.to/api/s/${serverNum}/tv/${id}/${s}/${e}` - : `https://cno.to/api/s/${serverNum}/movie/${id}?v=3`; + ? `${BASE_URL}/api/s/${serverNum}/tv/${id}/${s}/${e}` + : `${BASE_URL}/api/s/${serverNum}/movie/${id}?v=3`; try { const raw = await fetchText(url, { headers }); @@ -50,16 +53,18 @@ export async function getStream({ id, s, e }) { if (streamUrl) { if (streamUrl.startsWith('/')) { - streamUrl = `https://cno.to${streamUrl}`; + streamUrl = `${BASE_URL}${streamUrl}`; } return { url: streamUrl, server: `Cno - Server ${serverNum} (${quality})`, + quality, + type: 'hls', headers: { - Referer: 'https://cno.to/', - Origin: 'https://cno.to' - } + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + }, }; } } @@ -71,10 +76,12 @@ export async function getStream({ id, s, e }) { return { url: jsonUrl, server: `Cno - Server ${serverNum} (${json.quality || 'Auto'})`, + quality: json.quality || 'Auto', + type: jsonUrl.includes('.m3u8') ? 'hls' : 'mp4', headers: { - Referer: 'https://cno.to/', - Origin: 'https://cno.to' - } + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + }, }; } } catch { } @@ -90,6 +97,6 @@ export async function getStream({ id, s, e }) { return { url: allUrls[0].url, headers: allUrls[0].headers, - allUrls + allUrls, }; } \ No newline at end of file diff --git a/src/sources/dulo.js b/src/sources/dulo.js index 3da6527..57fa86e 100644 --- a/src/sources/dulo.js +++ b/src/sources/dulo.js @@ -1,28 +1,8 @@ -import { getUA } from '../utils/helpers.js'; +import { getTmdbMetadata, getUA, resolveTmdbKey } from '../utils/helpers.js'; const BASE_URL = 'https://dulo.mov'; -const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - -async function resolveDetails(sdk, id, isTv) { - const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY; - if (!apiKey) return null; - const type = isTv ? 'tv' : 'movie'; - try { - const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, { - signal: AbortSignal.timeout(6000), - }); - if (!res.ok) return null; - const data = await res.json(); - const title = isTv ? data.name : data.title; - const releaseDate = isTv ? data.first_air_date : data.release_date; - const year = releaseDate ? releaseDate.split('-')[0] : ''; - const runtime = isTv ? (data.episode_run_time?.[0] || 24) : (data.runtime || 90); - return { title, year, runtime }; - } catch { - return null; - } -} +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); async function fetchSource(url, headers) { try { @@ -37,15 +17,17 @@ async function fetchSource(url, headers) { } } -export async function getStream({ id, s, e, sdk }) { - const isTv = Boolean(s && e); +export async function getStream(args) { + const { id, s, e, sdk } = args || {}; + const isTv = s != null && e != null; const mediaType = isTv ? 'tv' : 'movie'; + const apiKey = resolveTmdbKey(args, sdk); const ua = getUA(); - const details = await resolveDetails(sdk, id, isTv); - const title = details?.title || ''; - const year = details?.year || ''; - const runtime = details?.runtime || ''; + const meta = await getTmdbMetadata(id, isTv, apiKey); + const title = meta?.title || ''; + const year = meta?.year || ''; + const runtime = meta?.runtime || ''; const referer = isTv ? `${BASE_URL}/watch/tv/${id}/${s}/${e}` @@ -97,7 +79,7 @@ export async function getStream({ id, s, e, sdk }) { while (pendingUrls.length > 0 && Date.now() < maxPollTime) { const results = await Promise.all( - pendingUrls.map((url) => fetchSource(url, defaultHeaders)) + pendingUrls.map(url => fetchSource(url, defaultHeaders)) ); const nextPending = []; @@ -119,7 +101,10 @@ export async function getStream({ id, s, e, sdk }) { allUrls.push({ url: streamUrl, + server: src.label || 'Dulo', label: src.label || 'Dulo', + quality: 'Auto', + type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', headers: { 'User-Agent': ua, Referer: referer, @@ -144,7 +129,7 @@ export async function getStream({ id, s, e, sdk }) { return { url: allUrls[0].url, - allUrls, headers: allUrls[0].headers, + allUrls, }; } \ No newline at end of file diff --git a/src/sources/flaxmovies.js b/src/sources/flaxmovies.js index 29c63db..e8ae2e0 100644 --- a/src/sources/flaxmovies.js +++ b/src/sources/flaxmovies.js @@ -1,106 +1,115 @@ -import { fetchJson } from '../utils/helpers.js'; - -export async function getStream({ id, s, e }) { - const isTv = s != null && e != null; - const allUrls = []; - - const endpoints = [ - async () => { - const url = isTv - ? `https://themoviebox.uk/api/resolve?type=tv&id=${id}&season=${s}&episode=${e}` - : `https://themoviebox.uk/api/resolve?type=movie&id=${id}`; - const res = await fetchJson(url); - if (res?.success && Array.isArray(res.sources)) { - for (const item of res.sources) { - const streamUrl = item.url || item.original_url; - if (streamUrl) { - allUrls.push({ - url: streamUrl, - server: `TheMovieBox - ${item.server || item.provider || 'Server'}`, - headers: { - Referer: 'https://themoviebox.uk/', - Origin: 'https://themoviebox.uk' - } - }); - } - } - } - }, - - async () => { - const url = isTv - ? `https://pro2.nabilekson.workers.dev/tv/${id}/${s}/${e}` - : `https://pro2.nabilekson.workers.dev/movie/${id}`; - const res = await fetchJson(url); - if (res?.success && Array.isArray(res.primarySources)) { - for (const item of res.primarySources) { - const streamUrl = item.url || item.originalUrl; - if (streamUrl) { - allUrls.push({ - url: streamUrl, - server: `CineBros - ${item.name || item.quality || 'Server'}`, - headers: item.headers || { - Referer: 'https://pro2.nabilekson.workers.dev/', - Origin: 'https://pro2.nabilekson.workers.dev' - } - }); - } - } - } - }, - - async () => { - const url = isTv - ? `https://praxi.nabilekson.workers.dev/tv/${id}/${s}/${e}` - : `https://praxi.nabilekson.workers.dev/movie/${id}`; - const res = await fetchJson(url); - if (res?.success && Array.isArray(res.primarySources)) { - for (const item of res.primarySources) { - const streamUrl = item.url || item.originalUrl; - if (streamUrl) { - allUrls.push({ - url: streamUrl, - server: `Praxi - ${item.name || item.providerName || 'Server'}`, - headers: item.headers || { - Referer: 'https://praxi.nabilekson.workers.dev/', - Origin: 'https://praxi.nabilekson.workers.dev' - } - }); - } - } - } - }, - - async () => { - const url = isTv - ? `https://freakyniki.elaxo.lol/?tmdb_id=${id}&season=${s}&episode=${e}` - : `https://freakyniki.elaxo.lol/?tmdb_id=${id}`; - const res = await fetchJson(url); - if (Array.isArray(res?.streams)) { - for (const item of res.streams) { - const streamUrl = item.url || item.raw_url; - if (streamUrl) { - allUrls.push({ - url: streamUrl, - server: `FreakyNiki - ${item.server || item.provider || 'Server'}`, - headers: { - Referer: 'https://freakyniki.elaxo.lol/', - Origin: 'https://freakyniki.elaxo.lol' - } - }); - } - } - } - } - ]; - - await Promise.allSettled(endpoints.map(fn => fn().catch(() => { }))); - - if (!allUrls.length) return null; - - return { - url: allUrls[0].url, - headers: allUrls[0].headers, - allUrls - }; +import { fetchJson } from '../utils/helpers.js'; + +export async function getStream(args) { + const { id, s, e } = args || {}; + const isTv = s != null && e != null; + const allUrls = []; + + const endpoints = [ + async () => { + const url = isTv + ? `https://themoviebox.uk/api/resolve?type=tv&id=${id}&season=${s}&episode=${e}` + : `https://themoviebox.uk/api/resolve?type=movie&id=${id}`; + const res = await fetchJson(url); + if (res?.success && Array.isArray(res.sources)) { + for (const item of res.sources) { + const streamUrl = item.url || item.original_url; + if (streamUrl) { + allUrls.push({ + url: streamUrl, + server: `TheMovieBox - ${item.server || item.provider || 'Server'}`, + quality: 'Auto', + type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', + headers: { + Referer: 'https://themoviebox.uk/', + Origin: 'https://themoviebox.uk', + }, + }); + } + } + } + }, + + async () => { + const url = isTv + ? `https://pro2.nabilekson.workers.dev/tv/${id}/${s}/${e}` + : `https://pro2.nabilekson.workers.dev/movie/${id}`; + const res = await fetchJson(url); + if (res?.success && Array.isArray(res.primarySources)) { + for (const item of res.primarySources) { + const streamUrl = item.url || item.originalUrl; + if (streamUrl) { + allUrls.push({ + url: streamUrl, + server: `CineBros - ${item.name || item.quality || 'Server'}`, + quality: item.quality || 'Auto', + type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', + headers: item.headers || { + Referer: 'https://pro2.nabilekson.workers.dev/', + Origin: 'https://pro2.nabilekson.workers.dev', + }, + }); + } + } + } + }, + + async () => { + const url = isTv + ? `https://praxi.nabilekson.workers.dev/tv/${id}/${s}/${e}` + : `https://praxi.nabilekson.workers.dev/movie/${id}`; + const res = await fetchJson(url); + if (res?.success && Array.isArray(res.primarySources)) { + for (const item of res.primarySources) { + const streamUrl = item.url || item.originalUrl; + if (streamUrl) { + allUrls.push({ + url: streamUrl, + server: `Praxi - ${item.name || item.providerName || 'Server'}`, + quality: 'Auto', + type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', + headers: item.headers || { + Referer: 'https://praxi.nabilekson.workers.dev/', + Origin: 'https://praxi.nabilekson.workers.dev', + }, + }); + } + } + } + }, + + async () => { + const url = isTv + ? `https://freakyniki.elaxo.lol/?tmdb_id=${id}&season=${s}&episode=${e}` + : `https://freakyniki.elaxo.lol/?tmdb_id=${id}`; + const res = await fetchJson(url); + if (Array.isArray(res?.streams)) { + for (const item of res.streams) { + const streamUrl = item.url || item.raw_url; + if (streamUrl) { + allUrls.push({ + url: streamUrl, + server: `FreakyNiki - ${item.server || item.provider || 'Server'}`, + quality: 'Auto', + type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', + headers: { + Referer: 'https://freakyniki.elaxo.lol/', + Origin: 'https://freakyniki.elaxo.lol', + }, + }); + } + } + } + }, + ]; + + await Promise.allSettled(endpoints.map(fn => fn().catch(() => { }))); + + if (!allUrls.length) return null; + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; } \ No newline at end of file diff --git a/src/sources/frost.js b/src/sources/frost.js index 4c8cd96..42bf20b 100644 --- a/src/sources/frost.js +++ b/src/sources/frost.js @@ -1,30 +1,39 @@ -import { fetchJson, resolveImdbId } from '../utils/helpers.js'; +import { fetchJson, resolveImdbId, resolveTmdbKey } from '../utils/helpers.js'; -export async function getStream({ id, s, e, tmdbApiKey, sdk }) { +const BASE_URL = 'https://froststream.cloutteam.com'; + +export async function getStream(args) { + const { id, s, e, sdk } = args || {}; const isTv = s != null && e != null; const type = isTv ? 'series' : 'movie'; - const imdb = await resolveImdbId(id, isTv, tmdbApiKey || sdk?.tmdbApiKey); + const apiKey = resolveTmdbKey(args, sdk); + + const imdb = await resolveImdbId(id, isTv, apiKey); if (!imdb) return null; const url = isTv - ? `https://froststream.cloutteam.com/stream/series/${imdb}:${s}:${e}.json` - : `https://froststream.cloutteam.com/stream/movie/${imdb}.json`; + ? `${BASE_URL}/stream/series/${imdb}:${s}:${e}.json` + : `${BASE_URL}/stream/movie/${imdb}.json`; const res = await fetchJson(url).catch(() => null); if (!res || !Array.isArray(res.streams) || !res.streams.length) return null; - const allUrls = res.streams.filter(st => st.url).map(st => ({ - url: st.url, - server: `Frost - ${st.name || 'Server'} ${st.title ? `- ${st.title}` : ''}`, - headers: st.behaviorHints?.proxyHeaders?.request || undefined, - skipProxy: st.behaviorHints?.notWebReady === false - })); + const allUrls = res.streams + .filter(st => st.url) + .map(st => ({ + url: st.url, + server: `Frost - ${st.name || 'Server'}${st.title ? ` - ${st.title}` : ''}`, + quality: 'Auto', + type: st.url.includes('.m3u8') ? 'hls' : 'mp4', + headers: st.behaviorHints?.proxyHeaders?.request || undefined, + skipProxy: st.behaviorHints?.notWebReady === false, + })); if (!allUrls.length) return null; return { url: allUrls[0].url, headers: allUrls[0].headers, - allUrls + allUrls, }; } \ No newline at end of file diff --git a/src/sources/fsonline.js b/src/sources/fsonline.js index 8a72506..3cbb672 100644 --- a/src/sources/fsonline.js +++ b/src/sources/fsonline.js @@ -1,48 +1,96 @@ -import { fetchText, USER_AGENT, getTmdbInfo } from '../utils/helpers.js'; - -const ORIGIN = 'https://www3.fsonline.app'; -const AJAX_URL = `${ORIGIN}/wp-admin/admin-ajax.php`; -const HEADERS = { 'User-Agent': USER_AGENT, 'Origin': ORIGIN, 'Referer': `${ORIGIN}/` }; - -export async function getStream({ id, s, e, sdk }) { - const tmdbApiKey = sdk?.tmdbApiKey || null; - if (!tmdbApiKey) return null; - try { - const isTv = s != null && e != null; - const info = await getTmdbInfo(tmdbApiKey, id, isTv ? 'tv' : 'movie'); - if (!info?.titles?.length) return null; - const searchHtml = await fetchText(`${ORIGIN}/?s=${encodeURIComponent(info.titles[0] + (info.year ? ' ' + info.year : ''))}`, { headers: HEADERS }); - const typeFolder = isTv ? 'seriale' : 'film'; - const linkMatch = searchHtml.match(new RegExp(`href=["'](https?://www3\\.fsonline\\.app/${typeFolder}/([^"'/]+)/)["']`, 'i')); - if (!linkMatch) return null; - const targetPageUrl = isTv ? `${ORIGIN}/episoade/${linkMatch[2].replace(/-\d{4}$/, '')}-sezonul-${s}-episodul-${e}/` : linkMatch[1]; - const pageHtml = await fetchText(targetPageUrl, { headers: HEADERS }); - const movieId = (pageHtml.match(/movie-id=['"]([^'"]+)['"]/) || pageHtml.match(/movie-id=([^ >]+)/))?.[1]; - if (!movieId) return null; - const ajaxHtml = await fetchText(AJAX_URL, { method: 'POST', headers: { ...HEADERS, 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', 'Referer': targetPageUrl }, body: `action=lazy_player&movieID=${movieId}` }); - const results = []; - let idx = 0; - while ((idx = ajaxHtml.indexOf('data-vs="', idx)) !== -1) { - const embedStart = idx + 9; - const embedEnd = ajaxHtml.indexOf('"', embedStart); - const embedUrl = ajaxHtml.slice(embedStart, embedEnd); - const spanStart = ajaxHtml.indexOf('', embedEnd); - const spanEnd = ajaxHtml.indexOf('', spanStart); - const serverLabel = ajaxHtml.slice(spanStart + 6, spanEnd).trim().toLowerCase(); - if (serverLabel.includes('filesun')) { - try { - const rHtml = await fetchText(embedUrl, { headers: { 'Referer': ORIGIN, 'User-Agent': USER_AGENT } }); - const m3u8Match = rHtml.match(/file:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/) || rHtml.match(/["']?file["']?:\s*["'](https?:\/\/[^"']+)["']/); - if (m3u8Match) results.push({ url: m3u8Match[1].replace(/\\\//g, '/'), server: 'FSOnline - FileSuN', headers: { 'Referer': embedUrl, 'Origin': 'https://player.fsonline.app', 'User-Agent': USER_AGENT } }); - } catch { } - } - idx = spanEnd; - } - return results.length ? { allUrls: results } : null; - } catch { return null; } -} - -export async function getSources(args) { - const res = await getStream(args); - return res?.allUrls ? [...new Set(res.allUrls.map(u => u.server))] : []; +import { fetchText, getTmdbMetadata, resolveTmdbKey, USER_AGENT } from '../utils/helpers.js'; + +const ORIGIN = 'https://www3.fsonline.app'; +const AJAX_URL = `${ORIGIN}/wp-admin/admin-ajax.php`; +const HEADERS = { + 'User-Agent': USER_AGENT, + Origin: ORIGIN, + Referer: `${ORIGIN}/`, +}; + +export async function getStream(args) { + const { id, s, e, sdk } = args || {}; + const isTv = s != null && e != null; + const apiKey = resolveTmdbKey(args, sdk); + if (!apiKey) return null; + + try { + const meta = await getTmdbMetadata(id, isTv, apiKey); + if (!meta?.titles?.length) return null; + + const query = meta.titles[0] + (meta.year ? ` ${meta.year}` : ''); + const searchHtml = await fetchText(`${ORIGIN}/?s=${encodeURIComponent(query)}`, { headers: HEADERS }); + const typeFolder = isTv ? 'seriale' : 'film'; + const linkMatch = searchHtml.match(new RegExp(`href=["'](https?://www3\\.fsonline\\.app/${typeFolder}/([^"'/]+)/)["']`, 'i')); + if (!linkMatch) return null; + + const targetPageUrl = isTv + ? `${ORIGIN}/episoade/${linkMatch[2].replace(/-\d{4}$/, '')}-sezonul-${s}-episodul-${e}/` + : linkMatch[1]; + + const pageHtml = await fetchText(targetPageUrl, { headers: HEADERS }); + const movieId = (pageHtml.match(/movie-id=['"]([^'"]+)['"]/) || pageHtml.match(/movie-id=([^ >]+)/))?.[1]; + if (!movieId) return null; + + const ajaxHtml = await fetchText(AJAX_URL, { + method: 'POST', + headers: { + ...HEADERS, + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-Requested-With': 'XMLHttpRequest', + Referer: targetPageUrl, + }, + body: `action=lazy_player&movieID=${movieId}`, + }); + + const allUrls = []; + let idx = 0; + while ((idx = ajaxHtml.indexOf('data-vs="', idx)) !== -1) { + const embedStart = idx + 9; + const embedEnd = ajaxHtml.indexOf('"', embedStart); + const embedUrl = ajaxHtml.slice(embedStart, embedEnd); + const spanStart = ajaxHtml.indexOf('', embedEnd); + const spanEnd = ajaxHtml.indexOf('', spanStart); + const serverLabel = ajaxHtml.slice(spanStart + 6, spanEnd).trim().toLowerCase(); + + if (serverLabel.includes('filesun')) { + try { + const rHtml = await fetchText(embedUrl, { + headers: { Referer: ORIGIN, 'User-Agent': USER_AGENT }, + }); + const m3u8Match = rHtml.match(/file:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/) || rHtml.match(/["']?file["']?:\s*["'](https?:\/\/[^"']+)["']/); + if (m3u8Match) { + const streamUrl = m3u8Match[1].replace(/\\\//g, '/'); + allUrls.push({ + url: streamUrl, + server: 'FSOnline - FileSuN', + quality: 'Auto', + type: 'hls', + headers: { + Referer: embedUrl, + Origin: 'https://player.fsonline.app', + 'User-Agent': USER_AGENT, + }, + }); + } + } catch { } + } + idx = spanEnd; + } + + if (!allUrls.length) return null; + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; + } catch { + return null; + } +} + +export async function getSources(args) { + const stream = await getStream(args); + return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : []; } \ No newline at end of file diff --git a/src/sources/hdhub.js b/src/sources/hdhub.js index ba5a6e8..1e2e749 100644 --- a/src/sources/hdhub.js +++ b/src/sources/hdhub.js @@ -1,13 +1,18 @@ -import { fetchJson, resolveImdbId } from '../utils/helpers.js'; +import { fetchJson, resolveImdbId, resolveTmdbKey } from '../utils/helpers.js'; -export async function getStream({ id, s, e, tmdbApiKey, sdk }) { +const BASE_URL = 'https://hdhub.thevolecitor.qzz.io'; + +export async function getStream(args) { + const { id, s, e, sdk } = args || {}; const isTv = s != null && e != null; const type = isTv ? 'series' : 'movie'; - const imdb = await resolveImdbId(id, isTv, tmdbApiKey || sdk?.tmdbApiKey); + const apiKey = resolveTmdbKey(args, sdk); + + const imdb = await resolveImdbId(id, isTv, apiKey); if (!imdb) return null; - const CONFIG = btoa(JSON.stringify({ "torbox": "unset", "qualities": "2160p,1080p,720p", "sort": "desc", "catalogs": "featured,popular" })); - const url = `https://hdhub.thevolecitor.qzz.io/${CONFIG}/stream/${type}/${imdb}${isTv ? `:${s}:${e}` : ""}.json`; + const config = btoa(JSON.stringify({ torbox: 'unset', qualities: '2160p,1080p,720p', sort: 'desc', catalogs: 'featured,popular' })); + const url = `${BASE_URL}/${config}/stream/${type}/${imdb}${isTv ? `:${s}:${e}` : ''}.json`; const res = await fetchJson(url).catch(() => null); if (!res || !Array.isArray(res.streams) || !res.streams.length) return null; @@ -24,13 +29,15 @@ export async function getStream({ id, s, e, tmdbApiKey, sdk }) { const allUrls = validStreams.map(st => ({ url: st.url, server: `HDHub - ${st.name || 'Server'}`, + quality: 'Auto', + type: st.url.includes('.m3u8') ? 'hls' : 'mp4', headers: st.behaviorHints?.proxyHeaders?.request || undefined, - skipProxy: st.behaviorHints?.notWebReady === false + skipProxy: st.behaviorHints?.notWebReady === false, })); return { url: allUrls[0].url, headers: allUrls[0].headers, - allUrls + allUrls, }; -} +} \ No newline at end of file diff --git a/src/sources/hexa.js b/src/sources/hexa.js index fbbc7b1..8da193d 100644 --- a/src/sources/hexa.js +++ b/src/sources/hexa.js @@ -1,54 +1,61 @@ -import { fetchJson, fetchText } from '../utils/helpers.js'; +import { fetchJson, fetchText, getRandomHex } from '../utils/helpers.js'; -const API = "https://enc-dec.app/api"; +const API_BASE = 'https://enc-dec.app/api'; +const BASE_URL = 'https://hexa.su'; +const TMDB_PROXY_BASE = 'https://theemoviedb.hexa.su'; -function getRandomHex(size = 32) { - const array = new Uint8Array(size); - crypto.getRandomValues(array); - return Array.from(array).map(b => b.toString(16).padStart(2, '0')).join(''); -} - -export async function getStream({ id, s, e }) { +export async function getStream(args) { + const { id, s, e } = args || {}; const isTv = s != null && e != null; const key = getRandomHex(32); 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", - "Referer": "https://hexa.su/", - "Accept": "text/plain", - "X-Fingerprint-Lite": "e9136c41504646444", - "X-Api-Key": key + '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', + Referer: `${BASE_URL}/`, + Accept: 'text/plain', + 'X-Fingerprint-Lite': 'e9136c41504646444', + 'X-Api-Key': key, }; - const encHexaRes = await fetchJson(`${API}/enc-hexa`); + const encHexaRes = await fetchJson(`${API_BASE}/enc-hexa`); if (!encHexaRes || encHexaRes.status !== 200) return null; - headers["X-Cap-Token"] = encHexaRes.result.token; + headers['X-Cap-Token'] = encHexaRes.result.token; const url = isTv - ? `https://theemoviedb.hexa.su/api/tmdb/tv/${id}/season/${s}/episode/${e}/images` - : `https://theemoviedb.hexa.su/api/tmdb/movie/${id}/images`; + ? `${TMDB_PROXY_BASE}/api/tmdb/tv/${id}/season/${s}/episode/${e}/images` + : `${TMDB_PROXY_BASE}/api/tmdb/movie/${id}/images`; const encrypted = await fetchText(url, { headers }); if (!encrypted) return null; - const decRes = await fetchJson(`${API}/dec-hexa`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ text: encrypted, key }) + const decRes = await fetchJson(`${API_BASE}/dec-hexa`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text: encrypted, key }), }); if (!decRes || decRes.status !== 200 || !decRes.result) return null; const data = decRes.result; const streamUrl = data.url || data.file || (Array.isArray(data.sources) ? data.sources[0]?.url : null); - if (!streamUrl) return null; + const streamHeaders = { + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + }; + + const allUrls = [{ + url: streamUrl, + server: 'Hexa', + quality: 'Auto', + type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', + headers: streamHeaders, + }]; + return { url: streamUrl, - headers: { - Referer: "https://hexa.su/", - Origin: "https://hexa.su" - } + headers: streamHeaders, + allUrls, }; } \ No newline at end of file diff --git a/src/sources/hyper.js b/src/sources/hyper.js index f62033e..494eac9 100644 --- a/src/sources/hyper.js +++ b/src/sources/hyper.js @@ -1,13 +1,18 @@ import { fetchJson } from '../utils/helpers.js'; -export async function getStream({ id, s, e }) { +const BASE_URL = 'https://hyper.stravo.site'; +const DEFAULT_ORIGIN = 'https://mzfi.me'; + +export async function getStream(args) { + const { id, s, e } = args || {}; const isTv = s != null && e != null; + const path = isTv ? `/local/stream/series/tmdb:${id}:${s}:${e}.json` : `/local/stream/movie/tmdb:${id}.json`; - const url = `https://hyper.stravo.site${path}`; - const res = await fetchJson(url); + const url = `${BASE_URL}${path}`; + const res = await fetchJson(url).catch(() => null); if (!res || !Array.isArray(res.streams)) return null; const validStreams = res.streams.filter(item => { @@ -21,16 +26,18 @@ export async function getStream({ id, s, e }) { const allUrls = validStreams.map(item => ({ url: item.url, server: `Hyper - ${item.title?.replace(/[^\x20-\x7E]+/g, '').trim() || 'Server'}`, + quality: 'Auto', + type: item.url.includes('.m3u8') ? 'hls' : 'mp4', headers: item.behaviorHints?.proxyHeaders?.request || { - Referer: 'https://mzfi.me/', - Accept: '*/*' + Referer: `${DEFAULT_ORIGIN}/`, + Accept: '*/*', }, - skipProxy: item.behaviorHints?.notWebReady === false + skipProxy: item.behaviorHints?.notWebReady === false, })); return { url: allUrls[0].url, headers: allUrls[0].headers, - allUrls + allUrls, }; } \ No newline at end of file diff --git a/src/sources/kickassanime.js b/src/sources/kickassanime.js index 0ae94a6..7fdcad2 100644 --- a/src/sources/kickassanime.js +++ b/src/sources/kickassanime.js @@ -1,24 +1,24 @@ -import { fetchJson, USER_AGENT } from '../utils/helpers.js'; +import { fetchJson, getTmdbMetadata, resolveTmdbKey, USER_AGENT } from '../utils/helpers.js'; -const BASE = "https://kaa.lt"; -const HLS_BASE = "https://hls.krussdomi.com/manifest"; -const HEADERS = { "User-Agent": USER_AGENT, Accept: "application/json" }; +const BASE_URL = 'https://kaa.lt'; +const HLS_BASE = 'https://hls.krussdomi.com/manifest'; +const HEADERS = { 'User-Agent': USER_AGENT, Accept: 'application/json' }; async function kaaSearch(query) { - const res = await fetchJson(`${BASE}/api/fsearch`, { - method: "POST", - headers: { ...HEADERS, "Content-Type": "application/json" }, + const res = await fetchJson(`${BASE_URL}/api/fsearch`, { + method: 'POST', + headers: { ...HEADERS, 'Content-Type': 'application/json' }, body: JSON.stringify({ page: 1, query }), }).catch(() => null); return Array.isArray(res?.result) ? res.result : []; } async function kaaShowInfo(showSlug) { - return fetchJson(`${BASE}/api/show/${showSlug}`, { headers: HEADERS }).catch(() => null); + return fetchJson(`${BASE_URL}/api/show/${showSlug}`, { headers: HEADERS }).catch(() => null); } async function kaaEpisodePage(showSlug, ep) { - return fetchJson(`${BASE}/api/show/${showSlug}/episodes?ep=${ep}&lang=ja-JP`, { headers: HEADERS }).catch(() => null); + return fetchJson(`${BASE_URL}/api/show/${showSlug}/episodes?ep=${ep}&lang=ja-JP`, { headers: HEADERS }).catch(() => null); } async function kaaAllEpisodes(showSlug) { @@ -29,7 +29,7 @@ async function kaaAllEpisodes(showSlug) { if (pages.length > 1) { const rest = await Promise.all( - pages.slice(1).map(async (pg) => { + pages.slice(1).map(async pg => { const startEp = pg.eps?.[0]; if (!startEp) return []; const d = await kaaEpisodePage(showSlug, startEp); @@ -43,35 +43,18 @@ async function kaaAllEpisodes(showSlug) { } async function kaaEpisodeServers(showSlug, fullEpSlug) { - return fetchJson(`${BASE}/api/show/${showSlug}/episode/${fullEpSlug}`, { headers: HEADERS }).catch(() => null); + return fetchJson(`${BASE_URL}/api/show/${showSlug}/episode/${fullEpSlug}`, { headers: HEADERS }).catch(() => null); } -export async function getStream({ id, s, e, tmdbApiKey, sdk }) { +export async function getStream(args) { + const { id, s, e, sdk } = args || {}; + const isTv = s != null && e != null; + const apiKey = resolveTmdbKey(args, sdk); + const epNum = e ? Number(e) : 1; + try { - const apiKey = tmdbApiKey || sdk?.tmdbApiKey || process.env.TMDB_API_KEY || '45dbdd5e714da212424bda0336167447'; - const isTv = !!s; - const mediaType = isTv ? 'tv' : 'movie'; - const epNum = e ? Number(e) : 1; - - let titles = []; - try { - const tmdbRes = await fetchJson(`https://api.themoviedb.org/3/${mediaType}/${id}?api_key=${apiKey}&append_to_response=alternative_titles`); - if (tmdbRes) { - const primary = tmdbRes.title || tmdbRes.name; - const original = tmdbRes.original_title || tmdbRes.original_name; - if (primary) titles.push(primary); - if (original && !titles.includes(original)) titles.push(original); - - const alts = tmdbRes.alternative_titles?.titles || tmdbRes.alternative_titles?.results || []; - for (const alt of alts) { - const candidate = alt.title || alt.name; - if (candidate && !titles.includes(candidate)) { - titles.push(candidate); - } - } - } - } catch { } - + const meta = await getTmdbMetadata(id, isTv, apiKey); + const titles = meta?.titles || []; if (!titles.length) return null; let showSlug = null; @@ -88,12 +71,12 @@ export async function getStream({ id, s, e, tmdbApiKey, sdk }) { const showInfo = await kaaShowInfo(showSlug); let fullEpSlug = null; - if (showInfo?.type === "movie") { - const m = (showInfo.watch_uri || "").match(/\/(ep-(\d+)-([a-f0-9]+))$/i); + if (showInfo?.type === 'movie') { + const m = (showInfo.watch_uri || '').match(/\/(ep-(\d+)-([a-f0-9]+))$/i); if (m) fullEpSlug = m[1]; } else { const episodes = await kaaAllEpisodes(showSlug); - const epItem = episodes.find((item) => item.episode_number === epNum); + const epItem = episodes.find(item => item.episode_number === epNum); if (epItem) fullEpSlug = `ep-${epItem.episode_number}-${epItem.slug}`; } @@ -102,24 +85,34 @@ export async function getStream({ id, s, e, tmdbApiKey, sdk }) { const episodeData = await kaaEpisodeServers(showSlug, fullEpSlug); const servers = Array.isArray(episodeData?.servers) ? episodeData.servers : []; - const urls = []; + const allUrls = []; for (const srv of servers) { if (!srv.src) continue; const m = srv.src.match(/[?&]id=([^&]+)/); if (m) { - urls.push({ + allUrls.push({ url: `${HLS_BASE}/${m[1]}/master.m3u8`, server: `KickAssAnime (${srv.name || 'Server'})`, + quality: 'Auto', + type: 'hls', + headers: HEADERS, }); } } - return urls.length ? { allUrls: urls } : null; - } catch (err) { + if (!allUrls.length) return null; + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; + } catch { return null; } } -export async function getSources() { - return ['KickAssAnime']; +export async function getSources(args) { + const stream = await getStream(args); + return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : ['KickAssAnime']; } \ No newline at end of file diff --git a/src/sources/kisskh.js b/src/sources/kisskh.js index 0e23ece..2f6947c 100644 --- a/src/sources/kisskh.js +++ b/src/sources/kisskh.js @@ -1,60 +1,115 @@ -import { fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js'; - -export const SKIP_VERIFY = true; -export const MULTI_URL = false; - -const ENC_API = "https://enc-dec.app/api"; -const BASE = "https://kisskh.do"; -const HEADERS = { 'User-Agent': USER_AGENT, Accept: 'application/json' }; - -function cleanTitle(t) { return t ? t.toLowerCase().replace(/\(\d{4}\)/g, '').replace(/[^a-z0-9]/g, '') : ''; } - -async function encKey(episodeId, type) { - try { - const data = await fetchJson(`${ENC_API}/enc-kisskh?text=${episodeId}&type=${type}`, { signal: AbortSignal.timeout(6000) }); - return data?.status === 200 ? data.result : null; - } catch { return null; } -} - -export async function getStream({ id, s, e }) { - try { - const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s); - if (!info) return null; - let drama = null, bestScore = -1; - for (const title of (info.titles.length > 1 ? [...info.titles].reverse() : info.titles)) { - const results = await fetchJson(`${BASE}/api/DramaList/Search?q=${encodeURIComponent(title)}`, { headers: HEADERS, signal: AbortSignal.timeout(6000) }).catch(() => []); - const targetClean = cleanTitle(title); - for (const item of results) { - const parts = (item.title || '').split(/\s*-\s*/).map(cleanTitle); - let score = 0; - if (parts.some(p => p === targetClean)) score += 5; - else if (parts.some(p => p.includes(targetClean) || targetClean.includes(p))) score += 3; - else continue; - if (score > bestScore) { bestScore = score; drama = item; } - } - if (drama && bestScore >= 5) break; - } - if (!drama) return null; - const detail = await fetchJson(`${BASE}/api/DramaList/Drama/${drama.id}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) }); - const episodeId = detail?.episodes?.find(ep => Math.floor(ep.number) === Number(e || 1))?.id; - if (!episodeId) return null; - const vidKey = await encKey(episodeId, 'vid'); - if (!vidKey) return null; - const videoData = await fetchJson(`${BASE}/api/DramaList/Episode/${episodeId}.png?err=false&ts=&time=&kkey=${vidKey}`, { headers: HEADERS, signal: AbortSignal.timeout(10000) }); - if (!videoData?.Video) return null; - let subtitles = []; - const subKey = await encKey(episodeId, 'sub'); - if (subKey) { - try { - const subList = await fetchJson(`${BASE}/api/Sub/${episodeId}?kkey=${subKey}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) }); - if (Array.isArray(subList)) subtitles = subList.map(sub => ({ url: sub.src, label: sub.label, lang: sub.land, default: !!sub.default })); - } catch { } - } - return { allUrls: [{ url: videoData.Video, type: videoData.Video.includes('.m3u8') ? 'hls' : 'mp4', audio: 'sub', server: 'KissKH', skipProxy: false, subtitles }] }; - } catch { return null; } -} - -export async function getSources(args) { - const stream = await getStream(args); - return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : []; +import { cleanTitle, fetchJson, getTmdbMetadata, resolveTmdbKey, USER_AGENT } from '../utils/helpers.js'; + +const ENC_API = 'https://enc-dec.app/api'; +const BASE_URL = 'https://kisskh.do'; + +const HEADERS = { 'User-Agent': USER_AGENT, Accept: 'application/json' }; + +async function encKey(episodeId, type) { + try { + const data = await fetchJson(`${ENC_API}/enc-kisskh?text=${episodeId}&type=${type}`, { signal: AbortSignal.timeout(6000) }); + return data?.status === 200 ? data.result : null; + } catch { + return null; + } +} + +export async function getStream(args) { + const { id, s, e, sdk } = args || {}; + const isTv = s != null && e != null; + const apiKey = resolveTmdbKey(args, sdk); + + try { + const meta = await getTmdbMetadata(id, isTv, apiKey); + if (!meta?.titles?.length) return null; + + let drama = null; + let bestScore = -1; + const searchTitles = meta.titles.length > 1 ? [...meta.titles].reverse() : meta.titles; + + for (const title of searchTitles) { + const results = await fetchJson(`${BASE_URL}/api/DramaList/Search?q=${encodeURIComponent(title)}`, { + headers: HEADERS, + signal: AbortSignal.timeout(6000), + }).catch(() => []); + + const targetClean = cleanTitle(title); + for (const item of results) { + const parts = (item.title || '').split(/\s*-\s*/).map(cleanTitle); + let score = 0; + if (parts.some(p => p === targetClean)) score += 5; + else if (parts.some(p => p.includes(targetClean) || targetClean.includes(p))) score += 3; + else continue; + if (score > bestScore) { + bestScore = score; + drama = item; + } + } + if (drama && bestScore >= 5) break; + } + + if (!drama) return null; + + const detail = await fetchJson(`${BASE_URL}/api/DramaList/Drama/${drama.id}`, { + headers: HEADERS, + signal: AbortSignal.timeout(8000), + }); + + const episodeId = detail?.episodes?.find(ep => Math.floor(ep.number) === Number(e || 1))?.id; + if (!episodeId) return null; + + const vidKey = await encKey(episodeId, 'vid'); + if (!vidKey) return null; + + const videoData = await fetchJson(`${BASE_URL}/api/DramaList/Episode/${episodeId}.png?err=false&ts=&time=&kkey=${vidKey}`, { + headers: HEADERS, + signal: AbortSignal.timeout(10000), + }); + if (!videoData?.Video) return null; + + let subtitles = []; + const subKey = await encKey(episodeId, 'sub'); + if (subKey) { + try { + const subList = await fetchJson(`${BASE_URL}/api/Sub/${episodeId}?kkey=${subKey}`, { + headers: HEADERS, + signal: AbortSignal.timeout(8000), + }); + if (Array.isArray(subList)) { + subtitles = subList.map(sub => ({ + url: sub.src, + label: sub.label, + lang: sub.land, + default: Boolean(sub.default), + })); + } + } catch { } + } + + const streamUrl = videoData.Video; + const allUrls = [{ + url: streamUrl, + type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', + audio: 'sub', + server: 'KissKH', + quality: 'Auto', + headers: HEADERS, + skipProxy: false, + subtitles, + }]; + + return { + url: streamUrl, + headers: HEADERS, + allUrls, + subtitles, + }; + } catch { + return null; + } +} + +export async function getSources(args) { + const stream = await getStream(args); + return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : ['KissKH']; } \ No newline at end of file diff --git a/src/sources/lmscript.js b/src/sources/lmscript.js index e669625..6ff68f5 100644 --- a/src/sources/lmscript.js +++ b/src/sources/lmscript.js @@ -1,25 +1,25 @@ -import { getTmdbInfo } from '../utils/helpers.js'; +import { fetchJson, getTmdbMetadata, resolveTmdbKey, USER_AGENT } from '../utils/helpers.js'; + +const BASE_URL = 'https://lmscript.xyz'; +const REFERER = 'https://lookmovie2.to/'; export async function getStream(args) { - const { id, s, e, tmdbApiKey, sdk } = args; - const type = (s != null && e != null) ? 'tv' : 'movie'; - const apiKey = tmdbApiKey || sdk?.tmdbApiKey || process.env.TMDB_API_KEY || '45dbdd5e714da212424bda0336167447'; + const { id, s, e, sdk } = args || {}; + const isTv = s != null && e != null; + if (isTv) return null; + + const apiKey = resolveTmdbKey(args, sdk); try { - if (type === 'tv') { - return null; - } - - const tmdbInfo = await getTmdbInfo(apiKey, id, type, s); - const titles = tmdbInfo?.titles || []; + const meta = await getTmdbMetadata(id, isTv, apiKey); + const titles = meta?.titles || []; if (!titles.length) return null; let movie = null; for (const title of titles) { - const url = `https://lmscript.xyz/v1/movies?filters[q]=${encodeURIComponent(title)}&expand=streams`; - const response = await fetch(url); - if (!response.ok) continue; - const data = await response.json(); + const url = `${BASE_URL}/v1/movies?filters[q]=${encodeURIComponent(title)}&expand=streams`; + const data = await fetchJson(url).catch(() => null); + if (!data) continue; const found = data.items?.find(item => { const tmdbPrefixMatch = `${item.tmdb_prefix}` === `${id}`; @@ -37,33 +37,35 @@ export async function getStream(args) { if (!movie) return null; const streams = movie.streams || {}; - const SOURCE_ORDER = ["1080p", "720p", "480p", "360p"]; + const sourceOrder = ['1080p', '720p', '480p', '360p']; const sortedStreams = Object.keys(streams).sort((a, b) => { - const indexA = SOURCE_ORDER.indexOf(a); - const indexB = SOURCE_ORDER.indexOf(b); - return (indexA === -1 ? 99 : indexA) - (indexB === -1 ? 99 : indexB); + const idxA = sourceOrder.indexOf(a); + const idxB = sourceOrder.indexOf(b); + if (idxA !== -1 && idxB !== -1) return idxA - idxB; + if (idxA !== -1) return -1; + if (idxB !== -1) return 1; + return 0; }); - if (!sortedStreams.length) return null; + const headers = { + Referer: REFERER, + 'User-Agent': USER_AGENT, + }; - const allUrls = sortedStreams.map(quality => { - const streamUrl = streams[quality]; - return { - url: streamUrl, - quality, - server: `LMScript - ${quality}`, - type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', - headers: { - Referer: 'https://lookmovie2.to/', - Origin: 'https://lookmovie2.to' - } - }; - }); + const allUrls = sortedStreams.map(res => ({ + url: streams[res], + quality: res, + server: `LookMovie - ${res}`, + type: streams[res].includes('.m3u8') ? 'hls' : 'mp4', + headers, + })); + + if (!allUrls.length) return null; return { url: allUrls[0].url, headers: allUrls[0].headers, - allUrls + allUrls, }; } catch { return null; diff --git a/src/sources/lookmovie.js b/src/sources/lookmovie.js index 997f157..1c4f367 100644 --- a/src/sources/lookmovie.js +++ b/src/sources/lookmovie.js @@ -1,80 +1,81 @@ -import { USER_AGENT, fetchText, fetchJson } from '../utils/helpers.js'; - -const TMDB_BASE = 'https://api.themoviedb.org/3'; -const LM_DOMAINS = ['https://www.lookmovie2.to', 'https://lookmovie2.to', 'https://lookmovie.foundation']; -const HEADERS_BASE = { 'User-Agent': USER_AGENT, 'Accept-Language': 'en-US,en;q=0.9' }; - -async function searchLookMovie(type, title, year, clientIP) { - for (const base of LM_DOMAINS) { - try { - const headers = { ...HEADERS_BASE, 'Accept': 'application/json', 'Referer': `${base}/`, 'X-Requested-With': 'XMLHttpRequest', ...(clientIP && { 'X-Forwarded-For': clientIP }) }; - const data = await fetchJson(`${base}/api/v1/${type}/do-search/?q=${encodeURIComponent(title)}`, { headers, signal: AbortSignal.timeout(4000) }); - const results = data?.result; - if (results?.length) { - const match = results.find(r => String(r.year) === String(year)) ?? results.find(r => r.title?.toLowerCase() === title.toLowerCase()) ?? results[0]; - if (match) return { match, base }; - } - } catch { } - } - return null; -} - -const STORAGE_RE = /window\[['"](?:movie|show)_storage['"]\]\s*=\s*\{([^}]+)\}/s; -const ID_MOVIE_RE = /['"]?(?:id_movie|movieId)['"]?\s*[:=]\s*['"]?(\d+)['"]?/i; -const SEASONS_RE = /seasons\s*:\s*(\[[\s\S]+?\])\s*[,}]/; - -function getEpisodeIdFast(html, s, e) { - const sMatch = html.match(STORAGE_RE); - if (sMatch) { - const block = sMatch[1], sm = block.match(SEASONS_RE); - if (sm) { - try { - const seasons = JSON.parse(sm[1]); - const season = seasons.find(x => String(x?.season ?? x?.meta?.season) === String(s)); - if (season) { - const ep = Array.isArray(season.episodes) ? season.episodes.find(x => String(x.episode) === String(e)) : (season.episodes?.[String(e)] || Object.values(season.episodes || {}).find(x => String(x.episode) === String(e))); - if (ep) return String(ep.id_episode ?? ep.id); - } - } catch { } - } - } - const parts = html.split(/id_episode['"]?\s*[:=]\s*['"]?(\d+)['"]?/i); - for (let i = 1; i < parts.length; i += 2) { - const context = parts[i - 1].slice(-300) + parts[i + 1].slice(0, 300); - if (context.match(new RegExp(`['"]?episode['"]?\\s*[:=]\\s*['"]?${e}['"]?`, 'i')) && context.match(new RegExp(`['"]?season['"]?\\s*[:=]\\s*['"]?${s}['"]?`, 'i'))) return parts[i]; - } - const am = html.match(new RegExp(`data-season=["']${s}["'][^>]*?data-episode=["']${e}["'][^>]*?data-id=["'](\\d+)["']`, 'i')) || html.match(new RegExp(`data-episode=["']${e}["'][^>]*?data-season=["']${s}["'][^>]*?data-id=["'](\\d+)["']`, 'i')); - return am ? am[1] : null; -} - -export async function getStream({ id, s, e, sdk, clientIP }) { - const tmdbApiKey = sdk?.tmdbApiKey || null; - if (!tmdbApiKey) return null; - try { - const tmdbKey = tmdbApiKey; - if (!tmdbKey) return null; - const isTV = s != null && e != null; - const typeStr = isTV ? 'shows' : 'movies'; - const tmdbData = await fetchJson(`${TMDB_BASE}/${isTV ? 'tv' : 'movie'}/${id}?api_key=${tmdbKey}`, { signal: AbortSignal.timeout(3000) }); - const title = tmdbData?.title || tmdbData?.name; - const year = (tmdbData?.first_air_date || tmdbData?.release_date || '').slice(0, 4); - if (!title) return null; - const searchRes = await searchLookMovie(typeStr, title, year, clientIP); - if (!searchRes?.match?.slug) return null; - const { match, base } = searchRes; - const headers = { ...HEADERS_BASE, 'Accept': 'text/html', 'Referer': `${base}/`, ...(clientIP && { 'X-Forwarded-For': clientIP }) }; - const html = await fetchText(`${base}/${typeStr}/play/${match.slug}`, { headers, signal: AbortSignal.timeout(8000) }); - const storageMatch = html.match(STORAGE_RE); - if (!storageMatch) return null; - const hashMatch = storageMatch[1].match(/hash\s*:\s*['"]([^'"]+)['"]/); - const expiresMatch = storageMatch[1].match(/expires\s*:\s*(\d+)/); - if (!hashMatch || !expiresMatch) return null; - const streamId = isTV ? getEpisodeIdFast(html, s, e) : (match.id_movie || match.id || html.match(ID_MOVIE_RE)?.[1]); - if (!streamId) return null; - const accessHeaders = { ...HEADERS_BASE, 'Accept': 'application/json', 'Referer': `${base}/`, 'X-Requested-With': 'XMLHttpRequest', ...(clientIP && { 'X-Forwarded-For': clientIP }) }; - const data = await fetchJson(`${base}/api/v1/security/${isTV ? 'episode' : 'movie'}-access?id_${isTV ? 'episode' : 'movie'}=${streamId}&hash=${hashMatch[1]}&expires=${expiresMatch[1]}`, { headers: accessHeaders, signal: AbortSignal.timeout(8000) }); - const streams = data?.streams ?? data?.result?.streams ?? data?.data?.streams ?? data; - const allUrls = Object.entries(streams || {}).filter(([, v]) => typeof v === 'string' && v.includes('.m3u8')).map(([quality, url]) => ({ url, quality })); - return allUrls.length ? { allUrls } : null; - } catch { return null; } +import { USER_AGENT, fetchText, fetchJson } from '../utils/helpers.js'; + +const TMDB_BASE = 'https://api.themoviedb.org/3'; +const LM_DOMAINS = ['https://www.lookmovie2.to', 'https://lookmovie2.to', 'https://lookmovie.foundation']; + +const HEADERS_BASE = { 'User-Agent': USER_AGENT, 'Accept-Language': 'en-US,en;q=0.9' }; + +async function searchLookMovie(type, title, year, clientIP) { + for (const base of LM_DOMAINS) { + try { + const headers = { ...HEADERS_BASE, 'Accept': 'application/json', 'Referer': `${base}/`, 'X-Requested-With': 'XMLHttpRequest', ...(clientIP && { 'X-Forwarded-For': clientIP }) }; + const data = await fetchJson(`${base}/api/v1/${type}/do-search/?q=${encodeURIComponent(title)}`, { headers, signal: AbortSignal.timeout(4000) }); + const results = data?.result; + if (results?.length) { + const match = results.find(r => String(r.year) === String(year)) ?? results.find(r => r.title?.toLowerCase() === title.toLowerCase()) ?? results[0]; + if (match) return { match, base }; + } + } catch { } + } + return null; +} + +const STORAGE_RE = /window\[['"](?:movie|show)_storage['"]\]\s*=\s*\{([^}]+)\}/s; +const ID_MOVIE_RE = /['"]?(?:id_movie|movieId)['"]?\s*[:=]\s*['"]?(\d+)['"]?/i; +const SEASONS_RE = /seasons\s*:\s*(\[[\s\S]+?\])\s*[,}]/; + +function getEpisodeIdFast(html, s, e) { + const sMatch = html.match(STORAGE_RE); + if (sMatch) { + const block = sMatch[1], sm = block.match(SEASONS_RE); + if (sm) { + try { + const seasons = JSON.parse(sm[1]); + const season = seasons.find(x => String(x?.season ?? x?.meta?.season) === String(s)); + if (season) { + const ep = Array.isArray(season.episodes) ? season.episodes.find(x => String(x.episode) === String(e)) : (season.episodes?.[String(e)] || Object.values(season.episodes || {}).find(x => String(x.episode) === String(e))); + if (ep) return String(ep.id_episode ?? ep.id); + } + } catch { } + } + } + const parts = html.split(/id_episode['"]?\s*[:=]\s*['"]?(\d+)['"]?/i); + for (let i = 1; i < parts.length; i += 2) { + const context = parts[i - 1].slice(-300) + parts[i + 1].slice(0, 300); + if (context.match(new RegExp(`['"]?episode['"]?\\s*[:=]\\s*['"]?${e}['"]?`, 'i')) && context.match(new RegExp(`['"]?season['"]?\\s*[:=]\\s*['"]?${s}['"]?`, 'i'))) return parts[i]; + } + const am = html.match(new RegExp(`data-season=["']${s}["'][^>]*?data-episode=["']${e}["'][^>]*?data-id=["'](\\d+)["']`, 'i')) || html.match(new RegExp(`data-episode=["']${e}["'][^>]*?data-season=["']${s}["'][^>]*?data-id=["'](\\d+)["']`, 'i')); + return am ? am[1] : null; +} + +export async function getStream({ id, s, e, sdk, clientIP }) { + const tmdbApiKey = sdk?.tmdbApiKey || null; + if (!tmdbApiKey) return null; + try { + const tmdbKey = tmdbApiKey; + if (!tmdbKey) return null; + const isTV = s != null && e != null; + const typeStr = isTV ? 'shows' : 'movies'; + const tmdbData = await fetchJson(`${TMDB_BASE}/${isTV ? 'tv' : 'movie'}/${id}?api_key=${tmdbKey}`, { signal: AbortSignal.timeout(3000) }); + const title = tmdbData?.title || tmdbData?.name; + const year = (tmdbData?.first_air_date || tmdbData?.release_date || '').slice(0, 4); + if (!title) return null; + const searchRes = await searchLookMovie(typeStr, title, year, clientIP); + if (!searchRes?.match?.slug) return null; + const { match, base } = searchRes; + const headers = { ...HEADERS_BASE, 'Accept': 'text/html', 'Referer': `${base}/`, ...(clientIP && { 'X-Forwarded-For': clientIP }) }; + const html = await fetchText(`${base}/${typeStr}/play/${match.slug}`, { headers, signal: AbortSignal.timeout(8000) }); + const storageMatch = html.match(STORAGE_RE); + if (!storageMatch) return null; + const hashMatch = storageMatch[1].match(/hash\s*:\s*['"]([^'"]+)['"]/); + const expiresMatch = storageMatch[1].match(/expires\s*:\s*(\d+)/); + if (!hashMatch || !expiresMatch) return null; + const streamId = isTV ? getEpisodeIdFast(html, s, e) : (match.id_movie || match.id || html.match(ID_MOVIE_RE)?.[1]); + if (!streamId) return null; + const accessHeaders = { ...HEADERS_BASE, 'Accept': 'application/json', 'Referer': `${base}/`, 'X-Requested-With': 'XMLHttpRequest', ...(clientIP && { 'X-Forwarded-For': clientIP }) }; + const data = await fetchJson(`${base}/api/v1/security/${isTV ? 'episode' : 'movie'}-access?id_${isTV ? 'episode' : 'movie'}=${streamId}&hash=${hashMatch[1]}&expires=${expiresMatch[1]}`, { headers: accessHeaders, signal: AbortSignal.timeout(8000) }); + const streams = data?.streams ?? data?.result?.streams ?? data?.data?.streams ?? data; + const allUrls = Object.entries(streams || {}).filter(([, v]) => typeof v === 'string' && v.includes('.m3u8')).map(([quality, url]) => ({ url, quality })); + return allUrls.length ? { allUrls } : null; + } catch { return null; } } \ No newline at end of file diff --git a/src/sources/movenerds.js b/src/sources/movenerds.js index 38a6ee0..7a70b78 100644 --- a/src/sources/movenerds.js +++ b/src/sources/movenerds.js @@ -1,125 +1,75 @@ -import { getUA } from "../utils/helpers.js"; +import { getTmdbMetadata, getUA, resolveTmdbKey } from '../utils/helpers.js'; -const API_BASE = "https://api.movenerds.space/api"; - -function getHeaders() { - return { - "User-Agent": getUA(), - Accept: "application/json", - Referer: "https://movenerds.space/", - Origin: "https://movenerds.space", - }; -} - -function resolveApiKey(args) { - return ( - args?.tmdbApiKey || - args?.sdk?.tmdbApiKey || - process.env.TMDB_API_KEY || - null - ); -} - -async function getMeta(id, isTv, apiKey) { - if (!apiKey) return null; - const type = isTv ? "tv" : "movie"; - try { - const res = await fetch( - `https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, - { signal: AbortSignal.timeout(5000) } - ); - if (!res.ok) return null; - const data = await res.json(); - const title = isTv ? data.name : data.title; - const date = isTv ? data.first_air_date : data.release_date; - const year = date ? parseInt(date.slice(0, 4), 10) : null; - return { title, year }; - } catch { - return null; - } -} +const BASE_URL = 'https://movenerds.space'; +const API_BASE = 'https://api.movenerds.space/api'; function mapSource(source) { if (!source || !source.url) return null; - if (source.type !== "hls" && source.type !== "mp4") return null; + if (source.type !== 'hls' && source.type !== 'mp4') return null; if (!/^https?:\/\//.test(source.url)) return null; return { url: source.url, headers: { - "User-Agent": source.headers?.["User-Agent"] || getUA(), - Referer: source.headers?.Referer || "https://movenerds.space/", - Origin: source.headers?.Origin || "https://movenerds.space", + 'User-Agent': source.headers?.['User-Agent'] || getUA(), + Referer: source.headers?.Referer || `${BASE_URL}/`, + Origin: source.headers?.Origin || BASE_URL, }, - server: source.name || source.providerName || "MoverNerds", - quality: source.quality || "1080p", + server: source.name || source.providerName || 'MoverNerds', + quality: source.quality || '1080p', type: source.type, skipProxy: true, }; } export async function getStream(args) { - const { id, s = null, e = null } = args; + const { id, s, e, sdk } = args || {}; const isTv = s != null && e != null; + const apiKey = resolveTmdbKey(args, sdk); - const apiKey = resolveApiKey(args); - const meta = await getMeta(id, isTv, apiKey); - - if (!meta?.title) { - if (process.env.DEBUG_CAUSE) { - console.error( - `[movenerds] Could not resolve title for ${isTv ? "tv" : "movie"} ${id} ` + - `(tmdbApiKey ${apiKey ? "present" : "MISSING"})` - ); - } - return null; - } + const meta = await getTmdbMetadata(id, isTv, apiKey); + if (!meta?.title) return null; const params = new URLSearchParams({ tmdbId: String(id), - type: isTv ? "tv" : "movie", + type: isTv ? 'tv' : 'movie', title: meta.title, }); - if (meta.year) params.set("year", String(meta.year)); + if (meta.year) params.set('year', String(meta.year)); if (isTv) { - params.set("season", String(s)); - params.set("episode", String(e)); - params.set("language", "en"); + params.set('season', String(s)); + params.set('episode', String(e)); + params.set('language', 'en'); } const url = `${API_BASE}/stream?${params.toString()}`; + const headers = { + 'User-Agent': getUA(), + Accept: 'application/json', + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + }; try { const res = await fetch(url, { - headers: getHeaders(), + headers, signal: AbortSignal.timeout(20000), }); - - if (!res.ok) { - if (process.env.DEBUG_CAUSE) { - console.error(`[movenerds] HTTP ${res.status} for ${url}`); - } - return null; - } + if (!res.ok) return null; const data = await res.json(); const primary = Array.isArray(data?.primarySources) ? data.primarySources : []; - if (!primary.length) { - if (process.env.DEBUG_CAUSE) { - console.error(`[movenerds] No primarySources in response for ${url}`); - } - return null; - } + if (!primary.length) return null; const allUrls = primary.map(mapSource).filter(Boolean); if (!allUrls.length) return null; - return { allUrls }; - } catch (err) { - if (process.env.DEBUG_CAUSE) { - console.error(`[movenerds] fetch error for ${url}:`, err.message); - if (err.cause) console.error(" cause:", err.cause.code, err.cause.message); - } + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; + } catch { return null; } } \ No newline at end of file diff --git a/src/sources/movy.js b/src/sources/movy.js index a6ec1f2..063ddbb 100644 --- a/src/sources/movy.js +++ b/src/sources/movy.js @@ -1,10 +1,10 @@ -import { fetchJson, fetchText, getTmdbInfo } from '../utils/helpers.js'; +import { fetchJson, fetchText, getTmdbMetadata, resolveTmdbKey } from '../utils/helpers.js'; const K = [ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, - 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174 + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, ]; const MAGIC = [109, 118, 109, 49]; @@ -62,7 +62,7 @@ function initCipherState(keyStr, seedStr) { } return { S, - acc: scramble(0xa5a5a5a5 ^ n) >>> 0 + acc: scramble(0xa5a5a5a5 ^ n) >>> 0, }; } @@ -82,7 +82,7 @@ function nextKeystreamWord(state, step) { } function decodeBase64Url(str) { - const padded = str.replace(/-/g, "+").replace(/_/g, "/").padEnd(4 * Math.ceil(str.length / 4), "="); + const padded = str.replace(/-/g, '+').replace(/_/g, '/').padEnd(4 * Math.ceil(str.length / 4), '='); const bin = atob(padded); const bytes = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); @@ -111,45 +111,31 @@ function decryptPayload(ciphertext, seed, mediaId) { for (let i = 0; i < MAGIC.length; i++) { if (rawBytes[i] !== MAGIC[i]) { - throw new Error("decrypt failed: bad seed or tampered payload"); + throw new Error('decrypt failed: bad seed or tampered payload'); } } const payload = rawBytes.subarray(MAGIC.length); - return new TextDecoder("utf-8").decode(payload); + return new TextDecoder('utf-8').decode(payload); } -export async function getStream({ id, s, e, tmdbApiKey, sdk }) { +export async function getStream(args) { + const { id, s, e, sdk } = args || {}; const isTv = s != null && e != null; const type = isTv ? 'tv' : 'movie'; - const apiKey = tmdbApiKey || sdk?.tmdbApiKey || process.env.TMDB_API_KEY || '45dbdd5e714da212424bda0336167447'; + const apiKey = resolveTmdbKey(args, sdk); const headers = { - 'Accept': '*/*', - 'Origin': 'https://www.movy.sx', - 'Referer': 'https://www.movy.sx/', - 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1' + Accept: '*/*', + Origin: 'https://www.movy.sx', + Referer: 'https://www.movy.sx/', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1', }; - let title = ''; - let year = ''; - let imdbId = ''; - - try { - const info = await getTmdbInfo(apiKey, id, type, s); - title = info?.titles?.[0] || ''; - year = info?.year || ''; - imdbId = info?.imdbId || ''; - } catch { } - - if (!title) { - try { - const tmdbRes = await fetchJson(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`); - title = tmdbRes?.title || tmdbRes?.name || ''; - year = (tmdbRes?.release_date || tmdbRes?.first_air_date || '').split('-')[0] || ''; - imdbId = tmdbRes?.imdb_id || tmdbRes?.external_ids?.imdb_id || ''; - } catch { } - } + const meta = await getTmdbMetadata(id, isTv, apiKey); + const title = meta?.title || ''; + const year = meta?.year ? String(meta.year) : ''; + const imdbId = meta?.imdbId || ''; if (!title) return null; @@ -170,7 +156,7 @@ export async function getStream({ id, s, e, tmdbApiKey, sdk }) { tmdbId: String(id), imdbId: String(imdbId || ''), enc: '2', - seed + seed, }); const url = `https://api.wecollege.net/${srv}/sources?${params.toString()}`; @@ -187,10 +173,12 @@ export async function getStream({ id, s, e, tmdbApiKey, sdk }) { allUrls.push({ url: src.url, server: `Movy - ${srv.toUpperCase()} (${src.quality || 'Auto'})`, + quality: src.quality || 'Auto', + type: src.url.includes('.m3u8') ? 'hls' : 'mp4', headers: { Referer: 'https://www.movy.sx/', - Origin: 'https://www.movy.sx' - } + Origin: 'https://www.movy.sx', + }, }); } } @@ -205,6 +193,6 @@ export async function getStream({ id, s, e, tmdbApiKey, sdk }) { return { url: allUrls[0].url, headers: allUrls[0].headers, - allUrls + allUrls, }; } \ No newline at end of file diff --git a/src/sources/net79.js b/src/sources/net79.js index db1b08f..3de2c10 100644 --- a/src/sources/net79.js +++ b/src/sources/net79.js @@ -1,16 +1,20 @@ import { fetchJson } from '../utils/helpers.js'; -export async function getStream({ id, s, e }) { +const BASE_URL = 'https://net79.cc'; + +export async function getStream(args) { + const { id, s, e } = args || {}; const isTv = s != null && e != null; + const url = isTv - ? `https://net79.cc/api/embed-tmdb/${id}?type=tv&se=${s}&ep=${e}` - : `https://net79.cc/api/embed-tmdb/${id}?type=movie`; + ? `${BASE_URL}/api/embed-tmdb/${id}?type=tv&se=${s}&ep=${e}` + : `${BASE_URL}/api/embed-tmdb/${id}?type=movie`; const headers = { - 'Accept': 'application/json', - 'Referer': 'https://net79.cc/', - 'Origin': 'https://net79.cc', - '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' + Accept: 'application/json', + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + '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', }; const res = await fetchJson(url, { headers }).catch(() => null); @@ -27,9 +31,9 @@ export async function getStream({ id, s, e }) { server: `Net79 - ${stream.resolution ? `${stream.resolution}p` : 'MP4'}`, type: stream.url.includes('.m3u8') ? 'hls' : 'mp4', headers: { - Referer: 'https://net79.cc/', - Origin: 'https://net79.cc' - } + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + }, }); } } @@ -40,9 +44,9 @@ export async function getStream({ id, s, e }) { server: `Net79 - ${res.resolution ? `${res.resolution}p` : 'MP4'}`, type: res.mp4.includes('.m3u8') ? 'hls' : 'mp4', headers: { - Referer: 'https://net79.cc/', - Origin: 'https://net79.cc' - } + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + }, }); } @@ -51,6 +55,6 @@ export async function getStream({ id, s, e }) { return { url: allUrls[0].url, headers: allUrls[0].headers, - allUrls + allUrls, }; } \ No newline at end of file diff --git a/src/sources/peestream.js b/src/sources/peestream.js index 1bb656a..978045e 100644 --- a/src/sources/peestream.js +++ b/src/sources/peestream.js @@ -1,4 +1,4 @@ -import { USER_AGENT, fetchJson, getTmdbInfo } from '../utils/helpers.js'; +import { fetchJson, getTmdbMetadata, resolveTmdbKey, USER_AGENT } from '../utils/helpers.js'; const BASE_URL = 'https://providers.peestream.in'; @@ -6,58 +6,40 @@ const PROVIDERS = { purstream: 'Athena', vidrock: 'Thor', vaplayer: 'Poseidon', - netmirror: 'Ra' + netmirror: 'Ra', }; export async function getStream(args) { - const { id, s, e, server, tmdbApiKey } = args; + const { id, s, e, server, sdk } = args || {}; const isTv = s != null && e != null; const type = isTv ? 'tv' : 'movie'; + const apiKey = resolveTmdbKey(args, sdk); - let title = args.title || ''; - let releaseYear = args.year || ''; - let imdbId = args.imdbId || ''; - let seasonTmdbId = args.seasonTmdbId || ''; - let episodeTmdbId = args.episodeTmdbId || ''; + let title = args?.title || ''; + let releaseYear = args?.year || ''; + let imdbId = args?.imdbId || ''; + let seasonTmdbId = args?.seasonTmdbId || ''; + let episodeTmdbId = args?.episodeTmdbId || ''; try { - const info = await getTmdbInfo(id, type, s, tmdbApiKey); - if (info) { - title = title || info.name || info.title || ''; - - if (!releaseYear) { - const date = info.release_date || info.first_air_date || ''; - releaseYear = date ? date.split('-')[0] : (info.year || ''); - } - - if (!imdbId && info.external_ids?.imdb_id) { - imdbId = info.external_ids.imdb_id; - } else if (!imdbId && info.imdb_id) { - imdbId = info.imdb_id; - } + const meta = await getTmdbMetadata(id, isTv, apiKey); + if (meta) { + title = title || meta.title || ''; + if (!releaseYear && meta.year) releaseYear = String(meta.year); + if (!imdbId && meta.imdbId) imdbId = meta.imdbId; if (isTv) { - if (!seasonTmdbId && Array.isArray(info.seasons)) { - const matchedSeason = info.seasons.find( - item => item.season_number === Number(s) - ); - if (matchedSeason) { - seasonTmdbId = String(matchedSeason.id); - } + if (!seasonTmdbId && Array.isArray(meta.seasons)) { + const matchedSeason = meta.seasons.find(item => item.season_number === Number(s)); + if (matchedSeason) seasonTmdbId = String(matchedSeason.id); } - if (!episodeTmdbId && tmdbApiKey) { + if (!episodeTmdbId && apiKey) { try { - const seasonData = await fetchJson( - `https://api.themoviedb.org/3/tv/${id}/season/${s}?api_key=${tmdbApiKey}` - ); + const seasonData = await fetchJson(`https://api.themoviedb.org/3/tv/${id}/season/${s}?api_key=${apiKey}`); if (seasonData?.episodes) { - const matchedEp = seasonData.episodes.find( - ep => ep.episode_number === Number(e) - ); - if (matchedEp) { - episodeTmdbId = String(matchedEp.id); - } + const matchedEp = seasonData.episodes.find(ep => ep.episode_number === Number(e)); + if (matchedEp) episodeTmdbId = String(matchedEp.id); } } catch { } } @@ -68,7 +50,7 @@ export async function getStream(args) { const queryParams = new URLSearchParams({ type, tmdbId: String(id), - title: title || String(id) + title: title || String(id), }); if (releaseYear) queryParams.set('releaseYear', String(releaseYear)); @@ -88,10 +70,10 @@ export async function getStream(args) { const res = await fetch(scrapeUrl, { headers: { 'User-Agent': USER_AGENT, - 'Accept': 'text/event-stream', - 'Referer': `${BASE_URL}/` + Accept: 'text/event-stream', + Referer: `${BASE_URL}/`, }, - signal: AbortSignal.timeout(20000) + signal: AbortSignal.timeout(20000), }); if (res.ok) { @@ -114,8 +96,8 @@ export async function getStream(args) { quality: 'Auto', type: stream.type === 'hls' || streamUrl.includes('.m3u8') ? 'hls' : 'mp4', headers: stream.headers || { - 'User-Agent': USER_AGENT - } + 'User-Agent': USER_AGENT, + }, }); } } catch { } @@ -128,10 +110,16 @@ export async function getStream(args) { if (server && server !== 'all') { const clean = server.toLowerCase().replace('peestream - ', '').trim(); const filtered = allUrls.filter(u => u.server.toLowerCase().includes(clean)); - if (filtered.length) return { allUrls: filtered }; + if (filtered.length) return { url: filtered[0].url, headers: filtered[0].headers, allUrls: filtered }; } - return allUrls.length ? { allUrls } : null; + if (!allUrls.length) return null; + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; } export async function getSources() { diff --git a/src/sources/pengu.js b/src/sources/pengu.js index a736681..fdb98ec 100644 --- a/src/sources/pengu.js +++ b/src/sources/pengu.js @@ -1,50 +1,47 @@ -import { fetchJson } from '../utils/helpers.js'; - -// const PENGU_CONFIG = '%7B%22source_4khdhub%22%3A%22on%22%2C%22source_moviebox%22%3A%22on%22%2C%22source_moviesdrives%22%3A%22on%22%2C%22source_vaplayer%22%3A%22on%22%2C%22source_hdghartv%22%3A%22on%22%2C%22res_2160%22%3A%22on%22%2C%22res_1080%22%3A%22on%22%2C%22res_720%22%3A%22on%22%2C%22disable_direct%22%3A%22on%22%7D'; -// const BASE_URL = `https://pengu.uk/${PENGU_CONFIG}`; - -async function getImdbId(tmdbApiKey, tmdbId, s, e) { - const key = tmdbApiKey; - if (!key) return null; - try { - const url = s != null && e != null - ? `https://api.themoviedb.org/3/tv/${tmdbId}/external_ids?api_key=${key}` - : `https://api.themoviedb.org/3/movie/${tmdbId}?api_key=${key}`; - const res = await fetchJson(url); - return res?.imdb_id || null; - } catch { return null; } -} - -export async function getStream({ id, s, e, sdk }) { - const tmdbApiKey = sdk?.tmdbApiKey || null; - const manifest = sdk?.penguManifest || null; - if (!tmdbApiKey || !manifest || (manifest && !manifest.endsWith('manifest.json'))) return null; - const BASE_URL = manifest ? manifest.split('/manifest.json')[0] : null; - if (!BASE_URL) return null; - try { - const imdbId = await getImdbId(tmdbApiKey, id, s, e); - if (!imdbId) return null; - - const path = s != null && e != null - ? `/stream/series/${imdbId}:${s}:${e}.json` - : `/stream/movie/${imdbId}.json`; - - const res = await fetchJson(`${BASE_URL}${path}`); - if (!res?.streams?.length) return null; - - const allUrls = res.streams.map(stream => { - if (!stream.url) return null; - const headers = stream.behaviorHints?.proxyHeaders?.request || {}; - return { - url: stream.url, - server: `Pengu - ${stream.name || 'Server'}`, - headers: Object.keys(headers).length > 0 ? headers : undefined, - skipProxy: stream.behaviorHints?.notWebReady === false - }; - }).filter(Boolean); - - return allUrls.length ? { allUrls } : null; - } catch { - return null; - } +import { fetchJson, resolveImdbId, resolveTmdbKey } from '../utils/helpers.js'; + +export async function getStream(args) { + const { id, s, e, sdk } = args || {}; + const isTv = s != null && e != null; + const apiKey = resolveTmdbKey(args, sdk); + const manifest = sdk?.penguManifest || null; + + if (!apiKey || !manifest || !manifest.endsWith('manifest.json')) return null; + const baseUrl = manifest.split('/manifest.json')[0]; + if (!baseUrl) return null; + + try { + const imdbId = await resolveImdbId(id, isTv, apiKey); + if (!imdbId) return null; + + const path = isTv + ? `/stream/series/${imdbId}:${s}:${e}.json` + : `/stream/movie/${imdbId}.json`; + + const res = await fetchJson(`${baseUrl}${path}`); + if (!res?.streams?.length) return null; + + const allUrls = res.streams.map(stream => { + if (!stream.url) return null; + const headers = stream.behaviorHints?.proxyHeaders?.request || {}; + return { + url: stream.url, + server: `Pengu - ${stream.name || 'Server'}`, + quality: 'Auto', + type: stream.url.includes('.m3u8') ? 'hls' : 'mp4', + headers: Object.keys(headers).length > 0 ? headers : undefined, + skipProxy: stream.behaviorHints?.notWebReady === false, + }; + }).filter(Boolean); + + if (!allUrls.length) return null; + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; + } catch { + return null; + } } \ No newline at end of file diff --git a/src/sources/pstream.js b/src/sources/pstream.js index ecfd76b..327b95a 100644 --- a/src/sources/pstream.js +++ b/src/sources/pstream.js @@ -9,7 +9,7 @@ async function safeImport(moduleName) { } export async function getStream(args) { - const { id, s, e, clientIP, sdk } = args; + const { id, s, e, clientIP, sdk } = args || {}; const isTv = s != null && e != null; const type = isTv ? 'tv' : 'movie'; const allUrls = []; @@ -64,5 +64,11 @@ export async function getStream(args) { }) ); - return allUrls.length ? { allUrls } : null; + if (!allUrls.length) return null; + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; } \ No newline at end of file diff --git a/src/sources/purstream.js b/src/sources/purstream.js index c3e8f76..efe9648 100644 --- a/src/sources/purstream.js +++ b/src/sources/purstream.js @@ -1,104 +1,96 @@ -import { fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js'; - -const DOMAIN = 'https://purstream.ad'; -const API_BASE = 'https://api.purstream.ad/api/v1'; - -const HEADERS = { - 'User-Agent': USER_AGENT, - 'Accept': 'application/json, text/plain, */*', - 'Referer': `${DOMAIN}/`, - 'Origin': DOMAIN, - 'X-Requested-With': 'XMLHttpRequest', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-site' -}; - -export async function getStream({ id, s, e }) { - try { - const isTv = s != null; - - const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie'); - - - if (!info?.titles?.length) { - return null; - } - - const title = info.titles[0]; - - const searchUrl = `${API_BASE}/search-bar/search/${encodeURIComponent(title)}`; - - const searchData = await fetchJson(searchUrl, { - headers: HEADERS, - signal: AbortSignal.timeout(10000) - }); - - const items = searchData?.data?.items?.movies?.items || []; - - const type = isTv ? 'tv' : 'movie'; - const lowerTitle = title.toLowerCase(); - - let match = items.find(item => - item.type === type && - item.title?.toLowerCase() === lowerTitle && - (!info.year || item.release_date?.startsWith(String(info.year))) - ); - - if (!match) { - match = items.find(item => item.type === type); - } - - if (!match?.id) { - return null; - } - - const streamUrl = isTv - ? `${API_BASE}/stream/${match.id}/episode?season=${s}&episode=${e}` - : `${API_BASE}/stream/${match.id}`; - - const json = await fetchJson(streamUrl, { - headers: HEADERS, - signal: AbortSignal.timeout(10000) - }); - - const sources = json?.data?.items?.sources; - - if (json?.type !== 'success' || !Array.isArray(sources) || !sources.length) { - return null; - } - - const chosen = sources.find(src => src.stream_url); - - if (!chosen?.stream_url) { - return null; - } - - const result = { - url: chosen.stream_url, - headers: { - 'User-Agent': USER_AGENT, - 'Referer': `${DOMAIN}/` - }, - server: 'purstream', - quality: chosen.source_name || 'Auto', - type: chosen.format || 'hls', - skipProxy: true - }; - - return result; - - } catch (err) { - return null; - } -} - -export async function getSources(args) { - const stream = await getStream(args); - - return stream ? [stream] : []; -} - -export const VERIFY_HEADERS = { - ...HEADERS -}; \ No newline at end of file +import { fetchJson, getTmdbMetadata, resolveTmdbKey, USER_AGENT } from '../utils/helpers.js'; + +const DOMAIN = 'https://purstream.ad'; +const API_BASE = 'https://api.purstream.ad/api/v1'; + +const HEADERS = { + 'User-Agent': USER_AGENT, + Accept: 'application/json, text/plain, */*', + Referer: `${DOMAIN}/`, + Origin: DOMAIN, + 'X-Requested-With': 'XMLHttpRequest', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-site', +}; + +export async function getStream(args) { + const { id, s, e, sdk } = args || {}; + const isTv = s != null && e != null; + const type = isTv ? 'tv' : 'movie'; + const apiKey = resolveTmdbKey(args, sdk); + + try { + const meta = await getTmdbMetadata(id, isTv, apiKey); + if (!meta?.titles?.length) return null; + + const title = meta.titles[0]; + const searchUrl = `${API_BASE}/search-bar/search/${encodeURIComponent(title)}`; + + const searchData = await fetchJson(searchUrl, { + headers: HEADERS, + signal: AbortSignal.timeout(10000), + }); + + const items = searchData?.data?.items?.movies?.items || []; + const lowerTitle = title.toLowerCase(); + + let match = items.find(item => + item.type === type && + item.title?.toLowerCase() === lowerTitle && + (!meta.year || item.release_date?.startsWith(String(meta.year))) + ); + + if (!match) { + match = items.find(item => item.type === type); + } + + if (!match?.id) return null; + + const streamUrl = isTv + ? `${API_BASE}/stream/${match.id}/episode?season=${s}&episode=${e}` + : `${API_BASE}/stream/${match.id}`; + + const json = await fetchJson(streamUrl, { + headers: HEADERS, + signal: AbortSignal.timeout(10000), + }); + + const sources = json?.data?.items?.sources; + if (json?.type !== 'success' || !Array.isArray(sources) || !sources.length) return null; + + const chosen = sources.find(src => src.stream_url); + if (!chosen?.stream_url) return null; + + const streamHeaders = { + 'User-Agent': USER_AGENT, + Referer: `${DOMAIN}/`, + }; + + const allUrls = [{ + url: chosen.stream_url, + headers: streamHeaders, + server: 'purstream', + quality: chosen.source_name || 'Auto', + type: chosen.format || 'hls', + skipProxy: true, + }]; + + return { + url: chosen.stream_url, + headers: streamHeaders, + server: 'purstream', + quality: chosen.source_name || 'Auto', + type: chosen.format || 'hls', + skipProxy: true, + allUrls, + }; + } catch { + return null; + } +} + +export async function getSources(args) { + const stream = await getStream(args); + return stream?.allUrls ? stream.allUrls.map(u => u.server) : ['purstream']; +} \ No newline at end of file diff --git a/src/sources/rivestream.js b/src/sources/rivestream.js index e388c62..a7b31bb 100644 --- a/src/sources/rivestream.js +++ b/src/sources/rivestream.js @@ -1,4 +1,4 @@ -import { USER_AGENT, fetchJson } from '../utils/helpers.js'; +import { fetchJson, USER_AGENT } from '../utils/helpers.js'; const C_ARRAY = [ '4Z7lUo', 'gwIVSMD', 'PLmz2elE2v', 'Z4OFV0', 'SZ6RZq6Zc', 'zhJEFYxrz8', 'FOm7b0', 'axHS3q4KDq', 'o9zuXQ', '4Aebt', @@ -7,12 +7,12 @@ const C_ARRAY = [ 'W81hjts92', 'rJhAT', 'NON7LKoMQ', 'NMdY3nsKzI', 't4En5v', 'Qq5cOQ9H', 'Y9nwrp', 'VX5FYVfsf', 'cE5SJG', 'x1vj1', 'HegbLe', 'zJ3nmt4OA', 'gt7rxW57dq', 'clIE9b', 'jyJ9g', 'B5jXjMCSx', 'cOzZBZTV', 'FTXGy', 'Dfh1q1', 'ny9jqZ2POI', 'X2NnMn', 'MBtoyD', 'qz4Ilys7wB', '68lbOMye', '3YUJnmxp', '1fv5Imona', 'PlfvvXD7mA', 'ZarKfHCaPR', 'owORnX', 'dQP1YU', - 'dVdkx', 'qgiK0E', 'cx9wQ', '5F9bGa', '7UjkKrp', 'Yvhrj', 'wYXez5Dg3', 'pG4GMU', 'MwMAu', 'rFRD5wlM' + 'dVdkx', 'qgiK0E', 'cx9wQ', '5F9bGa', '7UjkKrp', 'Yvhrj', 'wYXez5Dg3', 'pG4GMU', 'MwMAu', 'rFRD5wlM', ]; const DEFAULT_SERVICES = [ 'apex', 'pulse', 'solstice', 'quasar', 'horizon', 'primevids', - 'flowcast', 'asiacloud', 'citadel', 'hindicast', 'guru' + 'flowcast', 'asiacloud', 'citadel', 'hindicast', 'guru', ]; function generateRiveSecretKey(e) { @@ -80,10 +80,10 @@ async function fetchRiveServices() { const res = await fetchJson(url, { headers: { 'User-Agent': USER_AGENT, - 'Referer': 'https://www.rivestream.app/', - 'Accept': 'application/json, text/plain, */*' + Referer: 'https://www.rivestream.app/', + Accept: 'application/json, text/plain, */*', }, - signal: AbortSignal.timeout(6000) + signal: AbortSignal.timeout(6000), }); return Array.isArray(res?.data) && res.data.length ? res.data : DEFAULT_SERVICES; } catch { @@ -106,10 +106,10 @@ async function fetchServiceStreams(service, isMovie, id, s, e, secretKey, refere const res = await fetchJson(url, { headers: { 'User-Agent': USER_AGENT, - 'Referer': referer, - 'Accept': 'application/json, text/plain, */*' + Referer: referer, + Accept: 'application/json, text/plain, */*', }, - signal: AbortSignal.timeout(10000) + signal: AbortSignal.timeout(10000), }); if (!res?.data?.sources || !Array.isArray(res.data.sources)) return []; @@ -129,8 +129,8 @@ async function fetchServiceStreams(service, isMovie, id, s, e, secretKey, refere type: isM3U8 ? 'hls' : 'mp4', headers: { 'User-Agent': USER_AGENT, - 'Referer': referer - } + Referer: referer, + }, }); } return streams; @@ -140,7 +140,7 @@ async function fetchServiceStreams(service, isMovie, id, s, e, secretKey, refere } export async function getStream(args) { - const { id, s, e, server } = args; + const { id, s, e, server } = args || {}; const isTv = s != null && e != null; const isMovie = !isTv; @@ -168,7 +168,13 @@ export async function getStream(args) { } } - return allUrls.length ? { allUrls } : null; + if (!allUrls.length) return null; + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; } export async function getSources() { diff --git a/src/sources/rivo.js b/src/sources/rivo.js index bac7fc0..0172ba4 100644 --- a/src/sources/rivo.js +++ b/src/sources/rivo.js @@ -1,23 +1,26 @@ import { fetchJson } from '../utils/helpers.js'; -export async function getStream({ id, s, e }) { +const BASE_URL = 'https://f.rivo.st'; + +export async function getStream(args) { + const { id, s, e } = args || {}; const isTv = s != null && e != null; - const separator = isTv ? '&' : '?'; const basePath = isTv - ? `https://f.rivo.st/api/source/tv/${id}?s=${s}&e=${e}` - : `https://f.rivo.st/api/source/movie/${id}`; + ? `${BASE_URL}/api/source/tv/${id}?s=${s}&e=${e}` + : `${BASE_URL}/api/source/movie/${id}`; const headers = { - 'Accept': '*/*', - 'Referer': 'https://f.rivo.st/', - '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' + Accept: '*/*', + Referer: `${BASE_URL}/`, + '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', }; const serverIndices = Array.from({ length: 26 }, (_, i) => i + 1); + const delimiter = basePath.includes('?') ? '&' : '?'; const responses = await Promise.allSettled( serverIndices.map(serverNum => - fetchJson(`${basePath}&server=${serverNum}`, { headers }) + fetchJson(`${basePath}${delimiter}server=${serverNum}`, { headers }) .catch(() => null) ) ); @@ -34,10 +37,12 @@ export async function getStream({ id, s, e }) { allUrls.push({ url: src.url, server: `Rivo - ${data.server || src.subProvider || 'Server'}`, + quality: src.quality || 'Auto', + type: src.kind || (src.url.includes('.m3u8') ? 'hls' : 'mp4'), headers: { - Referer: 'https://f.rivo.st/', - Origin: 'https://f.rivo.st' - } + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + }, }); } } @@ -49,6 +54,6 @@ export async function getStream({ id, s, e }) { return { url: allUrls[0].url, headers: allUrls[0].headers, - allUrls + allUrls, }; } \ No newline at end of file diff --git a/src/sources/streamaggregator.js b/src/sources/streamaggregator.js index a4bf616..a7c792f 100644 --- a/src/sources/streamaggregator.js +++ b/src/sources/streamaggregator.js @@ -1,34 +1,17 @@ -import { getUA } from '../utils/helpers.js'; +import { getTmdbMetadata, getUA, resolveTmdbKey } from '../utils/helpers.js'; const BASE_URL = 'https://streamaggregator.in'; -async function resolveDetails(sdk, id, isTv) { - const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY; - if (!apiKey) return null; - const type = isTv ? 'tv' : 'movie'; - try { - const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, { - signal: AbortSignal.timeout(6000), - }); - if (!res.ok) return null; - const data = await res.json(); - const title = isTv ? data.name : data.title; - const releaseDate = isTv ? data.first_air_date : data.release_date; - const year = releaseDate ? releaseDate.split('-')[0] : null; - return { title, year }; - } catch { - return null; - } -} - -export async function getStream({ id, s, e, sdk }) { - const isTv = Boolean(s && e); +export async function getStream(args) { + const { id, s, e, sdk } = args || {}; + const isTv = s != null && e != null; const mediaType = isTv ? 'tv' : 'movie'; + const apiKey = resolveTmdbKey(args, sdk); const ua = getUA(); - const details = await resolveDetails(sdk, id, isTv); - const title = details?.title || String(id); - const year = details?.year || ''; + const meta = await getTmdbMetadata(id, isTv, apiKey); + const title = meta?.title || String(id); + const year = meta?.year ? String(meta.year) : ''; const formattedTitle = encodeURIComponent(title).replaceAll('%20', '+'); const params = new URLSearchParams({ @@ -114,6 +97,9 @@ export async function getStream({ id, s, e, sdk }) { allUrls.push({ url: streamUrl, label, + server: serverName, + quality: item.quality || 'Auto', + type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', headers, }); } catch { } @@ -123,7 +109,7 @@ export async function getStream({ id, s, e, sdk }) { return { url: allUrls[0].url, - allUrls, headers: allUrls[0].headers, + allUrls, }; } \ No newline at end of file diff --git a/src/sources/thugtv.js b/src/sources/thugtv.js index 90a4b58..6772f28 100644 --- a/src/sources/thugtv.js +++ b/src/sources/thugtv.js @@ -1,15 +1,19 @@ import { fetchJson } from '../utils/helpers.js'; -export async function getStream({ id, s, e }) { +const BASE_URL = 'https://thugtv.online'; +const API_BASE = 'https://api.thugtv.online/api'; + +export async function getStream(args) { + const { id, s, e } = args || {}; const isTv = s != null && e != null; const path = isTv ? `/stream/tv/${id}/${s}/${e}` : `/stream/movie/${id}`; - const mainUrl = `https://api.thugtv.online/api${path}`; + const mainUrl = `${API_BASE}${path}`; const headers = { - 'Accept': 'application/json', - 'Referer': 'https://thugtv.online/', - 'Origin': 'https://thugtv.online', - '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' + Accept: 'application/json', + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + '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', }; const res = await fetchJson(mainUrl, { headers }).catch(() => null); @@ -24,17 +28,19 @@ export async function getStream({ id, s, e }) { allUrls.push({ url: src.url, server: `ThugTV - ${src.name || src.label || 'Direct'} (${src.quality || 'Auto'})`, + quality: src.quality || 'Auto', + type: src.url.includes('.m3u8') ? 'hls' : 'mp4', headers: { - Referer: 'https://thugtv.online/', - Origin: 'https://thugtv.online' - } + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + }, }); continue; } if (src.resolvePath) { const cleanPath = src.resolvePath.startsWith('/') ? src.resolvePath : `/${src.resolvePath}`; - const resolveUrl = `https://thugtv.online/api${cleanPath}`; + const resolveUrl = `${BASE_URL}/api${cleanPath}`; try { const resolved = await fetchJson(resolveUrl, { headers }); @@ -43,10 +49,12 @@ export async function getStream({ id, s, e }) { allUrls.push({ url: directUrl, server: `ThugTV - ${resolved.source.name || resolved.source.label || src.name || 'Direct'}`, + quality: resolved.source.quality || src.quality || 'Auto', + type: directUrl.includes('.m3u8') ? 'hls' : 'mp4', headers: { - Referer: 'https://thugtv.online/', - Origin: 'https://thugtv.online' - } + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + }, }); } } catch { } @@ -58,6 +66,6 @@ export async function getStream({ id, s, e }) { return { url: allUrls[0].url, headers: allUrls[0].headers, - allUrls + allUrls, }; } \ No newline at end of file diff --git a/src/sources/vidapi.js b/src/sources/vidapi.js index c5f7c17..bb8fed3 100644 --- a/src/sources/vidapi.js +++ b/src/sources/vidapi.js @@ -1,115 +1,123 @@ -import { fetchJson, USER_AGENT } from '../utils/helpers.js'; - -const API_BASE = 'https://streamdata.vaplayer.ru/api.php'; -const EMBED_ORIGIN = 'https://nextgencloudfabric.com'; - -export const VERIFY_HEADERS = { - 'User-Agent': USER_AGENT, - 'Referer': `${EMBED_ORIGIN}/`, - 'Origin': EMBED_ORIGIN -}; - -function extractStreamUrls(obj, urls = new Set()) { - if (!obj) return urls; - if (typeof obj === 'string') { - if (obj.startsWith('http') && (obj.includes('.m3u8') || obj.includes('.mp4') || obj.includes('.mpd'))) { - urls.add(obj); - } - } else if (Array.isArray(obj)) { - for (const item of obj) extractStreamUrls(item, urls); - } else if (typeof obj === 'object') { - for (const val of Object.values(obj)) { - extractStreamUrls(val, urls); - } - } - return urls; -} - -export async function getStream({ id, s, e }) { - try { - const isTv = s != null && e != null; - const type = isTv ? 'tv' : 'movie'; - - const url = `${API_BASE}?tmdb=${id}&type=${type}${isTv ? `&season=${s}&episode=${e}` : ''}`; - - const data = await fetchJson(url, { - headers: { - 'Accept': '*/*', - 'Origin': EMBED_ORIGIN, - 'Referer': `${EMBED_ORIGIN}/`, - 'User-Agent': USER_AGENT - }, - signal: AbortSignal.timeout(10000) - }); - - const streamUrls = Array.from(extractStreamUrls(data)); - if (!streamUrls.length) return null; - - let subs = []; - if (data.subtitles && Array.isArray(data.subtitles)) { - subs = data.subtitles; - } else if (data.captions && Array.isArray(data.captions)) { - subs = data.captions; - } - - const subtitles = subs.map(sub => ({ - url: sub.url || sub.file || sub.src, - lang: sub.display || sub.label || sub.language || sub.lang || 'Unknown' - })).filter(s => s.url); - - const allUrls = streamUrls.map(streamUrl => { - let type = 'mp4'; - if (streamUrl.includes('.mpd')) type = 'dash'; - else if (streamUrl.includes('.m3u8')) type = 'hls'; - - return { - url: streamUrl, - server: 'VidAPI', - quality: 'Auto', - type, - headers: VERIFY_HEADERS, - subtitles: subtitles.length > 0 ? subtitles : undefined, - skipProxy: false - }; - }); - - return allUrls.length ? { allUrls } : null; - } catch { - return null; - } -} - -export async function getSources() { - return ['VidAPI']; -} - -export async function proxyStream({ url, res, proxyUtils: { fetchUpstream, rewriteM3u8 } }) { - try { - const upstream = await fetchUpstream(url, 0, VERIFY_HEADERS); - if (!upstream) return res.end(); - - const ct = (upstream.headers?.['content-type'] || '').toLowerCase(); - const isM3u8 = ct.includes('mpegurl') || ct.includes('m3u8') || /\.m3u8?(\?|$)/i.test(url); - - if (isM3u8) { - const chunks = []; - for await (const c of upstream) chunks.push(c); - let body = Buffer.concat(chunks).toString('utf8'); - - res.setHeader('Content-Type', 'application/vnd.apple.mpegurl'); - res.setHeader('Access-Control-Allow-Origin', '*'); - - return res.end(rewriteM3u8(body, url, '&va=1&tt=1')); - } - - res.setHeader('Content-Type', ct || 'application/octet-stream'); - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Cache-Control', 'public, max-age=3600'); - upstream.pipe(res); - } catch { - if (!res.headersSent) { - res.statusCode = 502; - res.end(); - } - } +import { fetchJson, USER_AGENT } from '../utils/helpers.js'; + +const API_BASE = 'https://streamdata.vaplayer.ru/api.php'; +const EMBED_ORIGIN = 'https://nextgencloudfabric.com'; + +export const VERIFY_HEADERS = { + 'User-Agent': USER_AGENT, + Referer: `${EMBED_ORIGIN}/`, + Origin: EMBED_ORIGIN, +}; + +function extractStreamUrls(obj, urls = new Set()) { + if (!obj) return urls; + if (typeof obj === 'string') { + if (obj.startsWith('http') && (obj.includes('.m3u8') || obj.includes('.mp4') || obj.includes('.mpd'))) { + urls.add(obj); + } + } else if (Array.isArray(obj)) { + for (const item of obj) extractStreamUrls(item, urls); + } else if (typeof obj === 'object') { + for (const val of Object.values(obj)) { + extractStreamUrls(val, urls); + } + } + return urls; +} + +export async function getStream(args) { + const { id, s, e } = args || {}; + const isTv = s != null && e != null; + const type = isTv ? 'tv' : 'movie'; + + const url = `${API_BASE}?tmdb=${id}&type=${type}${isTv ? `&season=${s}&episode=${e}` : ''}`; + + try { + const data = await fetchJson(url, { + headers: { + Accept: '*/*', + Origin: EMBED_ORIGIN, + Referer: `${EMBED_ORIGIN}/`, + 'User-Agent': USER_AGENT, + }, + signal: AbortSignal.timeout(10000), + }); + + const streamUrls = Array.from(extractStreamUrls(data)); + if (!streamUrls.length) return null; + + let subs = []; + if (data.subtitles && Array.isArray(data.subtitles)) { + subs = data.subtitles; + } else if (data.captions && Array.isArray(data.captions)) { + subs = data.captions; + } + + const subtitles = subs.map(sub => ({ + url: sub.url || sub.file || sub.src, + lang: sub.display || sub.label || sub.language || sub.lang || 'Unknown', + })).filter(subItem => subItem.url); + + const allUrls = streamUrls.map(streamUrl => { + let type = 'mp4'; + if (streamUrl.includes('.mpd')) type = 'dash'; + else if (streamUrl.includes('.m3u8')) type = 'hls'; + + return { + url: streamUrl, + server: 'VidAPI', + quality: 'Auto', + type, + headers: VERIFY_HEADERS, + subtitles: subtitles.length > 0 ? subtitles : undefined, + skipProxy: false, + }; + }); + + if (!allUrls.length) return null; + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + ...(subtitles.length > 0 ? { subtitles } : {}), + }; + } catch { + return null; + } +} + +export async function getSources() { + return ['VidAPI']; +} + +export async function proxyStream({ url, res, proxyUtils: { fetchUpstream, rewriteM3u8 } }) { + try { + const upstream = await fetchUpstream(url, 0, VERIFY_HEADERS); + if (!upstream) return res.end(); + + const ct = (upstream.headers?.['content-type'] || '').toLowerCase(); + const isM3u8 = ct.includes('mpegurl') || ct.includes('m3u8') || /\.m3u8?(\?|$)/i.test(url); + + if (isM3u8) { + const chunks = []; + for await (const c of upstream) chunks.push(c); + let body = Buffer.concat(chunks).toString('utf8'); + + res.setHeader('Content-Type', 'application/vnd.apple.mpegurl'); + res.setHeader('Access-Control-Allow-Origin', '*'); + + return res.end(rewriteM3u8(body, url, '&va=1&tt=1')); + } + + res.setHeader('Content-Type', ct || 'application/octet-stream'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Cache-Control', 'public, max-age=3600'); + upstream.pipe(res); + } catch { + if (!res.headersSent) { + res.statusCode = 502; + res.end(); + } + } } \ No newline at end of file diff --git a/src/sources/vidcore.js b/src/sources/vidcore.js index 4927ce6..9466e15 100644 --- a/src/sources/vidcore.js +++ b/src/sources/vidcore.js @@ -1,123 +1,15 @@ -import { getUA } from '../utils/helpers.js'; +import { scrapeVidcoreFamily } from '../utils/helpers.js'; const BASE_URL = 'https://vidcore.io'; -const API = 'https://enc-dec.app/api'; -export async function getStream({ id, s, e }) { - const isTv = Boolean(s && e); - const targetUrl = isTv - ? `${BASE_URL}/tv/${id}/${s}/${e}/` - : `${BASE_URL}/movie/${id}`; - - const ua = getUA(); - const baseHeaders = { - 'User-Agent': ua, - 'Referer': `${BASE_URL}/`, - 'X-Requested-With': 'XMLHttpRequest', - }; - - try { - const pageRes = await fetch(targetUrl, { - headers: { 'User-Agent': ua }, - signal: AbortSignal.timeout(10000), - }); - if (!pageRes.ok) return null; - const html = await pageRes.text(); - - const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/); - if (!match || !match[1]) return null; - const enToken = match[1]; - - const s1Res = await fetch(`${API}/enc-vidcore?text=${encodeURIComponent(enToken)}&stage=1`, { - signal: AbortSignal.timeout(8000), - }); - const s1Data = await s1Res.json(); - if (s1Data?.status !== 200 || !s1Data?.result?.stage1) return null; - - const token1 = s1Data.result.token; - const stage1Url = s1Data.result.stage1; - - const post1Res = await fetch(stage1Url, { - method: 'POST', - headers: { ...baseHeaders, 'X-CSRF-Token': token1 }, - signal: AbortSignal.timeout(10000), - }); - if (!post1Res.ok) return null; - const stage1Text = await post1Res.text(); - - const s2Res = await fetch(`${API}/enc-vidcore?text=${encodeURIComponent(stage1Text)}&stage=2`, { - signal: AbortSignal.timeout(8000), - }); - const s2Data = await s2Res.json(); - if (s2Data?.status !== 200 || !s2Data?.result?.servers) return null; - - const { servers: serversUrl, stream: streamBaseUrl, token: token2 } = s2Data.result; - const currentHeaders = { ...baseHeaders, 'X-CSRF-Token': token2 }; - - const serversRes = await fetch(serversUrl, { - method: 'POST', - headers: currentHeaders, - signal: AbortSignal.timeout(10000), - }); - if (!serversRes.ok) return null; - const serversEncrypted = await serversRes.text(); - - const decServersRes = await fetch(`${API}/dec-vidcore`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ text: serversEncrypted }), - signal: AbortSignal.timeout(8000), - }); - const decServersData = await decServersRes.json(); - if (decServersData?.status !== 200 || !Array.isArray(decServersData.result)) return null; - - const allUrls = []; - - for (const server of decServersData.result) { - if (!server?.data) continue; - try { - const streamUrl = `${streamBaseUrl}/${server.data}`; - const streamEncRes = await fetch(streamUrl, { - method: 'POST', - headers: currentHeaders, - signal: AbortSignal.timeout(8000), - }); - if (!streamEncRes.ok) continue; - const streamEncText = await streamEncRes.text(); - - const decStreamRes = await fetch(`${API}/dec-vidcore`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ text: streamEncText }), - signal: AbortSignal.timeout(8000), - }); - const decStreamData = await decStreamRes.json(); - if (decStreamData?.status !== 200 || !decStreamData.result) continue; - - const streamResult = decStreamData.result; - const resultUrl = typeof streamResult === 'string' ? streamResult : streamResult?.url || streamResult?.file; - - if (typeof resultUrl === 'string' && resultUrl.startsWith('http')) { - allUrls.push({ - url: resultUrl, - label: server.name || server.title || 'VidCore', - headers: { - 'User-Agent': ua, - 'Referer': `${BASE_URL}/`, - }, - }); - } - } catch { } - } - - if (allUrls.length === 0) return null; - - return { - url: allUrls[0].url, - allUrls, - headers: allUrls[0].headers, - }; - } catch { - return null; - } +export async function getStream(args) { + const { id, s, e } = args || {}; + return await scrapeVidcoreFamily({ + id, + s, + e, + baseUrl: BASE_URL, + apiName: 'vidcore', + serverLabel: 'VidCore', + }); } \ No newline at end of file diff --git a/src/sources/vidfast.js b/src/sources/vidfast.js index d1f186a..b405868 100644 --- a/src/sources/vidfast.js +++ b/src/sources/vidfast.js @@ -1,123 +1,15 @@ -import { getUA } from '../utils/helpers.js'; +import { scrapeVidcoreFamily } from '../utils/helpers.js'; const BASE_URL = 'https://vidfast.vc'; -const API = 'https://enc-dec.app/api'; -export async function getStream({ id, s, e }) { - const isTv = Boolean(s && e); - const targetUrl = isTv - ? `${BASE_URL}/tv/${id}/${s}/${e}/` - : `${BASE_URL}/movie/${id}`; - - const ua = getUA(); - const baseHeaders = { - 'User-Agent': ua, - 'Referer': `${BASE_URL}/`, - 'X-Requested-With': 'XMLHttpRequest', - }; - - try { - const pageRes = await fetch(targetUrl, { - headers: { 'User-Agent': ua }, - signal: AbortSignal.timeout(10000), - }); - if (!pageRes.ok) return null; - const html = await pageRes.text(); - - const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/); - if (!match || !match[1]) return null; - const enToken = match[1]; - - const s1Res = await fetch(`${API}/enc-vidfast?text=${encodeURIComponent(enToken)}&stage=1`, { - signal: AbortSignal.timeout(8000), - }); - const s1Data = await s1Res.json(); - if (s1Data?.status !== 200 || !s1Data?.result?.stage1) return null; - - const token1 = s1Data.result.token; - const stage1Url = s1Data.result.stage1; - - const post1Res = await fetch(stage1Url, { - method: 'POST', - headers: { ...baseHeaders, 'X-CSRF-Token': token1 }, - signal: AbortSignal.timeout(10000), - }); - if (!post1Res.ok) return null; - const stage1Text = await post1Res.text(); - - const s2Res = await fetch(`${API}/enc-vidfast?text=${encodeURIComponent(stage1Text)}&stage=2`, { - signal: AbortSignal.timeout(8000), - }); - const s2Data = await s2Res.json(); - if (s2Data?.status !== 200 || !s2Data?.result?.servers) return null; - - const { servers: serversUrl, stream: streamBaseUrl, token: token2 } = s2Data.result; - const currentHeaders = { ...baseHeaders, 'X-CSRF-Token': token2 }; - - const serversRes = await fetch(serversUrl, { - method: 'POST', - headers: currentHeaders, - signal: AbortSignal.timeout(10000), - }); - if (!serversRes.ok) return null; - const serversEncrypted = await serversRes.text(); - - const decServersRes = await fetch(`${API}/dec-vidfast`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ text: serversEncrypted }), - signal: AbortSignal.timeout(8000), - }); - const decServersData = await decServersRes.json(); - if (decServersData?.status !== 200 || !Array.isArray(decServersData.result)) return null; - - const allUrls = []; - - for (const server of decServersData.result) { - if (!server?.data) continue; - try { - const streamUrl = `${streamBaseUrl}/${server.data}`; - const streamEncRes = await fetch(streamUrl, { - method: 'POST', - headers: currentHeaders, - signal: AbortSignal.timeout(8000), - }); - if (!streamEncRes.ok) continue; - const streamEncText = await streamEncRes.text(); - - const decStreamRes = await fetch(`${API}/dec-vidfast`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ text: streamEncText }), - signal: AbortSignal.timeout(8000), - }); - const decStreamData = await decStreamRes.json(); - if (decStreamData?.status !== 200 || !decStreamData.result) continue; - - const streamResult = decStreamData.result; - const resultUrl = typeof streamResult === 'string' ? streamResult : streamResult?.url || streamResult?.file; - - if (typeof resultUrl === 'string' && resultUrl.startsWith('http')) { - allUrls.push({ - url: resultUrl, - label: server.name || server.title || 'VidFast', - headers: { - 'User-Agent': ua, - 'Referer': `${BASE_URL}/`, - }, - }); - } - } catch { } - } - - if (allUrls.length === 0) return null; - - return { - url: allUrls[0].url, - allUrls, - headers: allUrls[0].headers, - }; - } catch { - return null; - } +export async function getStream(args) { + const { id, s, e } = args || {}; + return await scrapeVidcoreFamily({ + id, + s, + e, + baseUrl: BASE_URL, + apiName: 'vidfast', + serverLabel: 'VidFast', + }); } \ No newline at end of file diff --git a/src/sources/vidlink.js b/src/sources/vidlink.js index 3a00283..e9802b3 100644 --- a/src/sources/vidlink.js +++ b/src/sources/vidlink.js @@ -1,76 +1,79 @@ -import { fetchJson, USER_AGENT } from '../utils/helpers.js'; - -const BASE = "https://vidlink.pro"; -const HEADERS = { 'User-Agent': USER_AGENT, Origin: BASE, Referer: `${BASE}/` }; - -export async function getStream({ id, s, e }) { - try { - const encData = await fetchJson(`https://enc-dec.app/api/enc-vidlink?text=${id}`, { signal: AbortSignal.timeout(6000) }); - if (encData?.status !== 200 || !encData?.result) return null; - - const basePath = s ? `${BASE}/api/b/tv/${encData.result}/${s}/${e || 1}` : `${BASE}/api/b/movie/${encData.result}`; - const responses = await Promise.allSettled([ - fetchJson(basePath, { headers: HEADERS, signal: AbortSignal.timeout(10000) }), - fetchJson(`${basePath}?multiLang=0`, { headers: HEADERS, signal: AbortSignal.timeout(10000) }) - ]); - - const streamList = []; - const seenUrls = new Set(); - const seenSubs = new Set(); - - for (const res of responses) { - if (res.status !== 'fulfilled' || !res.value?.stream) continue; - const stream = res.value.stream; - - if (stream.playlist && !seenUrls.has(stream.playlist)) { - seenUrls.add(stream.playlist); - const isDash = stream.type === 'dash' || stream.deliveryType === 'dash' || stream.playlist.includes('.mpd'); - streamList.push({ - url: stream.playlist, - quality: stream.playbackMetadata?.resolutions?.[0] ? `${stream.playbackMetadata.resolutions[0]}p` : 'Auto', - label: `Vidlink ${isDash ? 'DASH' : 'HLS'}`, - name: `Vidlink ${isDash ? 'DASH' : 'HLS'}`, - type: isDash ? 'dash' : 'hls', - headers: stream.playlistHeaders || {}, - server: 'Vidlink', - }); - } - - if (stream.qualities && typeof stream.qualities === 'object') { - const qualityOrder = ['1080', '720', '480', '360', ...Object.keys(stream.qualities)]; - const seenQualities = new Set(); - - for (const q of qualityOrder) { - if (seenQualities.has(q)) continue; - seenQualities.add(q); - const qData = stream.qualities[q]; - if (!qData?.url || seenUrls.has(qData.url)) continue; - seenUrls.add(qData.url); - streamList.push({ - url: qData.url, - quality: `${q}p`, - label: `Vidlink ${q}p`, - name: `Vidlink ${q}p`, - type: qData.type || 'mp4', - headers: qData.headers || {}, - server: 'Vidlink', - }); - } - } - } - - if (!streamList.length) return null; - - return { - allUrls: streamList, - streams: streamList, - }; - } catch { - return null; - } -} - -export async function getSources(args) { - const stream = await getStream(args); - return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server || 'Vidlink'))] : []; +import { fetchJson, USER_AGENT } from '../utils/helpers.js'; + +const BASE_URL = 'https://vidlink.pro'; +const HEADERS = { 'User-Agent': USER_AGENT, Origin: BASE_URL, Referer: `${BASE_URL}/` }; + +export async function getStream(args) { + const { id, s, e } = args || {}; + + try { + const encData = await fetchJson(`https://enc-dec.app/api/enc-vidlink?text=${id}`, { signal: AbortSignal.timeout(6000) }); + if (encData?.status !== 200 || !encData?.result) return null; + + const basePath = s ? `${BASE_URL}/api/b/tv/${encData.result}/${s}/${e || 1}` : `${BASE_URL}/api/b/movie/${encData.result}`; + const responses = await Promise.allSettled([ + fetchJson(basePath, { headers: HEADERS, signal: AbortSignal.timeout(10000) }), + fetchJson(`${basePath}?multiLang=0`, { headers: HEADERS, signal: AbortSignal.timeout(10000) }), + ]); + + const streamList = []; + const seenUrls = new Set(); + + for (const res of responses) { + if (res.status !== 'fulfilled' || !res.value?.stream) continue; + const stream = res.value.stream; + + if (stream.playlist && !seenUrls.has(stream.playlist)) { + seenUrls.add(stream.playlist); + const isDash = stream.type === 'dash' || stream.deliveryType === 'dash' || stream.playlist.includes('.mpd'); + streamList.push({ + url: stream.playlist, + quality: stream.playbackMetadata?.resolutions?.[0] ? `${stream.playbackMetadata.resolutions[0]}p` : 'Auto', + label: `Vidlink ${isDash ? 'DASH' : 'HLS'}`, + name: `Vidlink ${isDash ? 'DASH' : 'HLS'}`, + server: 'Vidlink', + type: isDash ? 'dash' : 'hls', + headers: stream.playlistHeaders || {}, + }); + } + + if (stream.qualities && typeof stream.qualities === 'object') { + const qualityOrder = ['1080', '720', '480', '360', ...Object.keys(stream.qualities)]; + const seenQualities = new Set(); + + for (const q of qualityOrder) { + if (seenQualities.has(q)) continue; + seenQualities.add(q); + const qData = stream.qualities[q]; + if (!qData?.url || seenUrls.has(qData.url)) continue; + seenUrls.add(qData.url); + streamList.push({ + url: qData.url, + quality: `${q}p`, + label: `Vidlink ${q}p`, + name: `Vidlink ${q}p`, + server: 'Vidlink', + type: qData.type || 'mp4', + headers: qData.headers || {}, + }); + } + } + } + + if (!streamList.length) return null; + + return { + url: streamList[0].url, + headers: streamList[0].headers, + allUrls: streamList, + streams: streamList, + }; + } catch { + return null; + } +} + +export async function getSources(args) { + const stream = await getStream(args); + return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server || 'Vidlink'))] : []; } \ No newline at end of file diff --git a/src/sources/vidrock.js b/src/sources/vidrock.js deleted file mode 100644 index 7ff5f00..0000000 --- a/src/sources/vidrock.js +++ /dev/null @@ -1,145 +0,0 @@ -import { webcrypto } from 'node:crypto'; -import { fetchJson, USER_AGENT } from '../utils/helpers.js'; - -const GCM_HEX_KEY = '7f3e9c2a8b5d1f4e6a9c3b7d2e5f8a1c4b6d9e2f5a8c1b4d7e9f2a5c8b1d4e7f'; -const BASE_URL = 'https://vidrock.ru/'; -const PROXY_PREFIX = 'https://proxy.vidrock.store/'; - -let cryptoKey = null; - -function hexToBytes(hex) { - const bytes = new Uint8Array(hex.length / 2); - - for (let i = 0; i < bytes.length; i++) { - bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); - } - - return bytes; -} - -function base64UrlToBytes(value) { - let base64 = value.replace(/-/g, '+').replace(/_/g, '/'); - - const padding = base64.length % 4; - - if (padding === 2) { - base64 += '=='; - } else if (padding === 3) { - base64 += '='; - } else if (padding === 1) { - throw new Error('Invalid base64url'); - } - - return new Uint8Array(Buffer.from(base64, 'base64')); -} - -async function getKey() { - if (cryptoKey) return cryptoKey; - - const keyBytes = hexToBytes(GCM_HEX_KEY); - - cryptoKey = await webcrypto.subtle.importKey( - 'raw', - keyBytes, - 'AES-GCM', - false, - ['decrypt'] - ); - - return cryptoKey; -} - -async function decryptStreamUrl(value) { - try { - const data = base64UrlToBytes(value); - - if (data.length < 28) { - throw new Error('Ciphertext too short'); - } - - const iv = data.slice(0, 12); - const ciphertext = data.slice(12); - - const key = await getKey(); - - const decrypted = await webcrypto.subtle.decrypt( - { - name: 'AES-GCM', - iv - }, - key, - ciphertext - ); - - return new TextDecoder().decode(decrypted); - } catch (error) { - return null; - } -} - -export async function getStream({ id, s, e }) { - try { - const path = s - ? `tv/${id}/${s}/${e || 1}` - : `movie/${id}`; - - const apiData = await fetchJson(`${BASE_URL}api/${path}`, { - headers: { - 'User-Agent': USER_AGENT, - Referer: BASE_URL, - Origin: BASE_URL - } - }); - - if (!apiData || typeof apiData !== 'object') { - return null; - } - - const streams = []; - - for (const [provider, info] of Object.entries(apiData)) { - if (!info?.url) { - continue; - } - - const decryptedUrl = await decryptStreamUrl(info.url); - - if (!decryptedUrl) { - continue; - } - - let url = decryptedUrl; - - if (url.startsWith(PROXY_PREFIX)) { - url = decodeURIComponent( - url.slice(PROXY_PREFIX.length).replace(/^\/+/, '') - ); - } - - const isXpass = url.includes('xpass.top'); - - streams.push({ - name: provider, - url, - file: url, - stream: url, - src: url, - quality: info.type || 'auto', - type: info.type || 'hls', - headers: { - 'User-Agent': USER_AGENT, - Referer: isXpass - ? 'https://play.xpass.top/' - : BASE_URL, - Origin: isXpass - ? 'https://play.xpass.top' - : 'https://vidrock.ru' - } - }); - } - - return streams.length ? { streams } : null; - } catch (error) { - return null; - } -} \ No newline at end of file diff --git a/src/sources/vidup.js b/src/sources/vidup.js index ca6ad65..dc51beb 100644 --- a/src/sources/vidup.js +++ b/src/sources/vidup.js @@ -1,123 +1,15 @@ -import { getUA } from '../utils/helpers.js'; - -const BASE_URL = 'https://vidup.to'; -const API = 'https://enc-dec.app/api'; - -export async function getStream({ id, s, e }) { - const isTv = Boolean(s && e); - const targetUrl = isTv - ? `${BASE_URL}/tv/${id}/${s}/${e}/` - : `${BASE_URL}/movie/${id}`; - - const ua = getUA(); - const baseHeaders = { - 'User-Agent': ua, - 'Referer': `${BASE_URL}/`, - 'X-Requested-With': 'XMLHttpRequest', - }; - - try { - const pageRes = await fetch(targetUrl, { - headers: { 'User-Agent': ua }, - signal: AbortSignal.timeout(10000), - }); - if (!pageRes.ok) return null; - const html = await pageRes.text(); - - const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/); - if (!match || !match[1]) return null; - const enToken = match[1]; - - const s1Res = await fetch(`${API}/enc-vidup?text=${encodeURIComponent(enToken)}&stage=1`, { - signal: AbortSignal.timeout(8000), - }); - const s1Data = await s1Res.json(); - if (s1Data?.status !== 200 || !s1Data?.result?.stage1) return null; - - const token1 = s1Data.result.token; - const stage1Url = s1Data.result.stage1; - - const post1Res = await fetch(stage1Url, { - method: 'POST', - headers: { ...baseHeaders, 'X-CSRF-Token': token1 }, - signal: AbortSignal.timeout(10000), - }); - if (!post1Res.ok) return null; - const stage1Text = await post1Res.text(); - - const s2Res = await fetch(`${API}/enc-vidup?text=${encodeURIComponent(stage1Text)}&stage=2`, { - signal: AbortSignal.timeout(8000), - }); - const s2Data = await s2Res.json(); - if (s2Data?.status !== 200 || !s2Data?.result?.servers) return null; - - const { servers: serversUrl, stream: streamBaseUrl, token: token2 } = s2Data.result; - const currentHeaders = { ...baseHeaders, 'X-CSRF-Token': token2 }; - - const serversRes = await fetch(serversUrl, { - method: 'POST', - headers: currentHeaders, - signal: AbortSignal.timeout(10000), - }); - if (!serversRes.ok) return null; - const serversEncrypted = await serversRes.text(); - - const decServersRes = await fetch(`${API}/dec-vidup`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ text: serversEncrypted }), - signal: AbortSignal.timeout(8000), - }); - const decServersData = await decServersRes.json(); - if (decServersData?.status !== 200 || !Array.isArray(decServersData.result)) return null; - - const allUrls = []; - - for (const server of decServersData.result) { - if (!server?.data) continue; - try { - const streamUrl = `${streamBaseUrl}/${server.data}`; - const streamEncRes = await fetch(streamUrl, { - method: 'POST', - headers: currentHeaders, - signal: AbortSignal.timeout(8000), - }); - if (!streamEncRes.ok) continue; - const streamEncText = await streamEncRes.text(); - - const decStreamRes = await fetch(`${API}/dec-vidup`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ text: streamEncText }), - signal: AbortSignal.timeout(8000), - }); - const decStreamData = await decStreamRes.json(); - if (decStreamData?.status !== 200 || !decStreamData.result) continue; - - const streamResult = decStreamData.result; - const resultUrl = typeof streamResult === 'string' ? streamResult : streamResult?.url || streamResult?.file; - - if (typeof resultUrl === 'string' && resultUrl.startsWith('http')) { - allUrls.push({ - url: resultUrl, - label: server.name || server.title || 'VidUp', - headers: { - 'User-Agent': ua, - 'Referer': `${BASE_URL}/`, - }, - }); - } - } catch { } - } - - if (allUrls.length === 0) return null; - - return { - url: allUrls[0].url, - allUrls, - headers: allUrls[0].headers, - }; - } catch { - return null; - } +import { scrapeVidcoreFamily } from '../utils/helpers.js'; + +const BASE_URL = 'https://vidup.to'; + +export async function getStream(args) { + const { id, s, e } = args || {}; + return await scrapeVidcoreFamily({ + id, + s, + e, + baseUrl: BASE_URL, + apiName: 'vidup', + serverLabel: 'VidUp', + }); } \ No newline at end of file diff --git a/src/sources/vidvault.js b/src/sources/vidvault.js index 8ba0bef..12670a4 100644 --- a/src/sources/vidvault.js +++ b/src/sources/vidvault.js @@ -1,4 +1,4 @@ -import { USER_AGENT } from '../utils/helpers.js'; +import { formatBytes, USER_AGENT } from '../utils/helpers.js'; const BASE_URL = 'https://vidvault.to'; const API_BASE = `${BASE_URL}/api`; @@ -6,21 +6,13 @@ const DL_PROXY_BASE = 'https://dl.gemlelispe.workers.dev'; const SUB_PROXY_BASE = 'https://sub.k5s7sjozpn.workers.dev'; const HEADERS = { - 'Referer': `${BASE_URL}/`, - 'Origin': BASE_URL, + Referer: `${BASE_URL}/`, + Origin: BASE_URL, 'User-Agent': USER_AGENT, - 'Accept': 'application/json, text/plain, */*', + Accept: 'application/json, text/plain, */*', 'Accept-Language': 'en-US,en;q=0.9', }; -function formatBytes(bytes, decimals = 2) { - if (!bytes || bytes <= 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1); - return `${(bytes / Math.pow(k, i)).toFixed(decimals)} ${sizes[i]}`; -} - async function getToken() { const res = await fetch(`${API_BASE}/get-token`, { method: 'GET', @@ -36,26 +28,73 @@ function getCaptionType(url) { return String(url).toLowerCase().includes('.vtt') ? 'vtt' : 'srt'; } -export async function getStream(args) { - const { id, s, e } = args; - const isTv = s != null && e != null; - const type = isTv ? 'tv' : 'movie'; - +async function fetchJson(url, options = {}) { try { - const token = await getToken(); - if (!token) return null; + const res = await fetch(url, options); + if (!res.ok) return null; + return await res.json(); + } catch { + return null; + } +} - const payload = { - type, - tmdbId: String(id), - }; +async function resolveDirect(directPath, headers) { + const fullUrl = directPath.startsWith('http') ? directPath : `${BASE_URL}${directPath}`; + try { + const res = await fetch(fullUrl, { + method: 'HEAD', + redirect: 'manual', + headers, + }); + const loc = res.headers.get('location'); + if (loc) return loc; + } catch { } + return fullUrl; +} - if (isTv) { - payload.season = Number(s); - payload.episode = Number(e); - } +export async function getStream(args) { + const { id, s, e } = args || {}; + const isTv = s != null && e != null; - const res = await fetch(`${API_BASE}/download-proxy`, { + const mediaPath = isTv + ? `${BASE_URL}/media/tv/${id}/${s}/${e}` + : `${BASE_URL}/media/movie/${id}`; + + const [mediaData, tokenData] = await Promise.all([ + fetchJson(mediaPath, { headers: HEADERS }).catch(() => null), + fetchJson(`${BASE_URL}/api/get-token`, { headers: HEADERS }).catch(() => null), + ]); + + const allUrls = []; + const seen = new Set(); + + if (Array.isArray(mediaData?.direct)) { + await Promise.all( + mediaData.direct.map(async (item) => { + if (!item?.path) return; + const streamUrl = await resolveDirect(item.path, HEADERS); + if (streamUrl && !seen.has(streamUrl)) { + seen.add(streamUrl); + const format = item.format || (streamUrl.includes('.mkv') ? 'mkv' : 'mp4'); + allUrls.push({ + url: streamUrl, + server: `VidVault - Direct ${format.toUpperCase()} (${item.quality || item.size || 'Auto'})`, + quality: item.quality || 'Auto', + type: format, + headers: HEADERS, + }); + } + }) + ); + } + + const token = tokenData?.t; + if (token) { + const payload = isTv + ? { type: 'tv', tmdbId: String(id), season: String(s), episode: String(e) } + : { type: 'movie', tmdbId: String(id) }; + + const proxyData = await fetchJson(`${BASE_URL}/api/download-proxy`, { method: 'POST', headers: { ...HEADERS, @@ -63,126 +102,33 @@ export async function getStream(args) { 'x-request-token': token, }, body: JSON.stringify(payload), - signal: AbortSignal.timeout(15000), - }); + }).catch(() => null); - if (!res.ok) return null; - const data = await res.json(); - - const allUrls = []; - const subtitles = []; - const titleTag = encodeURIComponent(isTv ? `Show_${id}_S${s}E${e}` : `Movie_${id}`); - - const mp4 = data?.mp4Data; - const downloadData = mp4?.downloadInfo?.data || mp4?.data?.data || mp4?.data || mp4; - - if (downloadData) { - const streams = downloadData.streams || downloadData.downloads || []; - for (const item of streams) { - if (!item?.url) continue; - - const res = item.resolutions || item.resolution || 'Auto'; - const sizeStr = item.size ? ` (${typeof item.size === 'number' ? formatBytes(item.size) : item.size})` : ''; - const proxiedUrl = `${DL_PROXY_BASE}/${encodeURIComponent(item.url)}?n=${titleTag}`; + const candidates = [ + { data: proxyData?.mp4Data, type: 'mp4', label: 'MP4' }, + { data: proxyData?.mkvData, type: 'mkv', label: 'MKV' }, + { data: proxyData?.mkvV2Data, type: 'mkv', label: 'MKV V2' }, + ]; + for (const { data, type, label } of candidates) { + if (data?.url && !seen.has(data.url)) { + seen.add(data.url); allUrls.push({ - url: proxiedUrl, - server: `VidVault - MP4 ${res}p${sizeStr}`.trim(), - quality: typeof res === 'number' ? `${res}p` : res, - type: 'mp4', + url: data.url, + server: `VidVault - ${label} (${data.quality || data.size || 'Auto'})`, + quality: data.quality || 'Auto', + type, headers: HEADERS, }); } - - const captions = downloadData.captions || []; - for (const cap of captions) { - if (!cap?.url) continue; - const subUrl = `${SUB_PROXY_BASE}/?url=${encodeURIComponent(cap.url)}&title=${titleTag}`; - subtitles.push({ - label: cap.lanName || cap.lan || 'Unknown', - type: getCaptionType(cap.url), - language: cap.lan || '', - url: subUrl, - }); - } } - - const mkvFiles = data?.mkvData?.files || []; - for (const file of mkvFiles) { - if (!file?.url) continue; - const sizeStr = file.size ? ` (${typeof file.size === 'number' ? formatBytes(file.size) : file.size})` : ''; - allUrls.push({ - url: file.url, - server: `VidVault - MKV 480p${sizeStr}`.trim(), - quality: '480p', - type: 'mp4', - headers: HEADERS, - }); - } - - const mkvV2 = data?.mkvV2Data; - const mkvV2List = Array.isArray(mkvV2) - ? mkvV2 - : Array.isArray(mkvV2?.files) - ? mkvV2.files - : (mkvV2?.url ? [mkvV2] : []); - - for (const file of mkvV2List) { - if (!file?.url) continue; - const quality = file.quality ? String(file.quality).replace(/p$/i, '') : 'Auto'; - const sizeStr = file.size ? ` (${typeof file.size === 'number' ? formatBytes(file.size) : file.size})` : ''; - const meta = [file.country, file.language].filter(Boolean).join(' • '); - const metaStr = meta ? ` [${meta}]` : ''; - - allUrls.push({ - url: file.url, - server: `VidVault - MKV v2 ${quality}p${sizeStr}${metaStr}`.trim(), - quality: `${quality}p`, - type: 'mp4', - headers: HEADERS, - }); - } - - const mkvV3 = data?.mkvV3Data; - if (mkvV3?.downloads && Array.isArray(mkvV3.downloads)) { - const country = mkvV3.country || ''; - const language = mkvV3.language || ''; - - for (const down of mkvV3.downloads) { - if (Array.isArray(down?.qualities)) { - for (const qObj of down.qualities) { - for (const ep of qObj?.episodes || []) { - if (!ep?.url) continue; - const quality = qObj.quality ? String(qObj.quality).replace(/p$/i, '') : 'Auto'; - const sizeStr = ep.size ? ` (${typeof ep.size === 'number' ? formatBytes(ep.size) : ep.size})` : ''; - const meta = [ep.country || country, ep.language || language].filter(Boolean).join(' • '); - const metaStr = meta ? ` [${meta}]` : ''; - - allUrls.push({ - url: ep.url, - server: `VidVault - MKV v3 ${quality}p${sizeStr}${metaStr}`.trim(), - quality: `${quality}p`, - type: 'mp4', - headers: HEADERS, - }); - } - } - } else if (down?.url) { - const quality = down.quality ? String(down.quality).replace(/p$/i, '') : 'Auto'; - const sizeStr = down.size ? ` (${typeof down.size === 'number' ? formatBytes(down.size) : down.size})` : ''; - allUrls.push({ - url: down.url, - server: `VidVault - MKV v3 ${quality}p${sizeStr}`.trim(), - quality: `${quality}p`, - type: 'mp4', - headers: HEADERS, - }); - } - } - } - - return allUrls.length ? { allUrls, subtitles: subtitles.length ? subtitles : undefined } : null; - } catch (err) { - return null; } + + if (!allUrls.length) return null; + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; } \ No newline at end of file diff --git a/src/sources/vidy.js b/src/sources/vidy.js index fbe309c..2e51c12 100644 --- a/src/sources/vidy.js +++ b/src/sources/vidy.js @@ -1,7 +1,7 @@ -import { getUA } from "../utils/helpers.js"; +import { getUA } from '../utils/helpers.js'; -const API_URL = "https://api.wecollege.net"; -const SERVERS = ["miami", "atlanta", "seattle", "denver", "dallas", "tampa"]; +const API_URL = 'https://api.wecollege.net'; +const SERVERS = ['miami', 'atlanta', 'seattle', 'denver', 'dallas', 'tampa']; const MAGIC = [109, 118, 109, 49]; function imul(a, b) { @@ -26,10 +26,10 @@ function gFn(e, t) { } function decryptVidy(ciphertext, seedStr, mediaId) { - let padded = ciphertext.replace(/-/g, "+").replace(/_/g, "/"); - while (padded.length % 4) padded += "="; + let padded = ciphertext.replace(/-/g, '+').replace(/_/g, '/'); + while (padded.length % 4) padded += '='; - const raw = new Uint8Array(Buffer.from(padded, "base64")); + const raw = new Uint8Array(Buffer.from(padded, 'base64')); const length = raw.length; let tFnv = 2166136261; @@ -85,7 +85,7 @@ function decryptVidy(ciphertext, seedStr, mediaId) { for (let i = 0; i < MAGIC.length; i++) { if (raw[i] !== MAGIC[i]) { - throw new Error("Vidy decrypt failed: magic header mismatch"); + throw new Error('Vidy decrypt failed: magic header mismatch'); } } @@ -93,19 +93,19 @@ function decryptVidy(ciphertext, seedStr, mediaId) { } export async function getStream(args) { - const { id, s = null, e = null } = args; + const { id, s, e } = args || {}; const isTv = s != null && e != null; const mediaId = Number(id); const headers = { - "User-Agent": getUA(), - Referer: "https://www.vidy.st/", - Origin: "https://www.vidy.st", + 'User-Agent': getUA(), + Referer: 'https://www.vidy.st/', + Origin: 'https://www.vidy.st', }; try { const seedUrl = new URL(`${API_URL}/seed`); - seedUrl.searchParams.set("mediaId", String(id)); + seedUrl.searchParams.set('mediaId', String(id)); const seedRes = await fetch(seedUrl.toString(), { headers, @@ -120,13 +120,13 @@ export async function getStream(args) { for (const server of SERVERS) { try { const srcUrl = new URL(`${API_URL}/${server}/sources`); - srcUrl.searchParams.set("mediaType", isTv ? "tv" : "movie"); - srcUrl.searchParams.set("tmdbId", String(id)); - srcUrl.searchParams.set("enc", "2"); - srcUrl.searchParams.set("seed", seed); + srcUrl.searchParams.set('mediaType', isTv ? 'tv' : 'movie'); + srcUrl.searchParams.set('tmdbId', String(id)); + srcUrl.searchParams.set('enc', '2'); + srcUrl.searchParams.set('seed', seed); if (isTv) { - srcUrl.searchParams.set("seasonId", String(s)); - srcUrl.searchParams.set("episodeId", String(e)); + srcUrl.searchParams.set('seasonId', String(s)); + srcUrl.searchParams.set('episodeId', String(e)); } const res = await fetch(srcUrl.toString(), { @@ -145,35 +145,40 @@ export async function getStream(args) { } const sources = data?.sources || []; - const playlist = data?.playlist || ""; + const playlist = data?.playlist || ''; if (!sources.length && !playlist) continue; let streamUrl = playlist; - let quality = "1080p"; + let quality = '1080p'; for (const src of sources) { - if (src?.quality === "1080p" && src?.url) { + if (src?.quality === '1080p' && src?.url) { streamUrl = src.url; - quality = "1080p"; + quality = '1080p'; break; } } if (!streamUrl && sources.length) { - streamUrl = sources[0]?.url || ""; - quality = sources[0]?.quality || ""; + streamUrl = sources[0]?.url || ''; + quality = sources[0]?.quality || ''; } if (!streamUrl) continue; + const allUrls = [{ + url: streamUrl, + headers, + server: 'Vidy', + quality, + type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', + }]; + return { url: streamUrl, - headers: { - "User-Agent": getUA(), - Referer: "https://www.vidy.st/", - Origin: "https://www.vidy.st", - }, - server: "Vidy", + headers, + server: 'Vidy', quality, - type: streamUrl.includes(".m3u8") ? "hls" : "mp4", + type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', + allUrls, }; } catch { continue; @@ -184,4 +189,4 @@ export async function getStream(args) { } catch { return null; } -} +} \ No newline at end of file diff --git a/src/sources/vidzee.js b/src/sources/vidzee.js index d133e3a..4aa244e 100644 --- a/src/sources/vidzee.js +++ b/src/sources/vidzee.js @@ -1,184 +1,190 @@ -import { USER_AGENT, fetchJson } from '../utils/helpers.js'; - -const BASE_URL = 'https://core.vidzee.wtf'; -const PLAYER_URL = 'https://player.vidzee.wtf'; -const SERVICES = ['dcloud', 'ipcloud', 'tik', 'v6:Hindi', '3AHindi']; -const WASM_B64 = 'AGFzbQEAAAABPgtgAX8Bf2ACf38Bf2ABfwBgAn9/AGADf39/AGAAAGAEf39/fwBgBH9/f38Bf2ADf39/AX9gA39/fgBgAAF/Ag0BA2VudgVhYm9ydAAGAygnAgEAAAQBAAEDBAADBQAAAgMHAAEAAAQIAwEJAgIBBQAAAgEABQoBBQMBAAEGJgd/AUEAC38BQQALfwFBAAt/AUEAC38BQQALfwFBAAt/AEGA2wALB0gHB2RlY3J5cHQAJwVfX25ldwAGBV9fcGluACQHX191bnBpbgAiCV9fY29sbGVjdAAlC19fcnR0aV9iYXNlAwYGbWVtb3J5AgAIAR8MAqkBCogkJyUAIABFBEAPCyAAQRRrIgAQByMCRgRAIAAQECAAIwUjAkUQCgsLCgAgACABai0AAAsKACAAKAIEQXxxCw0AIABBFGsoAhBBAXYLDAAgACABaiACOgAAC1MBAX8gAEHs////A0sEQEHwDEGwDUH9AEEeEAAACyMARQRAEA0LIwAgAEEQahAaIgIgATYCDCACIAA2AhAgAiMBIwIQCiACEAsjA2okAyACQRRqCwoAIAAoAgRBA3ELKQAgACAAIAFBCHZqLQAAQQV0aiABQf8BcUEDdmotAAAgAUEHcXZBAXELwwEBBH8gASgCAEF8cSIDQYACSQR/IANBBHYFQR9B/P///wMgAyADQfz///8DTxsiA2drIgRBB2shAiADIARBBGt2QRBzCyEEIAEoAgghBSABKAIEIgMEQCADIAU2AggLIAUEQCAFIAM2AgQLIAEgACACQQR0IARqQQJ0aiIBKAJgRgRAIAEgBTYCYCAFRQRAIAAgAkECdGoiASgCBEF+IAR3cSEDIAEgAzYCBCADRQRAIAAgACgCAEF+IAJ3cTYCAAsLCwspAQF/IAEoAgghAyAAIAEgAnI2AgQgACADNgIIIAMgABARIAEgADYCCAsNACAAKAIAQXxxQQRqC7wCAQV/IAEoAgAhAyABQQRqIAEoAgBBfHFqIgQoAgAiAkEBcQRAIAAgBBAJIAEgA0EEaiACQXxxaiIDNgIAIAFBBGogASgCAEF8cWoiBCgCACECCyADQQJxBEAgAUEEaygCACIBKAIAIQYgACABEAkgASAGQQRqIANBfHFqIgM2AgALIAQgAkECcjYCACAEQQRrIAE2AgAgACADQXxxIgJBgAJJBH8gAkEEdgVBH0H8////AyACIAJB/P///wNPGyICZ2siA0EHayEFIAIgA0EEa3ZBEHMLIgIgBUEEdGpBAnRqKAJgIQMgAUEANgIEIAEgAzYCCCADBEAgAyABNgIECyAAIAVBBHQgAmpBAnRqIAE2AmAgACAAKAIAQQEgBXRyNgIAIAAgBUECdGoiACAAKAIEQQEgAnRyNgIEC5cBAQJ/PwAiAEEATAR/QQEgAGtAAEEASAVBAAsEQAALQbDbAEEANgIAQdDnAEEANgIAA0AgAUEXSQRAIAFBAnRBsNsAakEANgIEQQAhAANAIABBEEkEQCABQQR0IABqQQJ0QbDbAGkBADYCYCAAQQFqIQAMAQsLIAFBAWohAQwBCwtBsNsAQdTnAD8ArEIQhhAbQbDbACQACyYBAX8gAEEEayEBIABBD3FBASAAGwR/QQEFIAEoAgBBAXELGiABCxIAIAAgADYCBCAAIAA2AgggAAsnAQF/IAAQAyIBRQRAIAAoAggaDwsgASAAKAIIIgA2AgggACABEBELEgAgACABIAAoAgRBA3FyNgIEC0kBAX8gACABQQF0aiEBA0AgAyIAQQFrIQMgAARAIAEvAQAiACACLwEAIgRHBEAgACAEaw8LIAFBAmohASACQQJqIQIMAQsLQQALbgECf0EMQQYQBiIBRQRAQQxBAxAGIQELIAFBADYCACABQQA2AgQgAUEANgIIIABB/P///wNLBEBB4NcAQZDYAEETQTkQAAALIABBARAGIgJBACAA/AsAIAEgAjYCACABIAI2AgQgASAANgIIIAELjgEBAn8gAUGAAkkEfyABQQR2BUEfIAEQFSIBZ2siA0EHayECIAEgA0EEa3ZBEHMLIgEgACACQQJ0aigCBEF/IAF0cSIBBH8gACABaCACQQR0akECdGooAmAFIAAoAgBBfyACQQFqdHEiAQR/IAAgACABaCIAQQJ0aigCBGggAEEEdGpBAnRqKAJgBUEACwsLHQAgAEEBQRsgAGdrdGpBAWsgACAAQf7///8BSRsLLwAgAEH8////A0sEQEHwDEHwDUHNA0EdEAAAC0EMIABBE2pBcHFBBGsgAEEMTRsLZwECfyABKAIAIgNBfHEgAmsiBEEQTwRAIAEgAiADQQJxcjYCACABQQRqIAJqIgEgBEEEa0EBcjYCACAAIAEQDAUgASADQX5xNgIAIAFBBGogASgCAEF8cWoiACAAKAIAQX1xNgIACwswACAAIAIQGiICQQRqIAFBBGogASgCAEF8cfwKAAAgAUGk2wBPBEAgACABEBkLIAILFQAgASABKAIAQQFyNgIAIAAgARAMC48BAQJ/IAAgARAWIgIQFCIBRQRAQQQgACgCoAw/ACIBQRB0QQRrR3QgAhAVIAIgAkGAAk8bakH//wNqQYCAfHFBEHYhAyABIAMgASADShtAAEEASARAIANAAEEASARAAAsLIAAgAUEQdD8ArEIQhhAbIAAgAhAUIQELIAEoAgAaIAAgARAJIAAgASACEBcgAQuGAQEDfyABQRNqQXBxQQRrIQEgACgCoAwiAwRAIAFBEGsiBSADRgRAIAMoAgAhBCAFIQELCyACp0FwcSABayIDQRRJBEAPCyABIARBAnEgA0EIayIDQQFycjYCACABQQA2AgQgAUEANgIIIAFBBGogA2oiA0ECNgIAIAAgAzYCoAwgACABEAwLhQEBA38CQAJAAkACQAJAAkACQAJAAkAgAEEIaygCAA4IAAECAwQFBgcICw8LDwsPCyAAEB0PCyAAKAIAEAEPCyAAKAIEIgEgACgCDEECdGohAgNAIAEgAkkEQCABKAIAIgMEQCADEAELIAFBBGohAQwBCwsgACgCABABDwsgABAdDwsPCwALCQAgACgCABABC0MAIAEgACgCDE8EQEHw1QBBsNYAQfIAQSoQAAALIAAoAgQgAUECdGooAgAiAEUEQEHg1gBBsNYAQfYAQSgQAAALIAALGQBBoA4QDyQBQYDaABAPJARB4NoAEA8kBQv9AQEGfyAAIgFBCHYiAkHMzgBqLQAAIAJBlDJqLQAAQdYAbEGUMmogAEH/AXEiA0EDbmotAAAgA0EDcEECdEGAxwBqKAIAbEELdkEGcGpBAnRBjbrowserigCACICQf8BcSEAIAJBCHUhAgJAIABBAkkNACACQf8BcSEAIAJBCHYhAgNAIAAEQCAAQQF2IgYgAmpBAXRBzNIAaiIELQAAIgUgA0YEfyAELQABQQJ0QYzHAGooAgAiAkH/AXEhACACQQh1IQIgAEECSQ0DIAFBAWoPBSADIAVJBH8gBgUgAiAGaiECIAAgBmsLCyEADAELCyABDwsgASACQQAgAGtxag=='; - -let wasmInstanceCache = null; - -async function getWasmInstance() { - if (wasmInstanceCache) return wasmInstanceCache; - try { - const wasmBuffer = Buffer.from(WASM_B64, 'base64'); - const wasmModule = await WebAssembly.compile(wasmBuffer); - - let memory; - let exports; - - const env = { - abort() { - throw new Error('WASM abort'); - } - }; - - const res = await WebAssembly.instantiate(wasmModule, { env }); - exports = res.exports; - memory = exports.memory; - - const refMap = new Map(); - function pin(ptr) { - if (ptr) { - const count = refMap.get(ptr); - if (count) refMap.set(ptr, count + 1); - else refMap.set(exports.__pin(ptr), 1); - } - return ptr; - } - function unpin(ptr) { - if (ptr) { - const count = refMap.get(ptr); - if (count === 1) { - exports.__unpin(ptr); - refMap.delete(ptr); - } else if (count) { - refMap.set(ptr, count - 1); - } - } - } - - wasmInstanceCache = { - decrypt(encryptedBytes, keyStr) { - let dataPtr = 0; - let keyPtr = 0; - try { - const len = encryptedBytes.length; - const pinPtr = exports.__pin(exports.__new(len, 1)) >>> 0; - dataPtr = exports.__new(12, 6) >>> 0; - - const view = new DataView(memory.buffer); - view.setUint32(dataPtr, pinPtr, true); - view.setUint32(dataPtr + 4, pinPtr, true); - view.setUint32(dataPtr + 8, len, true); - new Uint8Array(memory.buffer, pinPtr, len).set(encryptedBytes); - exports.__unpin(pinPtr); - - pin(dataPtr); - - const kLen = keyStr.length; - keyPtr = exports.__new(kLen << 1, 2) >>> 0; - const u16View = new Uint16Array(memory.buffer); - for (let i = 0; i < kLen; i++) { - u16View[(keyPtr >>> 1) + i] = keyStr.charCodeAt(i); - } - - const resPtr = exports.decrypt(dataPtr, keyPtr) >>> 0; - if (!resPtr) return new Uint8Array(0); - - const resView = new DataView(memory.buffer); - const outPtr = resView.getUint32(resPtr + 4, true); - const outLen = resView.getUint32(resPtr + 8, true); - return new Uint8Array(memory.buffer, outPtr, outLen).slice(); - } finally { - unpin(dataPtr); - } - } - }; - return wasmInstanceCache; - } catch { - return null; - } -} - -async function requestEndpoint(url, headers) { - try { - return await fetchJson(url, { - headers, - signal: AbortSignal.timeout(6000) - }); - } catch { - return null; - } -} - -async function resolveServiceStream(service, isMovie, id, s, e, headers) { - const directEndpoint = isMovie - ? `${BASE_URL}/streams/movie/${id}?s=${service}` - : `${BASE_URL}/streams/tv/${id}/${s || 1}/${e || 1}?s=${service}`; - - let res = await requestEndpoint(directEndpoint, headers); - - if (!res || !res.url) { - const encEndpoint = `${directEndpoint}&e=1`; - const encRes = await requestEndpoint(encEndpoint, headers); - - if (encRes && encRes.c) { - const instance = await getWasmInstance(); - if (instance) { - const encryptedBytes = new Uint8Array(Buffer.from(encRes.c, 'base64')); - const decryptedBytes = instance.decrypt(encryptedBytes, 'player.vidzee.wtf'); - if (decryptedBytes.length > 0) { - const str = new TextDecoder().decode(decryptedBytes); - try { - res = JSON.parse(str); - } catch { } - } - } - } - } - - if (res && res.url) { - const format = res.url.includes('.m3u8') ? 'hls' : 'mp4'; - return { - url: res.url, - server: `VidZee (${service})`, - quality: res.language || 'Auto', - type: format, - headers: { - 'User-Agent': USER_AGENT, - 'Referer': `${PLAYER_URL}/`, - 'Origin': PLAYER_URL, - ...(res.headers || {}) - } - }; - } - return null; -} - -export async function getStream(args) { - const { id, s, e, server, clientIP } = args; - const isTv = s != null && e != null; - const isMovie = !isTv; - - const headers = { - 'User-Agent': USER_AGENT, - 'Accept': 'application/json, text/plain, */*', - 'Referer': `${PLAYER_URL}/`, - 'Origin': PLAYER_URL, - ...(clientIP && { 'X-Forwarded-For': clientIP }) - }; - - let targets = SERVICES; - if (server && server !== 'all') { - const clean = server.toLowerCase().replace('vidzee (', '').replace(')', '').replace('vidzee', '').trim(); - targets = SERVICES.filter(srv => srv.toLowerCase().includes(clean)); - if (!targets.length) targets = SERVICES; - } - - const settled = await Promise.allSettled( - targets.map(service => resolveServiceStream(service, isMovie, id, s, e, headers)) - ); - - const allUrls = []; - for (const r of settled) { - if (r.status === 'fulfilled' && r.value) { - allUrls.push(r.value); - } - } - - return allUrls.length ? { allUrls } : null; -} - -export async function getSources() { - return SERVICES.map(s => `VidZee (${s})`); +import { fetchJson, USER_AGENT } from '../utils/helpers.js'; + +const BASE_URL = 'https://core.vidzee.wtf'; +const PLAYER_URL = 'https://player.vidzee.wtf'; +const SERVICES = ['dcloud', 'ipcloud', 'tik', 'v6:Hindi', '3AHindi']; +const WASM_B64 = 'AGFzbQEAAAABPgtgAX8Bf2ACf38Bf2ABfwBgAn9/AGADf39/AGAAAGAEf39/fwBgBH9/f38Bf2ADf39/AX9gA39/fgBgAAF/Ag0BA2VudgVhYm9ydAAGAygnAgEAAAQBAAEDBAADBQAAAgMHAAEAAAQIAwEJAgIBBQAAAgEABQoBBQMBAAEGJgd/AUEAC38BQQALfwFBAAt/AUEAC38BQQALfwFBAAt/AEGA2wALB0gHB2RlY3J5cHQAJwVfX25ldwAGBV9fcGluACQHX191bnBpbgAiCV9fY29sbGVjdAAlC19fcnR0aV9iYXNlAwYGbWVtb3J5AgAIAR8MAqkBCogkJyUAIABFBEAPCyAAQRRrIgAQByMCRgRAIAAQECAAIwUjAkUQCgsLCgAgACABai0AAAsKACAAKAIEQXxxCw0AIABBFGsoAhBBAXYLDAAgACABaiACOgAAC1MBAX8gAEHs////A0sEQEHwDEGwDUH9AEEeEAAACyMARQRAEA0LIwAgAEEQahAaIgIgATYCDCACIAA2AhAgAiMBIwIQCiACEAsjA2okAyACQRRqCwoAIAAoAgRBA3ELKQAgACAAIAFBCHZqLQAAQQV0aiABQf8BcUEDdmotAAAgAUEHcXZBAXELwwEBBH8gASgCAEF8cSIDQYACSQR/IANBBHYFQR9B/P///wMgAyADQfz///8DTxsiA2drIgRBB2shAiADIARBBGt2QRBzCyEEIAEoAgghBSABKAIEIgMEQCADIAU2AggLIAUEQCAFIAM2AgQLIAEgACACQQR0IARqQQJ0aiIBKAJgRgRAIAEgBTYCYCAFRQRAIAAgAkECdGoiASgCBEF+IAR3cSEDIAEgAzYCBCADRQRAIAAgACgCAEF+IAJ3cTYCAAsLCwspAQF/IAEoAgghAyAAIAEgAnI2AgQgACADNgIIIAMgABARIAEgADYCCAsNACAAKAIAQXxxQQRqC7wCAQV/IAEoAgAhAyABQQRqIAEoAgBBfHFqIgQoAgAiAkEBcQRAIAAgBBAJIAEgA0EEaiACQXxxaiIDNgIAIAFBBGogASgCAEF8cWoiBCgCACECCyADQQJxBEAgAUEEaygCACIBKAIAIQYgACABEAkgASAGQQRqIANBfHFqIgM2AgALIAQgAkECcjYCACAEQQRrIAE2AgAgACADQXxxIgJBgAJJBH8gAkEEdgVBH0H8////AyACIAJB/P///wNPGyICZ2siA0EHayEFIAIgA0EEa3ZBEHMLIgIgBUEEdGpBAnRqKAJgIQMgAUEANgIEIAEgAzYCCCADBEAgAyABNgIECyAAIAVBBHQgAmpBAnRqIAE2AmAgACAAKAIAQQEgBXRyNgIAIAAgBUECdGoiACAAKAIEQQEgAnRyNgIEC5cBAQJ/PwAiAEEATAR/QQEgAGtAAEEASAVBAAsEQAALQbDbAEEANgIAQdDnAEEANgIAA0AgAUEXSQRAIAFBAnRBsNsAakEANgIEQQAhAANAIABBEEkEQCABQQR0IABqQQJ0QbDbAGkBADYCYCAAQQFqIQAMAQsLIAFBAWohAQwBCwtBsNsAQdTnAD8ArEIQhhAbQbDbACQACyYBAX8gAEEEayEBIABBD3FBASAAGwR/QQEFIAEoAgBBAXELGiABCxIAIAAgADYCBCAAIAA2AgggAAsnAQF/IAAQAyIBRQRAIAAoAggaDwsgASAAKAIIIgA2AgggACABEBELEgAgACABIAAoAgRBA3FyNgIEC0kBAX8gACABQQF0aiEBA0AgAyIAQQFrIQMgAARAIAEvAQAiACACLwEAIgRHBEAgACAEaw8LIAFBAmohASACQQJqIQIMAQsLQQALbgECf0EMQQYQBiIBRQRAQQxBAxAGIQELIAFBADYCACABQQA2AgQgAUEANgIIIABB/P///wNLBEBB4NcAQZDYAEETQTkQAAALIABBARAGIgJBACAA/AsAIAEgAjYCACABIAI2AgQgASAANgIIIAELjgEBAn8gAUGAAkkEfyABQQR2BUEfIAEQFSIBZ2siA0EHayECIAEgA0EEa3ZBEHMLIgEgACACQQJ0aigCBEF/IAF0cSIBBH8gACABaCACQQR0akECdGooAmAFIAAoAgBBfyACQQFqdHEiAQR/IAAgACABaCIAQQJ0aigCBGggAEEEdGpBAnRqKAJgBUEACwsLHQAgAEEBQRsgAGdrdGpBAWsgACAAQf7///8BSRsLLwAgAEH8////A0sEQEHwDEHwDUHNA0EdEAAAC0EMIABBE2pBcHFBBGsgAEEMTRsLZwECfyABKAIAIgNBfHEgAmsiBEEQTwRAIAEgAiADQQJxcjYCACABQQRqIAJqIgEgBEEEa0EBcjYCACAAIAEQDAUgASADQX5xNgIAIAFBBGogASgCAEF8cWoiACAAKAIAQX1xNgIACwswACAAIAIQGiICQQRqIAFBBGogASgCAEF8cfwKAAAgAUGk2wBPBEAgACABEBkLIAILFQAgASABKAIAQQFyNgIAIAAgARAMC48BAQJ/IAAgARAWIgIQFCIBRQRAQQQgACgCoAw/ACIBQRB0QQRrR3QgAhAVIAIgAkGAAk8bakH//wNqQYCAfHFBEHYhAyABIAMgASADShtAAEEASARAIANAAEEASARAAAsLIAAgAUEQdD8ArEIQhhAbIAAgAhAUIQELIAEoAgAaIAAgARAJIAAgASACEBcgAQuGAQEDfyABQRNqQXBxQQRrIQEgACgCoAwiAwRAIAFBEGsiBSADRgRAIAMoAgAhBCAFIQELCyACp0FwcSABayIDQRRJBEAPCyABIARBAnEgA0EIayIDQQFycjYCACABQQA2AgQgAUEANgIIIAFBBGogA2oiA0ECNgIAIAAgAzYCoAwgACABEAwLhQEBA38CQAJAAkACQAJAAkACQAJAAkAgAEEIaygCAA4IAAECAwQFBgcICw8LDwsPCyAAEB0PCyAAKAIAEAEPCyAAKAIEIgEgACgCDEECdGohAgNAIAEgAkkEQCABKAIAIgMEQCADEAELIAFBBGohAQwBCwsgACgCABABDwsgABAdDwsPCwALCQAgACgCABABC0MAIAEgACgCDE8EQEHw1QBBsNYAQfIAQSoQAAALIAAoAgQgAUECdGooAgAiAEUEQEHg1gBBsNYAQfYAQSgQAAALIAALGQBBoA4QDyQBQYDaABAPJARB4NoAEA8kBQv9AQEGfyAAIgFBCHYiAkHMzgBqLQAAIAJBlDJqLQAAQdYAbEGUMmogAEH/AXEiA0EDbmotAAAgA0EDcEECdEGAxwBqKAIAbEELdkEGcGpBAnRBjbrowserigCACICQf8BcSEAIAJBCHUhAgJAIABBAkkNACACQf8BcSEAIAJBCHYhAgNAIAAEQCAAQQF2IgYgAmpBAXRBzNIAaiIELQAAIgUgA0YEfyAELQABQQJ0QYzHAGooAgAiAkH/AXEhACACQQh1IQIgAEECSQ0DIAFBAWoPBSADIAVJBH8gBgUgAiAGaiECIAAgBmsLCyEADAELCyABDwsgASACQQAgAGtxag=='; + +let wasmInstanceCache = null; + +async function getWasmInstance() { + if (wasmInstanceCache) return wasmInstanceCache; + try { + const wasmBuffer = Buffer.from(WASM_B64, 'base64'); + const wasmModule = await WebAssembly.compile(wasmBuffer); + + let memory; + let exports; + + const env = { + abort() { + throw new Error('WASM abort'); + }, + }; + + const res = await WebAssembly.instantiate(wasmModule, { env }); + exports = res.exports; + memory = exports.memory; + + const refMap = new Map(); + function pin(ptr) { + if (ptr) { + const count = refMap.get(ptr); + if (count) refMap.set(ptr, count + 1); + else refMap.set(exports.__pin(ptr), 1); + } + return ptr; + } + function unpin(ptr) { + if (ptr) { + const count = refMap.get(ptr); + if (count === 1) { + refMap.delete(ptr); + exports.__unpin(ptr); + } else if (count > 1) { + refMap.set(ptr, count - 1); + } + } + } + + wasmInstanceCache = { + decrypt(encryptedBytes, keyStr) { + let dataPtr = 0; + let keyPtr = 0; + try { + const len = encryptedBytes.length; + const pinPtr = exports.__pin(exports.__new(len, 1)) >>> 0; + dataPtr = exports.__new(12, 6) >>> 0; + + const view = new DataView(memory.buffer); + view.setUint32(dataPtr, pinPtr, true); + view.setUint32(dataPtr + 4, pinPtr, true); + view.setUint32(dataPtr + 8, len, true); + new Uint8Array(memory.buffer, pinPtr, len).set(encryptedBytes); + exports.__unpin(pinPtr); + + pin(dataPtr); + + const kLen = keyStr.length; + keyPtr = exports.__new(kLen << 1, 2) >>> 0; + const u16View = new Uint16Array(memory.buffer); + for (let i = 0; i < kLen; i++) { + u16View[(keyPtr >>> 1) + i] = keyStr.charCodeAt(i); + } + + const resPtr = exports.decrypt(dataPtr, keyPtr) >>> 0; + if (!resPtr) return new Uint8Array(0); + + const resView = new DataView(memory.buffer); + const outPtr = resView.getUint32(resPtr + 4, true); + const outLen = resView.getUint32(resPtr + 8, true); + return new Uint8Array(memory.buffer, outPtr, outLen).slice(); + } finally { + unpin(dataPtr); + } + }, + }; + return wasmInstanceCache; + } catch { + return null; + } +} + +async function requestEndpoint(url, headers) { + try { + return await fetchJson(url, { + headers, + signal: AbortSignal.timeout(6000), + }); + } catch { + return null; + } +} + +async function resolveServiceStream(service, isMovie, id, s, e, headers) { + const directEndpoint = isMovie + ? `${BASE_URL}/streams/movie/${id}?s=${service}` + : `${BASE_URL}/streams/tv/${id}/${s || 1}/${e || 1}?s=${service}`; + + let res = await requestEndpoint(directEndpoint, headers); + + if (!res || !res.url) { + const encEndpoint = `${directEndpoint}&e=1`; + const encRes = await requestEndpoint(encEndpoint, headers); + + if (encRes && encRes.c) { + const instance = await getWasmInstance(); + if (instance) { + const encryptedBytes = new Uint8Array(Buffer.from(encRes.c, 'base64')); + const decryptedBytes = instance.decrypt(encryptedBytes, 'player.vidzee.wtf'); + if (decryptedBytes.length > 0) { + const str = new TextDecoder().decode(decryptedBytes); + try { + res = JSON.parse(str); + } catch { } + } + } + } + } + + if (res && res.url) { + const format = res.url.includes('.m3u8') ? 'hls' : 'mp4'; + return { + url: res.url, + server: `VidZee (${service})`, + quality: res.language || 'Auto', + type: format, + headers: { + 'User-Agent': USER_AGENT, + Referer: `${PLAYER_URL}/`, + Origin: PLAYER_URL, + ...(res.headers || {}), + }, + }; + } + return null; +} + +export async function getStream(args) { + const { id, s, e, server, clientIP } = args || {}; + const isTv = s != null && e != null; + const isMovie = !isTv; + + const headers = { + 'User-Agent': USER_AGENT, + Accept: 'application/json, text/plain, */*', + Referer: `${PLAYER_URL}/`, + Origin: PLAYER_URL, + ...(clientIP && { 'X-Forwarded-For': clientIP }), + }; + + let targets = SERVICES; + if (server && server !== 'all') { + const clean = server.toLowerCase().replace('vidzee (', '').replace(')', '').replace('vidzee', '').trim(); + targets = SERVICES.filter(srv => srv.toLowerCase().includes(clean)); + if (!targets.length) targets = SERVICES; + } + + const settled = await Promise.allSettled( + targets.map(service => resolveServiceStream(service, isMovie, id, s, e, headers)) + ); + + const allUrls = []; + for (const r of settled) { + if (r.status === 'fulfilled' && r.value) { + allUrls.push(r.value); + } + } + + if (!allUrls.length) return null; + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; +} + +export async function getSources() { + return SERVICES.map(s => `VidZee (${s})`); } \ No newline at end of file diff --git a/src/sources/vixsrc.js b/src/sources/vixsrc.js index 29b1f40..ce0c7d2 100644 --- a/src/sources/vixsrc.js +++ b/src/sources/vixsrc.js @@ -1,114 +1,124 @@ -import { fetchText, USER_AGENT } from '../utils/helpers.js'; - -const BASE_URL = 'https://vixsrc.to'; - -const HEADERS = { - 'User-Agent': USER_AGENT, - 'Accept': 'application/json, text/javascript, */*; q=0.01', - 'Accept-Language': 'en-US,en;q=0.9', - 'Referer': `${BASE_URL}/`, - 'Origin': BASE_URL -}; - -function extract(regex, text) { - const m = text?.match(regex); - if (!m) return null; - let val = m[1]; - val = val.replace(/\\u0026/gi, '&'); - val = val.replace(/\\\//g, '/'); - val = val.replace(/\\/g, ''); - return val; -} - -export async function getStream({ id, s, e, absoluteBase }) { - try { - const apiUrl = s && e - ? `${BASE_URL}/api/tv/${id}/${s}/${e}` - : `${BASE_URL}/api/movie/${id}`; - - const apiProxy = absoluteBase && absoluteBase !== '/' - ? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(apiUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}` - : apiUrl; - - const apiText = await fetchText(apiProxy, { headers: HEADERS }).catch(() => null); - const apiData = JSON.parse(apiText || '{}'); - if (!apiData.src) return null; - - const embedUrl = apiData.src.startsWith('http') ? apiData.src : `${BASE_URL}${apiData.src}`; - const embedProxy = absoluteBase && absoluteBase !== '/' - ? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(embedUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}` - : embedUrl; - - const html = await fetchText(embedProxy, { headers: HEADERS }).catch(() => null); - if (!html) return null; - - const token = extract(/token["']\s*:\s*["']([^"']+)/, html); - const expires = extract(/expires["']\s*:\s*["']([^"']+)/, html); - let playlist = extract(/url["']\s*:\s*["']([^"']+)/, html); - - if (!token || !expires || !playlist) return null; - - if (playlist.startsWith('/')) { - playlist = `${BASE_URL}${playlist}`; - } - - const defaultLang = extract(/lang(?:uage)?["']\s*:\s*["']([a-z]{2,5})/i, html) || 'en'; - const rawMasterUrl = `${playlist}${playlist.includes('?') ? '&' : '?'}token=${token}&expires=${expires}&h=1&lang=${defaultLang}`; - - const masterProxy = absoluteBase && absoluteBase !== '/' - ? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(rawMasterUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify({ ...HEADERS, Referer: embedUrl }))}` - : rawMasterUrl; - - const playlistText = await fetchText(masterProxy, { headers: { ...HEADERS, 'Referer': embedUrl } }).catch(() => null); - if (!playlistText?.includes('#EXTM3U')) return null; - - const lines = playlistText.split('\n'); - const audioTracks = []; - const seen = new Set(); - - for (const line of lines) { - if (line.startsWith('#EXT-X-MEDIA') && line.includes('TYPE=AUDIO')) { - const name = line.match(/NAME=["']([^"']+)["']/)?.[1] || 'Default'; - const rendition = line.match(/rendition=([a-zA-Z0-9_-]+)/)?.[1] - || line.match(/LANGUAGE=["']([^"']+)["']/)?.[1] - || name; - const key = rendition.toLowerCase(); - if (!seen.has(key)) { - seen.add(key); - audioTracks.push({ name, lang: rendition }); - } - } - } - - const allUrls = []; - - if (audioTracks.length > 0) { - for (const track of audioTracks) { - const streamUrl = `${playlist}${playlist.includes('?') ? '&' : '?'}token=${token}&expires=${expires}&h=1&lang=${track.lang}`; - allUrls.push({ - url: streamUrl, - server: `VixSrc (${track.name})`, - skipProxy: true, - headers: { - ...HEADERS, - 'Referer': `${BASE_URL}/` - } - }); - } - } else { - allUrls.push({ - url: rawMasterUrl, - server: 'VixSrc', - skipProxy: true, - headers: { - ...HEADERS, - 'Referer': `${BASE_URL}/` - } - }); - } - - return { allUrls }; - } catch (err) { - return null; - } +import { fetchText, USER_AGENT } from '../utils/helpers.js'; + +const BASE_URL = 'https://vixsrc.to'; +const HEADERS = { + 'User-Agent': USER_AGENT, + Accept: 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'en-US,en;q=0.9', + Referer: `${BASE_URL}/`, + Origin: BASE_URL, +}; + +function extract(regex, text) { + const m = text?.match(regex); + if (!m) return null; + let val = m[1]; + val = val.replace(/\\u0026/gi, '&'); + val = val.replace(/\\\//g, '/'); + val = val.replace(/\\/g, ''); + return val; +} + +export async function getStream(args) { + const { id, s, e, absoluteBase } = args || {}; + const isTv = s != null && e != null; + + try { + const apiUrl = isTv + ? `${BASE_URL}/api/tv/${id}/${s}/${e}` + : `${BASE_URL}/api/movie/${id}`; + + const apiProxy = absoluteBase && absoluteBase !== '/' + ? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(apiUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}` + : apiUrl; + + const apiText = await fetchText(apiProxy, { headers: HEADERS }).catch(() => null); + const apiData = JSON.parse(apiText || '{}'); + if (!apiData.src) return null; + + const embedUrl = apiData.src.startsWith('http') ? apiData.src : `${BASE_URL}${apiData.src}`; + const embedProxy = absoluteBase && absoluteBase !== '/' + ? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(embedUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}` + : embedUrl; + + const html = await fetchText(embedProxy, { headers: HEADERS }).catch(() => null); + if (!html) return null; + + const token = extract(/token["']\s*:\s*["']([^"']+)/, html); + const expires = extract(/expires["']\s*:\s*["']([^"']+)/, html); + let playlist = extract(/url["']\s*:\s*["']([^"']+)/, html); + + if (!token || !expires || !playlist) return null; + + if (playlist.startsWith('/')) { + playlist = `${BASE_URL}${playlist}`; + } + + const defaultLang = extract(/lang(?:uage)?["']\s*:\s*["']([a-z]{2,5})/i, html) || 'en'; + const rawMasterUrl = `${playlist}${playlist.includes('?') ? '&' : '?'}token=${token}&expires=${expires}&h=1&lang=${defaultLang}`; + + const masterProxy = absoluteBase && absoluteBase !== '/' + ? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(rawMasterUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify({ ...HEADERS, Referer: embedUrl }))}` + : rawMasterUrl; + + const playlistText = await fetchText(masterProxy, { headers: { ...HEADERS, Referer: embedUrl } }).catch(() => null); + if (!playlistText?.includes('#EXTM3U')) return null; + + const lines = playlistText.split('\n'); + const audioTracks = []; + const seen = new Set(); + + for (const line of lines) { + if (line.startsWith('#EXT-X-MEDIA') && line.includes('TYPE=AUDIO')) { + const name = line.match(/NAME=["']([^"']+)["']/)?.[1] || 'Default'; + const rendition = line.match(/rendition=([a-zA-Z0-9_-]+)/)?.[1] + || line.match(/LANGUAGE=["']([^"']+)["']/)?.[1] + || name; + const key = rendition.toLowerCase(); + if (!seen.has(key)) { + seen.add(key); + audioTracks.push({ name, lang: rendition }); + } + } + } + + const allUrls = []; + + if (audioTracks.length > 0) { + for (const track of audioTracks) { + const streamUrl = `${playlist}${playlist.includes('?') ? '&' : '?'}token=${token}&expires=${expires}&h=1&lang=${track.lang}`; + allUrls.push({ + url: streamUrl, + server: `VixSrc (${track.name})`, + quality: 'Auto', + type: 'hls', + skipProxy: true, + headers: { + ...HEADERS, + Referer: `${BASE_URL}/`, + }, + }); + } + } else { + allUrls.push({ + url: rawMasterUrl, + server: 'VixSrc', + quality: 'Auto', + type: 'hls', + skipProxy: true, + headers: { + ...HEADERS, + Referer: `${BASE_URL}/`, + }, + }); + } + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; + } catch { + return null; + } } \ No newline at end of file diff --git a/src/sources/watchout.js b/src/sources/watchout.js index 63ea7ac..c681976 100644 --- a/src/sources/watchout.js +++ b/src/sources/watchout.js @@ -1,8 +1,13 @@ import { fetchJson, fetchText } from '../utils/helpers.js'; -const API_URL = "https://raw.githubusercontent.com/Watchout2025/api/refs/heads/main/hls/"; +const API_URL = 'https://raw.githubusercontent.com/Watchout2025/api/refs/heads/main/hls/'; +const HEADERS = { + Origin: 'https://vidout.pages.dev', + Referer: 'https://vidout.pages.dev', +}; -export async function getStream({ id, s, e }) { +export async function getStream(args) { + const { id, s, e } = args || {}; const isTv = s != null && e != null; const type = isTv ? 'tv' : 'movie'; @@ -10,37 +15,46 @@ export async function getStream({ id, s, e }) { ? `${API_URL}tv/${id}/S${s}.json` : `${API_URL}movie/${id}`; - const headers = { - 'Origin': 'https://vidout.pages.dev', - 'Referer': 'https://vidout.pages.dev' - }; - if (type === 'movie') { const data = await fetchText(targetUrl).catch(() => null); if (!data || !data.includes('://')) return null; - return { - url: data.trim(), - headers, - allUrls: [{ - url: data.trim(), - server: 'Watchout - Direct', - headers - }] - }; - } else { - const data = await fetchJson(targetUrl).catch(() => null); - const streamUrl = data?.[String(e)] || null; - if (!streamUrl) return null; + const streamUrl = data.trim(); + const isHls = streamUrl.includes('.m3u8') || streamUrl.includes('.txt') || streamUrl.includes('master') || !streamUrl.includes('.mp4'); + const allUrls = [{ + url: streamUrl, + server: 'Watchout - Direct', + quality: 'Auto', + type: isHls ? 'hls' : 'mp4', + headers: HEADERS, + }]; return { url: streamUrl, - headers, - allUrls: [{ - url: streamUrl, - server: 'Watchout - Direct', - headers - }] + headers: HEADERS, + allUrls, + }; + } else { + let data = await fetchJson(targetUrl).catch(() => null); + if (!data) { + data = await fetchJson(`${API_URL}tv/${id}/s${s}.json`).catch(() => null); + } + const streamUrl = data?.[String(e)] || null; + if (!streamUrl) return null; + + const isHls = streamUrl.includes('.m3u8') || streamUrl.includes('.txt') || streamUrl.includes('master') || !streamUrl.includes('.mp4'); + const allUrls = [{ + url: streamUrl, + server: 'Watchout - Direct', + quality: 'Auto', + type: isHls ? 'hls' : 'mp4', + headers: HEADERS, + }]; + + return { + url: streamUrl, + headers: HEADERS, + allUrls, }; } -} +} \ No newline at end of file diff --git a/src/sources/xdownloader.js b/src/sources/xdownloader.js index 78dab6c..400f07a 100644 --- a/src/sources/xdownloader.js +++ b/src/sources/xdownloader.js @@ -1,3 +1,5 @@ +import { getTmdbMetadata, resolveTmdbKey } from '../utils/helpers.js'; + const BASE_URL = 'https://www.films365.org'; const HEADERS = { Authorization: 'Bearer 79a02956be35835728a044b11e2ae793149d45fb2c89cb6d029ec01aac19bfdb', @@ -5,125 +7,122 @@ const HEADERS = { 'User-Agent': 'MovieDownloader/1.0', }; -async function resolveTitle(sdk, id, isTv) { - const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY; - if (!apiKey) return null; - const type = isTv ? 'tv' : 'movie'; - try { - const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, { - signal: AbortSignal.timeout(6000), - }); - if (!res.ok) return null; - const data = await res.json(); - return isTv ? data.name : data.title; - } catch { - return null; - } -} - -export async function getStream({ id, s, e, sdk }) { - const isTv = Boolean(s && e); +export async function getStream(args) { + const { id, s, e, sdk } = args || {}; + const isTv = s != null && e != null; const targetType = isTv ? 'tv' : 'movie'; + const apiKey = resolveTmdbKey(args, sdk); - const title = await resolveTitle(sdk, id, isTv); + const meta = await getTmdbMetadata(id, isTv, apiKey); + const title = meta?.title; const query = title || String(id); - const searchRes = await fetch(`${BASE_URL}/api/mobile/search`, { - method: 'POST', - headers: HEADERS, - body: JSON.stringify({ query }), - signal: AbortSignal.timeout(8000), - }); + try { + const searchRes = await fetch(`${BASE_URL}/api/mobile/search`, { + method: 'POST', + headers: HEADERS, + body: JSON.stringify({ query }), + signal: AbortSignal.timeout(8000), + }); - if (!searchRes.ok) return null; - const searchJson = await searchRes.json(); - const resultsObj = searchJson?.results; - if (!resultsObj) return null; + if (!searchRes.ok) return null; + const searchJson = await searchRes.json(); + const resultsObj = searchJson?.results; + if (!resultsObj) return null; - const items = resultsObj.all || (targetType === 'tv' ? resultsObj.tvs : resultsObj.movies) || []; - if (!Array.isArray(items) || items.length === 0) return null; + const items = resultsObj.all || (targetType === 'tv' ? resultsObj.tvs : resultsObj.movies) || []; + if (!Array.isArray(items) || items.length === 0) return null; - const cleanSearchTitle = query.toLowerCase().replace(/[^a-z0-9]/g, ''); - let matchedItem = null; + const cleanSearchTitle = query.toLowerCase().replace(/[^a-z0-9]/g, ''); + let matchedItem = null; - for (const item of items) { - const itemType = String(item?.type || '').toLowerCase(); - if (itemType && itemType !== targetType) continue; + for (const item of items) { + const itemType = String(item?.type || '').toLowerCase(); + if (itemType && itemType !== targetType) continue; - const itemTitle = String(item?.title || ''); - const cleanItemTitle = itemTitle.toLowerCase().replace(/[^a-z0-9]/g, ''); + const itemTitle = String(item?.title || ''); + const cleanItemTitle = itemTitle.toLowerCase().replace(/[^a-z0-9]/g, ''); - if (cleanItemTitle === cleanSearchTitle || cleanItemTitle.includes(cleanSearchTitle)) { - matchedItem = item; - break; + if (cleanItemTitle === cleanSearchTitle || cleanItemTitle.includes(cleanSearchTitle)) { + matchedItem = item; + break; + } } - } - if (!matchedItem) { - matchedItem = items.find(i => String(i?.type || '').toLowerCase() === targetType) || items[0]; - } - - const itemId = matchedItem?.id || matchedItem?.tmdbId; - if (!itemId) return null; - - const detailsRes = await fetch(`${BASE_URL}/api/mobile/details?id=${itemId}&type=${targetType}`, { - headers: HEADERS, - signal: AbortSignal.timeout(8000), - }); - - if (!detailsRes.ok) return null; - const detailsJson = await detailsRes.json(); - const data = detailsJson?.data; - if (!data) return null; - - const spoken = Array.isArray(data.spokenLanguages) - ? data.spokenLanguages.map(l => String(l).trim()).filter(Boolean) - : []; - const langSuffix = spoken.length > 0 ? ` · ${spoken.join(', ')}` : ''; - - const allUrls = []; - - if (!isTv) { - const streamUrl = data.downloadUrl || data.videoUrl; - if (streamUrl && typeof streamUrl === 'string') { - allUrls.push({ - url: streamUrl.trim(), - label: `X-Downloader${langSuffix}`, - headers: { - 'User-Agent': HEADERS['User-Agent'], - }, - }); + if (!matchedItem) { + matchedItem = items.find(i => String(i?.type || '').toLowerCase() === targetType) || items[0]; } - } else { - const seasonNum = Number(s); - const episodeNum = Number(e); - const seasons = Array.isArray(data.seasons) ? data.seasons : []; - const targetSeason = seasons.find(sn => Number(sn.seasonNumber) === seasonNum); - if (targetSeason) { - const episodes = Array.isArray(targetSeason.episodes) ? targetSeason.episodes : []; - const targetEp = episodes.find(ep => Number(ep.episodeNumber) === episodeNum); + const itemId = matchedItem?.id || matchedItem?.tmdbId; + if (!itemId) return null; - if (targetEp) { - const streamUrl = targetEp.downloadUrl || targetEp.videoUrl; - if (streamUrl && typeof streamUrl === 'string') { - allUrls.push({ - url: streamUrl.trim(), - label: `X-Downloader (S${seasonNum}E${episodeNum})${langSuffix}`, - headers: { - 'User-Agent': HEADERS['User-Agent'], - }, - }); + const detailsRes = await fetch(`${BASE_URL}/api/mobile/details?id=${itemId}&type=${targetType}`, { + headers: HEADERS, + signal: AbortSignal.timeout(8000), + }); + + if (!detailsRes.ok) return null; + const detailsJson = await detailsRes.json(); + const data = detailsJson?.data; + if (!data) return null; + + const spoken = Array.isArray(data.spokenLanguages) + ? data.spokenLanguages.map(l => String(l).trim()).filter(Boolean) + : []; + const langSuffix = spoken.length > 0 ? ` · ${spoken.join(', ')}` : ''; + + const allUrls = []; + + if (!isTv) { + const streamUrl = data.downloadUrl || data.videoUrl; + if (streamUrl && typeof streamUrl === 'string') { + allUrls.push({ + url: streamUrl.trim(), + label: `X-Downloader${langSuffix}`, + server: `X-Downloader${langSuffix}`, + quality: 'Auto', + type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', + headers: { + 'User-Agent': HEADERS['User-Agent'], + }, + }); + } + } else { + const seasonNum = Number(s); + const episodeNum = Number(e); + const seasons = Array.isArray(data.seasons) ? data.seasons : []; + const targetSeason = seasons.find(sn => Number(sn.seasonNumber) === seasonNum); + + if (targetSeason) { + const episodes = Array.isArray(targetSeason.episodes) ? targetSeason.episodes : []; + const targetEp = episodes.find(ep => Number(ep.episodeNumber) === episodeNum); + + if (targetEp) { + const streamUrl = targetEp.downloadUrl || targetEp.videoUrl; + if (streamUrl && typeof streamUrl === 'string') { + allUrls.push({ + url: streamUrl.trim(), + label: `X-Downloader (S${seasonNum}E${episodeNum})${langSuffix}`, + server: `X-Downloader (S${seasonNum}E${episodeNum})${langSuffix}`, + quality: 'Auto', + type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', + headers: { + 'User-Agent': HEADERS['User-Agent'], + }, + }); + } } } } + + if (allUrls.length === 0) return null; + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; + } catch { + return null; } - - if (allUrls.length === 0) return null; - - return { - url: allUrls[0].url, - allUrls, - headers: allUrls[0].headers, - }; } \ No newline at end of file diff --git a/src/sources/yapgrid.js b/src/sources/yapgrid.js index 8ba9731..990478c 100644 --- a/src/sources/yapgrid.js +++ b/src/sources/yapgrid.js @@ -1,17 +1,22 @@ import { fetchJson } from '../utils/helpers.js'; -export async function getStream({ id, s, e }) { +const BASE_URL = 'https://yapgrid.com'; +const EMBED_BASE = 'https://embedwave.cc'; + +export async function getStream(args) { + const { id, s, e } = args || {}; const isTv = s != null && e != null; + const targetEmbedUrl = isTv - ? `https://embedwave.cc/embed/tv/${id}/${s}/${e}` - : `https://embedwave.cc/embed/movie/${id}`; + ? `${EMBED_BASE}/embed/tv/${id}/${s}/${e}` + : `${EMBED_BASE}/embed/movie/${id}`; const headers = { - 'Accept': 'application/json', + Accept: 'application/json', 'Content-Type': 'application/json', - 'Origin': 'https://yapgrid.com', - 'Referer': isTv ? `https://yapgrid.com/embed/tv/${id}/${s}/${e}` : `https://yapgrid.com/embed/movie/${id}`, - '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' + Origin: BASE_URL, + Referer: isTv ? `${BASE_URL}/embed/tv/${id}/${s}/${e}` : `${BASE_URL}/embed/movie/${id}`, + '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', }; const payload = { @@ -20,13 +25,13 @@ export async function getStream({ id, s, e }) { strictSource: true, waitMs: 5000, nativeHls: false, - cacheOnly: true + cacheOnly: true, }; - const res = await fetchJson('https://yapgrid.com/player/api/extract', { + const res = await fetchJson(`${BASE_URL}/player/api/extract`, { method: 'POST', headers, - body: JSON.stringify(payload) + body: JSON.stringify(payload), }).catch(() => null); if (!res?.success || !Array.isArray(res.found) || !res.found.length) return null; @@ -38,18 +43,19 @@ export async function getStream({ id, s, e }) { let streamUrl = item.url; if (streamUrl.startsWith('/')) { - streamUrl = `https://yapgrid.com${streamUrl}`; + streamUrl = `${BASE_URL}${streamUrl}`; } allUrls.push({ url: streamUrl, server: `YapGrid - ${item.nxshaProvider || item.provider || 'Server'}`, + quality: 'Auto', type: item.type === 'master' ? 'hls' : (streamUrl.includes('.m3u8') ? 'hls' : 'mp4'), headers: { - Referer: 'https://yapgrid.com/', - Origin: 'https://yapgrid.com', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36' - } + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', + }, }); } @@ -58,6 +64,6 @@ export async function getStream({ id, s, e }) { return { url: allUrls[0].url, headers: allUrls[0].headers, - allUrls + allUrls, }; } \ No newline at end of file diff --git a/src/sources/yastream.js b/src/sources/yastream.js index dbade2a..a880927 100644 --- a/src/sources/yastream.js +++ b/src/sources/yastream.js @@ -1,13 +1,17 @@ import { fetchJson } from '../utils/helpers.js'; -export async function getStream({ id, s, e }) { +const BASE_URL = 'https://yastream.tamthai.de'; + +export async function getStream(args) { + const { id, s, e } = args || {}; const isTv = s != null && e != null; + const path = isTv ? `/stream/series/tmdb:${id}:${s}:${e}.json` : `/stream/movie/tmdb:${id}.json`; - const url = `https://yastream.tamthai.de${path}`; - const res = await fetchJson(url); + const url = `${BASE_URL}${path}`; + const res = await fetchJson(url).catch(() => null); if (!res || !Array.isArray(res.streams) || !res.streams.length) return null; const allUrls = res.streams.map(item => { @@ -15,8 +19,10 @@ export async function getStream({ id, s, e }) { return { url: item.url, server: `YaStream - ${item.name?.replace(/\n/g, ' ') || 'Server'}`, + quality: 'Auto', + type: item.url.includes('.m3u8') ? 'hls' : 'mp4', headers: item.behaviorHints?.proxyHeaders?.request || undefined, - skipProxy: item.behaviorHints?.notWebReady === false + skipProxy: item.behaviorHints?.notWebReady === false, }; }).filter(Boolean); @@ -25,6 +31,6 @@ export async function getStream({ id, s, e }) { return { url: allUrls[0].url, headers: allUrls[0].headers, - allUrls + allUrls, }; } \ No newline at end of file diff --git a/src/subtitles.js b/src/subtitles.js index 7893a7d..749fc43 100644 --- a/src/subtitles.js +++ b/src/subtitles.js @@ -1,122 +1,105 @@ -import { getUA } from './utils/helpers.js'; - -const SUBTITLE_BASES = [ - 'https://sub.vdrk.site/v1', - 'https://sub.vdrk.site/v2', - 'https://opensubtitles-v3.strem.io' -]; - -async function resolveImdbId(id, isTv) { - if (typeof id === 'string' && id.startsWith('tt')) return id; - const apiKey = process.env.TMDB_API_KEY; - if (!apiKey) return `tt${id}`; - const type = isTv ? 'tv' : 'movie'; - try { - const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`, { - signal: AbortSignal.timeout(5000), - }); - if (!res.ok) return `tt${id}`; - const data = await res.json(); - return data.imdb_id || data.external_ids?.imdb_id || `tt${id}`; - } catch { - return `tt${id}`; - } -} - -export async function fetchSubtitles(paths = []) { - try { - const results = await Promise.all( - paths.map(async ({ base, path }) => { - try { - const res = await fetch(`${base}${path}`, { - headers: { 'User-Agent': getUA() }, - signal: AbortSignal.timeout(6000), - }); - - if (!res.ok) { - res.body?.cancel(); - return []; - } - - const data = await res.json(); - - if (base.includes('/v2')) { - return Array.isArray(data) - ? data.map(x => ({ - label: x.label, - file: x.file || x.url, - type: 'vtt', - source: 'v2' - })) - : []; - } - - if (base.includes('opensubtitles-v3.strem.io')) { - if (!Array.isArray(data?.subtitles)) return []; - return data.subtitles.map(sub => { - if (!sub?.url) return null; - const isSrt = sub.url.toLowerCase().endsWith('.srt'); - return { - label: sub.lang ? sub.lang.toUpperCase() : 'OpenSubtitles', - file: sub.url, - type: isSrt ? 'srt' : 'vtt', - source: 'opensubtitles' - }; - }).filter(Boolean); - } - - if (base.includes('fed-subs.pstream.mov')) { - if (!data?.subtitles || typeof data.subtitles !== 'object') return []; - - return Object.entries(data.subtitles) - .map(([language, sub]) => { - if (!sub?.subtitle_link) return null; - const ext = sub.subtitle_link.split('.').pop()?.toLowerCase(); - return { - label: sub.subtitle_name || language, - file: sub.subtitle_link, - type: ext === 'vtt' ? 'vtt' : 'srt', - source: 'febbox' - }; - }) - .filter(Boolean); - } - - const v1 = Array.isArray(data) ? data : []; - return v1.map(x => ({ - label: x.label, - file: x.file || x.url, - type: 'vtt', - source: 'v1' - })); - } catch { - return []; - } - }) - ); - - return results.flat(); - } catch { - return []; - } -} - -export async function getSubPathsMovie(id) { - const imdbId = await resolveImdbId(id, false); - return [ - { base: SUBTITLE_BASES[0], path: `/movie/${id}` }, - { base: SUBTITLE_BASES[1], path: `/movie/${id}` }, - { base: SUBTITLE_BASES[2], path: `/subtitles/movie/${imdbId}.json` } - ]; -} - -export async function getSubPathsTv(id, season, episode) { - const imdbId = await resolveImdbId(id, true); - return [ - { base: SUBTITLE_BASES[0], path: `/tv/${id}/${season}/${episode}` }, - { base: SUBTITLE_BASES[1], path: `/tv/${id}/${season}/${episode}` }, - { base: SUBTITLE_BASES[2], path: `/subtitles/series/${imdbId}:${season}:${episode}.json` } - ]; -} - -export { SUBTITLE_BASES }; \ No newline at end of file +import { getUA, resolveImdbId } from './utils/helpers.js'; + +const SUBTITLE_BASES = [ + 'https://sub.vdrk.site/v1', + 'https://sub.vdrk.site/v2', + 'https://opensubtitles-v3.strem.io' +]; + +export async function fetchSubtitles(paths = []) { + try { + const results = await Promise.all( + paths.map(async ({ base, path }) => { + try { + const res = await fetch(`${base}${path}`, { + headers: { 'User-Agent': getUA() }, + signal: AbortSignal.timeout(6000), + }); + + if (!res.ok) { + res.body?.cancel(); + return []; + } + + const data = await res.json(); + + if (base.includes('/v2')) { + return Array.isArray(data) + ? data.map(x => ({ + label: x.label, + file: x.file || x.url, + type: 'vtt', + source: 'v2' + })) + : []; + } + + if (base.includes('opensubtitles-v3.strem.io')) { + if (!Array.isArray(data?.subtitles)) return []; + return data.subtitles.map(sub => { + if (!sub?.url) return null; + const isSrt = sub.url.toLowerCase().endsWith('.srt'); + return { + label: sub.lang ? sub.lang.toUpperCase() : 'OpenSubtitles', + file: sub.url, + type: isSrt ? 'srt' : 'vtt', + source: 'opensubtitles' + }; + }).filter(Boolean); + } + + if (base.includes('fed-subs.pstream.mov')) { + if (!data?.subtitles || typeof data.subtitles !== 'object') return []; + + return Object.entries(data.subtitles) + .map(([language, sub]) => { + if (!sub?.subtitle_link) return null; + const ext = sub.subtitle_link.split('.').pop()?.toLowerCase(); + return { + label: sub.subtitle_name || language, + file: sub.subtitle_link, + type: ext === 'vtt' ? 'vtt' : 'srt', + source: 'febbox' + }; + }) + .filter(Boolean); + } + + const v1 = Array.isArray(data) ? data : []; + return v1.map(x => ({ + label: x.label, + file: x.file || x.url, + type: 'vtt', + source: 'v1' + })); + } catch { + return []; + } + }) + ); + + return results.flat(); + } catch { + return []; + } +} + +export async function getSubPathsMovie(id) { + const imdbId = await resolveImdbId(id, false); + return [ + { base: SUBTITLE_BASES[0], path: `/movie/${id}` }, + { base: SUBTITLE_BASES[1], path: `/movie/${id}` }, + { base: SUBTITLE_BASES[2], path: `/subtitles/movie/${imdbId}.json` } + ]; +} + +export async function getSubPathsTv(id, season, episode) { + const imdbId = await resolveImdbId(id, true); + return [ + { base: SUBTITLE_BASES[0], path: `/tv/${id}/${season}/${episode}` }, + { base: SUBTITLE_BASES[1], path: `/tv/${id}/${season}/${episode}` }, + { base: SUBTITLE_BASES[2], path: `/subtitles/series/${imdbId}:${season}:${episode}.json` } + ]; +} + +export { SUBTITLE_BASES }; diff --git a/src/utils/helpers.js b/src/utils/helpers.js index 33f8305..c83d554 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -1,247 +1,586 @@ -import crypto from 'node:crypto'; - -export const UA_LIST = [ - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15', - 'Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0', - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', -]; - -export const getUA = () => UA_LIST[Math.floor(Math.random() * UA_LIST.length)]; -export const USER_AGENT = UA_LIST[0]; - -let sharedDcKey = null; - -export function decryptPartner(responseText, keyEnv) { - try { - const sep = responseText.indexOf(':'); - if (sep === -1) return null; - const iv = Buffer.from(responseText.slice(0, sep), 'base64'); - const encrypted = responseText.slice(sep + 1); - if (!sharedDcKey) sharedDcKey = crypto.createHash('sha256').update(keyEnv || '').digest(); - const decipher = crypto.createDecipheriv('aes-256-cbc', sharedDcKey, iv); - return JSON.parse(decipher.update(encrypted, 'base64', 'utf8') + decipher.final('utf8')); - } catch { - return null; - } -} - -export async function fetchJson(url, options) { - const res = await fetch(url, options); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - return res.json(); -} - -export async function fetchText(url, options) { - const res = await fetch(url, options); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - return res.text(); -} - -const tmdbInfoCache = new Map(); -const anilistCache = new Map(); -const tmdbValidationCache = new Map(); - -function pruneHelperCache(cache) { - const now = Date.now(); - for (const [k, v] of cache) if (now - v.ts >= v.ttl) cache.delete(k); - if (cache.size > 3000) { - const overflow = cache.size - 3000; - const it = cache.keys(); - for (let i = 0; i < overflow; i++) cache.delete(it.next().value); - } -} - -export async function resolveImdbId(id, isTv, tmdbApiKey) { - if (typeof id === 'string' && id.startsWith('tt')) return id; - const apiKey = tmdbApiKey || process.env.TMDB_API_KEY; - if (!apiKey) return `tt${id}`; - const type = isTv ? 'tv' : 'movie'; - try { - const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`, { - signal: AbortSignal.timeout(5000), - }); - if (!res.ok) return `tt${id}`; - const data = await res.json(); - return data.imdb_id || data.external_ids?.imdb_id || `tt${id}`; - } catch { - return `tt${id}`; - } -} - -setInterval(() => { - pruneHelperCache(tmdbValidationCache); - pruneHelperCache(tmdbInfoCache); - pruneHelperCache(anilistCache); -}, 60000).unref(); - -function cacheGet(cache, key) { - const entry = cache.get(key); - if (!entry) return undefined; - if (Date.now() - entry.ts >= entry.ttl) { cache.delete(key); return undefined; } - return entry.val; -} - -function cacheSet(cache, key, val, ttl) { cache.set(key, { val, ts: Date.now(), ttl }); } - -export async function createStreamArgs(source, sdk, id, s, e, clientIP = null) { - const audio = /dub$/.test(source.key) ? 'dub' : 'sub'; - return { - id: id, - s: s, - e: e, - clientIP: clientIP, - absoluteBase: '/', - audio, - sdk, - config: source, - } -} - -export async function validateTmdbId(tmdbApiKey, tmdbId, mediaType = 'movie') { - let apiKey = tmdbApiKey; - let id = tmdbId; - let type = mediaType; - if (id === 'movie' || id === 'tv' || !id) { - if (apiKey && apiKey !== 'movie' && apiKey !== 'tv') { - type = id || 'movie'; - id = apiKey; - apiKey = process.env.TMDB_API_KEY; - } - } - if (!apiKey) { - apiKey = process.env.TMDB_API_KEY; - } - const key = `${id}-${type}`; - const cached = cacheGet(tmdbValidationCache, key); - if (cached !== undefined) return cached; - if (!apiKey) return { valid: false, error: 'TMDB API key not configured' }; - try { - const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, { signal: AbortSignal.timeout(5000) }); - if (res.ok) { - const data = await res.json(); - const isValid = !!(data?.id && (data?.title || data?.name)); - const result = { valid: isValid, error: isValid ? null : 'Invalid TMDB ID' }; - cacheSet(tmdbValidationCache, key, result, 300000); - return result; - } - const result = { valid: false, error: `TMDB API error: ${res.status}` }; - cacheSet(tmdbValidationCache, key, result, 60000); - return result; - } catch (error) { - const result = { valid: false, error: `Failed to validate TMDB ID: ${error.message}` }; - cacheSet(tmdbValidationCache, key, result, 60000); - return result; - } -} - -export async function getTmdbInfo(tmdbApiKey, tmdbId, mediaType, season) { - let apiKey = tmdbApiKey; - let id = tmdbId; - let type = mediaType; - let s = season; - if (apiKey === 'movie' || apiKey === 'tv') { - type = apiKey; - id = tmdbId; - apiKey = process.env.TMDB_API_KEY; - } else if (id === 'movie' || id === 'tv') { - type = id; - id = apiKey; - apiKey = process.env.TMDB_API_KEY; - } - if (!apiKey) { - apiKey = process.env.TMDB_API_KEY; - } - const key = `${id}-${type}-${s || ''}`; - const cached = cacheGet(tmdbInfoCache, key); - if (cached !== undefined) return cached; - if (!apiKey) return { isAnime: false, titles: [], year: null, imdbId: null }; - try { - const [mainRes, seasonRes] = await Promise.all([ - fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`, { signal: AbortSignal.timeout(5000) }), - s && type === 'tv' ? fetch(`https://api.themoviedb.org/3/tv/${id}/season/${s}?api_key=${apiKey}`, { signal: AbortSignal.timeout(5000) }) : Promise.resolve(null), - ]); - let mainData = null, seasonData = null; - if (mainRes.ok) mainData = await mainRes.json(); else mainRes.body?.cancel(); - if (seasonRes) { if (seasonRes.ok) seasonData = await seasonRes.json(); else seasonRes.body?.cancel(); } - const genres = mainData?.genres || []; - const isAnime = genres.some(g => g.id === 16) && ((mainData?.origin_country || []).includes('JP') || mainData?.original_language === 'ja'); - const titles = []; - if (seasonData?.name) titles.push(seasonData.name); - const titleText = mainData?.title || mainData?.name || ''; - const ot = mainData?.original_title || mainData?.original_name || ''; - if (titleText) titles.push(titleText); - if (ot && ot !== titleText) titles.push(ot); - const dateStr = seasonData?.air_date || mainData?.release_date || mainData?.first_air_date || ''; - const result = { isAnime, titles: [...new Set(titles.filter(Boolean))], year: dateStr ? parseInt(dateStr.slice(0, 4), 10) : null, imdbId: mainData?.imdb_id || mainData?.external_ids?.imdb_id || null }; - cacheSet(tmdbInfoCache, key, result, 600000); - return result; - } catch { return { isAnime: false, titles: [], year: null, imdbId: null }; } -} - -export async function tmdbToAnilist(tmdbId, mediaType, season, titles = [], year = null) { - const key = `${tmdbId}-${mediaType}-${season || ''}`; - const cached = cacheGet(anilistCache, key); - if (cached !== undefined) return cached; - try { - const res = await fetch(`https://api.ani.zip/mappings?tmdb_id=${tmdbId}&type=${mediaType}&season=${season || 1}`, { signal: AbortSignal.timeout(6000) }); - if (res.ok) { - const id = (await res.json())?.mappings?.[0]?.anilist_id; - if (id) { cacheSet(anilistCache, key, id, 600000); return id; } - } else res.body?.cancel(); - } catch { } - if (!titles.length) return null; - const query = `query ($s: String) { Page(page:1,perPage:10) { media(search:$s,type:ANIME) { id title { romaji english native } startDate { year } format } } }`; - let bestId = null, bestScore = -1; - for (const searchTitle of titles) { - try { - const res = await fetch('https://graphql.anilist.co', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ query, variables: { s: searchTitle } }), signal: AbortSignal.timeout(6000) }); - if (!res.ok) { res.body?.cancel(); continue; } - for (const entry of (await res.json())?.data?.Page?.media || []) { - const entryTitles = [entry.title?.romaji, entry.title?.english, entry.title?.native].filter(Boolean).map(t => t.toLowerCase()); - const sl = searchTitle.toLowerCase(); - let score = 0; - if (entryTitles.some(t => t === sl)) score += 5; - else if (entryTitles.some(t => t.includes(sl) || sl.includes(t))) score += 3; - else if (entryTitles.some(t => { const w = sl.split(/\s+/).filter(x => x.length > 3); return w.length > 0 && w.every(x => t.includes(x)); })) score += 2; - else continue; - if (year && entry.startDate?.year) { - const diff = Math.abs(entry.startDate.year - year); - if (diff === 0) score += 3; else if (diff === 1) score += 1; else if (diff > 2) score -= 3; - } - if (mediaType === 'tv' && ['TV', 'TV_SHORT', 'ONA', 'OVA'].includes(entry.format)) score += 1; - if (mediaType === 'movie' && entry.format === 'MOVIE') score += 1; - if (score > bestScore) { bestScore = score; bestId = entry.id; } - } - if (bestScore >= 8) break; - } catch { } - } - if (bestId) cacheSet(anilistCache, key, bestId, 600000); - return bestId; -} - -export async function imdbToTmdb(tmdbApiKey, imdbId, mediaType = 'movie') { - let apiKey = tmdbApiKey; - let id = imdbId; - let type = mediaType; - if (id === 'movie' || id === 'tv') { - type = id; - id = apiKey; - apiKey = process.env.TMDB_API_KEY; - } - if (!apiKey) { - apiKey = process.env.TMDB_API_KEY; - } - const key = `imdb-${id}`; - const cached = cacheGet(tmdbInfoCache, key); - if (cached !== undefined) return cached; - if (!apiKey) return null; - try { - const data = await fetchJson(`https://api.themoviedb.org/3/find/${id}?api_key=${apiKey}&external_source=imdb_id`, { signal: AbortSignal.timeout(5000) }); - let result = null; - if (type === 'movie' && data.movie_results?.length) result = data.movie_results[0].id; - else if (type === 'tv' && data.tv_results?.length) result = data.tv_results[0].id; - if (result) cacheSet(tmdbInfoCache, key, result, 600000); - return result; - } catch { return null; } -} \ No newline at end of file +import crypto from 'node:crypto'; + +export const UA_LIST = [ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15', + 'Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', +]; + +export const getUA = () => UA_LIST[Math.floor(Math.random() * UA_LIST.length)]; +export const USER_AGENT = UA_LIST[0]; + +const DEFAULT_TMDB_KEY = '45dbdd5e714da212424bda0336167447'; + +let sharedDcKey = null; + +const tmdbInfoCache = new Map(); +const anilistCache = new Map(); +const tmdbValidationCache = new Map(); +const tmdbMetaCache = new Map(); + +function pruneCache(cache) { + const now = Date.now(); + for (const [k, v] of cache) { + if (now - v.ts >= v.ttl) cache.delete(k); + } + if (cache.size > 3000) { + const overflow = cache.size - 3000; + const it = cache.keys(); + for (let i = 0; i < overflow; i++) cache.delete(it.next().value); + } +} + +setInterval(() => { + pruneCache(tmdbValidationCache); + pruneCache(tmdbInfoCache); + pruneCache(anilistCache); + pruneCache(tmdbMetaCache); +}, 60000).unref(); + +function cacheGet(cache, key) { + const entry = cache.get(key); + if (!entry) return undefined; + if (Date.now() - entry.ts >= entry.ttl) { + cache.delete(key); + return undefined; + } + return entry.val; +} + +function cacheSet(cache, key, val, ttl) { + cache.set(key, { val, ts: Date.now(), ttl }); +} + +export function resolveTmdbKey(args = {}, sdk = null) { + return ( + args?.tmdbApiKey || + args?.sdk?.tmdbApiKey || + sdk?.tmdbApiKey || + process.env.TMDB_API_KEY || + DEFAULT_TMDB_KEY + ); +} + +export function slugify(text) { + if (!text) return ''; + return String(text) + .toLowerCase() + .normalize('NFKD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); +} + +export function cleanTitle(text) { + if (!text) return ''; + return String(text) + .toLowerCase() + .replace(/\(\d{4}\)/g, '') + .replace(/[^a-z0-9]/g, '') + .trim(); +} + +export function formatBytes(bytes, decimals = 2) { + if (!bytes || bytes <= 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1); + return `${(bytes / Math.pow(k, i)).toFixed(decimals)} ${sizes[i]}`; +} + +export function getRandomHex(size = 32) { + return crypto.randomBytes(size).toString('hex'); +} + +export async function fetchJson(url, options = {}) { + const res = await fetch(url, options); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); +} + +export async function fetchText(url, options = {}) { + const res = await fetch(url, options); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.text(); +} + +export function decryptPartner(responseText, keyEnv) { + try { + const sep = responseText.indexOf(':'); + if (sep === -1) return null; + const iv = Buffer.from(responseText.slice(0, sep), 'base64'); + const encrypted = responseText.slice(sep + 1); + if (!sharedDcKey) sharedDcKey = crypto.createHash('sha256').update(keyEnv || '').digest(); + const decipher = crypto.createDecipheriv('aes-256-cbc', sharedDcKey, iv); + return JSON.parse(decipher.update(encrypted, 'base64', 'utf8') + decipher.final('utf8')); + } catch { + return null; + } +} + +export async function getTmdbMetadata(id, isTv, apiKey = null) { + const key = apiKey || process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY; + const type = isTv ? 'tv' : 'movie'; + const cacheKey = `${id}-${type}-${key}`; + const cached = cacheGet(tmdbMetaCache, cacheKey); + if (cached !== undefined) return cached; + + const fallback = { + title: '', + originalTitle: '', + titles: [], + year: null, + releaseDate: '', + imdbId: null, + isAnime: false, + runtime: 0, + seasons: [], + }; + + try { + const url = `https://api.themoviedb.org/3/${type}/${id}?api_key=${key}&append_to_response=external_ids,alternative_titles`; + const res = await fetch(url, { signal: AbortSignal.timeout(6000) }); + if (!res.ok) { + cacheSet(tmdbMetaCache, cacheKey, fallback, 60000); + return fallback; + } + + const data = await res.json(); + const title = data.name || data.title || ''; + const originalTitle = data.original_name || data.original_title || ''; + const releaseDate = data.first_air_date || data.release_date || ''; + const year = releaseDate ? parseInt(releaseDate.slice(0, 4), 10) : null; + const imdbId = data.imdb_id || data.external_ids?.imdb_id || null; + const runtime = isTv ? (data.episode_run_time?.[0] || 24) : (data.runtime || 90); + + const titles = []; + if (title) titles.push(title); + if (originalTitle && !titles.includes(originalTitle)) titles.push(originalTitle); + + const alts = data.alternative_titles?.titles || data.alternative_titles?.results || []; + for (const alt of alts) { + const altTitle = alt.title || alt.name; + if (altTitle && !titles.includes(altTitle)) titles.push(altTitle); + } + + const genres = data.genres || []; + const isAnime = genres.some(g => g.id === 16) && ((data.origin_country || []).includes('JP') || data.original_language === 'ja'); + + const result = { + title, + originalTitle, + titles, + year, + releaseDate, + imdbId, + isAnime, + runtime, + seasons: Array.isArray(data.seasons) ? data.seasons : [], + }; + + cacheSet(tmdbMetaCache, cacheKey, result, 600000); + return result; + } catch { + cacheSet(tmdbMetaCache, cacheKey, fallback, 60000); + return fallback; + } +} + +export async function resolveImdbId(id, isTv, tmdbApiKey = null) { + if (typeof id === 'string' && id.startsWith('tt')) return id; + const meta = await getTmdbMetadata(id, isTv, tmdbApiKey); + return meta?.imdbId || `tt${id}`; +} + +export async function getTmdbInfo(arg1, arg2, arg3, arg4) { + let apiKey = null; + let id = null; + let type = null; + let season = null; + + if (arg1 === 'movie' || arg1 === 'tv') { + type = arg1; + id = arg2; + season = arg3; + apiKey = arg4 || process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY; + } else if (arg2 === 'movie' || arg2 === 'tv') { + if (typeof arg1 === 'string' && arg1.length > 20 && !/^\d+$/.test(arg1)) { + apiKey = arg1; + id = arg3; + type = arg2; + season = arg4; + } else { + id = arg1; + type = arg2; + season = arg3; + apiKey = arg4 || process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY; + } + } else if (arg3 === 'movie' || arg3 === 'tv') { + apiKey = arg1; + id = arg2; + type = arg3; + season = arg4; + } else { + id = arg1; + type = arg2 || 'movie'; + season = arg3; + apiKey = arg4 || process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY; + } + + if (!apiKey) apiKey = process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY; + + const isTv = type === 'tv'; + const key = `${id}-${type}-${season || ''}-${apiKey}`; + const cached = cacheGet(tmdbInfoCache, key); + if (cached !== undefined) return cached; + + try { + const meta = await getTmdbMetadata(id, isTv, apiKey); + let seasonTitle = null; + + if (season && isTv) { + try { + const seasonRes = await fetch(`https://api.themoviedb.org/3/tv/${id}/season/${season}?api_key=${apiKey}`, { signal: AbortSignal.timeout(5000) }); + if (seasonRes.ok) { + const seasonData = await seasonRes.json(); + if (seasonData?.name) seasonTitle = seasonData.name; + } + } catch { } + } + + const titles = seasonTitle ? [seasonTitle, ...meta.titles] : meta.titles; + const result = { + name: meta.title, + title: meta.title, + isAnime: meta.isAnime, + titles: [...new Set(titles.filter(Boolean))], + year: meta.year, + release_date: meta.releaseDate, + first_air_date: meta.releaseDate, + imdbId: meta.imdbId, + imdb_id: meta.imdbId, + external_ids: { imdb_id: meta.imdbId }, + seasons: meta.seasons, + runtime: meta.runtime, + }; + + cacheSet(tmdbInfoCache, key, result, 600000); + return result; + } catch { + const fallback = { + name: '', + title: '', + isAnime: false, + titles: [], + year: null, + release_date: '', + first_air_date: '', + imdbId: null, + imdb_id: null, + external_ids: { imdb_id: null }, + seasons: [], + runtime: 0, + }; + return fallback; + } +} + +export async function validateTmdbId(tmdbApiKey, tmdbId, mediaType = 'movie') { + let apiKey = tmdbApiKey; + let id = tmdbId; + let type = mediaType; + + if (id === 'movie' || id === 'tv' || !id) { + if (apiKey && apiKey !== 'movie' && apiKey !== 'tv') { + type = id || 'movie'; + id = apiKey; + apiKey = process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY; + } + } + if (!apiKey) apiKey = process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY; + + const key = `${id}-${type}`; + const cached = cacheGet(tmdbValidationCache, key); + if (cached !== undefined) return cached; + + try { + const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, { signal: AbortSignal.timeout(5000) }); + if (res.ok) { + const data = await res.json(); + const isValid = Boolean(data?.id && (data?.title || data?.name)); + const result = { valid: isValid, error: isValid ? null : 'Invalid TMDB ID' }; + cacheSet(tmdbValidationCache, key, result, 300000); + return result; + } + const result = { valid: false, error: `TMDB API error: ${res.status}` }; + cacheSet(tmdbValidationCache, key, result, 60000); + return result; + } catch (error) { + const result = { valid: false, error: `Failed to validate TMDB ID: ${error.message}` }; + cacheSet(tmdbValidationCache, key, result, 60000); + return result; + } +} + +export async function imdbToTmdb(tmdbApiKey, imdbId, mediaType = 'movie') { + let apiKey = tmdbApiKey; + let id = imdbId; + let type = mediaType; + + if (id === 'movie' || id === 'tv') { + type = id; + id = apiKey; + apiKey = process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY; + } + if (!apiKey) apiKey = process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY; + + const key = `imdb-${id}`; + const cached = cacheGet(tmdbInfoCache, key); + if (cached !== undefined) return cached; + + try { + const data = await fetchJson(`https://api.themoviedb.org/3/find/${id}?api_key=${apiKey}&external_source=imdb_id`, { signal: AbortSignal.timeout(5000) }); + let result = null; + if (type === 'movie' && data.movie_results?.length) result = data.movie_results[0].id; + else if (type === 'tv' && data.tv_results?.length) result = data.tv_results[0].id; + if (result) cacheSet(tmdbInfoCache, key, result, 600000); + return result; + } catch { + return null; + } +} + +export async function tmdbToAnilist(tmdbId, mediaType, season, titles = [], year = null) { + const key = `${tmdbId}-${mediaType}-${season || ''}`; + const cached = cacheGet(anilistCache, key); + if (cached !== undefined) return cached; + + try { + const res = await fetch(`https://api.ani.zip/mappings?tmdb_id=${tmdbId}&type=${mediaType}&season=${season || 1}`, { signal: AbortSignal.timeout(6000) }); + if (res.ok) { + const data = await res.json(); + const id = data?.mappings?.[0]?.anilist_id; + if (id) { + cacheSet(anilistCache, key, id, 600000); + return id; + } + } + } catch { } + + if (!titles.length) return null; + + const query = `query ($s: String) { Page(page:1,perPage:10) { media(search:$s,type:ANIME) { id title { romaji english native } startDate { year } format } } }`; + let bestId = null; + let bestScore = -1; + + for (const searchTitle of titles) { + try { + const res = await fetch('https://graphql.anilist.co', { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify({ query, variables: { s: searchTitle } }), + signal: AbortSignal.timeout(6000) + }); + if (!res.ok) continue; + + const resJson = await res.json(); + for (const entry of resJson?.data?.Page?.media || []) { + const entryTitles = [entry.title?.romaji, entry.title?.english, entry.title?.native].filter(Boolean).map(t => t.toLowerCase()); + const sl = searchTitle.toLowerCase(); + let score = 0; + + if (entryTitles.some(t => t === sl)) score += 5; + else if (entryTitles.some(t => t.includes(sl) || sl.includes(t))) score += 3; + else if (entryTitles.some(t => { + const w = sl.split(/\s+/).filter(x => x.length > 3); + return w.length > 0 && w.every(x => t.includes(x)); + })) score += 2; + else continue; + + if (year && entry.startDate?.year) { + const diff = Math.abs(entry.startDate.year - year); + if (diff === 0) score += 3; + else if (diff === 1) score += 1; + else if (diff > 2) score -= 3; + } + + if (mediaType === 'tv' && ['TV', 'TV_SHORT', 'ONA', 'OVA'].includes(entry.format)) score += 1; + if (mediaType === 'movie' && entry.format === 'MOVIE') score += 1; + + if (score > bestScore) { + bestScore = score; + bestId = entry.id; + } + } + if (bestScore >= 8) break; + } catch { } + } + + if (bestId) cacheSet(anilistCache, key, bestId, 600000); + return bestId; +} + +export async function createStreamArgs(source, sdk, id, s, e, clientIP = null) { + const audio = /dub$/.test(source.key) ? 'dub' : 'sub'; + return { + id, + s, + e, + clientIP, + absoluteBase: '/', + audio, + sdk, + config: source, + tmdbApiKey: resolveTmdbKey(null, sdk), + }; +} + +export function formatStreamResult(streams, defaultHeaders = {}, extra = {}) { + if (!streams) return null; + const list = Array.isArray(streams) ? streams.filter(Boolean) : [streams]; + if (!list.length) return null; + + const allUrls = list.map(item => { + const itemUrl = item.url || item.file || item.stream || item.src; + if (!itemUrl) return null; + return { + url: itemUrl, + server: item.server || item.label || item.name || 'Server', + quality: item.quality || 'Auto', + type: item.type || (itemUrl.includes('.m3u8') ? 'hls' : (itemUrl.includes('.mpd') ? 'dash' : 'mp4')), + headers: item.headers || defaultHeaders, + ...(item.subtitles ? { subtitles: item.subtitles } : {}), + ...(item.skipProxy !== undefined ? { skipProxy: item.skipProxy } : {}), + ...(item.audio ? { audio: item.audio } : {}), + ...(item.label ? { label: item.label } : {}), + }; + }).filter(Boolean); + + if (!allUrls.length) return null; + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + server: allUrls[0].server, + quality: allUrls[0].quality, + type: allUrls[0].type, + allUrls, + ...extra, + }; +} + +export async function scrapeVidcoreFamily({ id, s, e, baseUrl, apiName, serverLabel }) { + const isTv = Boolean(s && e); + const targetUrl = isTv + ? `${baseUrl}/tv/${id}/${s}/${e}/` + : `${baseUrl}/movie/${id}`; + + const ua = getUA(); + const baseHeaders = { + 'User-Agent': ua, + Referer: `${baseUrl}/`, + 'X-Requested-With': 'XMLHttpRequest', + }; + + try { + const pageRes = await fetch(targetUrl, { + headers: { 'User-Agent': ua }, + signal: AbortSignal.timeout(10000), + }); + if (!pageRes.ok) return null; + const html = await pageRes.text(); + + const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/); + if (!match || !match[1]) return null; + const enToken = match[1]; + + const s1Res = await fetch(`https://enc-dec.app/api/enc-${apiName}?text=${encodeURIComponent(enToken)}&stage=1`, { + signal: AbortSignal.timeout(8000), + }); + const s1Data = await s1Res.json(); + if (s1Data?.status !== 200 || !s1Data?.result?.stage1) return null; + + const token1 = s1Data.result.token; + const stage1Url = s1Data.result.stage1; + + const post1Res = await fetch(stage1Url, { + method: 'POST', + headers: { ...baseHeaders, 'X-CSRF-Token': token1 }, + signal: AbortSignal.timeout(10000), + }); + if (!post1Res.ok) return null; + const stage1Text = await post1Res.text(); + + const s2Res = await fetch(`https://enc-dec.app/api/enc-${apiName}?text=${encodeURIComponent(stage1Text)}&stage=2`, { + signal: AbortSignal.timeout(8000), + }); + const s2Data = await s2Res.json(); + if (s2Data?.status !== 200 || !s2Data?.result?.servers) return null; + + const { servers: serversUrl, stream: streamBaseUrl, token: token2 } = s2Data.result; + const currentHeaders = { ...baseHeaders, 'X-CSRF-Token': token2 }; + + const serversRes = await fetch(serversUrl, { + method: 'POST', + headers: currentHeaders, + signal: AbortSignal.timeout(10000), + }); + if (!serversRes.ok) return null; + const serversEncrypted = await serversRes.text(); + + const decServersRes = await fetch(`https://enc-dec.app/api/dec-${apiName}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text: serversEncrypted }), + signal: AbortSignal.timeout(8000), + }); + const decServersData = await decServersRes.json(); + if (decServersData?.status !== 200 || !Array.isArray(decServersData.result)) return null; + + const allUrls = []; + + for (const server of decServersData.result) { + if (!server?.data) continue; + try { + const streamUrl = `${streamBaseUrl}/${server.data}`; + const streamEncRes = await fetch(streamUrl, { + method: 'POST', + headers: currentHeaders, + signal: AbortSignal.timeout(8000), + }); + if (!streamEncRes.ok) continue; + const streamEncText = await streamEncRes.text(); + + const decStreamRes = await fetch(`https://enc-dec.app/api/dec-${apiName}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text: streamEncText }), + signal: AbortSignal.timeout(8000), + }); + const decStreamData = await decStreamRes.json(); + if (decStreamData?.status !== 200 || !decStreamData.result) continue; + + const streamResult = decStreamData.result; + const resultUrl = typeof streamResult === 'string' ? streamResult : streamResult?.url || streamResult?.file; + + if (typeof resultUrl === 'string' && resultUrl.startsWith('http')) { + allUrls.push({ + url: resultUrl, + server: `${serverLabel} - ${server.name || server.title || 'Server'}`, + label: server.name || server.title || serverLabel, + type: resultUrl.includes('.m3u8') ? 'hls' : 'mp4', + headers: { + 'User-Agent': ua, + Referer: `${baseUrl}/`, + }, + }); + } + } catch { } + } + + return formatStreamResult(allUrls); + } catch { + return null; + } +} diff --git a/test/index.js b/test/index.js index bbe0c45..c6dd831 100644 --- a/test/index.js +++ b/test/index.js @@ -18,7 +18,7 @@ const CONFIG = { TEST_SUBTITLE: false, TEST_DOWNLOAD: false, TEST_MOVIE: true, - TEST_TV: true, + TEST_TV: false, TEST_MANGA: false, MANGA_QUERY: "Steel Ball Run", @@ -32,7 +32,7 @@ dotenv.config({ }); // debug -if (CONFIG.DEBUG) { +if (CONFIG.DEBUG && !process.argv.includes("all")) { const originalFetch = globalThis.fetch; const debugFile = path.resolve("test/debug-output.json"); @@ -118,6 +118,45 @@ const sdk = new VylaSDK({ ...CONFIG.EXTRA_SDK_PARAMS }); +if (process.argv.includes("all")) { + const sources = sdk.getSources(true); + console.log(`Testing ${sources.length} sources in parallel...\n`); + + const results = {}; + const failed = []; + + await Promise.all( + sources.map(async (cfg) => { + try { + const res = await sdk.probeSource(cfg.key); + results[cfg.key] = res; + if (res?.ok) { + console.log(`[PASS] ${cfg.key} (${res.ms}ms)`); + } else { + failed.push(cfg.key); + console.log(`[FAIL] ${cfg.key} (${res?.ms ?? 0}ms)`); + } + } catch (err) { + results[cfg.key] = { ok: false, ms: null, error: err.message }; + failed.push(cfg.key); + console.log(`[FAIL] ${cfg.key} (${err.message})`); + } + }) + ); + + console.log(`\nFinished testing. Total: ${sources.length}, Passed: ${sources.length - failed.length}, Failed: ${failed.length}`); + console.log("Sources that returned nothing:", failed); + + const debugFile = path.resolve("test/debug-output.json"); + fs.mkdirSync(path.dirname(debugFile), { recursive: true }); + fs.writeFileSync(debugFile, JSON.stringify({ + sourcesThatReturnedNothing: failed, + results + }, null, 2)); + + process.exit(0); +} + console.log("Starting..."); if (CONFIG.TEST_SUBTITLE && CONFIG.TEST_TV) {