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,