mirror of
https://gitlab.com/vyla-entertainment/sdk.git
synced 2026-11-13 21:57:27 -05:00
added 3eyeddraven, cinemaarmy, movenerds, thugtv, vidy
This commit is contained in:
103
src/config.js
103
src/config.js
@@ -1,11 +1,23 @@
|
||||
// Currently active sources reported on 6:18:22 PM PST 9/13/2026
|
||||
|
||||
// None Anime Tested: 936075
|
||||
// Anime Tested: 37854
|
||||
|
||||
// Fshare/Fsonic Tested: 155 ( they don't have Michael )
|
||||
|
||||
export const SOURCES = [
|
||||
{
|
||||
key: '3eyedraven',
|
||||
label: '3eyedraven',
|
||||
sourceFile: '3eyedraven',
|
||||
proxyParam: '3er',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: false,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /3eyedraven\.watch/i,
|
||||
headers: {
|
||||
Referer: 'https://3eyedraven.watch/',
|
||||
Origin: 'https://3eyedraven.watch',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: '123anime',
|
||||
label: '123Anime',
|
||||
@@ -106,6 +118,25 @@ export const SOURCES = [
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'cinemaarmy',
|
||||
label: 'CinemaArmy',
|
||||
sourceFile: 'cinemaarmy',
|
||||
proxyParam: 'ca',
|
||||
timeout: 30000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /cinema\.army/i,
|
||||
headers: {
|
||||
Referer: 'https://cinema.army/',
|
||||
Origin: 'https://cinema.army',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'dulo',
|
||||
label: 'Dulo',
|
||||
@@ -191,6 +222,25 @@ export const SOURCES = [
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
key: 'movenerds',
|
||||
label: 'MoverNerds',
|
||||
sourceFile: 'movenerds',
|
||||
proxyParam: 'mnr',
|
||||
timeout: 30000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /movenerds\.space/i,
|
||||
headers: {
|
||||
Referer: 'https://movenerds.space/',
|
||||
Origin: 'https://movenerds.space',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'movienight',
|
||||
label: 'MovieNight',
|
||||
@@ -293,6 +343,26 @@ export const SOURCES = [
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'thugtv',
|
||||
label: 'ThugTV',
|
||||
sourceFile: 'thugtv',
|
||||
proxyParam: 'tv',
|
||||
timeout: 30000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /thugtv\.online|thuktv\.onrender\.com/i,
|
||||
headers: {
|
||||
Referer: 'https://thugtv.online/',
|
||||
Origin: 'https://thugtv.online',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'vidapi',
|
||||
label: 'VidAPI',
|
||||
@@ -426,6 +496,25 @@ export const SOURCES = [
|
||||
multiUrl: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'vidy',
|
||||
label: 'Vidy',
|
||||
sourceFile: 'vidy',
|
||||
proxyParam: 'vy',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /wecollege\.net|vidy\.st/i,
|
||||
headers: {
|
||||
Referer: 'https://www.vidy.st/',
|
||||
Origin: 'https://www.vidy.st',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'vidzee',
|
||||
label: 'VidZee',
|
||||
|
||||
136
src/sources/3eyedraven.js
Normal file
136
src/sources/3eyedraven.js
Normal file
@@ -0,0 +1,136 @@
|
||||
import { getUA } from "../utils/helpers.js";
|
||||
|
||||
const BASE_URL = "https://3eyedraven.watch";
|
||||
const API_BASE = "https://3eyedraven.watch/api";
|
||||
|
||||
function getHeaders() {
|
||||
return {
|
||||
"User-Agent": getUA(),
|
||||
Accept: "application/json",
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveApiKey(args) {
|
||||
return (
|
||||
args?.tmdbApiKey ||
|
||||
args?.sdk?.tmdbApiKey ||
|
||||
process.env.TMDB_API_KEY ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
async function getTitle(id, isTv, apiKey) {
|
||||
if (!apiKey) return null;
|
||||
const type = isTv ? "tv" : "movie";
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`,
|
||||
{ signal: AbortSignal.timeout(5000) }
|
||||
);
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
return isTv ? data.name : data.title;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function slugify(text) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.normalize("NFKD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
function absolute(url) {
|
||||
if (!url) return "";
|
||||
if (/^https?:\/\//.test(url)) return url;
|
||||
return `${BASE_URL}${url.startsWith("/") ? "" : "/"}${url}`;
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s = null, e = null } = args;
|
||||
const isTv = s != null && e != null;
|
||||
|
||||
const apiKey = resolveApiKey(args);
|
||||
const title = await getTitle(id, isTv, apiKey);
|
||||
|
||||
if (!title) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(
|
||||
`[3eyedraven] Could not resolve title for ${isTv ? "tv" : "movie"} ${id} ` +
|
||||
`(tmdbApiKey ${apiKey ? "present" : "MISSING"})`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const identifier = `${slugify(title)}-${id}`;
|
||||
|
||||
const url = isTv
|
||||
? `${API_BASE}/stream/${identifier}?season=${s}&episode=${e}`
|
||||
: `${API_BASE}/stream/${identifier}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: getHeaders(),
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(`[3eyedraven] HTTP ${res.status} for ${url}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const stream = data?.stream;
|
||||
if (!stream?.url) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(`[3eyedraven] No stream.url in response for ${url}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return {
|
||||
url: streamUrl,
|
||||
headers: {
|
||||
"User-Agent": getUA(),
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
},
|
||||
server: "3eyedraven",
|
||||
quality: "1080p",
|
||||
type: stream.format === "hls" ? "hls" : "mp4",
|
||||
subtitles,
|
||||
skipProxy: true,
|
||||
};
|
||||
} catch (err) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(`[3eyedraven] fetch error for ${url}:`, err.message);
|
||||
if (err.cause) console.error(" cause:", err.cause.code, err.cause.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
181
src/sources/cinemaarmy.js
Normal file
181
src/sources/cinemaarmy.js
Normal file
@@ -0,0 +1,181 @@
|
||||
import crypto from "crypto";
|
||||
import { Session, ClientIdentifier, initTLS, destroyTLS } from "node-tls-client";
|
||||
|
||||
const BASE_URL = "https://cinema.army";
|
||||
const API_BASE = "https://cinema.army/api";
|
||||
|
||||
const XOR_KEY = "f16a6b92fcd6eed3e9476297f893f2505c32f79bc6efef9b6c95496aa5914548";
|
||||
const HMAC_KEY = "a2cfc9eaa2a3690b5d4f6b5f958dbdeae52a097283d375bd253ee7c7062524f5";
|
||||
|
||||
const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
||||
|
||||
const SERVERS = [
|
||||
{ key: "cookiebakers", method: "POST", label: "CookieBakers" },
|
||||
{ key: "tcloud", method: "GET", label: "TCloud" },
|
||||
{ key: "ipcloud", method: "GET", label: "IPcloud" },
|
||||
{ key: "dcloud", method: "GET", label: "Dcloud" },
|
||||
];
|
||||
|
||||
let tlsInitialized = false;
|
||||
let session = null;
|
||||
|
||||
async function ensureSession() {
|
||||
if (!tlsInitialized) {
|
||||
await initTLS();
|
||||
tlsInitialized = true;
|
||||
}
|
||||
if (!session) {
|
||||
session = new Session({
|
||||
clientIdentifier: ClientIdentifier.chrome_131,
|
||||
timeout: 15000,
|
||||
insecureSkipVerify: false,
|
||||
});
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
function getHeaders(token, id, isTv, s, e) {
|
||||
const referer = isTv
|
||||
? `${BASE_URL}/watch?id=${id}&type=tv&season=${s}&episode=${e}`
|
||||
: `${BASE_URL}/watch?id=${id}&type=movie`;
|
||||
|
||||
return {
|
||||
"User-Agent": UA,
|
||||
"Accept": "application/json",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"Cache-Control": "no-cache",
|
||||
"Pragma": "no-cache",
|
||||
"Referer": referer,
|
||||
"Origin": BASE_URL,
|
||||
"Sec-Ch-Ua": '"Chromium";v="131", "Not_A Brand";v="24"',
|
||||
"Sec-Ch-Ua-Mobile": "?0",
|
||||
"Sec-Ch-Ua-Platform": '"Windows"',
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "same-origin",
|
||||
"Priority": "u=1, i",
|
||||
"X-Stream-Token": token,
|
||||
};
|
||||
}
|
||||
|
||||
function generateToken(id) {
|
||||
try {
|
||||
const minute = Math.floor(Date.now() / 1000 / 60);
|
||||
return crypto
|
||||
.createHmac("sha256", HMAC_KEY)
|
||||
.update(`${id}:${minute}`)
|
||||
.digest("hex");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function decryptStreamUrl(encoded) {
|
||||
if (!encoded) return "";
|
||||
if (encoded.startsWith("http://") || encoded.startsWith("https://")) return encoded;
|
||||
try {
|
||||
const decoded = Buffer.from(encoded, "base64").toString("binary");
|
||||
let out = "";
|
||||
for (let i = 0; i < decoded.length; i++) {
|
||||
out += String.fromCharCode(
|
||||
decoded.charCodeAt(i) ^ XOR_KEY.charCodeAt(i % XOR_KEY.length)
|
||||
);
|
||||
}
|
||||
return out;
|
||||
} catch {
|
||||
return encoded;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s = null, e = null } = args;
|
||||
const isTv = s != null && e != null;
|
||||
|
||||
const token = generateToken(id);
|
||||
const headers = getHeaders(token, id, isTv, s, e);
|
||||
|
||||
const sess = await ensureSession();
|
||||
|
||||
const requests = SERVERS.map(server => {
|
||||
const path = isTv
|
||||
? `/${server.key}/tv/${id}/${s}/${e}`
|
||||
: `/${server.key}/movie/${id}`;
|
||||
return { url: `${API_BASE}${path}`, server };
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
requests.map(async ({ url, server }) => {
|
||||
try {
|
||||
const res = server.method === "POST"
|
||||
? await sess.post(url, { headers })
|
||||
: await sess.get(url, { headers });
|
||||
|
||||
if (res.status !== 200) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(`[CinemaArmy ${server.key}] HTTP ${res.status}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const encrypted = data?.stream?.url;
|
||||
if (!encrypted) return null;
|
||||
|
||||
const streamUrl = decryptStreamUrl(encrypted);
|
||||
if (!streamUrl || !/^https?:\/\//.test(streamUrl)) return null;
|
||||
|
||||
return {
|
||||
url: streamUrl,
|
||||
headers: {
|
||||
"User-Agent": UA,
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
},
|
||||
server: `CinemaArmy (${server.label})`,
|
||||
quality: "1080p",
|
||||
type: streamUrl.includes(".m3u8") ? "hls" : "mp4",
|
||||
};
|
||||
} catch (err) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(`[CinemaArmy ${server.key}] ${err.message}`);
|
||||
if (err.cause) {
|
||||
console.error(` cause.code: ${err.cause.code || "n/a"}`);
|
||||
console.error(` cause.message: ${err.cause.message || err.cause}`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const allUrls = results
|
||||
.filter(r => r.status === "fulfilled" && r.value)
|
||||
.map(r => r.value);
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return { allUrls };
|
||||
}
|
||||
|
||||
export async function cleanup() {
|
||||
if (session) {
|
||||
try {
|
||||
await session.close();
|
||||
} catch { }
|
||||
session = null;
|
||||
}
|
||||
if (tlsInitialized) {
|
||||
try {
|
||||
await destroyTLS();
|
||||
} catch { }
|
||||
tlsInitialized = false;
|
||||
}
|
||||
}
|
||||
125
src/sources/movenerds.js
Normal file
125
src/sources/movenerds.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import { getUA } from "../utils/helpers.js";
|
||||
|
||||
const API_BASE = "https://api.movenerds.space/api";
|
||||
|
||||
function getHeaders() {
|
||||
return {
|
||||
"User-Agent": getUA(),
|
||||
Accept: "application/json",
|
||||
Referer: "https://movenerds.space/",
|
||||
Origin: "https://movenerds.space",
|
||||
};
|
||||
}
|
||||
|
||||
function resolveApiKey(args) {
|
||||
return (
|
||||
args?.tmdbApiKey ||
|
||||
args?.sdk?.tmdbApiKey ||
|
||||
process.env.TMDB_API_KEY ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
async function getMeta(id, isTv, apiKey) {
|
||||
if (!apiKey) return null;
|
||||
const type = isTv ? "tv" : "movie";
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`,
|
||||
{ signal: AbortSignal.timeout(5000) }
|
||||
);
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
const title = isTv ? data.name : data.title;
|
||||
const date = isTv ? data.first_air_date : data.release_date;
|
||||
const year = date ? parseInt(date.slice(0, 4), 10) : null;
|
||||
return { title, year };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function mapSource(source) {
|
||||
if (!source || !source.url) return null;
|
||||
if (source.type !== "hls" && source.type !== "mp4") return null;
|
||||
if (!/^https?:\/\//.test(source.url)) return null;
|
||||
|
||||
return {
|
||||
url: source.url,
|
||||
headers: {
|
||||
"User-Agent": source.headers?.["User-Agent"] || getUA(),
|
||||
Referer: source.headers?.Referer || "https://movenerds.space/",
|
||||
Origin: source.headers?.Origin || "https://movenerds.space",
|
||||
},
|
||||
server: source.name || source.providerName || "MoverNerds",
|
||||
quality: source.quality || "1080p",
|
||||
type: source.type,
|
||||
skipProxy: true,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s = null, e = null } = args;
|
||||
const isTv = s != null && e != null;
|
||||
|
||||
const apiKey = resolveApiKey(args);
|
||||
const meta = await getMeta(id, isTv, apiKey);
|
||||
|
||||
if (!meta?.title) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(
|
||||
`[movenerds] Could not resolve title for ${isTv ? "tv" : "movie"} ${id} ` +
|
||||
`(tmdbApiKey ${apiKey ? "present" : "MISSING"})`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
tmdbId: String(id),
|
||||
type: isTv ? "tv" : "movie",
|
||||
title: meta.title,
|
||||
});
|
||||
if (meta.year) params.set("year", String(meta.year));
|
||||
if (isTv) {
|
||||
params.set("season", String(s));
|
||||
params.set("episode", String(e));
|
||||
params.set("language", "en");
|
||||
}
|
||||
|
||||
const url = `${API_BASE}/stream?${params.toString()}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: getHeaders(),
|
||||
signal: AbortSignal.timeout(20000),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(`[movenerds] HTTP ${res.status} for ${url}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const primary = Array.isArray(data?.primarySources) ? data.primarySources : [];
|
||||
if (!primary.length) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(`[movenerds] No primarySources in response for ${url}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const allUrls = primary.map(mapSource).filter(Boolean);
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return { allUrls };
|
||||
} catch (err) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(`[movenerds] fetch error for ${url}:`, err.message);
|
||||
if (err.cause) console.error(" cause:", err.cause.code, err.cause.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
// im being salty
|
||||
|
||||
import { USER_AGENT, fetchJson } from '../utils/helpers.js';
|
||||
|
||||
const C_ARRAY = [
|
||||
|
||||
62
src/sources/thugtv.js
Normal file
62
src/sources/thugtv.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import { getUA } from "../utils/helpers.js";
|
||||
|
||||
const API_BASE = "https://thuktv.onrender.com/api";
|
||||
|
||||
function getHeaders() {
|
||||
return {
|
||||
"User-Agent": getUA(),
|
||||
Accept: "application/json",
|
||||
Referer: "https://thugtv.online/",
|
||||
Origin: "https://thugtv.online",
|
||||
};
|
||||
}
|
||||
|
||||
function mapSource(source) {
|
||||
if (!source || !source.url) return null;
|
||||
if (source.type !== "hls" && source.type !== "mp4") return null;
|
||||
if (!/^https?:\/\//.test(source.url)) return null;
|
||||
|
||||
return {
|
||||
url: source.url,
|
||||
headers: {
|
||||
"User-Agent": getUA(),
|
||||
Referer: "https://thugtv.online/",
|
||||
Origin: "https://thugtv.online",
|
||||
},
|
||||
server: source.name || source.label || "ThugTV",
|
||||
quality: source.quality || "1080p",
|
||||
type: source.type,
|
||||
skipProxy: true,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s = null, e = null } = args;
|
||||
const isTv = s != null && e != null;
|
||||
|
||||
const url = isTv
|
||||
? `${API_BASE}/stream/tv/${id}/${s}/${e}`
|
||||
: `${API_BASE}/stream/movie/${id}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: getHeaders(),
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
|
||||
const data = await res.json();
|
||||
const sources = Array.isArray(data?.sources) ? data.sources : [];
|
||||
if (!sources.length) return null;
|
||||
|
||||
const allUrls = sources
|
||||
.map(mapSource)
|
||||
.filter(Boolean);
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return { allUrls };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
187
src/sources/vidy.js
Normal file
187
src/sources/vidy.js
Normal file
@@ -0,0 +1,187 @@
|
||||
import { getUA } from "../utils/helpers.js";
|
||||
|
||||
const API_URL = "https://api.wecollege.net";
|
||||
const SERVERS = ["miami", "atlanta", "seattle", "denver", "dallas", "tampa"];
|
||||
const MAGIC = [109, 118, 109, 49];
|
||||
|
||||
function imul(a, b) {
|
||||
return Math.imul(a | 0, b | 0) >>> 0;
|
||||
}
|
||||
|
||||
function pFn(e) {
|
||||
e = e >>> 0;
|
||||
e = (e ^ (e >>> 16)) >>> 0;
|
||||
e = imul(e, 2246822507);
|
||||
e = (e ^ (e >>> 13)) >>> 0;
|
||||
e = imul(e, 3266489909);
|
||||
e = (e ^ (e >>> 16)) >>> 0;
|
||||
return e;
|
||||
}
|
||||
|
||||
function gFn(e, t) {
|
||||
e = e >>> 0;
|
||||
t = t & 31;
|
||||
if (t === 0) return e;
|
||||
return ((e << t) | (e >>> (32 - t))) >>> 0;
|
||||
}
|
||||
|
||||
function decryptVidy(ciphertext, seedStr, mediaId) {
|
||||
let padded = ciphertext.replace(/-/g, "+").replace(/_/g, "/");
|
||||
while (padded.length % 4) padded += "=";
|
||||
|
||||
const raw = new Uint8Array(Buffer.from(padded, "base64"));
|
||||
const length = raw.length;
|
||||
|
||||
let tFnv = 2166136261;
|
||||
for (let i = 0; i < seedStr.length; i++) {
|
||||
tFnv = imul((tFnv ^ seedStr.charCodeAt(i)) >>> 0, 16777619);
|
||||
}
|
||||
const fnvRes = pFn(tFnv);
|
||||
|
||||
const innerArg = (((mediaId >>> 0) ^ 2654435769) >>> 0);
|
||||
const inner = pFn(innerArg);
|
||||
let n = pFn(((fnvRes ^ inner) >>> 0));
|
||||
|
||||
const S = {};
|
||||
for (let e = 0; e < 8; e++) {
|
||||
const t = n % 61;
|
||||
n = gFn((n + 2654435769) >>> 0, 7 + (7 & e));
|
||||
S[t] = ((n ^ pFn(n)) >>> 0);
|
||||
n = pFn((n + t) >>> 0);
|
||||
}
|
||||
|
||||
let acc = pFn(((2779096485 ^ n) >>> 0));
|
||||
const keystream = new Uint8Array(length);
|
||||
let sCnt = 0;
|
||||
let idx = 0;
|
||||
|
||||
while (idx < length) {
|
||||
const sVal = acc;
|
||||
const o = sVal % 61;
|
||||
const hasO = Object.prototype.hasOwnProperty.call(S, o);
|
||||
const iVal = hasO ? -1 : 0;
|
||||
const d = hasO ? S[o] : 0;
|
||||
const nTerm = ((d ^ imul(2654435769, sCnt + 1)) >>> 0);
|
||||
sCnt += 1;
|
||||
const aTerm = sVal;
|
||||
let lVal = (((aTerm ^ nTerm) | (aTerm & nTerm & iVal)) >>> 0);
|
||||
const term1 = gFn((lVal + sVal) >>> 0, 31 & o);
|
||||
const term2 = gFn(sVal, (31 & imul(o, 7)) >>> 0);
|
||||
lVal = ((term1 ^ term2) >>> 0);
|
||||
const newS = pFn((lVal + 2654435769) >>> 0);
|
||||
S[o] = newS;
|
||||
acc = newS;
|
||||
const tVal = newS;
|
||||
|
||||
keystream[idx++] = tVal & 0xFF;
|
||||
if (idx < length) keystream[idx++] = (tVal >>> 8) & 0xFF;
|
||||
if (idx < length) keystream[idx++] = (tVal >>> 16) & 0xFF;
|
||||
if (idx < length) keystream[idx++] = (tVal >>> 24) & 0xFF;
|
||||
}
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
raw[i] ^= keystream[i];
|
||||
}
|
||||
|
||||
for (let i = 0; i < MAGIC.length; i++) {
|
||||
if (raw[i] !== MAGIC[i]) {
|
||||
throw new Error("Vidy decrypt failed: magic header mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(raw.slice(MAGIC.length));
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s = null, e = null } = args;
|
||||
const isTv = s != null && e != null;
|
||||
const mediaId = Number(id);
|
||||
|
||||
const headers = {
|
||||
"User-Agent": getUA(),
|
||||
Referer: "https://www.vidy.st/",
|
||||
Origin: "https://www.vidy.st",
|
||||
};
|
||||
|
||||
try {
|
||||
const seedUrl = new URL(`${API_URL}/seed`);
|
||||
seedUrl.searchParams.set("mediaId", String(id));
|
||||
|
||||
const seedRes = await fetch(seedUrl.toString(), {
|
||||
headers,
|
||||
signal: AbortSignal.timeout(6000),
|
||||
});
|
||||
if (!seedRes.ok) return null;
|
||||
|
||||
const seedData = await seedRes.json();
|
||||
const seed = seedData?.seed;
|
||||
if (!seed) return null;
|
||||
|
||||
for (const server of SERVERS) {
|
||||
try {
|
||||
const srcUrl = new URL(`${API_URL}/${server}/sources`);
|
||||
srcUrl.searchParams.set("mediaType", isTv ? "tv" : "movie");
|
||||
srcUrl.searchParams.set("tmdbId", String(id));
|
||||
srcUrl.searchParams.set("enc", "2");
|
||||
srcUrl.searchParams.set("seed", seed);
|
||||
if (isTv) {
|
||||
srcUrl.searchParams.set("seasonId", String(s));
|
||||
srcUrl.searchParams.set("episodeId", String(e));
|
||||
}
|
||||
|
||||
const res = await fetch(srcUrl.toString(), {
|
||||
headers,
|
||||
signal: AbortSignal.timeout(6000),
|
||||
});
|
||||
if (!res.ok) continue;
|
||||
|
||||
const text = await res.text();
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(decryptVidy(text, seed, mediaId));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sources = data?.sources || [];
|
||||
const playlist = data?.playlist || "";
|
||||
if (!sources.length && !playlist) continue;
|
||||
|
||||
let streamUrl = playlist;
|
||||
let quality = "1080p";
|
||||
|
||||
for (const src of sources) {
|
||||
if (src?.quality === "1080p" && src?.url) {
|
||||
streamUrl = src.url;
|
||||
quality = "1080p";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!streamUrl && sources.length) {
|
||||
streamUrl = sources[0]?.url || "";
|
||||
quality = sources[0]?.quality || "";
|
||||
}
|
||||
if (!streamUrl) continue;
|
||||
|
||||
return {
|
||||
url: streamUrl,
|
||||
headers: {
|
||||
"User-Agent": getUA(),
|
||||
Referer: "https://www.vidy.st/",
|
||||
Origin: "https://www.vidy.st",
|
||||
},
|
||||
server: "Vidy",
|
||||
quality,
|
||||
type: streamUrl.includes(".m3u8") ? "hls" : "mp4",
|
||||
};
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user