diff --git a/src/config.js b/src/config.js index 02d544b..8c0c923 100644 --- a/src/config.js +++ b/src/config.js @@ -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, }, { diff --git a/src/health.js b/src/health.js index d39ae34..3de34d4 100644 --- a/src/health.js +++ b/src/health.js @@ -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, }; diff --git a/src/sdk.js b/src/sdk.js index 015dc5b..0cfe748 100644 --- a/src/sdk.js +++ b/src/sdk.js @@ -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) { diff --git a/src/sources/anineko.js b/src/sources/anineko.js index a6757f5..94b4c23 100644 --- a/src/sources/anineko.js +++ b/src/sources/anineko.js @@ -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; diff --git a/src/sources/fsonic.js b/src/sources/fsonic.js index 93ca054..e906426 100644 --- a/src/sources/fsonic.js +++ b/src/sources/fsonic.js @@ -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'); diff --git a/src/sources/fsonline.js b/src/sources/fsonline.js index d8e91d7..8a72506 100644 --- a/src/sources/fsonline.js +++ b/src/sources/fsonline.js @@ -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'); diff --git a/src/sources/lookmovie.js b/src/sources/lookmovie.js index d674740..997f157 100644 --- a/src/sources/lookmovie.js +++ b/src/sources/lookmovie.js @@ -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; diff --git a/src/sources/megasource.js b/src/sources/megasource.js new file mode 100644 index 0000000..004160e --- /dev/null +++ b/src/sources/megasource.js @@ -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; + } +} \ No newline at end of file diff --git a/src/sources/nova.js b/src/sources/nova.js new file mode 100644 index 0000000..eb4dcca --- /dev/null +++ b/src/sources/nova.js @@ -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; + } +} \ No newline at end of file diff --git a/src/sources/pengu.js b/src/sources/pengu.js index 0180d7a..a736681 100644 --- a/src/sources/pengu.js +++ b/src/sources/pengu.js @@ -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; diff --git a/src/sources/videasy.js b/src/sources/videasy.js index 05968f6..35a62cc 100644 --- a/src/sources/videasy.js +++ b/src/sources/videasy.js @@ -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'); diff --git a/src/sources/vidnest.js b/src/sources/vidnest.js index 88355cc..caa8009 100644 --- a/src/sources/vidnest.js +++ b/src/sources/vidnest.js @@ -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; diff --git a/src/utils/helpers.js b/src/utils/helpers.js index b0700d2..2b64311 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -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, } } diff --git a/test/index.js b/test/index.js index 07eb63f..d4ac670 100644 --- a/test/index.js +++ b/test/index.js @@ -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...");