From 3e768d6fc9b700cf1fc9c0ea1b15f17407d7f5a1 Mon Sep 17 00:00:00 2001 From: alwaysmesmerizingyou Date: Sat, 7 Nov 2026 10:41:52 -0800 Subject: [PATCH] added vidly, and streamrip, removed rivo, added anizone, added anv, added --- package-lock.json | 4 +- src/config.js | 85 +++++++++++-- src/sources/3eyedraven.js | 25 +--- src/sources/anizone.js | 202 ++++++++++++++++++++++++++++++ src/sources/anv.js | 167 +++++++++++++++++++++++++ src/sources/kisskh.js | 33 +---- src/sources/lookmovie.js | 52 +++++--- src/sources/moviebox.js | 82 ++++++++++++ src/sources/purstream.js | 15 +-- src/sources/rivo.js | 59 --------- src/sources/streamrip.js | 109 ++++++++++++++++ src/sources/vidapi.js | 26 +--- src/sources/vidlink.js | 28 ++--- src/sources/vidly.js | 255 ++++++++++++++++++++++++++++++++++++++ src/sources/vidy.js | 26 ++-- test/index.js | 2 +- 16 files changed, 972 insertions(+), 198 deletions(-) create mode 100644 src/sources/anizone.js create mode 100644 src/sources/anv.js create mode 100644 src/sources/moviebox.js delete mode 100644 src/sources/rivo.js create mode 100644 src/sources/streamrip.js create mode 100644 src/sources/vidly.js diff --git a/package-lock.json b/package-lock.json index 4372b6c..3324793 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vyla-entertainment/sdk", - "version": "1.18.0", + "version": "1.25.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vyla-entertainment/sdk", - "version": "1.18.0", + "version": "1.25.0", "license": "CC-BY-NC-4.0", "dependencies": { "node-tls-client": "^2.1.0" diff --git a/src/config.js b/src/config.js index 2ebc7da..03a5979 100644 --- a/src/config.js +++ b/src/config.js @@ -89,6 +89,42 @@ export const SOURCES = [ }], }, + { + key: 'anizone', + label: 'AniZone', + sourceFile: 'anizone', + proxyParam: 'anz', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + cdnHeaders: [{ + pattern: /anizone\.to/i, + headers: { + Referer: 'https://anizone.to/', + Origin: 'https://anizone.to', + }, + }], + }, + + { + key: 'anv', + label: 'ANV', + sourceFile: 'anv', + proxyParam: 'anv', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + cdnHeaders: [{ + pattern: /anv\.to/i, + headers: { + Referer: 'https://anv.to/', + Origin: 'https://anv.to', + }, + }], + }, + { key: 'braflix', label: 'Braflix', @@ -384,6 +420,23 @@ export const SOURCES = [ }], }, + { + key: 'moviebox', + label: 'MovieBox', + sourceFile: 'moviebox', + proxyParam: 'mb', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + cdnHeaders: [{ + pattern: /mzfi\.me|net79\.cc/i, + headers: { + Referer: 'https://mzfi.me/', + }, + }], + }, + { key: 'movy', label: 'Movy', @@ -477,19 +530,19 @@ export const SOURCES = [ }, { - key: 'rivo', - label: 'Rivo', - sourceFile: 'rivo', - proxyParam: 'rv', + key: 'streamrip', + label: 'Streamrip', + sourceFile: 'streamrip', + proxyParam: 'srip', timeout: 25000, jitter: 500, retries: 2, multiUrl: true, cdnHeaders: [{ - pattern: /rivo\.st/i, + pattern: /streamrip\.fun/i, headers: { - Referer: 'https://f.rivo.st/', - Origin: 'https://f.rivo.st', + Referer: 'https://streamrip.fun/', + Origin: 'https://streamrip.fun', }, }], }, @@ -617,6 +670,24 @@ export const SOURCES = [ multiUrl: true, }, + { + key: 'vidly', + label: 'Vidly', + sourceFile: 'vidly', + proxyParam: 'vdly', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + cdnHeaders: [{ + pattern: /rousav\.tech|hakunaymatata\.com|johndoesdmc\.workers\.dev|vidly\.pro/i, + headers: { + Referer: 'https://vidly.pro/', + Origin: 'https://vidly.pro', + }, + }], + }, + { key: 'vidora', label: 'Vidora', diff --git a/src/sources/3eyedraven.js b/src/sources/3eyedraven.js index 568757e..7d0bb5f 100644 --- a/src/sources/3eyedraven.js +++ b/src/sources/3eyedraven.js @@ -44,37 +44,20 @@ export async function getStream(args) { const streamUrl = absolute(stream.url); if (!/^https?:\/\//.test(streamUrl)) return null; - const subtitles = (stream.subtitles || []) - .map(sub => { - const subUrl = absolute(sub.url); - if (!subUrl) return null; - return { - label: sub.label || sub.language || 'Subtitle', - file: subUrl, - type: 'vtt', - source: '3eyedraven', - }; - }) - .filter(Boolean); - const allUrls = [{ url: streamUrl, headers, server: '3eyedraven', quality: '1080p', type: stream.format === 'hls' ? 'hls' : 'mp4', - subtitles, skipProxy: true, }]; + if (!allUrls.length) return null; + return { - url: streamUrl, - headers, - server: '3eyedraven', - quality: '1080p', - type: stream.format === 'hls' ? 'hls' : 'mp4', - subtitles, - skipProxy: true, + url: allUrls[0].url, + headers: allUrls[0].headers, allUrls, }; } catch { diff --git a/src/sources/anizone.js b/src/sources/anizone.js new file mode 100644 index 0000000..d156c82 --- /dev/null +++ b/src/sources/anizone.js @@ -0,0 +1,202 @@ +import { USER_AGENT, fetchText, getTmdbMetadata, resolveTmdbKey } from '../utils/helpers.js'; + +const BASE_URL = 'https://anizone.to'; + +const HEADERS = { + 'User-Agent': USER_AGENT, + Referer: `${BASE_URL}/`, + Origin: BASE_URL, +}; + +const PLAYBACK_HEADERS = { + Referer: `${BASE_URL}/`, + Origin: BASE_URL, +}; + +function decodeJsonArgument(raw) { + if (!raw) return null; + const marker = '\x01U\x01'; + let value = String(raw).replace(/\\\\u([0-9a-fA-F]{4})/g, `${marker}$1`); + value = value.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))); + value = value.replace(/\x01U\x01([0-9a-fA-F]{4})/g, '\\u$1'); + try { + return JSON.parse(value); + } catch { + return null; + } +} + +function parseJsonNamedArg(html, name) { + const pattern = new RegExp(`${name}\\s*:\\s*JSON\\.parse\\('((?:[^'\\\\]|\\\\.)*)'\\)`, 'i'); + const match = String(html || '').match(pattern); + return decodeJsonArgument(match?.[1]); +} + +function parsePlayerData(html) { + const match = String(html || '').match(/vidstackPlayer\s*\(\s*JSON\.parse\('((?:[^'\\]|\\.)*)'\)\s*\)/i); + return decodeJsonArgument(match?.[1]); +} + +function cleanTitle(t) { + return String(t || '') + .toLowerCase() + .replace(/\s*[-–—:]\s*\d+(?:st|nd|rd|th)?\s*(?:&|\+)?\s*\d*(?:st|nd|rd|th)?\s*(?:stage|cour|part|season).*$/i, '') + .replace(/\s*[-–—:]\s*part\s+[ivx\d]+.*$/i, '') + .replace(/\s*[-–—:]\s*season\s+\d+.*$/i, '') + .replace(/\s*\(\d{4}\)$/, '') + .replace(/[^a-z0-9]/g, ' ') + .replace(/\s+/g, ' ') + .trim(); +} + +async function resolveHlsVariants(masterUrl, headers) { + try { + const text = await fetchText(masterUrl, { + headers, + signal: AbortSignal.timeout(4000), + }); + if (!text || !text.includes('#EXTM3U') || !text.includes('#EXT-X-STREAM-INF')) { + return null; + } + + const baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1); + const lines = text.split('\n').map(l => l.trim()); + const variants = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith('#EXT-X-STREAM-INF:')) { + let quality = 'Auto'; + const resMatch = line.match(/RESOLUTION=\d+x(\d+)/i); + if (resMatch) quality = `${resMatch[1]}p`; + + const nextLine = lines[i + 1]; + if (nextLine && !nextLine.startsWith('#')) { + const fullUrl = nextLine.startsWith('http') ? nextLine : `${baseUrl}${nextLine}`; + variants.push({ url: fullUrl, quality }); + } + } + } + + return variants.length ? variants : null; + } catch { + return null; + } +} + +function parseQualityNumber(q) { + const m = String(q).match(/\d+/); + return m ? parseInt(m[0], 10) : 0; +} + +export async function getStream(args) { + const { id, s, e, sdk } = args || {}; + if (!id) return null; + + const isTv = s != null && e != null; + const apiKey = resolveTmdbKey(args, sdk); + const meta = await getTmdbMetadata(id, isTv, apiKey); + if (!meta?.isAnime) return null; + + const targetEpisode = isTv ? (parseInt(e, 10) || 1) : 1; + const titles = Array.isArray(meta.titles) && meta.titles.length > 0 + ? meta.titles + : [meta.title || String(id)]; + + try { + const allFoundItems = []; + const seenSlugs = new Set(); + + for (const title of titles.slice(0, 4)) { + const searchHtml = await fetchText(`${BASE_URL}/anime?search=${encodeURIComponent(title)}`, { + headers: HEADERS, + signal: AbortSignal.timeout(8000), + }); + + const items = parseJsonNamedArg(searchHtml, 'items'); + if (Array.isArray(items)) { + for (const item of items) { + if (item?.slug && !seenSlugs.has(item.slug)) { + seenSlugs.add(item.slug); + allFoundItems.push(item); + } + } + } + if (allFoundItems.length >= 3) break; + } + + if (!allFoundItems.length) return null; + + const cleanTargets = titles.map(cleanTitle).filter(Boolean); + let matched = null; + + for (const item of allFoundItems) { + const itemTitles = [item.main_title, ...Object.values(item.title_list || {})] + .map(cleanTitle) + .filter(Boolean); + + const hasExact = cleanTargets.some(ct => itemTitles.some(it => it === ct)); + if (hasExact) { + matched = item; + break; + } + } + + if (!matched) matched = allFoundItems[0]; + if (!matched?.slug) return null; + + const watchUrl = `${BASE_URL}/anime/${matched.slug}/${targetEpisode}`; + const watchHtml = await fetchText(watchUrl, { + headers: HEADERS, + signal: AbortSignal.timeout(8000), + }); + + const player = parsePlayerData(watchHtml); + const masterUrl = player?.src ? String(player.src).replace(/\\+\//g, '/') : null; + if (!masterUrl || !masterUrl.startsWith('http')) return null; + + const allUrls = []; + const seen = new Set(); + + if (masterUrl.includes('.m3u8')) { + const variants = await resolveHlsVariants(masterUrl, PLAYBACK_HEADERS); + if (variants && variants.length) { + for (const v of variants) { + if (seen.has(v.url)) continue; + seen.add(v.url); + + allUrls.push({ + url: v.url, + server: `AniZone (${v.quality})`, + quality: v.quality, + type: 'hls', + headers: PLAYBACK_HEADERS, + }); + } + } + } + + if (!seen.has(masterUrl)) { + seen.add(masterUrl); + allUrls.push({ + url: masterUrl, + server: 'AniZone', + quality: 'Auto', + type: masterUrl.includes('.m3u8') ? 'hls' : 'mp4', + headers: PLAYBACK_HEADERS, + }); + } + + if (!allUrls.length) return null; + + allUrls.sort((a, b) => parseQualityNumber(b.quality) - parseQualityNumber(a.quality)); + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; + } catch { + return null; + } +} \ No newline at end of file diff --git a/src/sources/anv.js b/src/sources/anv.js new file mode 100644 index 0000000..cb0b858 --- /dev/null +++ b/src/sources/anv.js @@ -0,0 +1,167 @@ +import { USER_AGENT, fetchText, getTmdbMetadata, resolveTmdbKey } from '../utils/helpers.js'; + +const BASE_URL = 'https://anv.to'; +let cachedCookie = null; +let cookieExpiresAt = 0; +let cookiePromise = null; + +async function getShiroCookie() { + if (cachedCookie && Date.now() < cookieExpiresAt) { + return cachedCookie; + } + if (cookiePromise) return cookiePromise; + + cookiePromise = (async () => { + try { + let targetUrl = `${BASE_URL}/anime/aoki-denshou-welsh-shedar-626fd-episode-5`; + try { + const xmlText = await fetchText(`${BASE_URL}/recents.xml`, { + signal: AbortSignal.timeout(3500), + }); + const match = xmlText?.match(/(https:\/\/anv\.to\/anime\/[^<]+)<\/link>/); + if (match?.[1]) targetUrl = match[1]; + } catch { } + + const res = await fetch(targetUrl, { + headers: { + 'User-Agent': USER_AGENT, + Referer: `${BASE_URL}/`, + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + }, + signal: AbortSignal.timeout(4000), + }); + + const getCookieHeader = typeof res.headers.getSetCookie === 'function' + ? res.headers.getSetCookie() + : [res.headers.get('set-cookie')].filter(Boolean); + + for (const c of getCookieHeader) { + const m = String(c).match(/shiro_watch=([^;]+)/); + if (m) { + cachedCookie = `shiro_watch=${m[1].trim()}`; + cookieExpiresAt = Date.now() + 20 * 60 * 60 * 1000; + break; + } + } + + return cachedCookie || ''; + } catch { + return ''; + } finally { + cookiePromise = null; + } + })(); + + return cookiePromise; +} + +async function resolveAnimeIds(title) { + if (!title) return { malId: null, anilistId: null }; + try { + const res = await fetch('https://graphql.anilist.co', { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify({ + query: 'query($search: String) { Media(search: $search, type: ANIME) { id idMal } }', + variables: { search: title }, + }), + signal: AbortSignal.timeout(5000), + }); + if (res.ok) { + const data = await res.json(); + return { + anilistId: data?.data?.Media?.id || null, + malId: data?.data?.Media?.idMal || null, + }; + } + } catch { } + return { malId: null, anilistId: null }; +} + +export async function getStream(args) { + const { id, s, e, sdk } = args || {}; + if (!id) return null; + + const isTv = s != null && e != null; + const apiKey = resolveTmdbKey(args, sdk); + const meta = await getTmdbMetadata(id, isTv, apiKey); + if (!meta?.isAnime) return null; + + const title = meta.title || (Array.isArray(meta.titles) ? meta.titles[0] : null); + const { malId, anilistId } = await resolveAnimeIds(title); + if (!anilistId) return null; + + const episode = isTv ? (parseInt(e, 10) || 1) : 1; + const cookie = await getShiroCookie(); + + const requestHeaders = { + 'Content-Type': 'application/json', + Origin: BASE_URL, + Referer: `${BASE_URL}/`, + 'User-Agent': USER_AGENT, + ...(cookie ? { Cookie: cookie } : {}), + Accept: '*/*', + }; + + try { + const res = await fetch(`${BASE_URL}/api/episode`, { + method: 'POST', + headers: requestHeaders, + body: JSON.stringify({ + anilistId, + malId: malId || null, + episode, + }), + signal: AbortSignal.timeout(5000), + }); + + if (!res.ok) return null; + const data = await res.json(); + if (data?.status !== 'ready' || !Array.isArray(data?.variants) || data.variants.length === 0) { + return null; + } + + const playbackHeaders = { + Origin: BASE_URL, + Referer: `${BASE_URL}/`, + ...(cookie ? { Cookie: cookie } : {}), + 'User-Agent': USER_AGENT, + }; + + const allUrls = []; + const seen = new Set(); + + for (const variant of data.variants) { + const variantType = (variant.id || 'sub').toLowerCase(); + const variantLabel = variant.label || (variantType === 'dub' ? 'Dub' : 'Sub'); + const sources = Array.isArray(variant.sources) ? variant.sources : []; + + for (const src of sources) { + if (!src?.url) continue; + const fullUrl = src.url.startsWith('http') ? src.url : `${BASE_URL}${src.url}`; + if (seen.has(fullUrl)) continue; + seen.add(fullUrl); + + const serverName = (src.label || 'Default').trim(); + + allUrls.push({ + url: fullUrl, + server: `ANV - ${serverName} (${variantLabel})`, + quality: 'Auto', + type: fullUrl.includes('.m3u8') ? 'hls' : 'mp4', + headers: playbackHeaders, + }); + } + } + + if (!allUrls.length) return null; + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; + } catch { + return null; + } +} \ No newline at end of file diff --git a/src/sources/kisskh.js b/src/sources/kisskh.js index 2f6947c..df26aeb 100644 --- a/src/sources/kisskh.js +++ b/src/sources/kisskh.js @@ -67,49 +67,24 @@ export async function getStream(args) { }); if (!videoData?.Video) return null; - let subtitles = []; - const subKey = await encKey(episodeId, 'sub'); - if (subKey) { - try { - const subList = await fetchJson(`${BASE_URL}/api/Sub/${episodeId}?kkey=${subKey}`, { - headers: HEADERS, - signal: AbortSignal.timeout(8000), - }); - if (Array.isArray(subList)) { - subtitles = subList.map(sub => ({ - url: sub.src, - label: sub.label, - lang: sub.land, - default: Boolean(sub.default), - })); - } - } catch { } - } - const streamUrl = videoData.Video; const allUrls = [{ url: streamUrl, type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', - audio: 'sub', server: 'KissKH', quality: 'Auto', headers: HEADERS, skipProxy: false, - subtitles, }]; + if (!allUrls.length) return null; + return { - url: streamUrl, - headers: HEADERS, + url: allUrls[0].url, + headers: allUrls[0].headers, allUrls, - subtitles, }; } catch { return null; } -} - -export async function getSources(args) { - const stream = await getStream(args); - return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : ['KissKH']; } \ No newline at end of file diff --git a/src/sources/lookmovie.js b/src/sources/lookmovie.js index 1c4f367..9380bdd 100644 --- a/src/sources/lookmovie.js +++ b/src/sources/lookmovie.js @@ -2,7 +2,6 @@ import { USER_AGENT, fetchText, fetchJson } from '../utils/helpers.js'; const TMDB_BASE = 'https://api.themoviedb.org/3'; const LM_DOMAINS = ['https://www.lookmovie2.to', 'https://lookmovie2.to', 'https://lookmovie.foundation']; - const HEADERS_BASE = { 'User-Agent': USER_AGENT, 'Accept-Language': 'en-US,en;q=0.9' }; async function searchLookMovie(type, title, year, clientIP) { @@ -48,34 +47,59 @@ function getEpisodeIdFast(html, s, e) { return am ? am[1] : null; } -export async function getStream({ id, s, e, sdk, clientIP }) { - const tmdbApiKey = sdk?.tmdbApiKey || null; - if (!tmdbApiKey) return null; +export async function getStream(args) { + const { id, s, e, sdk, clientIP } = args || {}; + const apiKey = sdk?.tmdbApiKey; + if (!apiKey) return null; + + const isTv = s != null && e != null; + const typeStr = isTv ? 'shows' : 'movies'; + try { - const tmdbKey = tmdbApiKey; - if (!tmdbKey) return null; - const isTV = s != null && e != null; - const typeStr = isTV ? 'shows' : 'movies'; - const tmdbData = await fetchJson(`${TMDB_BASE}/${isTV ? 'tv' : 'movie'}/${id}?api_key=${tmdbKey}`, { signal: AbortSignal.timeout(3000) }); + const tmdbData = await fetchJson(`${TMDB_BASE}/${isTv ? 'tv' : 'movie'}/${id}?api_key=${apiKey}`, { signal: AbortSignal.timeout(3000) }); const title = tmdbData?.title || tmdbData?.name; const year = (tmdbData?.first_air_date || tmdbData?.release_date || '').slice(0, 4); if (!title) return null; + const searchRes = await searchLookMovie(typeStr, title, year, clientIP); if (!searchRes?.match?.slug) return null; + const { match, base } = searchRes; const headers = { ...HEADERS_BASE, 'Accept': 'text/html', 'Referer': `${base}/`, ...(clientIP && { 'X-Forwarded-For': clientIP }) }; const html = await fetchText(`${base}/${typeStr}/play/${match.slug}`, { headers, signal: AbortSignal.timeout(8000) }); + const storageMatch = html.match(STORAGE_RE); if (!storageMatch) return null; + const hashMatch = storageMatch[1].match(/hash\s*:\s*['"]([^'"]+)['"]/); const expiresMatch = storageMatch[1].match(/expires\s*:\s*(\d+)/); if (!hashMatch || !expiresMatch) return null; - const streamId = isTV ? getEpisodeIdFast(html, s, e) : (match.id_movie || match.id || html.match(ID_MOVIE_RE)?.[1]); + + const streamId = isTv ? getEpisodeIdFast(html, s, e) : (match.id_movie || match.id || html.match(ID_MOVIE_RE)?.[1]); if (!streamId) return null; + const accessHeaders = { ...HEADERS_BASE, 'Accept': 'application/json', 'Referer': `${base}/`, 'X-Requested-With': 'XMLHttpRequest', ...(clientIP && { 'X-Forwarded-For': clientIP }) }; - const data = await fetchJson(`${base}/api/v1/security/${isTV ? 'episode' : 'movie'}-access?id_${isTV ? 'episode' : 'movie'}=${streamId}&hash=${hashMatch[1]}&expires=${expiresMatch[1]}`, { headers: accessHeaders, signal: AbortSignal.timeout(8000) }); + const data = await fetchJson(`${base}/api/v1/security/${isTv ? 'episode' : 'movie'}-access?id_${isTv ? 'episode' : 'movie'}=${streamId}&hash=${hashMatch[1]}&expires=${expiresMatch[1]}`, { headers: accessHeaders, signal: AbortSignal.timeout(8000) }); const streams = data?.streams ?? data?.result?.streams ?? data?.data?.streams ?? data; - const allUrls = Object.entries(streams || {}).filter(([, v]) => typeof v === 'string' && v.includes('.m3u8')).map(([quality, url]) => ({ url, quality })); - return allUrls.length ? { allUrls } : null; - } catch { return null; } + + const allUrls = Object.entries(streams || {}) + .filter(([, v]) => typeof v === 'string' && v.includes('.m3u8')) + .map(([quality, url]) => ({ + url, + server: `LookMovie - ${quality}`, + quality, + type: 'hls', + headers: accessHeaders, + })); + + if (!allUrls.length) return null; + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; + } catch { + return null; + } } \ No newline at end of file diff --git a/src/sources/moviebox.js b/src/sources/moviebox.js new file mode 100644 index 0000000..15402b9 --- /dev/null +++ b/src/sources/moviebox.js @@ -0,0 +1,82 @@ +import { USER_AGENT, fetchJson } from '../utils/helpers.js'; + +const BASE_URL = 'https://net79.cc'; +const PLAYBACK_REFERER = 'https://mzfi.me/'; + +const HEADERS = { + Accept: 'application/json', + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + 'User-Agent': USER_AGENT, +}; + +const PLAYBACK_HEADERS = { + Referer: PLAYBACK_REFERER, +}; + +function formatSize(sizeBytes) { + if (!sizeBytes || typeof sizeBytes !== 'number') return ''; + const gb = sizeBytes / (1024 * 1024 * 1024); + return gb >= 1 ? `${gb.toFixed(2)} GB` : `${Math.round(sizeBytes / (1024 * 1024))} MB`; +} + +export async function getStream(args) { + const { id, s, e } = args || {}; + if (!id) return null; + + const isTv = s != null && e != null; + const season = isTv ? (parseInt(s, 10) || 1) : 1; + const episode = isTv ? (parseInt(e, 10) || 1) : 1; + + const apiUrl = isTv + ? `${BASE_URL}/api/embed-tmdb/${id}?type=tv&se=${season}&ep=${episode}` + : `${BASE_URL}/api/embed-tmdb/${id}?type=movie`; + + try { + const payload = await fetchJson(apiUrl, { + headers: HEADERS, + signal: AbortSignal.timeout(8000), + }); + + if (!payload || !payload.ok || payload.noSource) return null; + + const streamList = Array.isArray(payload.streams) && payload.streams.length > 0 + ? payload.streams + : (payload.mp4 ? [{ url: payload.mp4, resolution: payload.resolution || 720 }] : []); + + if (!streamList.length) return null; + + const allUrls = []; + const seen = new Set(); + + for (const item of streamList) { + const streamUrl = item?.url; + if (!streamUrl || typeof streamUrl !== 'string' || !streamUrl.startsWith('http') || seen.has(streamUrl)) { + continue; + } + seen.add(streamUrl); + + const resolution = item.resolution ? `${item.resolution}p` : '720p'; + const sizeStr = item.size ? formatSize(parseInt(item.size, 10)) : ''; + const sizeLabel = sizeStr ? ` [${sizeStr}]` : ''; + + allUrls.push({ + url: streamUrl, + server: `MovieBox ${resolution}${sizeLabel}`, + quality: resolution, + type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', + headers: PLAYBACK_HEADERS, + }); + } + + if (!allUrls.length) return null; + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; + } catch { + return null; + } +} \ No newline at end of file diff --git a/src/sources/purstream.js b/src/sources/purstream.js index efe9648..65ce743 100644 --- a/src/sources/purstream.js +++ b/src/sources/purstream.js @@ -76,21 +76,14 @@ export async function getStream(args) { skipProxy: true, }]; + if (!allUrls.length) return null; + return { - url: chosen.stream_url, - headers: streamHeaders, - server: 'purstream', - quality: chosen.source_name || 'Auto', - type: chosen.format || 'hls', - skipProxy: true, + url: allUrls[0].url, + headers: allUrls[0].headers, allUrls, }; } catch { return null; } -} - -export async function getSources(args) { - const stream = await getStream(args); - return stream?.allUrls ? stream.allUrls.map(u => u.server) : ['purstream']; } \ No newline at end of file diff --git a/src/sources/rivo.js b/src/sources/rivo.js deleted file mode 100644 index 0172ba4..0000000 --- a/src/sources/rivo.js +++ /dev/null @@ -1,59 +0,0 @@ -import { fetchJson } from '../utils/helpers.js'; - -const BASE_URL = 'https://f.rivo.st'; - -export async function getStream(args) { - const { id, s, e } = args || {}; - const isTv = s != null && e != null; - const basePath = isTv - ? `${BASE_URL}/api/source/tv/${id}?s=${s}&e=${e}` - : `${BASE_URL}/api/source/movie/${id}`; - - const headers = { - Accept: '*/*', - Referer: `${BASE_URL}/`, - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', - }; - - const serverIndices = Array.from({ length: 26 }, (_, i) => i + 1); - const delimiter = basePath.includes('?') ? '&' : '?'; - - const responses = await Promise.allSettled( - serverIndices.map(serverNum => - fetchJson(`${basePath}${delimiter}server=${serverNum}`, { headers }) - .catch(() => null) - ) - ); - - const allUrls = []; - - for (const res of responses) { - if (res.status !== 'fulfilled' || !res.value) continue; - const data = res.value; - - if (Array.isArray(data.sources)) { - for (const src of data.sources) { - if (src?.url) { - allUrls.push({ - url: src.url, - server: `Rivo - ${data.server || src.subProvider || 'Server'}`, - quality: src.quality || 'Auto', - type: src.kind || (src.url.includes('.m3u8') ? 'hls' : 'mp4'), - headers: { - Referer: `${BASE_URL}/`, - Origin: BASE_URL, - }, - }); - } - } - } - } - - if (!allUrls.length) return null; - - return { - url: allUrls[0].url, - headers: allUrls[0].headers, - allUrls, - }; -} \ No newline at end of file diff --git a/src/sources/streamrip.js b/src/sources/streamrip.js new file mode 100644 index 0000000..328ce6d --- /dev/null +++ b/src/sources/streamrip.js @@ -0,0 +1,109 @@ +import { USER_AGENT, fetchText } from '../utils/helpers.js'; + +const BASE_URL = 'https://streamrip.fun'; + +const HEADERS = { + 'User-Agent': USER_AGENT, + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.9', +}; + +const PLAYBACK_HEADERS = { + 'User-Agent': USER_AGENT, + Referer: `${BASE_URL}/`, +}; + +function qualityWeight(q) { + const s = String(q).toLowerCase(); + if (s.includes('4k') || s.includes('2160')) return 2160; + const m = s.match(/\d+/); + return m ? parseInt(m[0], 10) : 0; +} + +export async function getStream(args) { + const { id, s, e } = args || {}; + if (!id) return null; + + const isTv = s != null && e != null; + const targetUrl = isTv + ? `${BASE_URL}/tv/${id}/${s}/${e}` + : `${BASE_URL}/movie/${id}`; + + try { + const html = await fetchText(targetUrl, { + headers: HEADERS, + signal: AbortSignal.timeout(10000), + }); + + if (!html || !html.includes('/get?t=')) return null; + + const allUrls = []; + const seen = new Set(); + const linkRegex = /]+href=["'](\/get\?t=[^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi; + let match; + + while ((match = linkRegex.exec(html)) !== null) { + const rawHref = match[1]; + const inner = match[2]; + + const fullUrl = rawHref.startsWith('http') ? rawHref : `${BASE_URL}${rawHref}`; + if (seen.has(fullUrl)) continue; + seen.add(fullUrl); + + const resMatch = inner.match(/font-extrabold[^>]*>([^<]+)<\/span>/i); + const serverMatch = inner.match(/font-semibold[^>]*>([^<]+)<\/span>/i); + const sizeMatch = inner.match(/(\d+(?:\.\d+)?\s*(?:GB|MB|KB)|Size unknown)/i); + const langMatch = inner.match(/line-clamp-2[^>]*>([^<]+)<\/span>/i); + + const quality = resMatch ? resMatch[1].trim() : 'Auto'; + const serverName = serverMatch ? serverMatch[1].trim() : 'Streamrip'; + const size = sizeMatch ? sizeMatch[1].trim() : ''; + const lang = langMatch ? langMatch[1].trim() : ''; + + let serverLabel = `Streamrip - ${serverName}`; + if (quality && quality !== 'Auto') serverLabel += ` ${quality}`; + if (lang) serverLabel += ` (${lang})`; + if (size && size !== 'Size unknown') serverLabel += ` [${size}]`; + + allUrls.push({ + url: fullUrl, + server: serverLabel, + quality, + type: 'mp4', + headers: PLAYBACK_HEADERS, + }); + } + + if (!allUrls.length) { + const tokenRegex = /["'](\/get\?t=[a-zA-Z0-9_-]+)["']/g; + let tokenMatch; + while ((tokenMatch = tokenRegex.exec(html)) !== null) { + const fullUrl = `${BASE_URL}${tokenMatch[1]}`; + if (seen.has(fullUrl)) continue; + seen.add(fullUrl); + + allUrls.push({ + url: fullUrl, + server: 'Streamrip', + quality: 'Auto', + type: 'mp4', + headers: PLAYBACK_HEADERS, + }); + } + } + + if (!allUrls.length) return null; + + allUrls.sort((a, b) => qualityWeight(b.quality) - qualityWeight(a.quality)); + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; + } catch { + return null; + } +} \ No newline at end of file diff --git a/src/sources/vidapi.js b/src/sources/vidapi.js index bb8fed3..ccabd2e 100644 --- a/src/sources/vidapi.js +++ b/src/sources/vidapi.js @@ -46,30 +46,17 @@ export async function getStream(args) { const streamUrls = Array.from(extractStreamUrls(data)); if (!streamUrls.length) return null; - let subs = []; - if (data.subtitles && Array.isArray(data.subtitles)) { - subs = data.subtitles; - } else if (data.captions && Array.isArray(data.captions)) { - subs = data.captions; - } - - const subtitles = subs.map(sub => ({ - url: sub.url || sub.file || sub.src, - lang: sub.display || sub.label || sub.language || sub.lang || 'Unknown', - })).filter(subItem => subItem.url); - const allUrls = streamUrls.map(streamUrl => { - let type = 'mp4'; - if (streamUrl.includes('.mpd')) type = 'dash'; - else if (streamUrl.includes('.m3u8')) type = 'hls'; + let streamType = 'mp4'; + if (streamUrl.includes('.mpd')) streamType = 'dash'; + else if (streamUrl.includes('.m3u8')) streamType = 'hls'; return { url: streamUrl, server: 'VidAPI', quality: 'Auto', - type, + type: streamType, headers: VERIFY_HEADERS, - subtitles: subtitles.length > 0 ? subtitles : undefined, skipProxy: false, }; }); @@ -80,17 +67,12 @@ export async function getStream(args) { url: allUrls[0].url, headers: allUrls[0].headers, allUrls, - ...(subtitles.length > 0 ? { subtitles } : {}), }; } catch { return null; } } -export async function getSources() { - return ['VidAPI']; -} - export async function proxyStream({ url, res, proxyUtils: { fetchUpstream, rewriteM3u8 } }) { try { const upstream = await fetchUpstream(url, 0, VERIFY_HEADERS); diff --git a/src/sources/vidlink.js b/src/sources/vidlink.js index e9802b3..b301d19 100644 --- a/src/sources/vidlink.js +++ b/src/sources/vidlink.js @@ -16,7 +16,7 @@ export async function getStream(args) { fetchJson(`${basePath}?multiLang=0`, { headers: HEADERS, signal: AbortSignal.timeout(10000) }), ]); - const streamList = []; + const allUrls = []; const seenUrls = new Set(); for (const res of responses) { @@ -26,12 +26,10 @@ export async function getStream(args) { if (stream.playlist && !seenUrls.has(stream.playlist)) { seenUrls.add(stream.playlist); const isDash = stream.type === 'dash' || stream.deliveryType === 'dash' || stream.playlist.includes('.mpd'); - streamList.push({ + allUrls.push({ url: stream.playlist, quality: stream.playbackMetadata?.resolutions?.[0] ? `${stream.playbackMetadata.resolutions[0]}p` : 'Auto', - label: `Vidlink ${isDash ? 'DASH' : 'HLS'}`, - name: `Vidlink ${isDash ? 'DASH' : 'HLS'}`, - server: 'Vidlink', + server: `Vidlink ${isDash ? 'DASH' : 'HLS'}`, type: isDash ? 'dash' : 'hls', headers: stream.playlistHeaders || {}, }); @@ -47,12 +45,10 @@ export async function getStream(args) { const qData = stream.qualities[q]; if (!qData?.url || seenUrls.has(qData.url)) continue; seenUrls.add(qData.url); - streamList.push({ + allUrls.push({ url: qData.url, quality: `${q}p`, - label: `Vidlink ${q}p`, - name: `Vidlink ${q}p`, - server: 'Vidlink', + server: `Vidlink ${q}p`, type: qData.type || 'mp4', headers: qData.headers || {}, }); @@ -60,20 +56,14 @@ export async function getStream(args) { } } - if (!streamList.length) return null; + if (!allUrls.length) return null; return { - url: streamList[0].url, - headers: streamList[0].headers, - allUrls: streamList, - streams: streamList, + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, }; } catch { return null; } -} - -export async function getSources(args) { - const stream = await getStream(args); - return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server || 'Vidlink'))] : []; } \ No newline at end of file diff --git a/src/sources/vidly.js b/src/sources/vidly.js new file mode 100644 index 0000000..7a00a20 --- /dev/null +++ b/src/sources/vidly.js @@ -0,0 +1,255 @@ +import { USER_AGENT, fetchText } from '../utils/helpers.js'; + +const BASE_URL = 'https://vidly.pro'; +const SECRET_KEY = 'vY7#qL2!mZ9@wK4$'; + +const HEADERS = { + 'User-Agent': USER_AGENT, + Referer: `${BASE_URL}/`, + Origin: BASE_URL, + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.9', +}; + +const PLAYBACK_HEADERS = { + 'User-Agent': USER_AGENT, + Referer: `${BASE_URL}/`, + Origin: BASE_URL, +}; + +function decryptSealed(sealed) { + if (!sealed || typeof sealed !== 'string' || !sealed.startsWith('v1.')) return null; + try { + const b64 = sealed.slice(3).replace(/-/g, '+').replace(/_/g, '/'); + const binary = atob(b64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + + const salt = bytes.subarray(0, 12); + const ciphertext = bytes.slice(12); + + const keys = []; + for (let s = 0; s < 4; s++) { + let n = (0x811c9dc5 ^ (0x9e3779b9 * s)) >>> 0; + for (let i = 0; i < SECRET_KEY.length; i++) { + n = Math.imul(n ^ SECRET_KEY.charCodeAt(i), 0x1000193) >>> 0; + } + for (let i = 0; i < salt.length; i++) { + n = Math.imul(n ^ salt[i], 0x1000193) >>> 0; + } + keys.push(n); + } + + let [sKey, nKey, lKey, aKey] = keys; + let r = 0; + + for (let i = 0; i < ciphertext.length; i++) { + if ((3 & i) === 0) { + lKey >>>= 0; + aKey >>>= 0; + let e = ((sKey >>>= 0) + (nKey >>>= 0)) | 0; + sKey = nKey ^ (nKey >>> 9); + nKey = (lKey + (lKey << 3)) | 0; + aKey = (aKey + 1) | 0; + e = (e + aKey) | 0; + lKey = ((lKey << 21) | (lKey >>> 11)) + e | 0; + r = e >>> 0; + } + ciphertext[i] ^= (r >>> ((3 & i) * 8)) & 255; + } + + const decodedText = new TextDecoder().decode(ciphertext); + return JSON.parse(decodedText); + } catch { + return null; + } +} + +function extractSealedPayload(html) { + if (!html) return null; + const match = html.match(/v1\.[a-zA-Z0-9_-]{100,}/); + return match ? match[0] : null; +} + +function extractMediaFromThumbs(html) { + if (!html) return null; + const match = html.match(/https?:\/\/[^"'\\<>\s]+\/media\/([a-zA-Z0-9_-]+)\/thumbs/i); + if (match && match[1]) { + const origin = match[0].split('/media/')[0]; + return `${origin}/media/${match[1]}/index.m3u8`; + } + return null; +} + +function isInvalidStreamUrl(url) { + return !url || /tiles|thumbs?|sprite/i.test(url); +} + +async function resolveHlsVariants(masterUrl, headers) { + try { + const text = await fetchText(masterUrl, { + headers, + signal: AbortSignal.timeout(6000), + }); + if (!text || !text.includes('#EXTM3U') || !text.includes('#EXT-X-STREAM-INF')) { + return null; + } + + const baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1); + const lines = text.split('\n').map(l => l.trim()); + const variants = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith('#EXT-X-STREAM-INF:')) { + let quality = 'Auto'; + const resMatch = line.match(/RESOLUTION=\d+x(\d+)/i); + if (resMatch) { + quality = `${resMatch[1]}p`; + } else { + const bwMatch = line.match(/BANDWIDTH=(\d+)/i); + if (bwMatch) { + const bw = parseInt(bwMatch[1], 10); + quality = bw > 3000000 ? '1080p' : bw > 1800000 ? '960p' : bw > 1200000 ? '720p' : bw > 800000 ? '640p' : '480p'; + } + } + + const nextLine = lines[i + 1]; + if (nextLine && !nextLine.startsWith('#')) { + if (isInvalidStreamUrl(nextLine)) continue; + const fullUrl = nextLine.startsWith('http') ? nextLine : `${baseUrl}${nextLine}`; + variants.push({ url: fullUrl, quality }); + } + } + } + + return variants.length ? variants : null; + } catch { + return null; + } +} + +function parseQualityNumber(q) { + const m = String(q).match(/\d+/); + return m ? parseInt(m[0], 10) : 0; +} + +export async function getStream(args) { + const { id, s, e } = args || {}; + if (!id) return null; + + const isTv = s != null && e != null; + const targetUrl = isTv + ? `${BASE_URL}/tv/${id}/${s}/${e}?preview=1` + : `${BASE_URL}/movie/${id}?preview=1`; + + try { + const html = await fetchText(targetUrl, { + headers: HEADERS, + signal: AbortSignal.timeout(10000), + }); + + const allUrls = []; + const seen = new Set(); + + const sealed = extractSealedPayload(html); + if (sealed) { + const data = decryptSealed(sealed); + if (data && Array.isArray(data.streams)) { + const serverLabel = data.provider?.label || 'Vidly'; + + for (const stream of data.streams) { + const streamUrl = stream?.url; + if (!streamUrl || typeof streamUrl !== 'string' || !streamUrl.startsWith('http') || isInvalidStreamUrl(streamUrl)) { + continue; + } + + if (streamUrl.includes('.m3u8')) { + const variants = await resolveHlsVariants(streamUrl, PLAYBACK_HEADERS); + if (variants && variants.length) { + for (const v of variants) { + if (seen.has(v.url) || isInvalidStreamUrl(v.url)) continue; + seen.add(v.url); + + allUrls.push({ + url: v.url, + server: `Vidly - ${serverLabel} (${v.quality})`, + quality: v.quality, + type: 'hls', + headers: PLAYBACK_HEADERS, + }); + } + continue; + } + } + + if (seen.has(streamUrl) || isInvalidStreamUrl(streamUrl)) continue; + seen.add(streamUrl); + + let quality = stream.quality ? String(stream.quality).trim() : 'Auto'; + if (/^\d+$/i.test(quality)) quality = `${quality}p`; + + const type = stream.type || (streamUrl.includes('.m3u8') + ? 'hls' + : streamUrl.includes('.mpd') + ? 'dash' + : 'mp4'); + + const labelSuffix = stream.language ? ` (${stream.language})` : ''; + + allUrls.push({ + url: streamUrl, + server: `Vidly - ${serverLabel}${labelSuffix}`, + quality, + type, + headers: PLAYBACK_HEADERS, + }); + } + } + } + + if (!allUrls.length) { + const fallbackMaster = extractMediaFromThumbs(html); + if (fallbackMaster) { + const variants = await resolveHlsVariants(fallbackMaster, PLAYBACK_HEADERS); + if (variants && variants.length) { + for (const v of variants) { + if (seen.has(v.url) || isInvalidStreamUrl(v.url)) continue; + seen.add(v.url); + + allUrls.push({ + url: v.url, + server: `Vidly - Direct (${v.quality})`, + quality: v.quality, + type: 'hls', + headers: PLAYBACK_HEADERS, + }); + } + } else if (!seen.has(fallbackMaster) && !isInvalidStreamUrl(fallbackMaster)) { + seen.add(fallbackMaster); + allUrls.push({ + url: fallbackMaster, + server: 'Vidly - Direct', + quality: 'Auto', + type: 'hls', + headers: PLAYBACK_HEADERS, + }); + } + } + } + + if (!allUrls.length) return null; + + allUrls.sort((a, b) => parseQualityNumber(b.quality) - parseQualityNumber(a.quality)); + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; + } catch { + return null; + } +} \ No newline at end of file diff --git a/src/sources/vidy.js b/src/sources/vidy.js index 2e51c12..965bc6a 100644 --- a/src/sources/vidy.js +++ b/src/sources/vidy.js @@ -117,6 +117,8 @@ export async function getStream(args) { const seed = seedData?.seed; if (!seed) return null; + const allUrls = []; + for (const server of SERVERS) { try { const srcUrl = new URL(`${API_URL}/${server}/sources`); @@ -164,28 +166,26 @@ export async function getStream(args) { } if (!streamUrl) continue; - const allUrls = [{ + allUrls.push({ url: streamUrl, headers, - server: 'Vidy', + server: `Vidy - ${server.toUpperCase()}`, quality, type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', - }]; - - return { - url: streamUrl, - headers, - server: 'Vidy', - quality, - type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', - allUrls, - }; + }); + break; } catch { continue; } } - return null; + if (!allUrls.length) return null; + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; } catch { return null; } diff --git a/test/index.js b/test/index.js index 601a6db..a808864 100644 --- a/test/index.js +++ b/test/index.js @@ -13,7 +13,7 @@ const CONFIG = { 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 - STREAM_SOURCE: "", // Source key to test + STREAM_SOURCE: "vidvault", // Source key to test TEST_SUBTITLE: false, TEST_DOWNLOAD: false,