added vidly, and streamrip, removed rivo, added anizone, added anv, added

This commit is contained in:
2026-11-07 10:41:52 -08:00
parent 62e7fafe51
commit 3e768d6fc9
16 changed files with 972 additions and 198 deletions

4
package-lock.json generated
View File

@@ -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"

View File

@@ -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',

View File

@@ -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 {

202
src/sources/anizone.js Normal file
View File

@@ -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;
}
}

167
src/sources/anv.js Normal file
View File

@@ -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(/<link>(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;
}
}

View File

@@ -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'];
}

View File

@@ -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;
}
}

82
src/sources/moviebox.js Normal file
View File

@@ -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;
}
}

View File

@@ -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'];
}

View File

@@ -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,
};
}

109
src/sources/streamrip.js Normal file
View File

@@ -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 = /<a[^>]+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;
}
}

View File

@@ -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);

View File

@@ -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'))] : [];
}

255
src/sources/vidly.js Normal file
View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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,