Add nova/mega, enable pengu with new config, provide SDK directly

This commit is contained in:
Gavin Fox
2026-08-20 23:33:53 -07:00
parent f419c26158
commit 71abff6604
14 changed files with 164 additions and 22 deletions

View File

@@ -300,7 +300,29 @@ export const SOURCES = [
retries: 1,
multiUrl: true,
skipProxy: true,
disabled: true, // Temporarily disabled because they now require google auth
disabled: false, // Enabled, but only returns streams when a manifest is provided
},
{
key: 'nova',
label: 'Nova',
sourceFile: 'nova',
proxyParam: 'nv',
timeout: 25000,
jitter: 500,
retries: 1,
multiUrl: true,
},
{
key: 'megasource',
label: 'MegaSource',
sourceFile: 'megasource',
proxyParam: 'mg',
timeout: 25000,
jitter: 500,
retries: 1,
multiUrl: true,
},
{

View File

@@ -14,7 +14,7 @@ async function withRetry(fn, attempts = 2, delay = 300) {
return null;
}
export async function probeSource(tmdbApiKey, cfg, mod) {
export async function probeSource(sdk, cfg, mod) {
const audio = /dub$/.test(cfg.key) ? 'dub' : 'sub';
const streamArgs = {
id: HEALTH_PROBE_ID,
@@ -23,7 +23,7 @@ export async function probeSource(tmdbApiKey, cfg, mod) {
clientIP: null,
absoluteBase: null,
audio,
tmdbApiKey,
sdk,
config: cfg,
};

View File

@@ -13,8 +13,11 @@ const SOURCE_MODULES = Object.fromEntries(
);
export default class VylaSDK {
constructor({ tmdbApiKey } = {}) {
this.tmdbApiKey = tmdbApiKey;
tmdbApiKey = null;
penguManifest = null;
constructor({ tmdbApiKey, penguManifest } = {}) {
this.tmdbApiKey = tmdbApiKey || null;
this.penguManifest = penguManifest || null;
}
getSources(excludeDisabled = false) {
if (excludeDisabled) {
@@ -43,14 +46,14 @@ export default class VylaSDK {
if (!cfg) throw new Error(`Source with key "${key}" not found`);
const mod = SOURCE_MODULES[key];
if (!mod) throw new Error(`Source module for key "${key}" not found`);
return await probeSource(this.tmdbApiKey, cfg, mod);
return await probeSource(this, cfg, mod);
}
async getStream(key, id, s = null, e = null, clientIP = null) {
const cfg = SOURCES.find(cfg => cfg.key === key);
if (!cfg) throw new Error(`Source with key "${key}" not found`);
const mod = SOURCE_MODULES[key];
if (!mod) throw new Error(`Source module for key "${key}" not found`);
const streamArgs = await createStreamArgs(cfg, this.tmdbApiKey, id, s, e, clientIP);
const streamArgs = await createStreamArgs(cfg, this, id, s, e, clientIP);
return await mod.getStream(streamArgs);
}
async getSubtitles(id, s = null, e = null) {

View File

@@ -69,7 +69,9 @@ async function extractHls(embedUrl) {
return null;
}
export async function getStream({ id, s, e, audio, tmdbApiKey }) {
export async function getStream({ id, s, e, audio, sdk }) {
const tmdbApiKey = sdk?.tmdbApiKey || null;
if (!tmdbApiKey) return null;
try {
const info = await getTmdbInfo(tmdbApiKey, id, s ? 'tv' : 'movie', s);
if (!info || !info.isAnime) return null;

View File

@@ -4,7 +4,9 @@ const BASE_URL = 'https://www.fsonic.net';
const FSHARE_BASE = 'https://fsharetv.co';
const HEADERS = { 'User-Agent': USER_AGENT, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9' };
export async function getStream({ id, s, tmdbApiKey }) {
export async function getStream({ id, s, sdk }) {
const tmdbApiKey = sdk?.tmdbApiKey || null;
if (!tmdbApiKey) return null;
if (s != null) return null;
try {
const info = await getTmdbInfo(tmdbApiKey, id, 'movie');

View File

@@ -4,7 +4,9 @@ const ORIGIN = 'https://www3.fsonline.app';
const AJAX_URL = `${ORIGIN}/wp-admin/admin-ajax.php`;
const HEADERS = { 'User-Agent': USER_AGENT, 'Origin': ORIGIN, 'Referer': `${ORIGIN}/` };
export async function getStream({ id, s, e, tmdbApiKey }) {
export async function getStream({ id, s, e, sdk }) {
const tmdbApiKey = sdk?.tmdbApiKey || null;
if (!tmdbApiKey) return null;
try {
const isTv = s != null && e != null;
const info = await getTmdbInfo(tmdbApiKey, id, isTv ? 'tv' : 'movie');

View File

@@ -47,7 +47,9 @@ function getEpisodeIdFast(html, s, e) {
return am ? am[1] : null;
}
export async function getStream({ id, s, e, tmdbApiKey, clientIP }) {
export async function getStream({ id, s, e, sdk, clientIP }) {
const tmdbApiKey = sdk?.tmdbApiKey || null;
if (!tmdbApiKey) return null;
try {
const tmdbKey = tmdbApiKey;
if (!tmdbKey) return null;

47
src/sources/megasource.js Normal file
View File

@@ -0,0 +1,47 @@
import { fetchJson } from '../utils/helpers.js';
const CONFIGURATION = 'W3siaWQiOiJkZWZhdWx0IiwibmFtZSI6Ik1lZ2FTb3VyY2UgZGVmYXVsdCIsInVybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS96b3JldS9tZWdhc291cmNlX3NjcmFwZXJzL3Jhdy9yZWZzL2hlYWRzL21haW4vZGVmYXVsdF9zY3JhcGVyLnB5IiwiZGVzY3JpcHRpb24iOiJEZWZhdWx0IHNjcmFwZXIgaG9zdGVkIG9uIEdpdEh1Yi4ifV0';
const BASE_URL = `https://megasource.wasmer.app/${CONFIGURATION}`;
async function getImdbId(tmdbApiKey, tmdbId, s, e) {
const key = tmdbApiKey;
if (!key) return null;
try {
const url = s != null && e != null
? `https://api.themoviedb.org/3/tv/${tmdbId}/external_ids?api_key=${key}`
: `https://api.themoviedb.org/3/movie/${tmdbId}?api_key=${key}`;
const res = await fetchJson(url);
return res?.imdb_id || null;
} catch { return null; }
}
export async function getStream({ id, s, e, sdk }) {
const tmdbApiKey = sdk?.tmdbApiKey || null;
if (!tmdbApiKey) return null;
try {
const imdbId = await getImdbId(tmdbApiKey, id, s, e);
if (!imdbId) return null;
const path = s != null && e != null
? `/stream/series/${imdbId}:${s}:${e}.json`
: `/stream/movie/${imdbId}.json`;
const res = await fetchJson(`${BASE_URL}${path}`);
if (!res?.streams?.length) return null;
const allUrls = res.streams.map(stream => {
if (!stream.url) return null;
const headers = stream.behaviorHints?.proxyHeaders?.request || {};
return {
url: stream.url,
server: `MegaSource - ${stream.title ? ` ${stream.title}` : ''}`,
headers: Object.keys(headers).length > 0 ? headers : undefined,
skipProxy: Object.keys(headers).length > 0 && stream.behaviorHints?.notWebReady === false
};
}).filter(Boolean);
return allUrls.length ? { allUrls } : null;
} catch {
return null;
}
}

46
src/sources/nova.js Normal file
View File

@@ -0,0 +1,46 @@
import { fetchJson } from '../utils/helpers.js';
const BASE_URL = `https://nova-streamz.vercel.app`;
async function getImdbId(tmdbApiKey, tmdbId, s, e) {
const key = tmdbApiKey;
if (!key) return null;
try {
const url = s != null && e != null
? `https://api.themoviedb.org/3/tv/${tmdbId}/external_ids?api_key=${key}`
: `https://api.themoviedb.org/3/movie/${tmdbId}?api_key=${key}`;
const res = await fetchJson(url);
return res?.imdb_id || null;
} catch { return null; }
}
export async function getStream({ id, s, e, sdk }) {
const tmdbApiKey = sdk?.tmdbApiKey || null;
if (!tmdbApiKey) return null;
try {
const imdbId = await getImdbId(tmdbApiKey, id, s, e);
if (!imdbId) return null;
const path = s != null && e != null
? `/stream/series/${imdbId}:${s}:${e}.json`
: `/stream/movie/${imdbId}.json`;
const res = await fetchJson(`${BASE_URL}${path}`);
if (!res?.streams?.length) return null;
const allUrls = res.streams.map(stream => {
if (!stream.url) return null;
const headers = stream.behaviorHints?.proxyHeaders?.request || {};
return {
url: stream.url,
server: `Nova - ${stream.name.replace('Nova ', '') || 'Server'}${stream.title ? ` ${stream.title.split(' | ')[0] || ''}` : ''}`,
headers: Object.keys(headers).length > 0 ? headers : undefined,
skipProxy: Object.keys(headers).length > 0 && stream.behaviorHints?.notWebReady === false
};
}).filter(Boolean);
return allUrls.length ? { allUrls } : null;
} catch {
return null;
}
}

View File

@@ -1,7 +1,7 @@
import { fetchJson } from '../utils/helpers.js';
const PENGU_CONFIG = '%7B%22source_4khdhub%22%3A%22on%22%2C%22source_moviebox%22%3A%22on%22%2C%22source_moviesdrives%22%3A%22on%22%2C%22source_vaplayer%22%3A%22on%22%2C%22source_hdghartv%22%3A%22on%22%2C%22res_2160%22%3A%22on%22%2C%22res_1080%22%3A%22on%22%2C%22res_720%22%3A%22on%22%2C%22disable_direct%22%3A%22on%22%7D';
const BASE_URL = `https://pengu.uk/${PENGU_CONFIG}`;
// const PENGU_CONFIG = '%7B%22source_4khdhub%22%3A%22on%22%2C%22source_moviebox%22%3A%22on%22%2C%22source_moviesdrives%22%3A%22on%22%2C%22source_vaplayer%22%3A%22on%22%2C%22source_hdghartv%22%3A%22on%22%2C%22res_2160%22%3A%22on%22%2C%22res_1080%22%3A%22on%22%2C%22res_720%22%3A%22on%22%2C%22disable_direct%22%3A%22on%22%7D';
// const BASE_URL = `https://pengu.uk/${PENGU_CONFIG}`;
async function getImdbId(tmdbApiKey, tmdbId, s, e) {
const key = tmdbApiKey;
@@ -15,7 +15,12 @@ async function getImdbId(tmdbApiKey, tmdbId, s, e) {
} catch { return null; }
}
export async function getStream({ id, s, e, tmdbApiKey }) {
export async function getStream({ id, s, e, sdk }) {
const tmdbApiKey = sdk?.tmdbApiKey || null;
const manifest = sdk?.penguManifest || null;
if (!tmdbApiKey || !manifest || (manifest && !manifest.endsWith('manifest.json'))) return null;
const BASE_URL = manifest ? manifest.split('/manifest.json')[0] : null;
if (!BASE_URL) return null;
try {
const imdbId = await getImdbId(tmdbApiKey, id, s, e);
if (!imdbId) return null;

View File

@@ -12,7 +12,9 @@ const SERVERS = [
{ id: 'superflix', name: 'Raze' }
];
export async function getStream({ id, s, e, server, tmdbApiKey }) {
export async function getStream({ id, s, e, server, sdk }) {
const tmdbApiKey = sdk?.tmdbApiKey || null;
if (!tmdbApiKey) return null;
try {
const isTv = s != null && e != null;
const info = await getTmdbInfo(tmdbApiKey, id, isTv ? 'tv' : 'movie');

View File

@@ -39,7 +39,9 @@ async function tmdbToAnilist(tmdbId, season) {
const SERVER_KEYS = ['hollymoviehd', 'allmovies', 'catflix', 'purstream', 'lamda', 'vidlink', 'klikxxi'];
export async function getStream({ id, s, e, tmdbApiKey, audio }) {
export async function getStream({ id, s, e, sdk, audio }) {
const tmdbApiKey = sdk?.tmdbApiKey || null;
if (!tmdbApiKey) return null;
if (s) {
const k = tmdbApiKey;
const showData = k ? await fetchJson(`https://api.themoviedb.org/3/tv/${id}?api_key=${k}`, { signal: AbortSignal.timeout(5000) }).catch(() => null) : null;

View File

@@ -67,7 +67,7 @@ function cacheGet(cache, key) {
function cacheSet(cache, key, val, ttl) { cache.set(key, { val, ts: Date.now(), ttl }); }
export async function createStreamArgs(source, tmdbApiKey, id, s, e, clientIP = null) {
export async function createStreamArgs(source, sdk, id, s, e, clientIP = null) {
const audio = /dub$/.test(source.key) ? 'dub' : 'sub';
return {
id: id,
@@ -76,7 +76,7 @@ export async function createStreamArgs(source, tmdbApiKey, id, s, e, clientIP =
clientIP: clientIP,
absoluteBase: '/',
audio,
tmdbApiKey,
sdk,
config: source,
}
}

View File

@@ -5,7 +5,6 @@ import fs from "fs";
import path from "path";
import VylaSDK from "../src/sdk.js";
import dotenv from "dotenv";
dotenv.config();
const CONFIG = {
DEBUG: true, // Set to true to print outgoing requests, headers, formats, and responses
@@ -14,13 +13,20 @@ 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: "cinejoy", // Source key to test
STREAM_SOURCE: "pengu", // Source key to test
TEST_SUBTITLE: false, // Toggle subtitle testing
TEST_DOWNLOAD: false, // Toggle download testing
TEST_MOVIE: true, // Toggle movie testing
TEST_TV: true // Toggle TV testing
TEST_TV: true, // Toggle TV testing
EXTRA_SDK_PARAMS: { // Additional parameters for the SDK constructor
// penguManifest: ""
}
};
dotenv.config({
debug: CONFIG.DEBUG
});
// debug
if (CONFIG.DEBUG) {
@@ -105,7 +111,8 @@ if (CONFIG.DEBUG) {
}
const sdk = new VylaSDK({
tmdbApiKey: process.env.TMDB_API_KEY
tmdbApiKey: process.env.TMDB_API_KEY,
...CONFIG.EXTRA_SDK_PARAMS
});
console.log("Starting...");