mirror of
https://gitlab.com/vyla-entertainment/sdk.git
synced 2026-11-09 19:57:23 -05:00
added vidly, and streamrip, removed rivo, added anizone, added anv, added
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
202
src/sources/anizone.js
Normal 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
167
src/sources/anv.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
@@ -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
82
src/sources/moviebox.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
@@ -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
109
src/sources/streamrip.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
255
src/sources/vidly.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user