diff --git a/src/config.js b/src/config.js index 0d132fa..547b70a 100644 --- a/src/config.js +++ b/src/config.js @@ -1,4 +1,4 @@ -// Currently active sources reported on 9:22:41 PM PST 7/30/2026 +// Currently active sources reported on 6:18:22 PM PST 9/13/2026 // None Anime Tested: 936075 // Anime Tested: 37854 @@ -6,6 +6,18 @@ // Fshare/Fsonic Tested: 155 ( they don't have Michael ) export const SOURCES = [ + { + key: '4khdhub', + label: '4KHDHub', + sourceFile: '4khdhub', + proxyParam: 'khd', + timeout: 35000, + jitter: 500, + retries: 1, + multiUrl: true, + skipProxy: true, + }, + { key: '123anime', label: '123Anime', @@ -25,6 +37,25 @@ export const SOURCES = [ },], }, + { + key: '111477', + label: '111477', + sourceFile: '111477', + proxyParam: 'a11', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + skipProxy: true, + cdnHeaders: [{ + pattern: /111477\.xyz/i, + headers: { + Referer: 'https://st.111477.xyz/', + Origin: 'https://st.111477.xyz', + }, + }], + }, + { key: 'anihq-sub', label: 'AniHQ (Sub)', @@ -69,40 +100,6 @@ export const SOURCES = [ retries: 2 }, - { - key: 'anipm-sub', - sourceFile: 'anipm', - label: 'AniPM (Sub)', - proxyParam: 'apsub', - timeout: 25000, - jitter: 500, - retries: 2, - skipProxy: true, - skipVerify: true, - multiUrl: true, - verifyHeaders: { - Referer: 'https://ani.pm/', - Origin: 'https://ani.pm', - }, - }, - - { - key: 'anipm-dub', - sourceFile: 'anipm', - label: 'AniPM (Dub)', - proxyParam: 'apdub', - timeout: 25000, - jitter: 500, - retries: 2, - skipProxy: true, - skipVerify: true, - multiUrl: true, - verifyHeaders: { - Referer: 'https://ani.pm/', - Origin: 'https://ani.pm', - }, - }, - { key: 'bcine', label: 'Bcine', @@ -173,6 +170,25 @@ export const SOURCES = [ }], }, + { + key: 'downloadeverything', + label: 'DownloadEverything', + sourceFile: 'downloadeverything', + proxyParam: 'de', + timeout: 35000, + jitter: 500, + retries: 1, + multiUrl: true, + skipProxy: true, + cdnHeaders: [{ + pattern: /cloudflarestorage\.com|pixeldrain\.com|hakunaymatata\.com/i, + headers: { + Referer: 'https://downloadeverythingfromeverywhere.com/', + Origin: 'https://downloadeverythingfromeverywhere.com', + }, + }], + }, + { key: 'flaxmovies', label: 'FlaxMovies', @@ -314,16 +330,6 @@ export const SOURCES = [ skipProxy: true, }, - { - key: 'mapple', - label: 'Mapple', - sourceFile: 'mapple', - proxyParam: 'mp', - timeout: 15000, - retries: 1, - jitter: 0 - }, - { key: 'meowtv', label: 'MeowTV', @@ -336,6 +342,44 @@ export const SOURCES = [ skipProxy: true }, + { + key: 'movienight', + label: 'MovieNight', + sourceFile: 'movienight', + proxyParam: 'mn', + timeout: 20000, + jitter: 500, + retries: 2, + multiUrl: true, + skipProxy: true, + cdnHeaders: [{ + pattern: /movienig\.ht/i, + headers: { + Referer: 'https://movienig.ht/', + Origin: 'https://movienig.ht', + }, + }], + }, + + { + key: 'movy', + label: 'Movy', + sourceFile: 'movy', + proxyParam: 'mv', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + skipProxy: true, + cdnHeaders: [{ + pattern: /wecollege\.net|movy\.bz/i, + headers: { + Referer: 'https://www.movy.bz/', + Origin: 'https://www.movy.bz', + }, + }], + }, + { key: 'peestream', label: 'PeeStream', @@ -372,28 +416,6 @@ export const SOURCES = [ multiUrl: true, }, - { - key: 'nova', - label: 'Nova', - sourceFile: 'nova', - proxyParam: 'nv', - timeout: 25000, - jitter: 500, - retries: 1, - multiUrl: true, - }, - - { - key: 'megasource', - label: 'MegaSource', - sourceFile: 'megasource', - proxyParam: 'mg', - timeout: 25000, - jitter: 500, - retries: 1, - multiUrl: true, - }, - { key: 'purstream', sourceFile: 'purstream', @@ -416,6 +438,44 @@ export const SOURCES = [ multiUrl: true, }, + { + key: 'streamaggregator', + label: 'StreamAggregator', + sourceFile: 'streamaggregator', + proxyParam: 'sa', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + skipProxy: true, + cdnHeaders: [{ + pattern: /streamaggregator\.in/i, + headers: { + Referer: 'https://streamaggregator.in/', + Origin: 'https://streamaggregator.in', + }, + }], + }, + + { + key: 'vadapav', + label: 'Vadapav', + sourceFile: 'vadapav', + proxyParam: 'vp', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + skipProxy: true, + cdnHeaders: [{ + pattern: /vadapav\.mov/i, + headers: { + Referer: 'https://stremio.vadapav.mov/', + Origin: 'https://stremio.vadapav.mov', + }, + }], + }, + { key: 'vidapi', label: 'VidAPI', @@ -438,23 +498,6 @@ export const SOURCES = [ sourcesTimeout: 10000, }, - { - key: 'videasy', - label: 'Videasy', - sourceFile: 'videasy', - proxyParam: 'vy', - timeout: 40000, - sourcesTimeout: 10000, - jitter: 900, - retries: 3, - multiUrl: true, - verifyHeaders: { - Accept: 'application/json, /; q=0.01', - Referer: 'https://player.videasy.net/', - Origin: 'https://player.videasy.net', - }, - }, - { key: 'vidfast', label: 'Vidfast', @@ -487,7 +530,6 @@ export const SOURCES = [ retries: 2, skipProxy: true, multiUrl: true, - disabled: true, // Temporarily disabled because their site is down, all streams are returning 403's }, { @@ -603,25 +645,41 @@ export const SOURCES = [ }, { - key: 'xpass', - sourceFile: 'xpass', - label: 'XPass', - proxyParam: 'xp', - timeout: 20000, - jitter: 500, - retries: 2, - skipProxy: true, - }, - - { - key: 'zxcstream', - label: 'ZxcStream', - sourceFile: 'zxcstream', - proxyParam: 'zxc', + key: 'vuflix', + label: 'Vuflix', + sourceFile: 'vuflix', + proxyParam: 'vfx', timeout: 25000, jitter: 500, retries: 2, multiUrl: true, + skipProxy: true, + cdnHeaders: [{ + pattern: /vuflix\.co/i, + headers: { + Referer: 'https://vuflix.co/', + Origin: 'https://vuflix.co', + }, + }], + }, + + { + key: 'xdownloader', + label: 'XDownloader', + sourceFile: 'xdownloader', + proxyParam: 'xd', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + skipProxy: true, + cdnHeaders: [{ + pattern: /films365\.org/i, + headers: { + Referer: 'https://www.films365.org/', + Origin: 'https://www.films365.org', + }, + }], }, ]; diff --git a/src/downloads/downloads.js b/src/downloads/downloads.js index 709994a..587ff74 100644 --- a/src/downloads/downloads.js +++ b/src/downloads/downloads.js @@ -1,12 +1,53 @@ -import { getDownloads, getDownloadsTv } from "./sources/trendimovies.js"; +import SOURCES from "../config.js"; -export async function fetchDownloads(id, s = null, e = null) { - try { - const res = s != null && e != null - ? await getDownloadsTv(id, s, e) - : await getDownloads(id); - return res || []; - } catch { - return []; +export async function fetchDownloads(sdkOrId, idOrS = null, sOrE = null, maybeE = null) { + let sdk = null; + let id = sdkOrId; + let s = idOrS; + let e = sOrE; + + if (sdkOrId && typeof sdkOrId === 'object' && typeof sdkOrId.getStream === 'function') { + sdk = sdkOrId; + id = idOrS; + s = sOrE; + e = maybeE; } + + if (!sdk || !id) return []; + + const activeSources = SOURCES.filter(cfg => !cfg.disabled && cfg.skipProxy); + const seenUrls = new Set(); + const downloads = []; + + const results = await Promise.allSettled( + activeSources.map(async (cfg) => { + try { + const res = await sdk.getStream(cfg.key, id, s, e); + if (!res) return; + + const items = Array.isArray(res.allUrls) && res.allUrls.length > 0 + ? res.allUrls + : (res.url ? [{ url: res.url, label: cfg.label }] : []); + + for (const item of items) { + if (!item?.url || typeof item.url !== 'string') continue; + const url = item.url.trim(); + + if (!url.startsWith('http') || url.includes('.m3u8')) continue; + if (!/\.mp4(\?|$)/i.test(url) && !url.includes('.mp4')) continue; + if (seenUrls.has(url)) continue; + + seenUrls.add(url); + + downloads.push({ + source: cfg.key, + label: item.label || cfg.label, + url, + }); + } + } catch { } + }) + ); + + return downloads; } \ No newline at end of file diff --git a/src/downloads/sources/trendimovies.js b/src/downloads/sources/trendimovies.js deleted file mode 100644 index b9f92d3..0000000 --- a/src/downloads/sources/trendimovies.js +++ /dev/null @@ -1,63 +0,0 @@ -const BASE = "https://trendimovies.com"; - -const HEADERS = { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", - Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Accept-Language": "en-US,en;q=0.9", - "Referer": "https://trendimovies.com/", -}; - -async function fetchPage(path) { - const res = await fetch(`${BASE}${path}`, { headers: HEADERS }); - if (!res.ok) throw new Error(`trendimovies ${res.status}`); - return res.text(); -} - -function parseLinks(html) { - const m = html.match(/DownloadSection[^>]*props="([^"]+)"/s) || - html.match(/props="([^"]+)"[^>]*ssr[^>]*client="load"[^>]*opts="[^"]*DownloadSection/s); - if (!m) { - const alt = html.match(/"DownloadSection"/); - if (!alt) return []; - const propsMatch = html.match(/props="({[^"]*DownloadSection[^"]*})"/s); - if (!propsMatch) return []; - return extractLinks(propsMatch[1].replace(/"/g, '"').replace(/'/g, "'")); - } - return extractLinks(m[1].replace(/"/g, '"').replace(/'/g, "'")); -} - -function extractLinks(propsJson) { - try { - const parsed = JSON.parse(propsJson); - const rawLinks = parsed?.links?.[1]; - if (!Array.isArray(rawLinks)) return []; - return rawLinks - .map(item => item?.[1]) - .filter(l => l && typeof l === "object") - .map(l => ({ - url: unwrap(l.url), - quality: unwrap(l.quality) || "HD", - size: unwrap(l.file_size) || null, - type: "mkv", - active: unwrap(l.is_active) !== false, - })) - .filter(l => l.url && l.active); - } catch { - return []; - } -} - -function unwrap(v) { - if (Array.isArray(v) && v.length === 2 && (v[0] === 0 || v[0] === 1)) return v[1]; - return v; -} - -export async function getDownloads(tmdbId) { - const html = await fetchPage(`/movie/${tmdbId}`); - return parseLinks(html); -} - -export async function getDownloadsTv(tmdbId, season, episode) { - const html = await fetchPage(`/tv/${tmdbId}/season/${season}/episode/${episode}`); - return parseLinks(html); -} \ No newline at end of file diff --git a/src/sdk.d.ts b/src/sdk.d.ts index fe3eba0..0f27998 100644 --- a/src/sdk.d.ts +++ b/src/sdk.d.ts @@ -59,7 +59,6 @@ export interface VylaSDKOptions { tmdbApiKey?: string; } -// Descriptions written with Claude. export default class VylaSDK { tmdbApiKey?: string; diff --git a/src/sdk.js b/src/sdk.js index 0cfe748..e5fece4 100644 --- a/src/sdk.js +++ b/src/sdk.js @@ -63,6 +63,6 @@ export default class VylaSDK { return await fetchSubtitles(paths); } async getDownloads(id, s = null, e = null) { - return await fetchDownloads(id, s, e); + return await fetchDownloads(this, id, s, e); } } \ No newline at end of file diff --git a/src/sources/111477.js b/src/sources/111477.js new file mode 100644 index 0000000..ca88abd --- /dev/null +++ b/src/sources/111477.js @@ -0,0 +1,101 @@ +import { getUA } from '../utils/helpers.js'; + +const SERVICE_ORIGIN = 'https://st.111477.xyz'; + +function generateManifestBaseUrl(tmdbKey) { + let config = 'https://a.111477.xyz/::sort=file-desc::limit=3'; + if (tmdbKey) { + config += `::tmdb=${tmdbKey}`; + } + const b64 = Buffer.from(config, 'utf8') + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); + 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); + const ua = getUA(); + const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY; + + const imdbId = await resolveImdbId(sdk, id, isTv); + const targetIds = []; + + if (imdbId && imdbId.startsWith('tt')) { + targetIds.push(imdbId); + } + targetIds.push(`tmdb:${id}`); + + const addonBase = generateManifestBaseUrl(apiKey); + const seenUrls = new Set(); + const allUrls = []; + + const defaultHeaders = { + 'User-Agent': ua, + Accept: 'application/json, text/plain, */*', + }; + + for (const targetId of targetIds) { + const endpoint = isTv + ? `${addonBase}/stream/series/${targetId}:${s || 1}:${e || 1}.json` + : `${addonBase}/stream/movie/${targetId}.json`; + + try { + const res = await fetch(endpoint, { + headers: defaultHeaders, + signal: AbortSignal.timeout(12000), + }); + + if (!res.ok) continue; + const data = await res.json(); + + 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; + + seenUrls.add(url); + + const rawTitle = item.title ? String(item.title).trim() : ''; + const rawName = item.name ? String(item.name).trim() : '111477'; + const label = rawTitle || rawName; + + allUrls.push({ + url, + label, + headers: defaultHeaders, + }); + } + + if (allUrls.length > 0) break; + } + } catch { } + } + + 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/4khdhub.js b/src/sources/4khdhub.js new file mode 100644 index 0000000..fa96f27 --- /dev/null +++ b/src/sources/4khdhub.js @@ -0,0 +1,324 @@ +import { getUA } from '../utils/helpers.js'; + +const INITIAL_BASE_URL = 'https://4khdhub.one'; +let cachedBaseUrl = null; +let cachedBaseUrlExpiry = 0; + +function rot13(str) { + return str.replace(/[a-zA-Z]/g, (c) => { + const code = c.charCodeAt(0); + if (code >= 65 && code <= 90) { + return String.fromCharCode(((code - 65 + 13) % 26) + 65); + } + return String.fromCharCode(((code - 97 + 13) % 26) + 97); + }); +} + +function levenshtein(a, b) { + if (a === b) return 0; + if (!a.length) return b.length; + if (!b.length) return a.length; + const la = a.length; + const lb = b.length; + const d = Array.from({ length: la + 1 }, () => new Array(lb + 1).fill(0)); + for (let i = 0; i <= la; i++) d[i][0] = i; + for (let j = 0; j <= lb; j++) d[0][j] = j; + for (let i = 1; i <= la; i++) { + for (let j = 1; j <= lb; j++) { + const cost = a[i - 1] === b[j - 1] ? 0 : 1; + d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost); + } + } + return d[la][lb]; +} + +async function getBaseUrl(ua) { + if (cachedBaseUrl && Date.now() < cachedBaseUrlExpiry) { + return cachedBaseUrl; + } + + let currentUrl = INITIAL_BASE_URL; + for (let i = 0; i < 10; i++) { + try { + const res = await fetch(currentUrl, { + method: 'HEAD', + headers: { 'User-Agent': ua }, + redirect: 'manual', + signal: AbortSignal.timeout(10000), + }); + + if (res.status >= 300 && res.status < 400) { + const location = res.headers.get('location'); + if (location) { + currentUrl = new URL(location, currentUrl).toString(); + continue; + } + } + break; + } catch { + break; + } + } + + const parsed = new URL(currentUrl); + cachedBaseUrl = `${parsed.protocol}//${parsed.host}`; + cachedBaseUrlExpiry = Date.now() + 3600000; + return cachedBaseUrl; +} + +async function resolveMetadata(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(8000), + }); + 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 ? parseInt(releaseDate.split('-')[0], 10) : null; + return { title, year }; + } catch { + return null; + } +} + +function sanitizeStreamUrl(rawUrl) { + try { + const parsed = new URL(rawUrl); + parsed.pathname = parsed.pathname.replaceAll('::', '%3A%3A'); + return parsed.toString(); + } catch { + return rawUrl.replaceAll('::', '%3A%3A'); + } +} + +async function validateStreamUrl(url, ua) { + try { + const res = await fetch(url, { + headers: { + 'User-Agent': ua, + Range: 'bytes=0-1024', + }, + signal: AbortSignal.timeout(4000), + }); + + const contentType = (res.headers.get('content-type') || '').toLowerCase(); + const isSuccess = (res.status >= 200 && res.status < 300) || res.status === 206; + const isHtmlOrJsonError = contentType.includes('text/html') || contentType.includes('application/json'); + + if (!isSuccess || isHtmlOrJsonError) return false; + return true; + } catch { + return true; + } +} + +async function resolveRedirectUrl(redirectUrl, ua) { + try { + const res = await fetch(redirectUrl, { + headers: { 'User-Agent': ua }, + signal: AbortSignal.timeout(15000), + }); + if (!res.ok) return null; + const html = await res.text(); + + let nextUrl = ''; + const downloadMatch = html.match(/]*class="[^"]*btn-success/); + if (finalMatch) { + return finalMatch[1]; + } + + return null; + } catch { + return null; + } +} + +async function extractSourceResults(itemHtml, ua) { + const sizeMatch = itemHtml.match(/([\d.]+ ?[GM]B)/); + const sizeStr = sizeMatch ? sizeMatch[1] : null; + + const heightMatch = itemHtml.match(/\d{3,}p/); + const heightStr = heightMatch ? heightMatch[0] : null; + + const fileTitleMatch = itemHtml.match(/class="(?:file-title|episode-file-title)"[^>]*>([^<]+)/); + const fileTitle = fileTitleMatch ? fileTitleMatch[1].trim() : 'Unknown'; + + const displayParts = [fileTitle]; + if (heightStr) displayParts.push(heightStr); + if (sizeStr) displayParts.push(sizeStr); + + let hubCloudUrl = null; + let hubDriveUrl = null; + + const linkRegex = /]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi; + let match; + while ((match = linkRegex.exec(itemHtml)) !== null) { + const href = match[1]; + const text = match[2]; + if (text.includes('HubCloud') && !hubCloudUrl) { + hubCloudUrl = href; + } else if (text.includes('HubDrive') && !hubDriveUrl) { + hubDriveUrl = href; + } + } + + const redirectUrl = hubCloudUrl || hubDriveUrl; + if (!redirectUrl) return null; + + const resolvedUrl = await resolveRedirectUrl(redirectUrl, ua); + if (!resolvedUrl) return null; + + const safeUrl = sanitizeStreamUrl(resolvedUrl); + const isValid = await validateStreamUrl(safeUrl, ua); + if (!isValid) return null; + + return { + url: safeUrl, + label: displayParts.join(' · '), + headers: { + 'User-Agent': ua, + }, + }; +} + +export async function getStream({ id, s, e, sdk }) { + const isTv = Boolean(s && e); + const ua = getUA(); + + const meta = await resolveMetadata(sdk, id, isTv); + if (!meta?.title) return null; + + const baseUrl = await getBaseUrl(ua); + const searchUrl = `${baseUrl}/?s=${encodeURIComponent(meta.title)}`; + + const searchRes = await fetch(searchUrl, { + headers: { 'User-Agent': ua }, + signal: AbortSignal.timeout(15000), + }); + if (!searchRes.ok) return null; + const searchHtml = await searchRes.text(); + + const cardRegex = /]*\bclass="[^"]*movie-card[^"]*"[^>]*)>([\s\S]*?)<\/a>/gi; + const formatFilter = isTv ? 'Series' : 'Movies'; + let targetPageUrl = null; + + let cardMatch; + while ((cardMatch = cardRegex.exec(searchHtml)) !== null) { + const attributes = cardMatch[1]; + const inner = cardMatch[2]; + + const hrefMatch = attributes.match(/href="([^"]+)"/); + if (!hrefMatch) continue; + const href = hrefMatch[1]; + + const formatMatches = [...inner.matchAll(/class="movie-card-format"[^>]*>([\s\S]*?)<\//gi)]; + const formatTexts = formatMatches.map(m => m[1].trim()); + if (!formatTexts.some(t => t.includes(formatFilter))) continue; + + if (meta.year != null) { + const metaMatch = inner.match(/class="movie-card-meta"[^>]*>([\s\S]*?)<\//i); + const yearMatch = metaMatch ? metaMatch[1].match(/\d{4}/) : null; + if (yearMatch) { + const cardYear = parseInt(yearMatch[0], 10); + if (Math.abs(cardYear - meta.year) > 1) continue; + } + } + + const titleMatch = inner.match(/class="movie-card-title"[^>]*>([\s\S]*?)<\//i); + let cardTitle = titleMatch ? titleMatch[1] : ''; + cardTitle = cardTitle.replace(/\[.*?\]/g, '').trim(); + + const diff = levenshtein(cardTitle.toLowerCase(), meta.title.toLowerCase()); + const isMatch = diff < 5 || (cardTitle.toLowerCase().includes(meta.title.toLowerCase()) && diff < 16); + if (!isMatch) continue; + + targetPageUrl = new URL(href, baseUrl).toString(); + break; + } + + if (!targetPageUrl) return null; + + const pageRes = await fetch(targetPageUrl, { + headers: { 'User-Agent': ua }, + signal: AbortSignal.timeout(15000), + }); + if (!pageRes.ok) return null; + const pageHtml = await pageRes.text(); + + const matchedHtmlBlocks = []; + + if (isTv) { + const seasonStr = String(s).padStart(2, '0'); + const episodeStr = String(e).padStart(2, '0'); + + const epItemRegex = /class="[^"]*episode-item[^"]*"[^>]*>([\s\S]*?)(?=(?:class="[^"]*episode-item"|<\/main>|$))/gi; + let epMatch; + while ((epMatch = epItemRegex.exec(pageHtml)) !== null) { + const epBlock = epMatch[1]; + const titleMatch = epBlock.match(/class="episode-title"[^>]*>([\s\S]*?)<\//i); + const titleText = titleMatch ? titleMatch[1] : ''; + if (!titleText.includes(`S${seasonStr}`)) continue; + + const dlItemRegex = /class="[^"]*episode-download-item[^"]*"[^>]*>([\s\S]*?)(?=(?:class="[^"]*episode-download-item"|<\/div>\s*<\/div>|$))/gi; + let dlMatch; + while ((dlMatch = dlItemRegex.exec(epBlock)) !== null) { + const dlBlock = dlMatch[1]; + if (dlBlock.includes(`Episode-${episodeStr}`)) { + matchedHtmlBlocks.push(dlBlock); + } + } + } + } else { + const dlItemRegex = /class="[^"]*download-item[^"]*"[^>]*>([\s\S]*?)(?=(?:class="[^"]*download-item"|<\/main>|$))/gi; + let dlMatch; + while ((dlMatch = dlItemRegex.exec(pageHtml)) !== null) { + matchedHtmlBlocks.push(dlMatch[1]); + } + } + + if (matchedHtmlBlocks.length === 0) return null; + + const results = await Promise.all(matchedHtmlBlocks.map((block) => extractSourceResults(block, ua))); + const allUrls = results.filter(Boolean); + + 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/anipm.js b/src/sources/anipm.js deleted file mode 100644 index e0d1b09..0000000 --- a/src/sources/anipm.js +++ /dev/null @@ -1,70 +0,0 @@ -import { getTmdbInfo, fetchJson } from '../utils/helpers.js'; - -const BASE_URL = "https://ani.pm"; - -function cleanTitle(t) { return t ? t.toLowerCase().replace(/[^a-z0-9]/g, '') : ''; } - -async function searchCatalog(query) { - try { - const data = await fetchJson(`${BASE_URL}/api/anime/catalog?q=${encodeURIComponent(query)}&page=1`, { signal: AbortSignal.timeout(6000) }); - return Array.isArray(data?.items) ? data.items : []; - } catch { return []; } -} - -async function resolveEntry(titles, year) { - const orderedTitles = titles.length > 1 ? [...titles].reverse() : titles; - let best = null, bestScore = -1; - for (const title of orderedTitles) { - const items = await searchCatalog(title); - const targetClean = cleanTitle(title); - for (const item of items) { - const itemTitles = [item.title, item.native].filter(Boolean).map(cleanTitle); - let score = 0; - if (itemTitles.some(t => t === targetClean)) score += 5; - else if (itemTitles.some(t => t.includes(targetClean) || targetClean.includes(t))) score += 3; - else continue; - if (item.type === 'TV') score += 4; - else if (item.type === 'Movie' || item.type === 'Special' || item.type === 'ONA') score -= 2; - if (year && item.year) { - const diff = Math.abs(item.year - year); - if (diff === 0) score += 3; - else if (diff === 1) score += 1; - else if (diff > 2) score -= 3; - } - if (score > bestScore) { bestScore = score; best = item; } - } - if (bestScore >= 9) break; - } - return best; -} - -export async function getStream({ id, s, e, audio }) { - try { - const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s); - if (!info || !info.isAnime) return null; - const entry = await resolveEntry(info.titles, info.year); - if (!entry) return null; - const episodeNum = e || 1; - const params = new URLSearchParams({ title: entry.title || '', ep: String(episodeNum) }); - if (entry.year) params.set('year', entry.year); - if (entry.anilistId) params.set('anilistId', entry.anilistId); - if (entry.malId) params.set('malId', entry.malId); - const servers = await fetchJson(`${BASE_URL}/api/anime/src/servers?${params}`, { signal: AbortSignal.timeout(10000) }); - if (!servers) return null; - const audiosToTry = audio === "all" ? ["sub", "dub"] : (audio === "dub" ? ["dub", "sub"] : ["sub", "dub"]); - for (const aud of audiosToTry) { - const list = servers[aud]; - if (!Array.isArray(list) || !list.length) continue; - const allUrls = list.filter(srv => (srv.kind === "hls" || srv.kind === "file") && srv.url) - .sort((a, b) => (b.priority || 0) - (a.priority || 0)) - .map(srv => ({ url: srv.url.startsWith('/') ? `${BASE_URL}${srv.url}` : srv.url, type: srv.kind === "hls" ? "hls" : "mp4", audio: aud, server: srv.provider ? `AniPM-${srv.provider}` : "AniPM", headers: { Referer: `${BASE_URL}/`, Origin: BASE_URL }, skipProxy: false })); - if (allUrls.length) return { allUrls }; - } - return null; - } 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/downloadeverything.js b/src/sources/downloadeverything.js new file mode 100644 index 0000000..29552cb --- /dev/null +++ b/src/sources/downloadeverything.js @@ -0,0 +1,338 @@ +import { getUA } from '../utils/helpers.js'; + +const SLAVE_URL = 'https://slave.downloadeverythingfromeverywhere.com/'; +const REFERER = 'https://downloadeverythingfromeverywhere.com/'; +const ORIGIN = 'https://downloadeverythingfromeverywhere.com'; +const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36'; + +function extractJsonObjects(text) { + const objects = []; + let depth = 0; + let start = -1; + let inString = false; + let escape = false; + + for (let i = 0; i < text.length; i++) { + const char = text[i]; + if (escape) { + escape = false; + continue; + } + if (char === '\\') { + escape = true; + continue; + } + if (char === '"') { + inString = !inString; + continue; + } + if (!inString) { + if (char === '{') { + if (depth === 0) start = i; + depth++; + } else if (char === '}') { + depth--; + if (depth === 0 && start !== -1) { + const jsonStr = text.substring(start, i + 1); + try { + objects.push(JSON.parse(jsonStr)); + } catch { } + start = -1; + } + } + } + } + return objects; +} + +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}&append_to_response=external_ids`, { + 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 ? parseInt(releaseDate.split('-')[0], 10) : null; + const imdbId = data.imdb_id || data.external_ids?.imdb_id || null; + return { title, year, imdbId }; + } catch { + return null; + } +} + +async function resolveHubCloud(hubUrl, ua) { + try { + const res1 = await fetch(hubUrl, { + headers: { + 'User-Agent': ua, + Referer: REFERER, + }, + signal: AbortSignal.timeout(8000), + }); + + const html = await res1.text(); + const hubPhpMatch = html.match(/https?:\/\/[^\s"<>]+\/hubcloud\.php\?[^\s"<>]+/i); + if (!hubPhpMatch) return null; + + const phpUrl = hubPhpMatch[0]; + const phpRes = await fetch(phpUrl, { + headers: { + 'User-Agent': ua, + Referer: hubUrl, + }, + signal: AbortSignal.timeout(8000), + }); + + const phpBody = await phpRes.text(); + + const r2Match = phpBody.match(/https?:\/\/[a-zA-Z0-9.\-_]+\.r2\.cloudflarestorage\.com\/[^\s"<>]+/i); + if (r2Match) { + return r2Match[0].replaceAll('&', '&'); + } + + const pixelMatch = phpBody.match(/https?:\/\/pixel\.hubcloud\.[a-z]+\/\?id=[^\s"<>]+/i); + if (pixelMatch) { + return pixelMatch[0]; + } + } catch { } + return null; +} + +async function resolveClicknUpload(clicknUrl, ua) { + try { + const res1 = await fetch(clicknUrl, { + headers: { + 'User-Agent': ua, + Referer: REFERER, + }, + signal: AbortSignal.timeout(8000), + }); + + const body1 = await res1.text(); + const formMatch1 = body1.match(/]+method=["']POST["'][^>]*>([\s\S]*?)<\/form>/i); + if (!formMatch1) return null; + + const inputMatches1 = [...formMatch1[1].matchAll(/]+name=["']([^"']+)["'][^>]+value=["']([^"']*)["']/gi)]; + const params1 = new URLSearchParams(); + for (const m of inputMatches1) { + params1.set(m[1], m[2]); + } + params1.set('method_free', 'Slow Download'); + + const rawCookies = res1.headers.getSetCookie?.() || [res1.headers.get('set-cookie')].filter(Boolean); + const cookieHeader = rawCookies.join('; '); + + const res2 = await fetch(clicknUrl, { + method: 'POST', + headers: { + 'User-Agent': ua, + Referer: clicknUrl, + 'Content-Type': 'application/x-www-form-urlencoded', + ...(cookieHeader ? { Cookie: cookieHeader } : {}), + }, + body: params1.toString(), + signal: AbortSignal.timeout(8000), + }); + + const body2 = await res2.text(); + const formMatch2 = body2.match(/]+method=["']POST["'][^>]*>([\s\S]*?)<\/form>/i); + if (!formMatch2) return null; + + const inputMatches2 = [...formMatch2[1].matchAll(/]+name=["']([^"']+)["'][^>]+value=["']([^"']*)["']/gi)]; + const params2 = new URLSearchParams(); + for (const m of inputMatches2) { + params2.set(m[1], m[2]); + } + params2.set('down_script', '1'); + + await new Promise(r => setTimeout(r, 4500)); + + const res3 = await fetch(clicknUrl, { + method: 'POST', + headers: { + 'User-Agent': ua, + Referer: clicknUrl, + 'Content-Type': 'application/x-www-form-urlencoded', + ...(cookieHeader ? { Cookie: cookieHeader } : {}), + }, + body: params2.toString(), + signal: AbortSignal.timeout(8000), + }); + + const body3 = await res3.text(); + const directMatch = body3.match(/https?:\/\/[a-zA-Z0-9.\-_:]+\/d\/[a-zA-Z0-9_\-\/]+/i) || + body3.match(/window\.open\(["'](https?:\/\/[^"']+)["']\)/i); + + if (directMatch) { + const found = directMatch[1] || directMatch[0]; + if (found.includes('clicknupload.') && !found.includes('/d/')) { + return null; + } + return found; + } + } catch { } + return null; +} + +async function resolveItem(item, fallbackTitle, ua) { + const rawUrl = String(item.url || '').trim(); + if (!rawUrl) return null; + + if ( + rawUrl.includes('111477.xyz') || + rawUrl.includes('vadapav.mov') || + rawUrl.includes('driveseed.org') || + rawUrl.includes('new3.gdflix.io') || + rawUrl.includes('rapidrar.cr') || + rawUrl.includes('megaup.net') || + rawUrl.includes('telegram.dog') || + rawUrl.includes('t.me') + ) { + return null; + } + + let directStreamUrl = null; + let provider = item.site || 'DownloadEverything'; + let streamHeaders = { 'User-Agent': ua }; + + try { + if (rawUrl.includes('hakunaymatata.com')) { + directStreamUrl = rawUrl; + provider = 'Moviebox'; + streamHeaders = { 'User-Agent': 'Lavf/60.16.100' }; + } else if (rawUrl.includes('pixeldrain.dev') || rawUrl.includes('pixeldrain.com')) { + const m = rawUrl.match(/pixeldrain\.(?:dev|com)\/(?:u|l)\/([a-zA-Z0-9_-]+)/); + if (m) { + directStreamUrl = `https://pixeldrain.com/api/file/${m[1]}`; + provider = 'Pixeldrain'; + } + } else if (rawUrl.includes('hubcloud.') || rawUrl.includes('vcloud.zip')) { + directStreamUrl = await resolveHubCloud(rawUrl, ua); + if (directStreamUrl) { + provider = 'HubCloud'; + } + } else if (rawUrl.includes('clicknupload.')) { + directStreamUrl = await resolveClicknUpload(rawUrl, ua); + if (directStreamUrl) { + provider = 'ClicknUpload'; + } + } else if ( + /\.(?:mp4|mkv)(?:\?|$)/i.test(rawUrl) && + !rawUrl.includes('111477.xyz') && + !rawUrl.includes('vadapav.mov') && + !rawUrl.includes('.cyou/res/') + ) { + try { + const check = await fetch(rawUrl, { + method: 'HEAD', + headers: { 'User-Agent': ua }, + signal: AbortSignal.timeout(2000), + }); + if (check.status === 200 || check.status === 206 || (check.status >= 300 && check.status < 400)) { + directStreamUrl = rawUrl; + provider = item.site || 'DirectStream'; + } + } catch { } + } + } catch { } + + if (!directStreamUrl) return null; + + const tags = Array.isArray(item.tags) ? item.tags.map(String) : []; + const qualityMatch = tags.find(t => /2160p|4k|1080p|720p|480p/i.test(t)) || '1080p'; + const rawTitle = item.name || item.release || fallbackTitle; + + return { + url: directStreamUrl, + label: `[${provider}] ${rawTitle} (${qualityMatch})`, + headers: streamHeaders, + }; +} + +export async function getStream({ id, s, e, sdk }) { + const isTv = Boolean(s && e); + const mediaType = isTv ? 'series' : 'movie'; + const ua = USER_AGENT; + + const details = await resolveDetails(sdk, id, isTv); + const title = details?.title || String(id); + const tmdbId = parseInt(id, 10); + + const payload = { + mode: mediaType, + title, + ...(details?.year ? { year: String(details.year) } : {}), + ...(tmdbId ? { tmdb_id: tmdbId } : {}), + ...(details?.imdbId ? { imdb_id: details.imdbId } : {}), + ...(isTv && s != null ? { season: Number(s) } : {}), + ...(isTv && e != null ? { episode: Number(e) } : {}), + }; + + const res = await fetch(SLAVE_URL, { + method: 'POST', + headers: { + 'User-Agent': ua, + Origin: ORIGIN, + Referer: REFERER, + 'Content-Type': 'application/json', + Accept: 'application/x-ndjson', + 'x-defe-manual': '1', + 'sec-ch-ua': '"Microsoft Edge";v="153", "Not_A Brand";v="8", "Chromium";v="153"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"Windows"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-site', + }, + body: JSON.stringify(payload), + signal: AbortSignal.timeout(16000), + }); + + if (!res.ok) return null; + + let fullText = ''; + if (res.body?.getReader) { + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + fullText += decoder.decode(value, { stream: true }); + } + } catch { } + } else { + fullText = await res.text(); + } + + const objects = extractJsonObjects(fullText); + const candidateItems = []; + + for (const parsed of objects) { + if (parsed?.t === 'hit' && Array.isArray(parsed.links)) { + const site = parsed.site || 'DownloadEverything'; + for (const l of parsed.links) { + if (l && typeof l === 'object') { + candidateItems.push({ site, ...l }); + } + } + } + } + + const resolved = await Promise.all(candidateItems.map(item => resolveItem(item, title, ua))); + const allUrls = resolved.filter(Boolean); + + 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/mapple.js b/src/sources/mapple.js deleted file mode 100644 index 88185ca..0000000 --- a/src/sources/mapple.js +++ /dev/null @@ -1,151 +0,0 @@ -import { USER_AGENT } from '../utils/helpers.js'; - -const BASE_URL = "https://mapple.club"; -const PROXY_HEADERS = { Referer: `${BASE_URL}/`, Origin: BASE_URL, "User-Agent": USER_AGENT }; - -const K = new Uint32Array([0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2]); -const W_BUF = new Uint32Array(64); - -function sha256Into(buf, total, out) { - let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a, h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19; - const dv = new DataView(buf.buffer, buf.byteOffset, total); - for (let off = 0; off < total; off += 64) { - for (let i = 0; i < 16; i++) W_BUF[i] = dv.getUint32(off + i * 4, false); - for (let i = 16; i < 64; i++) { - const a = W_BUF[i - 15], b = W_BUF[i - 2]; - const s0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3); - const s1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10); - W_BUF[i] = (W_BUF[i - 16] + s0 + W_BUF[i - 7] + s1) | 0; - } - let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7; - for (let i = 0; i < 64; i++) { - const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7)); - const ch = (e & f) ^ (~e & g); - const t1 = (h + S1 + ch + K[i] + W_BUF[i]) | 0; - const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10)); - const maj = (a & b) ^ (a & c) ^ (b & c); - const t2 = (S0 + maj) | 0; - h = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0; - } - h0 = (h0 + a) | 0; h1 = (h1 + b) | 0; h2 = (h2 + c) | 0; h3 = (h3 + d) | 0; h4 = (h4 + e) | 0; h5 = (h5 + f) | 0; h6 = (h6 + g) | 0; h7 = (h7 + h) | 0; - } - const odv = new DataView(out.buffer, out.byteOffset, 32); - odv.setUint32(0, h0 >>> 0, false); odv.setUint32(4, h1 >>> 0, false); odv.setUint32(8, h2 >>> 0, false); odv.setUint32(12, h3 >>> 0, false); odv.setUint32(16, h4 >>> 0, false); odv.setUint32(20, h5 >>> 0, false); odv.setUint32(24, h6 >>> 0, false); odv.setUint32(28, h7 >>> 0, false); -} - -function solvePoW(challenge, difficulty) { - const encoder = new TextEncoder(); - const cBuf = encoder.encode(challenge); - const maxLen = cBuf.length + 16; - const total = (maxLen + 1 + 8 + 63) & ~63; - const buf = new Uint8Array(total); - buf.set(cBuf); - const hash = new Uint8Array(32); - const maskBytes = Math.floor(difficulty / 8); - const maskBits = difficulty % 8; - const finalMask = maskBits > 0 ? (0xff << (8 - maskBits)) & 0xff : 0; - const dv = new DataView(buf.buffer); - for (let nonce = 0; nonce < 10000000; nonce++) { - const str = nonce.toString(); - const len = cBuf.length + str.length; - for (let i = 0; i < str.length; i++) buf[cBuf.length + i] = str.charCodeAt(i); - buf[len] = 0x80; - for (let i = len + 1; i < total - 8; i++) buf[i] = 0; - dv.setUint32(total - 4, len * 8, false); - dv.setUint32(total - 8, 0, false); - sha256Into(buf, total, hash); - let ok = true; - for (let i = 0; i < maskBytes; i++) if (hash[i] !== 0) { ok = false; break; } - if (ok && (finalMask === 0 || (hash[maskBytes] & finalMask) === 0)) return { found: true, nonce: str }; - } - return { found: false }; -} - -const SERVER_LIST = [{ id: "mapple", name: "Mapple" }, { id: "s1", name: "Nexus" }, { id: "s2", name: "Cipher" }, { id: "s3", name: "Pulse" }, { id: "s4", name: "Vertex" }, { id: "s10", name: "Chimp" }]; - -export async function getStream(args) { - const { id, s, e, server } = args; - const isTv = s != null && e != null; - const mediaType = isTv ? "tv" : "movie"; - const tvSlug = isTv ? `${s}-${e}` : ""; - const pageUrl = isTv ? `${BASE_URL}/watch/tv/${id}/${s}/${e}` : `${BASE_URL}/watch/movie/${id}`; - try { - const pageRes = await fetch(pageUrl, { headers: PROXY_HEADERS, signal: AbortSignal.timeout(10000) }); - if (!pageRes.ok) return null; - const html = await pageRes.text(); - const reqMatch = html.match(/window\.__REQUEST_TOKEN__\s*=\s*"([^"]+)"/); - if (!reqMatch) return null; - const requestToken = reqMatch[1]; - const cookies = pageRes.headers.getSetCookie(); - const cookieHeader = cookies.map(c => c.split(';')[0]).join('; '); - - const initHeaders = { "Content-Type": "application/json", ...PROXY_HEADERS, Referer: pageUrl }; - if (cookieHeader) initHeaders.Cookie = cookieHeader; - - const initRes = await fetch(`${BASE_URL}/api/playback-init`, { - method: "POST", - headers: initHeaders, - body: JSON.stringify({ mediaId: Number(id), mediaType, requestToken }), - signal: AbortSignal.timeout(10000) - }); - const initData = await initRes.json(); - let streamToken = initData.token; - if (initData.requiresPow && initData.pow) { - const powRes = solvePoW(initData.pow.challenge, initData.pow.difficulty); - if (powRes.found) { - const solveRes = await fetch(`${BASE_URL}/api/playback-init`, { - method: "POST", - headers: initHeaders, - body: JSON.stringify({ mediaId: Number(id), mediaType, requestToken, pow: { challengeId: initData.pow.challengeId, nonce: powRes.nonce } }), - signal: AbortSignal.timeout(10000) - }); - streamToken = (await solveRes.json()).token; - } - } - if (!streamToken) return null; - let targets = SERVER_LIST; - if (server && server !== "all") { - const clean = server.replace("Mapple - ", ""); - targets = SERVER_LIST.filter(sv => sv.name === clean); - if (!targets.length) targets = SERVER_LIST; - } - const allUrls = []; - for (const srv of targets) { - try { - const encryptRes = await fetch(`${BASE_URL}/api/encrypt`, { - method: "POST", - headers: initHeaders, - body: JSON.stringify({ - data: { - mediaId: Number(id), - mediaType, - tv_slug: tvSlug, - source: srv.id, - apikey: "mptv_sk_a8f29c4e7b3d1f" - } - }), - signal: AbortSignal.timeout(8000) - }); - const encryptData = await encryptRes.json(); - if (!encryptData.encrypted) continue; - - const streamUrl = `${BASE_URL}/api/stream-encrypted?data=${encodeURIComponent(encryptData.encrypted)}&requestToken=${encodeURIComponent(requestToken)}&token=${encodeURIComponent(streamToken)}`; - const streamRes = await fetch(streamUrl, { - headers: { ...PROXY_HEADERS, Referer: pageUrl, ...(cookieHeader ? { Cookie: cookieHeader } : {}) }, - signal: AbortSignal.timeout(8000) - }); - const streamData = await streamRes.json(); - if (streamData.success && streamData.data?.stream_url) { - let fileUrl = streamData.data.stream_url; - if (fileUrl.includes('omena-puu') || fileUrl.includes('nocach')) fileUrl += fileUrl.includes('?') ? '&format=.m3u8' : '?format=.m3u8'; - allUrls.push({ url: fileUrl, server: `Mapple - ${srv.name}`, headers: PROXY_HEADERS, skipProxy: false, skipVerify: true, skipHlsCheck: true }); - } - } catch { } - } - return allUrls.length ? { allUrls } : null; - } catch { return null; } -} - -export async function getSources() { return SERVER_LIST.map(s => `Mapple - ${s.name}`); } -export const SKIP_VERIFY = true; -export const MULTI_URL = true; \ No newline at end of file diff --git a/src/sources/megasource.js b/src/sources/megasource.js deleted file mode 100644 index 004160e..0000000 --- a/src/sources/megasource.js +++ /dev/null @@ -1,47 +0,0 @@ -import { fetchJson } from '../utils/helpers.js'; - -const CONFIGURATION = 'W3siaWQiOiJkZWZhdWx0IiwibmFtZSI6Ik1lZ2FTb3VyY2UgZGVmYXVsdCIsInVybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS96b3JldS9tZWdhc291cmNlX3NjcmFwZXJzL3Jhdy9yZWZzL2hlYWRzL21haW4vZGVmYXVsdF9zY3JhcGVyLnB5IiwiZGVzY3JpcHRpb24iOiJEZWZhdWx0IHNjcmFwZXIgaG9zdGVkIG9uIEdpdEh1Yi4ifV0'; -const BASE_URL = `https://megasource.wasmer.app/${CONFIGURATION}`; - -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; - if (!tmdbApiKey) 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: `MegaSource - ${stream.title ? ` ${stream.title}` : ''}`, - headers: Object.keys(headers).length > 0 ? headers : undefined, - skipProxy: Object.keys(headers).length > 0 && stream.behaviorHints?.notWebReady === false - }; - }).filter(Boolean); - - return allUrls.length ? { allUrls } : null; - } catch { - return null; - } -} \ No newline at end of file diff --git a/src/sources/meowtv.js b/src/sources/meowtv.js index 5fbe8ec..eb0818a 100644 --- a/src/sources/meowtv.js +++ b/src/sources/meowtv.js @@ -4,7 +4,7 @@ const API_BASE = 'https://api.meowtv.ru'; const REFERER = 'https://meowtv.ru/'; const ENC_DEC_API = 'https://enc-dec.app/api/dec-meowtv'; -const HEADERS = { +const BASE_HEADERS = { 'User-Agent': USER_AGENT, 'Accept': 'application/json', 'Referer': REFERER, @@ -13,13 +13,46 @@ const HEADERS = { }; const SERVERS = [ - 'hindiv3', 'tcloud' + 'dcloud', 'acme', 'tcloud', 'ipcloud', 'turkce', 'hindiv3' ]; const TIMEOUT_MS = 15000; export const VERIFY_HEADERS = { 'User-Agent': USER_AGENT, 'Referer': REFERER, 'Origin': 'https://meowtv.ru' }; +let cachedTicket = null; +let ticketExpiry = 0; + +/** + * Fetches a stream ticket or returns a cached valid ticket. + */ +async function getTicket(forceRefresh = false) { + const now = Date.now(); + if (!forceRefresh && cachedTicket && now < ticketExpiry - 10000) { + return cachedTicket; + } + + try { + const res = await fetchJson(`${API_BASE}/streams/ticket`, { + method: 'POST', + headers: { + ...BASE_HEADERS, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({}) + }); + + if (res?.ticket) { + cachedTicket = res.ticket; + ticketExpiry = typeof res.exp === 'number' ? res.exp : now + 60000; + return cachedTicket; + } + } catch { + } + + return null; +} + async function decryptPayload(payload) { try { const response = await fetchJson(ENC_DEC_API, { @@ -47,6 +80,12 @@ export async function getStream({ id, s, e, server }) { targets = SERVERS.includes(cleanName) ? [cleanName] : [targets[0]]; } + const ticket = await getTicket(); + const requestHeaders = { + ...BASE_HEADERS, + ...(ticket ? { 'x-stream-ticket': ticket } : {}) + }; + const results = await Promise.all(targets.map(async srv => { try { return await Promise.race([ @@ -56,7 +95,7 @@ export async function getStream({ id, s, e, server }) { : `/streams/movie/${id}?s=${encodeURIComponent(srv)}`; const payload = await fetchJson(`${API_BASE}${path}`, { - headers: HEADERS + headers: requestHeaders }); if (!payload) return null; diff --git a/src/sources/movienight.js b/src/sources/movienight.js new file mode 100644 index 0000000..4cc5928 --- /dev/null +++ b/src/sources/movienight.js @@ -0,0 +1,117 @@ +import { getUA } from '../utils/helpers.js'; + +const BASE_URL = 'https://movienig.ht'; + +const PRIORITY_SERVERS = [ + { id: 'dallas', label: 'Dallas 4K' }, + { id: 'austin', label: 'Austin' }, + { id: 'helena', label: 'Helena' }, + { id: 'seattle', label: 'Seattle 4K' }, + { id: 'vixsrc-1', label: 'Newport Beach' }, + { id: 'tucson', label: 'Tucson' }, + { id: 'salem', label: 'Salem' }, +]; + +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}&append_to_response=external_ids`, { + 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 ? parseInt(releaseDate.split('-')[0], 10) : null; + const imdbId = data.imdb_id || data.external_ids?.imdb_id || null; + return { title, year, imdbId }; + } catch { + return null; + } +} + +export async function getStream({ id, s, e, sdk }) { + const isTv = Boolean(s && e); + const ua = getUA(); + + const details = await resolveDetails(sdk, id, isTv); + const title = details?.title || String(id); + const year = details?.year; + const imdbId = details?.imdbId; + + const encTitle = encodeURIComponent(title); + const yearQuery = year ? `&year=${year}` : ''; + const imdbQuery = imdbId ? `&imdbId=${imdbId}` : ''; + + const seenUrls = new Set(); + const allUrls = []; + + const defaultHeaders = { + 'User-Agent': ua, + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + Accept: 'text/event-stream', + }; + + const tasks = PRIORITY_SERVERS.map(async (server) => { + try { + const url = isTv + ? `${BASE_URL}/api/stream/v1/tv/${id}/${s}/${e}?title=${encTitle}${yearQuery}${imdbQuery}&server=${server.id}&only=1` + : `${BASE_URL}/api/stream/v1/movie/${id}?title=${encTitle}${yearQuery}${imdbQuery}&server=${server.id}&only=1`; + + const res = await fetch(url, { + headers: defaultHeaders, + signal: AbortSignal.timeout(6000), + }); + + if (!res.ok) return; + const bodyStr = await res.text(); + + if (bodyStr.includes('event: done')) { + const doneIdx = bodyStr.indexOf('event: done'); + const dataIdx = bodyStr.indexOf('data: ', doneIdx); + + if (dataIdx !== -1) { + const jsonStart = dataIdx + 6; + const jsonEnd = bodyStr.indexOf('\n', jsonStart); + const jsonText = (jsonEnd !== -1 ? bodyStr.substring(jsonStart, jsonEnd) : bodyStr.substring(jsonStart)).trim(); + + const data = JSON.parse(jsonText); + if (Array.isArray(data?.sources)) { + for (const src of data.sources) { + if (!src || typeof src !== 'object') continue; + const rawUrl = String(src.url || '').trim(); + if (!rawUrl || seenUrls.has(rawUrl)) continue; + + seenUrls.add(rawUrl); + + const quality = src.quality ? String(src.quality) : 'Auto'; + const titleQuality = quality !== 'Auto' ? ` (${quality})` : ''; + + allUrls.push({ + url: rawUrl, + label: `MovieNight ${server.label}${titleQuality}`, + headers: { + 'User-Agent': ua, + Referer: `${BASE_URL}/`, + }, + }); + } + } + } + } + } catch { } + }); + + await Promise.all(tasks); + + 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/movy.js b/src/sources/movy.js new file mode 100644 index 0000000..b14503d --- /dev/null +++ b/src/sources/movy.js @@ -0,0 +1,282 @@ +import { getUA } from '../utils/helpers.js'; + +const API_BASE = 'https://api.wecollege.net'; +const REFERER = 'https://www.movy.bz/'; +const ORIGIN = 'https://www.movy.bz'; +const MAGIC = [109, 118, 109, 49]; + +const SERVERS = [ + { endpoint: 'miami', name: 'Miami', note: 'Original audio (Up to 4K)' }, + { endpoint: 'seattle', name: 'Seattle', note: 'Original audio' }, + { endpoint: 'denver', name: 'Denver', note: 'Original audio' }, + { endpoint: 'chicago', name: 'Chicago', note: 'Original audio' }, + { endpoint: 'dallas', name: 'Dallas', note: 'Original audio' }, + { endpoint: 'atlanta', name: 'Atlanta', note: 'Original audio' }, + { endpoint: 'houston', name: 'Houston', note: 'Original audio' }, + { endpoint: 'austin', name: 'Austin', note: 'Original audio' }, + { endpoint: 'boston', name: 'Boston', note: 'Original audio' }, + { endpoint: 'munich', name: 'Munich', note: 'German audio', extra: 'language=german' }, + { endpoint: 'berlin', name: 'Berlin', note: 'German audio' }, + { endpoint: 'paris', name: 'Paris', note: 'French audio' }, + { endpoint: 'delhi', name: 'Delhi', note: 'Hindi audio' }, + { endpoint: 'cancun', name: 'Cancun', note: 'Spanish audio' }, +]; + +const seedCache = new Map(); + +function _l(e) { + let v = e >>> 0; + v = (v ^ (v >>> 16)) >>> 0; + v = Math.imul(v, 0x85ebca6b) >>> 0; + v = (v ^ (v >>> 13)) >>> 0; + v = Math.imul(v, 0xc2b2ae35) >>> 0; + return (v ^ (v >>> 16)) >>> 0; +} + +function _u(e, t) { + const shift = t & 31; + if (shift === 0) return e >>> 0; + return (((e << shift) >>> 0) | (e >>> (32 - shift))) >>> 0; +} + +function _fnv1a(str) { + let t = 0x811c9dc5 >>> 0; + for (let i = 0; i < str.length; i++) { + const code = str.charCodeAt(i); + t = Math.imul((t ^ code) >>> 0, 0x1000193) >>> 0; + } + return _l(t); +} + +function _initKeyState(seed, tmdbId) { + const s = new Array(61).fill(0); + const isSet = new Array(61).fill(false); + const idVal = ((tmdbId >>> 0) ^ 0x9e3779b9) >>> 0; + let r = _l((_fnv1a(seed) ^ _l(idVal)) >>> 0); + + for (let e = 0; e < 8; e++) { + const t = r % 61; + r = _u((r + 0x9e3779b9) >>> 0, 7 + (7 & e)); + s[t] = (r ^ _l(r)) >>> 0; + isSet[t] = true; + r = _l((r + t) >>> 0); + } + + const acc = _l((0xa5a5a5a5 ^ r) >>> 0); + return { s, isSet, acc }; +} + +function _nextKeystreamWord(state, t) { + const r = state.s; + let nState = state.acc; + const i = nState % 61; + const d = state.isSet[i] ? r[i] : 0; + const c = Math.imul((t + 1) >>> 0, 0x9e3779b9) >>> 0; + const a = nState; + const sVal = (d ^ c) >>> 0; + const h = state.isSet[i] ? ((a | sVal) >>> 0) : ((a ^ sVal) >>> 0); + const term1 = _u((h + nState) >>> 0, 31 & i); + const term2 = _u(nState, 31 & (i * 7)); + nState = _l((((term1 ^ term2) >>> 0) + 0x9e3779b9) >>> 0); + r[i] = nState; + state.isSet[i] = true; + state.acc = nState; + return nState >>> 0; +} + +function _generateKeyStream(seed, tmdbId, len) { + const state = _initKeyState(seed, tmdbId); + const out = new Uint8Array(len); + let wordIdx = 0; + let byteIdx = 0; + + while (byteIdx < len) { + const word = _nextKeystreamWord(state, wordIdx++); + out[byteIdx++] = word & 0xFF; + if (byteIdx < len) out[byteIdx++] = (word >>> 8) & 0xFF; + if (byteIdx < len) out[byteIdx++] = (word >>> 16) & 0xFF; + if (byteIdx < len) out[byteIdx++] = (word >>> 24) & 0xFF; + } + return out; +} + +function decrypt(cipherB64, seed, tmdbId) { + try { + let normalized = cipherB64.replace(/-/g, '+').replace(/_/g, '/'); + while (normalized.length % 4 !== 0) normalized += '='; + const cipherBytes = Buffer.from(normalized, 'base64'); + if (cipherBytes.length <= MAGIC.length) return null; + + const ks = _generateKeyStream(seed, tmdbId, cipherBytes.length); + const decrypted = new Uint8Array(cipherBytes.length); + for (let i = 0; i < cipherBytes.length; i++) { + decrypted[i] = cipherBytes[i] ^ ks[i]; + } + + for (let k = 0; k < MAGIC.length; k++) { + if (decrypted[k] !== MAGIC[k]) return null; + } + + return Buffer.from(decrypted.subarray(MAGIC.length)).toString('utf8'); + } catch { + return null; + } +} + +async function getSeed(tmdbId, ua) { + const now = Date.now(); + const cached = seedCache.get(tmdbId); + if (cached && cached.expiresAt > now + 5000) { + return cached.seed; + } + + try { + const res = await fetch(`${API_BASE}/seed?mediaId=${tmdbId}`, { + headers: { + 'User-Agent': ua, + Referer: REFERER, + Origin: ORIGIN, + }, + signal: AbortSignal.timeout(8000), + }); + + if (res.ok) { + const data = await res.json(); + const seed = data?.seed ? String(data.seed) : ''; + const ttlMs = typeof data?.ttlMs === 'number' ? data.ttlMs : 30000; + if (seed) { + seedCache.set(tmdbId, { seed, expiresAt: now + ttlMs }); + return seed; + } + } + } catch { } + + return null; +} + +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}&append_to_response=external_ids`, { + 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 ? parseInt(releaseDate.split('-')[0], 10) : null; + const imdbId = data.imdb_id || data.external_ids?.imdb_id || null; + return { title, year, imdbId }; + } catch { + return null; + } +} + +function formatQuality(raw) { + const lower = String(raw).toLowerCase(); + if (lower.includes('2160') || lower.includes('4k')) return '4K'; + if (lower.includes('1080')) return '1080p'; + if (lower.includes('720')) return '720p'; + if (lower.includes('480')) return '480p'; + if (lower.includes('360')) return '360p'; + if (raw) return String(raw); + return 'Auto'; +} + +export async function getStream({ id, s, e, sdk }) { + const isTv = Boolean(s && e); + const mediaType = isTv ? 'tv' : 'movie'; + const tmdbId = parseInt(id, 10); + const ua = getUA(); + + const details = await resolveDetails(sdk, id, isTv); + const title = details?.title || String(id); + const year = details?.year; + const imdbId = details?.imdbId; + + const seed = await getSeed(tmdbId, ua); + if (!seed) return null; + + const params = new URLSearchParams({ + title, + mediaType, + tmdbId: String(tmdbId), + enc: '2', + seed, + }); + + if (year) params.set('year', String(year)); + if (isTv) { + if (s) params.set('seasonId', String(s)); + if (e) params.set('episodeId', String(e)); + } + if (imdbId) params.set('imdbId', imdbId); + + const baseQuery = params.toString(); + const seenUrls = new Set(); + const allUrls = []; + + const defaultHeaders = { + 'User-Agent': ua, + Referer: REFERER, + Origin: ORIGIN, + }; + + const tasks = SERVERS.map(async (server) => { + let fullUrl = `${API_BASE}/${server.endpoint}/sources?${baseQuery}`; + if (server.extra) fullUrl += `&${server.extra}`; + + try { + const res = await fetch(fullUrl, { + headers: defaultHeaders, + signal: AbortSignal.timeout(8000), + }); + + if (!res.ok) return; + const encText = (await res.text()).trim(); + if (!encText || encText.startsWith('<')) return; + + const decJsonStr = decrypt(encText, seed, tmdbId); + if (!decJsonStr) return; + + const parsed = JSON.parse(decJsonStr); + if (!Array.isArray(parsed?.sources)) return; + + for (const src of parsed.sources) { + if (!src || typeof src !== 'object') continue; + const streamUrl = String(src.url || '').trim(); + if (!streamUrl || seenUrls.has(streamUrl)) continue; + + seenUrls.add(streamUrl); + + const cleanQuality = formatQuality(src.quality || 'Auto'); + const isSpecificAudio = server.note.toLowerCase().includes('audio') && !server.note.toLowerCase().includes('original'); + const langLabel = isSpecificAudio ? server.note.split(' ')[0] : ''; + const label = isSpecificAudio + ? `${server.name} (${langLabel}) · ${cleanQuality}` + : `${server.name} · ${cleanQuality}`; + + allUrls.push({ + url: streamUrl, + label, + headers: { + 'User-Agent': ua, + Referer: REFERER, + }, + }); + } + } catch { } + }); + + await Promise.all(tasks); + + 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/nova.js b/src/sources/nova.js deleted file mode 100644 index eb4dcca..0000000 --- a/src/sources/nova.js +++ /dev/null @@ -1,46 +0,0 @@ -import { fetchJson } from '../utils/helpers.js'; - -const BASE_URL = `https://nova-streamz.vercel.app`; - -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; - if (!tmdbApiKey) 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: `Nova - ${stream.name.replace('Nova ', '') || 'Server'}${stream.title ? ` ${stream.title.split(' | ')[0] || ''}` : ''}`, - headers: Object.keys(headers).length > 0 ? headers : undefined, - skipProxy: Object.keys(headers).length > 0 && stream.behaviorHints?.notWebReady === false - }; - }).filter(Boolean); - - return allUrls.length ? { allUrls } : null; - } catch { - return null; - } -} \ No newline at end of file diff --git a/src/sources/purstream.js b/src/sources/purstream.js index 0799af8..c3e8f76 100644 --- a/src/sources/purstream.js +++ b/src/sources/purstream.js @@ -1,7 +1,7 @@ import { fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js'; -const DOMAIN = 'https://purstream.club'; -const API_BASE = 'https://api.purstream.club/api/v1'; +const DOMAIN = 'https://purstream.ad'; +const API_BASE = 'https://api.purstream.ad/api/v1'; const HEADERS = { 'User-Agent': USER_AGENT, diff --git a/src/sources/streamaggregator.js b/src/sources/streamaggregator.js new file mode 100644 index 0000000..d3b45a9 --- /dev/null +++ b/src/sources/streamaggregator.js @@ -0,0 +1,127 @@ +import { getUA } 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); + const mediaType = isTv ? 'tv' : 'movie'; + const ua = getUA(); + + const details = await resolveDetails(sdk, id, isTv); + const title = details?.title || String(id); + const year = details?.year || ''; + + const formattedTitle = encodeURIComponent(title).replaceAll('%20', '+'); + const params = new URLSearchParams({ + type: mediaType, + stream: 'true', + }); + + if (year) params.set('year', String(year)); + if (isTv) { + params.set('season', String(s)); + params.set('episode', String(e)); + } + + const scrapeUrl = `${BASE_URL}/api/scrape?title=${formattedTitle}&${params.toString()}`; + + const res = await fetch(scrapeUrl, { + headers: { + 'User-Agent': ua, + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + Accept: 'application/x-ndjson, text/plain, */*', + }, + signal: AbortSignal.timeout(18000), + }); + + if (!res.ok) return null; + + const lines = []; + if (res.body?.getReader) { + const reader = res.body.getReader(); + const decoder = new TextDecoder('utf-8'); + let buffer = ''; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const parts = buffer.split('\n'); + buffer = parts.pop() || ''; + for (const part of parts) { + const trimmed = part.trim(); + if (trimmed) lines.push(trimmed); + } + } + } catch { } + if (buffer.trim()) lines.push(buffer.trim()); + } else { + const text = await res.text(); + for (const line of text.split('\n')) { + const trimmed = line.trim(); + if (trimmed) lines.push(trimmed); + } + } + + const seenUrls = new Set(); + const allUrls = []; + + for (const line of lines) { + try { + const item = JSON.parse(line); + const streamUrl = String(item?.url || '').trim(); + if (!streamUrl || !streamUrl.startsWith('http') || seenUrls.has(streamUrl)) continue; + + seenUrls.add(streamUrl); + + const serverName = item.server || item.providerKey || 'Stream'; + const quality = item.quality ? ` · ${item.quality}` : ''; + const size = item.size ? ` (${item.size})` : ''; + const label = `${serverName}${quality}${size}`; + + const headers = { + 'User-Agent': ua, + ...(item.headers && typeof item.headers === 'object' ? item.headers : {}), + }; + + if (item.referer) { + headers.Referer = item.referer; + } + + allUrls.push({ + url: streamUrl, + label, + headers, + }); + } catch { } + } + + 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/vadapav.js b/src/sources/vadapav.js new file mode 100644 index 0000000..1a78257 --- /dev/null +++ b/src/sources/vadapav.js @@ -0,0 +1,85 @@ +import { getUA } from '../utils/helpers.js'; + +const ADDON_BASE = 'https://stremio.vadapav.mov'; + +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); + const ua = getUA(); + + const imdbId = await resolveImdbId(sdk, id, isTv); + const targetIds = []; + if (imdbId && imdbId.startsWith('tt')) { + targetIds.push(imdbId); + } + targetIds.push(`tmdb:${id}`); + + const seenUrls = new Set(); + const allUrls = []; + + const defaultHeaders = { + 'User-Agent': ua, + Accept: 'application/json, text/plain, */*', + }; + + for (const targetId of targetIds) { + const streamPath = isTv + ? `${ADDON_BASE}/stream/series/${targetId}:${s}:${e}.json` + : `${ADDON_BASE}/stream/movie/${targetId}.json`; + + try { + const res = await fetch(streamPath, { + headers: defaultHeaders, + signal: AbortSignal.timeout(12000), + }); + + if (!res.ok) continue; + const data = await res.json(); + + 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; + + seenUrls.add(url); + + const rawTitle = item.title ? String(item.title).trim() : ''; + const rawName = item.name ? String(item.name).trim() : 'vadapav.mov'; + const label = rawTitle || rawName; + + allUrls.push({ + url, + label, + headers: defaultHeaders, + }); + } + + if (allUrls.length > 0) break; + } + } catch { } + } + + 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/videasy.js b/src/sources/videasy.js deleted file mode 100644 index 35a62cc..0000000 --- a/src/sources/videasy.js +++ /dev/null @@ -1,94 +0,0 @@ -import { getTmdbInfo, fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js'; - -const SPEEDRACE_BASE = "https://api.speedracelight.com"; -const HEADERS = { "Accept": "*/*", "Origin": "https://player.videasy.to", "Referer": "https://player.videasy.to/", "User-Agent": USER_AGENT }; -const SERVERS = [ - { id: 'cdn', name: 'Yoru' }, - { id: 'm4uhd', name: 'Breach' }, - { id: 'vsrc', name: 'Neon' }, - { id: 'hdmovie', name: 'Vyse' }, - { id: 'meine', name: 'Killjoy' }, - { id: 'lamovie', name: 'Omen' }, - { id: 'superflix', name: 'Raze' } -]; - -export async function getStream({ id, s, e, server, 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 seedData = await fetchJson(`${SPEEDRACE_BASE}/seed?mediaId=${id}`, { headers: HEADERS, signal: AbortSignal.timeout(5000) }); - const seed = seedData?.seed; - if (!seed) return null; - - const encTitle = encodeURIComponent(encodeURIComponent(info.titles[0])); - let targets = SERVERS; - - if (server && server !== 'all') { - const cleanName = server.replace('VidEasy - ', ''); - targets = SERVERS.filter(sv => sv.name === cleanName); - if (!targets.length) targets = SERVERS; - } - - const settled = await Promise.allSettled(targets.map(async srv => { - let url = `${SPEEDRACE_BASE}/${srv.id}/sources-with-title?title=${encTitle}&mediaType=${isTv ? 'tv' : 'movie'}&year=${info.year || ''}&tmdbId=${id}&imdbId=${info.imdbId || "tt0000000"}&enc=2&seed=${seed}`; - if (isTv) url += `&episodeId=${e}&seasonId=${s}`; - - const encText = await fetchText(url, { headers: HEADERS, signal: AbortSignal.timeout(10000) }); - const decJson = await fetchJson("https://enc-dec.app/api/dec-videasy", { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ text: encText, id: String(id), seed }), - signal: AbortSignal.timeout(10000) - }); - - if (decJson.status !== 200 || !decJson.result) throw new Error(); - - let sourcesArray = []; - let subsArray = []; - - if (Array.isArray(decJson.result)) { - sourcesArray = decJson.result; - } else if (decJson.result.sources && Array.isArray(decJson.result.sources)) { - sourcesArray = decJson.result.sources; - if (decJson.result.subtitles && Array.isArray(decJson.result.subtitles)) { - subsArray = decJson.result.subtitles.map(sub => ({ - url: sub.url || sub.file, - lang: sub.language || sub.lang || sub.label || 'Unknown' - })).filter(sub => sub.url); - } - } else { - sourcesArray = [decJson.result]; - } - - return sourcesArray.map(res => { - const streamUrl = res.url || res.file || res.link || res.playlist || res.stream; - return streamUrl ? { - url: streamUrl, - quality: res.quality || "Auto", - server: `VidEasy - ${srv.name}`, - type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', - headers: HEADERS, - subtitles: subsArray.length > 0 ? subsArray : undefined, - skipProxy: false, - skipVerify: true, - skipHlsCheck: true - } : null; - }).filter(Boolean); - })); - - const allUrls = []; - for (const r of settled) { - if (r.status === 'fulfilled' && r.value) allUrls.push(...r.value); - } - - return allUrls.length ? { allUrls } : null; - } catch { return null; } -} - -export async function getSources() { - return SERVERS.map(s => `VidEasy - ${s.name}`); -} \ No newline at end of file diff --git a/src/sources/vidvault.js b/src/sources/vidvault.js index df22159..8ba0bef 100644 --- a/src/sources/vidvault.js +++ b/src/sources/vidvault.js @@ -1,113 +1,187 @@ import { USER_AGENT } from '../utils/helpers.js'; -const BASE_URL = "https://vidvault.ru"; +const BASE_URL = 'https://vidvault.to'; +const API_BASE = `${BASE_URL}/api`; +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, - "User-Agent": USER_AGENT, + 'Referer': `${BASE_URL}/`, + 'Origin': BASE_URL, + 'User-Agent': USER_AGENT, + '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(`${BASE_URL}/api/get-token`, { - method: "GET", - headers: HEADERS + const res = await fetch(`${API_BASE}/get-token`, { + method: 'GET', + headers: HEADERS, + signal: AbortSignal.timeout(10000), }); + if (!res.ok) throw new Error(`Token fetch failed: ${res.status}`); const data = await res.json(); - return data.t; + return data?.t; } function getCaptionType(url) { - return url.toLowerCase().includes(".vtt") ? "vtt" : "srt"; + return String(url).toLowerCase().includes('.vtt') ? 'vtt' : 'srt'; } export async function getStream(args) { const { id, s, e } = args; - const type = (s != null && e != null) ? "tv" : "movie"; + const isTv = s != null && e != null; + const type = isTv ? 'tv' : 'movie'; try { const token = await getToken(); + if (!token) return null; - const payload = { type, tmdbId: String(id) }; - if (type === "tv") { - payload.season = String(s); - payload.episode = String(e); + const payload = { + type, + tmdbId: String(id), + }; + + if (isTv) { + payload.season = Number(s); + payload.episode = Number(e); } - const res = await fetch(`${BASE_URL}/api/download-proxy`, { - method: "POST", + const res = await fetch(`${API_BASE}/download-proxy`, { + method: 'POST', headers: { ...HEADERS, - "Content-Type": "application/json", - "x-request-token": token + 'Content-Type': 'application/json', + 'x-request-token': token, }, - body: JSON.stringify(payload) + body: JSON.stringify(payload), + signal: AbortSignal.timeout(15000), }); + + 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; - if (mp4) { - const country = mp4.country || ""; - const lanName = mp4.lanName || ""; - const detailPath = mp4.detailPath || ""; + 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 downloads = mp4.downloadInfo?.data?.downloads || []; - for (const d of downloads) { - if (!d?.url) continue; allUrls.push({ - quality: `${d.resolution}p ${detailPath} ${country} ${lanName}`.trim(), - url: d.url, - type: "mp4", - headers: HEADERS + url: proxiedUrl, + server: `VidVault - MP4 ${res}p${sizeStr}`.trim(), + quality: typeof res === 'number' ? `${res}p` : res, + type: 'mp4', + headers: HEADERS, }); } - const srtCaptions = mp4.downloadInfo?.data?.captions || []; - for (const c of srtCaptions) { - if (!c?.url) continue; + 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: c.lanName || c.lan || "Unknown", - type: getCaptionType(c.url), - language: c.lan || "", - url: c.url + label: cap.lanName || cap.lan || 'Unknown', + type: getCaptionType(cap.url), + language: cap.lan || '', + url: subUrl, }); } } const mkvFiles = data?.mkvData?.files || []; - for (const f of mkvFiles) { - if (!f?.url) continue; + for (const file of mkvFiles) { + if (!file?.url) continue; + const sizeStr = file.size ? ` (${typeof file.size === 'number' ? formatBytes(file.size) : file.size})` : ''; allUrls.push({ - quality: `MKV ${f.size || ""}`.trim(), - url: f.url, - type: "mp4", - headers: HEADERS + url: file.url, + server: `VidVault - MKV 480p${sizeStr}`.trim(), + quality: '480p', + type: 'mp4', + headers: HEADERS, }); } const mkvV2 = data?.mkvV2Data; - if (mkvV2?.url) { + 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({ - quality: `${mkvV2.quality || ""} ${mkvV2.language || ""} ${mkvV2.country || ""} (${mkvV2.size || ""})`.trim(), - url: mkvV2.url, - type: "mp4", - headers: HEADERS + 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?.url) { - allUrls.push({ - quality: `${mkvV3.quality || ""} ${mkvV3.language || ""} ${mkvV3.country || ""} (${mkvV3.size || ""})`.trim(), - url: mkvV3.url, - type: "mp4", - headers: HEADERS - }); + 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 } : null; + return allUrls.length ? { allUrls, subtitles: subtitles.length ? subtitles : undefined } : null; } catch (err) { return null; } diff --git a/src/sources/vuflix.js b/src/sources/vuflix.js new file mode 100644 index 0000000..6fb539e --- /dev/null +++ b/src/sources/vuflix.js @@ -0,0 +1,249 @@ +import { getUA } from '../utils/helpers.js'; + +const API_BASE = 'https://vuflix.co'; +const REFERER = 'https://vuflix.co/'; +const ORIGIN = 'https://vuflix.co'; + +let cachedProviders = null; +let lastProvidersFetch = 0; + +const FALLBACK_PROVIDERS = [ + { id: 'vsembed', name: 'Sigma' }, + { id: 'moonflix', name: 'Source 40' }, + { id: 'megasource', name: 'Source 39' }, + { id: 'hdghar', name: 'Source 44' }, + { id: 'moviebox', name: 'Pi' }, + { id: 'cineplay', name: '4K' }, + { id: 'huhu', name: 'Beta' }, + { id: 'bingr', name: 'Upsilon' }, + { id: 'onlyflix', name: 'Gamma' }, + { id: 'vaplayer', name: 'Alpha' }, + { id: 'flixhqz', name: 'Gamma' }, + { id: 'castle', name: 'Source 40' }, + { id: 'cinejoy', name: '4K2' }, + { id: 'filesun', name: 'Tau' }, + { id: 'yoru', name: 'Yoru' }, +]; + +async function getProviders(ua) { + if (cachedProviders && Date.now() - lastProvidersFetch < 900000) { + return cachedProviders; + } + + try { + const res = await fetch(`${API_BASE}/api/player/providers`, { + headers: { + 'User-Agent': ua, + Referer: REFERER, + Origin: ORIGIN, + Accept: 'application/json, text/plain, */*', + }, + signal: AbortSignal.timeout(6000), + }); + + if (res.ok) { + const data = await res.json(); + if (data?.ok && Array.isArray(data.providers)) { + const list = data.providers + .filter(p => p && typeof p.id === 'string' && p.id.trim().length > 0) + .map(p => ({ + id: p.id.trim(), + name: p.publicLabel || p.providerName || p.name || p.id, + })); + + if (list.length > 0) { + cachedProviders = list; + lastProvidersFetch = Date.now(); + return list; + } + } + } + } catch { } + + return FALLBACK_PROVIDERS; +} + +function unwrapUrl(rawUrl, ua) { + const defaultHeaders = { + 'User-Agent': ua, + Referer: REFERER, + Origin: ORIGIN, + }; + + if (!rawUrl || typeof rawUrl !== 'string') { + return { url: '', headers: defaultHeaders }; + } + + if (!rawUrl.includes('v-relay?t=') && !rawUrl.includes('a-relay?t=')) { + return { url: rawUrl, headers: defaultHeaders }; + } + + try { + const parsedUrl = new URL(rawUrl); + const t = parsedUrl.searchParams.get('t'); + if (!t) return { url: rawUrl, headers: defaultHeaders }; + + let b64 = t.replace(/-/g, '+').replace(/_/g, '/'); + while (b64.length % 4 !== 0) { + b64 += '='; + } + + const jsonStr = Buffer.from(b64, 'base64').toString('utf8'); + const parsed = JSON.parse(jsonStr); + + if (parsed && typeof parsed === 'object') { + const directUrl = (parsed.u || '').trim(); + const headersMap = { ...defaultHeaders }; + + if (parsed.h && typeof parsed.h === 'object') { + for (const [key, value] of Object.entries(parsed.h)) { + headersMap[key] = String(value); + } + } + + if (directUrl) { + return { url: directUrl, headers: headersMap }; + } + } + } catch { } + + return { url: rawUrl, headers: defaultHeaders }; +} + +export async function getStream({ id, s, e }) { + const isTv = Boolean(s && e); + const mediaType = isTv ? 'tv' : 'movie'; + const ua = getUA(); + + const baseParams = new URLSearchParams({ + type: mediaType, + tmdbId: String(id), + }); + + if (isTv) { + baseParams.set('season', String(s)); + baseParams.set('episode', String(e)); + } + + const providers = await getProviders(ua); + const seenUrls = new Set(); + const allUrls = []; + + const defaultHeaders = { + 'User-Agent': ua, + Referer: REFERER, + Origin: ORIGIN, + Accept: 'application/json, text/plain, */*', + }; + + const tasks = providers.map(async (prov) => { + try { + const url = `${API_BASE}/api/player/sources?${baseParams.toString()}&provider=${encodeURIComponent(prov.id)}`; + const res = await fetch(url, { + headers: defaultHeaders, + signal: AbortSignal.timeout(8000), + }); + + if (!res.ok) return; + const data = await res.json(); + if (!data?.ok || !Array.isArray(data.sources)) return; + + for (const item of data.sources) { + if (!item || typeof item !== 'object') continue; + + const providerName = item.providerName || item.publicLabel || prov.name; + const primaryRawUrl = (item.url || '').trim(); + const itemType = (item.type || 'hls').toLowerCase(); + const itemLabel = item.label || ''; + + if (Array.isArray(item.qualities) && item.qualities.length > 0) { + for (const q of item.qualities) { + if (!q || typeof q !== 'object') continue; + const qRawUrl = (q.url || '').trim(); + if (!qRawUrl) continue; + + const unwrapped = unwrapUrl(qRawUrl, ua); + if (!unwrapped.url || seenUrls.has(unwrapped.url)) continue; + seenUrls.add(unwrapped.url); + + const qQuality = q.quality || 'Auto'; + allUrls.push({ + url: unwrapped.url, + label: `${providerName} · ${qQuality}`, + headers: unwrapped.headers, + }); + } + } + + if (Array.isArray(item.candidates) && item.candidates.length > 0) { + let candIndex = 1; + for (const c of item.candidates) { + if (!c || typeof c !== 'object') continue; + const cRawUrl = (c.url || '').trim(); + if (!cRawUrl) continue; + + const unwrapped = unwrapUrl(cRawUrl, ua); + if (!unwrapped.url || seenUrls.has(unwrapped.url)) continue; + seenUrls.add(unwrapped.url); + + const cQuality = c.quality || '1080p'; + allUrls.push({ + url: unwrapped.url, + label: `${providerName} · Mirror ${candIndex} · ${cQuality}`, + headers: unwrapped.headers, + }); + candIndex++; + } + } + + if (Array.isArray(item.audioTracks) && item.audioTracks.length > 0) { + for (const a of item.audioTracks) { + if (!a || typeof a !== 'object') continue; + const aRawUrl = (a.switchUrl || a.url || '').trim(); + if (!aRawUrl) continue; + + const unwrapped = unwrapUrl(aRawUrl, ua); + if (!unwrapped.url || seenUrls.has(unwrapped.url)) continue; + seenUrls.add(unwrapped.url); + + const rawLabel = (a.label || a.name || a.language || 'Audio').trim(); + const cleanLabel = rawLabel.replace(/\s*audio\s*$/i, '').trim() || 'Audio'; + + allUrls.push({ + url: unwrapped.url, + label: `${providerName} · ${cleanLabel} Audio`, + headers: unwrapped.headers, + }); + } + } + + if (primaryRawUrl) { + const unwrapped = unwrapUrl(primaryRawUrl, ua); + if (unwrapped.url && !seenUrls.has(unwrapped.url)) { + seenUrls.add(unwrapped.url); + + let displayQuality = item.quality || (itemType === 'mp4' ? 'MP4' : 'HD'); + let cleanLabel = itemLabel || providerName; + let label = cleanLabel.startsWith('[') ? cleanLabel : `${providerName} · ${displayQuality}`; + + allUrls.push({ + url: unwrapped.url, + label, + headers: unwrapped.headers, + }); + } + } + } + } catch { } + }); + + await Promise.all(tasks); + + 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/xdownloader.js b/src/sources/xdownloader.js new file mode 100644 index 0000000..78dab6c --- /dev/null +++ b/src/sources/xdownloader.js @@ -0,0 +1,129 @@ +const BASE_URL = 'https://www.films365.org'; +const HEADERS = { + Authorization: 'Bearer 79a02956be35835728a044b11e2ae793149d45fb2c89cb6d029ec01aac19bfdb', + 'Content-Type': 'application/json', + '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); + const targetType = isTv ? 'tv' : 'movie'; + + const title = await resolveTitle(sdk, id, isTv); + 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), + }); + + 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 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; + + const itemTitle = String(item?.title || ''); + const cleanItemTitle = itemTitle.toLowerCase().replace(/[^a-z0-9]/g, ''); + + 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'], + }, + }); + } + } 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}`, + headers: { + 'User-Agent': HEADERS['User-Agent'], + }, + }); + } + } + } + } + + 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/xpass.js b/src/sources/xpass.js deleted file mode 100644 index 9473b75..0000000 --- a/src/sources/xpass.js +++ /dev/null @@ -1,40 +0,0 @@ -import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js'; - -const BASE = "https://play.xpass.top"; -const HEADERS = { "accept": "*/*", "accept-language": "en-US,en;q=0.9", "cache-control": "no-cache", "pragma": "no-cache", "User-Agent": USER_AGENT, "Origin": BASE, "Referrer": `${BASE}/`, "Cookie": "auth_token=de21073d24bca9b50f189b402ac870734cf945f2085cb7e1a4fc453fcfe4f57e" }; - -export async function getStream({ id, s, e, server }) { - const isTv = s && e && s !== 'null' && e !== 'null'; - const hdrs = { ...HEADERS, referer: `${BASE}/e/${isTv ? 'tv' : 'movie'}/${id}?autostart=true` }; - try { - let sources = []; - if (!isTv) { - const text = await fetchText(`${BASE}/e/movie/${id}?autostart=true`, { headers: hdrs, signal: AbortSignal.timeout(7000) }); - const match = text.match(/var backups=(\[[\s\S]*?\])/); - if (match) sources = JSON.parse(match[1]); - } else { - sources = await fetchJson(`${BASE}/data/tv/${id}/${s}/${e}?autostart=true&force=true`, { headers: hdrs, signal: AbortSignal.timeout(7000) }); - } - if (!sources?.length) return null; - if (server && server !== 'all') { - const exact = sources.find(src => src.name === server.replace('XPass - ', '')); - if (exact) sources = [exact]; else return null; - } - const allUrls = []; - for (const source of sources) { - if (!source.url) continue; - try { - const mdata = await fetchJson(`${BASE}${source.url}`, { headers: hdrs, signal: AbortSignal.timeout(5000) }); - if (!mdata?.playlist?.[0]?.sources) continue; - const targetStream = mdata.playlist[0].sources.find(st => st.type === 'hls') || mdata.playlist[0].sources[0]; - if (targetStream?.file) allUrls.push({ server: `XPass - ${source.name}`, type: targetStream.type === 'hls' ? 'hls' : 'mp4', url: targetStream.file, headers: { "Origin": HEADERS.Origin, "Referer": HEADERS.Referrer, "User-Agent": HEADERS["User-Agent"] } }); - } catch { } - } - return allUrls.length ? { allUrls } : null; - } catch { return null; } -} - -export async function getSources(args) { - const stream = await getStream(args); - return stream?.allUrls ? stream.allUrls.map(u => u.server) : []; -} \ No newline at end of file diff --git a/src/sources/zxcstream.js b/src/sources/zxcstream.js deleted file mode 100644 index f2107bf..0000000 --- a/src/sources/zxcstream.js +++ /dev/null @@ -1,141 +0,0 @@ -import crypto from 'crypto'; -import { getTmdbInfo, getUA } from '../utils/helpers.js'; - -const BASE_URL = 'https://player.zxcstream.xyz'; -const AES_KEY = '7f4c9e2a81d63b05c4f7a9e8126d3b50e1a8c7f23d9465ab0c6e9f1d4a7b832c'; -const SERVERS = ['berkas', 'orion', 'aquarius', 'resshin']; - -function decryptCryptoJS(ciphertextB64, passphrase) { - try { - const buf = Buffer.from(ciphertextB64, 'base64'); - if (buf.slice(0, 8).toString('utf8') !== 'Salted__') { - const key = crypto.createHash('md5').update(passphrase).digest(); - const iv = Buffer.alloc(16, 0); - const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv); - return Buffer.concat([decipher.update(buf), decipher.final()]).toString('utf8'); - } - - const salt = buf.slice(8, 16); - const cipherBytes = buf.slice(16); - - let hash = Buffer.alloc(0); - let keyAndIv = Buffer.alloc(0); - while (keyAndIv.length < 48) { - hash = crypto.createHash('md5').update(Buffer.concat([hash, Buffer.from(passphrase), salt])).digest(); - keyAndIv = Buffer.concat([keyAndIv, hash]); - } - - const key = keyAndIv.slice(0, 32); - const iv = keyAndIv.slice(32, 48); - - const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); - return Buffer.concat([decipher.update(cipherBytes), decipher.final()]).toString('utf8'); - } catch { - return null; - } -} - -export async function getStream({ id, s, e }) { - try { - const isTv = Boolean(s && e); - const mediaType = isTv ? 'tv' : 'movie'; - - let title = 'Media'; - let year = '2024'; - - try { - const info = await getTmdbInfo(id, mediaType, s); - if (info?.title) title = info.title; - if (info?.year) year = String(info.year); - } catch { } - - const headers = { - 'User-Agent': getUA(), - Referer: `${BASE_URL}/`, - Origin: BASE_URL, - Accept: 'application/json, text/plain, */*', - }; - - const serverResults = await Promise.all( - SERVERS.map(async (server) => { - try { - const tokenRes = await fetch(`${BASE_URL}/backend/you-are-gay`, { - method: 'POST', - headers: { ...headers, 'Content-Type': 'application/json' }, - body: JSON.stringify({ - id, - media_type: mediaType, - path: server, - ...(isTv ? { season: Number(s || 1), episode: Number(e || 1) } : {}), - }), - signal: AbortSignal.timeout(8000), - }); - - if (!tokenRes.ok) return []; - const tokenData = await tokenRes.json(); - if (!tokenData?.token) return []; - - const params = new URLSearchParams({ - id, - b: mediaType, - ts: String(tokenData.ts), - token: tokenData.token, - title, - year, - date: year, - }); - - if (isTv) { - params.append('season', String(s || 1)); - params.append('episode', String(e || 1)); - } - - const sourcesRes = await fetch(`${BASE_URL}/backend_/sources/${server}?${params.toString()}`, { - headers, - signal: AbortSignal.timeout(8000), - }); - - if (!sourcesRes.ok) return []; - const sourcesData = await sourcesRes.json(); - - const links = []; - if (Array.isArray(sourcesData?.links)) { - for (const item of sourcesData.links) { - if (!item.link) continue; - const decryptedUrl = decryptCryptoJS(item.link, AES_KEY); - if (decryptedUrl && decryptedUrl.startsWith('http')) { - links.push({ - url: decryptedUrl, - type: decryptedUrl.includes('.m3u8') ? 'hls' : decryptedUrl.includes('.mpd') ? 'dash' : 'mp4', - quality: item.resolution || 'Auto', - server: `ZxcStream-${server.toUpperCase()}`, - headers: { - Referer: `${BASE_URL}/`, - Origin: BASE_URL, - }, - skipProxy: true, - }); - } - } - } - - return links; - } catch { - return []; - } - }) - ); - - const allUrls = serverResults.flat(); - if (!allUrls.length) return null; - - return { 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/subtitles.js b/src/subtitles.js index cbb8ac2..7893a7d 100644 --- a/src/subtitles.js +++ b/src/subtitles.js @@ -3,36 +3,24 @@ import { getUA } from './utils/helpers.js'; const SUBTITLE_BASES = [ 'https://sub.vdrk.site/v1', 'https://sub.vdrk.site/v2', - 'https://fed-subs.pstream.mov' -]; -const VYLIAN_MESSAGES = [ - "Thanks for using Vyla!" + 'https://opensubtitles-v3.strem.io' ]; -function generateVylianVtt() { - let vttContent = 'WEBVTT\n\n'; - - let currentTime = 1; - while (currentTime <= 7200) { - const duration = Math.floor(Math.random() * 4) + 1; - const message = VYLIAN_MESSAGES[Math.floor(Math.random() * VYLIAN_MESSAGES.length)]; - - const startTime = formatVttTime(currentTime); - const endTime = formatVttTime(currentTime + duration); - - vttContent += `${startTime} --> ${endTime}\n${message}\n\n`; - - currentTime += duration + Math.floor(Math.random() * 60) + 30; +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}`; } - - return vttContent; -} - -function formatVttTime(seconds) { - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const secs = seconds % 60; - return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}.000`; } export async function fetchSubtitles(paths = []) { @@ -42,7 +30,7 @@ export async function fetchSubtitles(paths = []) { try { const res = await fetch(`${base}${path}`, { headers: { 'User-Agent': getUA() }, - signal: AbortSignal.timeout(5000), + signal: AbortSignal.timeout(6000), }); if (!res.ok) { @@ -63,6 +51,20 @@ export async function fetchSubtitles(paths = []) { : []; } + 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 []; @@ -93,34 +95,27 @@ export async function fetchSubtitles(paths = []) { }) ); - const val = results.flat(); - - val.push({ - label: 'Vylian', - file: 'data:text/vtt;base64,' + Buffer.from(generateVylianVtt()).toString('base64'), - type: 'vtt', - source: 'easter-egg' - }); - - return val; + 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: `/movie/tt${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: `/tv/tt${id}/${season}/${episode}` } + { base: SUBTITLE_BASES[2], path: `/subtitles/series/${imdbId}:${season}:${episode}.json` } ]; } diff --git a/test/index.js b/test/index.js index 2194e1c..7946b74 100644 --- a/test/index.js +++ b/test/index.js @@ -9,7 +9,7 @@ import dotenv from "dotenv"; const CONFIG = { DEBUG: true, // Set to true to print outgoing requests, headers, formats, and responses - MOVIE_ID: "1084242", // Movie TMDB ID to test + MOVIE_ID: "936075", // Movie TMDB ID to test TV_ID: "37854", // TV TMDB ID to test, for anime I recommend using a random ID like 37854 TV_SEASON: "1", // TV Season to test TV_EPISODE: "1", // TV Episode to test