mirror of
https://github.com/Inside4ndroid/TMDB-Embed-API.git
synced 2026-10-22 09:35:11 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
671bbb6159 | ||
|
|
47d2886fbe | ||
|
|
d0419321a4 |
@@ -2,6 +2,45 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [1.1.1] - 2026-05-29
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- **MoviesMod provider**: Permanently removed — `moviesmod.farm` returns HTTP 403 with Cloudflare managed challenge (`cf-mitigated: challenge`). No server-side workaround exists without a browser runtime.
|
||||||
|
- **VidZee provider**: Permanently removed — hardcoded AES-256-GCM salt is no longer valid against VidZee's current API key blob, causing all decryption to fail.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **LordFlix provider**: Updated upstream domain (`network.hasta-la-vista.site` → `snowhouse.lordflix.club`) and refreshed server list (10 servers).
|
||||||
|
- **Docker startup crash**: Added missing `COPY proxy ./proxy` to both build and runtime stages in `Dockerfile` — `apiServer.js` unconditionally requires `./proxy/proxyServer` at startup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.1.0] - 2026-05-16
|
||||||
|
|
||||||
|
## Dependency Security
|
||||||
|
- **npm vulnerabilities fixed**: Upgraded `cheerio` (1.0.0 → 1.2.0) and `axios-cookiejar-support` (6.0.2 → 7.0.0) to eliminate 14 vulnerabilities (including `undici <=6.23.0` high/critical CVEs). Result: 0 vulnerabilities.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Videasy provider**: 10-server parallel scraper via enc-dec.app decrypt relay.
|
||||||
|
- **Vidlink provider**: Single-stream HLS source via enc-dec.app encode relay.
|
||||||
|
- **LordFlix provider**: 9-server scraper via enc-dec.app (replaces defunct Vidsync).
|
||||||
|
- **NoTorrent provider**: Stremio addon API bridge (`addon-osvh.onrender.com`).
|
||||||
|
- **DahmerMovies provider**: Open-directory direct file link scraper with proxy rewrite.
|
||||||
|
- **GitHub Sponsors button**: Official iframe embed in admin sidebar and README badge.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- **Vidsync provider**: All 6 servers returned HTTP 401 — API locked. Replaced by LordFlix.
|
||||||
|
- **MP4Hydra provider**: Dead upstream, removed entirely.
|
||||||
|
- **UHDMovies provider**: Dead upstream, removed entirely.
|
||||||
|
- **vidsrcextractor.js**: Unused legacy file removed.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Provider enable/disable not working at runtime**: Removed the startup-time enabled/disabled filter. All providers are now always loaded into the registry array. `listProviders()` and `getProvider()` now call `isProviderEnabled(name)` which reads the live `config` object on every call. Changes take effect immediately without a server restart.
|
||||||
|
- **Showbox/FebBox provider**: Restored with correct `responseType:'text'`, AJAX headers, and double `ui=` cookie bug fix.
|
||||||
|
- **MoviesMod provider**: Updated class-based selectors and added support for `links.modpro.blog`, `cloud.unblockedgames.world`, `tech.examdegree.site`, and `driveleech.net` domains.
|
||||||
|
- **VixSrc provider**: Corrected to proper 2-step API flow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.0.9] - 2025-11-19
|
## [1.0.9] - 2025-11-19
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ RUN npm install --omit=dev
|
|||||||
# Copy only required source (avoid sending screenshots, node_modules already installed)
|
# Copy only required source (avoid sending screenshots, node_modules already installed)
|
||||||
COPY apiServer.js ./
|
COPY apiServer.js ./
|
||||||
COPY providers ./providers
|
COPY providers ./providers
|
||||||
|
COPY proxy ./proxy
|
||||||
COPY public ./public
|
COPY public ./public
|
||||||
COPY utils ./utils
|
COPY utils ./utils
|
||||||
COPY README.md ./
|
COPY README.md ./
|
||||||
@@ -33,6 +34,7 @@ COPY --from=build /app/node_modules ./node_modules
|
|||||||
COPY --from=build /app/apiServer.js ./
|
COPY --from=build /app/apiServer.js ./
|
||||||
COPY --from=build /app/public ./public
|
COPY --from=build /app/public ./public
|
||||||
COPY --from=build /app/providers ./providers
|
COPY --from=build /app/providers ./providers
|
||||||
|
COPY --from=build /app/proxy ./proxy
|
||||||
COPY --from=build /app/utils ./utils
|
COPY --from=build /app/utils ./utils
|
||||||
COPY --from=build /app/package.json ./
|
COPY --from=build /app/package.json ./
|
||||||
COPY --from=build /app/README.md ./
|
COPY --from=build /app/README.md ./
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<img src="https://img.shields.io/badge/Node.js-18%2B-brightgreen?style=flat" />
|
<img src="https://img.shields.io/badge/Node.js-18%2B-brightgreen?style=flat" />
|
||||||
<img src="https://img.shields.io/badge/Status-Active-success?style=flat" />
|
<img src="https://img.shields.io/badge/Status-Active-success?style=flat" />
|
||||||
<img src="https://img.shields.io/badge/License-MIT-blue?style=flat" />
|
<img src="https://img.shields.io/badge/License-MIT-blue?style=flat" />
|
||||||
<img src="https://img.shields.io/badge/Version-1.0.9-informational?style=flat" />
|
<img src="https://img.shields.io/badge/Version-1.1.1-informational?style=flat" />
|
||||||
<img src="https://img.shields.io/docker/pulls/inside4ndroid/tmdb-embed-api?label=Docker%20Pulls&style=flat" />
|
<img src="https://img.shields.io/docker/pulls/inside4ndroid/tmdb-embed-api?label=Docker%20Pulls&style=flat" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
- **Multi‑TMDB Key Rotation** – Supply multiple API keys; one is chosen randomly per request.
|
- **Multi‑TMDB Key Rotation** – Supply multiple API keys; one is chosen randomly per request.
|
||||||
- **Provider Aggregation** – Pluggable providers (4khdhub, MoviesMod, MP4Hydra, VidZee, Vixsrc, UHDMovies) with per‑provider enable toggles + default selection.
|
- **Provider Aggregation** – Pluggable providers (Showbox/FebBox, 4KHDHub, VixSrc, Videasy, Vidlink, LordFlix, NoTorrent, DahmerMovies) with per‑provider enable toggles + default selection.
|
||||||
- **🔥 Plugin System** – Drop new provider files in `providers/` and add its exported function to the registry map (`providers/registry.js` → `providerFunctionMap`).
|
- **🔥 Plugin System** – Drop new provider files in `providers/` and add its exported function to the registry map (`providers/registry.js` → `providerFunctionMap`).
|
||||||
- **Dynamic Filtering** – Minimum quality presets, custom JSON quality map, codec exclusion rules (presets + JSON).
|
- **Dynamic Filtering** – Minimum quality presets, custom JSON quality map, codec exclusion rules (presets + JSON).
|
||||||
- **Runtime Overrides UI** – Fully interactive web admin at `/` (login protected) writing to `utils/user-config.json`.
|
- **Runtime Overrides UI** – Fully interactive web admin at `/` (login protected) writing to `utils/user-config.json`.
|
||||||
@@ -199,7 +199,7 @@ Saving in the UI writes only changed keys. Setting a field to empty removes the
|
|||||||
**Override File:** `utils/user-config.json`
|
**Override File:** `utils/user-config.json`
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"defaultProviders": ["4khdhub","moviesmod"],
|
"defaultProviders": ["4khdhub"],
|
||||||
"tmdbApiKeys": ["KEY_A","KEY_B"],
|
"tmdbApiKeys": ["KEY_A","KEY_B"],
|
||||||
"enable4khdhubProvider": true
|
"enable4khdhubProvider": true
|
||||||
}
|
}
|
||||||
@@ -225,12 +225,14 @@ Session is revalidated on visibility and back/forward navigation to prevent stal
|
|||||||
The API supports a plugin system. Drop a new provider file in the `providers/` folder and register its exported function in `providers/registry.js` under `providerFunctionMap`.
|
The API supports a plugin system. Drop a new provider file in the `providers/` folder and register its exported function in `providers/registry.js` under `providerFunctionMap`.
|
||||||
|
|
||||||
### Current Built-in Providers
|
### Current Built-in Providers
|
||||||
|
- `showbox` - Showbox/FebBox streams (requires FebBox JWT cookie)
|
||||||
- `4khdhub` - 4KHDHub streams
|
- `4khdhub` - 4KHDHub streams
|
||||||
- `moviesmod` - MoviesMod streams
|
- `vixsrc` - VixSrc streams
|
||||||
- `mp4hydra` - MP4Hydra streams
|
- `videasy` - Videasy streams (10 servers via enc-dec.app)
|
||||||
- `vidzee` - VidZee streams
|
- `vidlink` - Vidlink streams (via enc-dec.app)
|
||||||
- `vixsrc` - Vixsrc streams
|
- `lordflix` - LordFlix streams (9 servers via enc-dec.app)
|
||||||
- `uhdmovies` - UHD Movies streams
|
- `notorrent` - NoTorrent streams (Stremio addon API)
|
||||||
|
- `dahmermovies` - DahmerMovies streams (direct file links)
|
||||||
|
|
||||||
|
|
||||||
### Adding a New Provider
|
### Adding a New Provider
|
||||||
@@ -240,12 +242,14 @@ The API supports a plugin system. Drop a new provider file in the `providers/` f
|
|||||||
```js
|
```js
|
||||||
// providers/registry.js
|
// providers/registry.js
|
||||||
const providerFunctionMap = {
|
const providerFunctionMap = {
|
||||||
|
'Showbox.js': 'getStreamsFromTmdbId',
|
||||||
'4khdhub.js': 'get4KHDHubStreams',
|
'4khdhub.js': 'get4KHDHubStreams',
|
||||||
'moviesmod.js': 'getMoviesModStreams',
|
|
||||||
'MP4Hydra.js': 'getMP4HydraStreams',
|
|
||||||
'VidZee.js': 'getVidZeeStreams',
|
|
||||||
'vixsrc.js': 'getVixsrcStreams',
|
'vixsrc.js': 'getVixsrcStreams',
|
||||||
'uhdmovies.js': 'getUHDMoviesStreams',
|
'videasy.js': 'getVideasyStreams',
|
||||||
|
'vidlink.js': 'getVidlinkStreams',
|
||||||
|
'lordflix.js': 'getLordflixStreams',
|
||||||
|
'notorrent.js': 'getNotorrentStreams',
|
||||||
|
'dahmermovies.js': 'getDahmermoviesStreams',
|
||||||
'yourprovider.js': 'getYourproviderStreams'
|
'yourprovider.js': 'getYourproviderStreams'
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
@@ -378,7 +382,11 @@ PRs welcome. Keep changes focused and avoid unrelated formatting churn. For new
|
|||||||
## ❤️ Sponsorship
|
## ❤️ Sponsorship
|
||||||
If this project helps you, consider sponsoring to support continued development & maintenance:
|
If this project helps you, consider sponsoring to support continued development & maintenance:
|
||||||
|
|
||||||
➡️ **GitHub Sponsors:** https://github.com/sponsors/Inside4ndroid
|
<p align="center">
|
||||||
|
<a href="https://github.com/sponsors/Inside4ndroid">
|
||||||
|
<img src="https://img.shields.io/badge/Sponsor-GitHub%20Sponsors-ea4aaa?style=for-the-badge&logo=github-sponsors&logoColor=white" alt="Sponsor on GitHub" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
Every contribution accelerates feature delivery & sustainability.
|
Every contribution accelerates feature delivery & sustainability.
|
||||||
|
|
||||||
|
|||||||
Generated
+316
-1077
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tmdb-embed-api",
|
"name": "tmdb-embed-api",
|
||||||
"version": "1.0.9",
|
"version": "1.1.1",
|
||||||
"description": "Standalone streaming embed source aggregation API (TMDB ID in, streams out)",
|
"description": "Standalone streaming embed source aggregation API (TMDB ID in, streams out)",
|
||||||
"main": "apiServer.js",
|
"main": "apiServer.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -11,9 +11,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"aromanize": "^0.1.5",
|
"aromanize": "^0.1.5",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"axios-cookiejar-support": "^6.0.2",
|
"axios-cookiejar-support": "^7.0.0",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.0",
|
||||||
"cheerio": "^1.0.0",
|
"cheerio": "^1.2.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
|
|||||||
@@ -1,488 +0,0 @@
|
|||||||
const axios = require('axios');
|
|
||||||
const FormData = require('form-data');
|
|
||||||
|
|
||||||
// Helper function to generate slug from title
|
|
||||||
function generateSlug(title) {
|
|
||||||
return title.toLowerCase()
|
|
||||||
.replace(/[^\w\s-]/g, '') // Remove special characters
|
|
||||||
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
||||||
.replace(/-+/g, '-'); // Replace multiple hyphens with a single hyphen
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process episode data from MP4Hydra response
|
|
||||||
function processEpisode(episode, baseServer, serverName, serverNumber) {
|
|
||||||
const videoUrl = `${baseServer}${episode.src}`;
|
|
||||||
const subtitles = episode.subs ? episode.subs.map(sub => ({
|
|
||||||
label: sub.label,
|
|
||||||
url: `${baseServer}${sub.src}`
|
|
||||||
})) : [];
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: episode.show_title || episode.title,
|
|
||||||
episode: episode.title,
|
|
||||||
type: episode.type,
|
|
||||||
quality: episode.quality || episode.label,
|
|
||||||
videoUrl: videoUrl,
|
|
||||||
server: serverName,
|
|
||||||
serverNumber: serverNumber,
|
|
||||||
subtitles: subtitles
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main function to get streams from MP4Hydra
|
|
||||||
async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNum = null) {
|
|
||||||
try {
|
|
||||||
console.log(`[MP4Hydra] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}, Season: ${seasonNum}, Episode: ${episodeNum}`);
|
|
||||||
|
|
||||||
// Get TMDB details
|
|
||||||
const details = await getTMDBDetails(tmdbId, mediaType);
|
|
||||||
if (!details) {
|
|
||||||
console.log(`[MP4Hydra] Could not fetch details for TMDB ID: ${tmdbId}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[MP4Hydra] Found title: ${details.title} (${details.year})`);
|
|
||||||
|
|
||||||
// Generate slug in the format "movie-name-year"
|
|
||||||
let slug = details.slug;
|
|
||||||
if (mediaType === 'movie' && details.year) {
|
|
||||||
slug = `${details.slug}-${details.year}`;
|
|
||||||
}
|
|
||||||
console.log(`[MP4Hydra] Using slug: ${slug}`);
|
|
||||||
|
|
||||||
// Create form data for multipart/form-data request
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('v', '8');
|
|
||||||
formData.append('z', JSON.stringify([{
|
|
||||||
s: slug,
|
|
||||||
t: mediaType,
|
|
||||||
se: seasonNum,
|
|
||||||
ep: episodeNum
|
|
||||||
}]));
|
|
||||||
|
|
||||||
// Make request to MP4Hydra API
|
|
||||||
const response = await axios({
|
|
||||||
method: 'post',
|
|
||||||
url: 'https://mp4hydra.org/info2?v=8',
|
|
||||||
data: formData,
|
|
||||||
headers: {
|
|
||||||
...formData.getHeaders(),
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36',
|
|
||||||
'Accept': '*/*',
|
|
||||||
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
|
|
||||||
'Origin': 'https://mp4hydra.org',
|
|
||||||
'Referer': `https://mp4hydra.org/${mediaType}/${slug}`,
|
|
||||||
'Sec-Fetch-Dest': 'empty',
|
|
||||||
'Sec-Fetch-Mode': 'cors',
|
|
||||||
'Sec-Fetch-Site': 'same-origin'
|
|
||||||
},
|
|
||||||
timeout: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process response
|
|
||||||
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
|
|
||||||
const playlist = response.data.playlist;
|
|
||||||
const servers = response.data.servers;
|
|
||||||
|
|
||||||
console.log(`[MP4Hydra] Found ${playlist.length} videos`);
|
|
||||||
console.log(`[MP4Hydra] Available servers: ${Object.keys(servers).join(', ')}`);
|
|
||||||
|
|
||||||
// For TV shows, find the specific episode
|
|
||||||
if (mediaType === 'tv' && seasonNum && episodeNum) {
|
|
||||||
const paddedSeason = seasonNum.toString().padStart(2, '0');
|
|
||||||
const paddedEpisode = episodeNum.toString().padStart(2, '0');
|
|
||||||
const seasonEpisode = `S${paddedSeason}E${paddedEpisode}`;
|
|
||||||
|
|
||||||
const targetEpisode = playlist.find(item =>
|
|
||||||
item.title && item.title.toUpperCase() === seasonEpisode.toUpperCase()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!targetEpisode) {
|
|
||||||
console.log(`[MP4Hydra] Could not find ${seasonEpisode}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[MP4Hydra] Found episode: ${targetEpisode.show_title || targetEpisode.title}`);
|
|
||||||
|
|
||||||
// Process streams from main servers
|
|
||||||
const streams = [];
|
|
||||||
const serverConfig = [
|
|
||||||
{ name: 'Beta', number: '#1' },
|
|
||||||
{ name: 'Beta#3', number: '#2' }
|
|
||||||
];
|
|
||||||
|
|
||||||
serverConfig.forEach(server => {
|
|
||||||
const serverName = server.name;
|
|
||||||
const serverNumber = server.number;
|
|
||||||
|
|
||||||
if (servers[serverName]) {
|
|
||||||
const baseServer = servers[serverName];
|
|
||||||
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
|
||||||
|
|
||||||
const processedEpisode = processEpisode(targetEpisode, baseServer, serverName, serverNumber);
|
|
||||||
|
|
||||||
// Convert to standard stream format
|
|
||||||
streams.push({
|
|
||||||
title: `${details.title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
|
|
||||||
url: processedEpisode.videoUrl,
|
|
||||||
quality: processedEpisode.quality,
|
|
||||||
provider: "mp4hydra",
|
|
||||||
headers: {
|
|
||||||
'Referer': 'https://mp4hydra.org/'
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
|
||||||
if (processedEpisode.subtitles && processedEpisode.subtitles.length > 0) {
|
|
||||||
streams[streams.length - 1].subtitles = processedEpisode.subtitles.map(sub => ({
|
|
||||||
url: sub.url,
|
|
||||||
lang: sub.label
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return streams;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For movies, process all videos
|
|
||||||
const streams = [];
|
|
||||||
const serverConfig = [
|
|
||||||
{ name: 'Beta', number: '#1' },
|
|
||||||
{ name: 'Beta#3', number: '#2' }
|
|
||||||
];
|
|
||||||
|
|
||||||
serverConfig.forEach(server => {
|
|
||||||
const serverName = server.name;
|
|
||||||
const serverNumber = server.number;
|
|
||||||
|
|
||||||
if (servers[serverName]) {
|
|
||||||
const baseServer = servers[serverName];
|
|
||||||
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
|
||||||
|
|
||||||
playlist.forEach(item => {
|
|
||||||
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
|
|
||||||
|
|
||||||
// Convert to standard stream format
|
|
||||||
streams.push({
|
|
||||||
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
|
||||||
url: processedItem.videoUrl,
|
|
||||||
quality: processedItem.quality,
|
|
||||||
provider: "mp4hydra",
|
|
||||||
headers: {
|
|
||||||
'Referer': 'https://mp4hydra.org/'
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
|
||||||
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
|
|
||||||
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
|
|
||||||
url: sub.url,
|
|
||||||
lang: sub.label
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return streams;
|
|
||||||
} else {
|
|
||||||
// Try with original title if the first attempt failed
|
|
||||||
if (details.title !== details.original_title) {
|
|
||||||
return await tryAlternativeTitle(details, mediaType, seasonNum, episodeNum);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try without year for movies
|
|
||||||
if (mediaType === 'movie' && details.year) {
|
|
||||||
return await tryWithoutYear(details, mediaType, seasonNum, episodeNum);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[MP4Hydra] No streaming links found');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[MP4Hydra] Error fetching streams:`, error.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to try with original title
|
|
||||||
async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) {
|
|
||||||
try {
|
|
||||||
console.log(`[MP4Hydra] Retrying with original title: ${details.original_title}`);
|
|
||||||
const originalSlug = generateSlug(details.original_title);
|
|
||||||
let originalFullSlug = originalSlug;
|
|
||||||
|
|
||||||
if (mediaType === 'movie' && details.year) {
|
|
||||||
originalFullSlug = `${originalSlug}-${details.year}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new form data with original title
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('v', '8');
|
|
||||||
formData.append('z', JSON.stringify([{
|
|
||||||
s: originalFullSlug,
|
|
||||||
t: mediaType,
|
|
||||||
se: seasonNum,
|
|
||||||
ep: episodeNum
|
|
||||||
}]));
|
|
||||||
|
|
||||||
const response = await axios({
|
|
||||||
method: 'post',
|
|
||||||
url: 'https://mp4hydra.org/info2?v=8',
|
|
||||||
data: formData,
|
|
||||||
headers: {
|
|
||||||
...formData.getHeaders(),
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36',
|
|
||||||
'Accept': '*/*',
|
|
||||||
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
|
|
||||||
'Origin': 'https://mp4hydra.org',
|
|
||||||
'Referer': `https://mp4hydra.org/${mediaType}/${originalFullSlug}`,
|
|
||||||
'Sec-Fetch-Dest': 'empty',
|
|
||||||
'Sec-Fetch-Mode': 'cors',
|
|
||||||
'Sec-Fetch-Site': 'same-origin'
|
|
||||||
},
|
|
||||||
timeout: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process response
|
|
||||||
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
|
|
||||||
const playlist = response.data.playlist;
|
|
||||||
const servers = response.data.servers;
|
|
||||||
|
|
||||||
console.log(`[MP4Hydra] Found ${playlist.length} videos with original title`);
|
|
||||||
|
|
||||||
// For TV shows, find the specific episode
|
|
||||||
if (mediaType === 'tv' && seasonNum && episodeNum) {
|
|
||||||
const paddedSeason = seasonNum.toString().padStart(2, '0');
|
|
||||||
const paddedEpisode = episodeNum.toString().padStart(2, '0');
|
|
||||||
const seasonEpisode = `S${paddedSeason}E${paddedEpisode}`;
|
|
||||||
|
|
||||||
const targetEpisode = playlist.find(item =>
|
|
||||||
item.title && item.title.toUpperCase() === seasonEpisode.toUpperCase()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!targetEpisode) {
|
|
||||||
console.log(`[MP4Hydra] Could not find ${seasonEpisode} with original title`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process streams from main servers
|
|
||||||
const streams = [];
|
|
||||||
const serverConfig = [
|
|
||||||
{ name: 'Beta', number: '#1' },
|
|
||||||
{ name: 'Beta#3', number: '#2' }
|
|
||||||
];
|
|
||||||
|
|
||||||
serverConfig.forEach(server => {
|
|
||||||
const serverName = server.name;
|
|
||||||
const serverNumber = server.number;
|
|
||||||
|
|
||||||
if (servers[serverName]) {
|
|
||||||
const baseServer = servers[serverName];
|
|
||||||
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
|
||||||
|
|
||||||
const processedEpisode = processEpisode(targetEpisode, baseServer, serverName, serverNumber);
|
|
||||||
|
|
||||||
// Convert to standard stream format
|
|
||||||
streams.push({
|
|
||||||
title: `${details.original_title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
|
|
||||||
url: processedEpisode.videoUrl,
|
|
||||||
quality: processedEpisode.quality,
|
|
||||||
provider: "mp4hydra",
|
|
||||||
headers: {
|
|
||||||
'Referer': 'https://mp4hydra.org/'
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
|
||||||
if (processedEpisode.subtitles && processedEpisode.subtitles.length > 0) {
|
|
||||||
streams[streams.length - 1].subtitles = processedEpisode.subtitles.map(sub => ({
|
|
||||||
url: sub.url,
|
|
||||||
lang: sub.label
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return streams;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For movies, process all videos
|
|
||||||
const streams = [];
|
|
||||||
const serverConfig = [
|
|
||||||
{ name: 'Beta', number: '#1' },
|
|
||||||
{ name: 'Beta#3', number: '#2' }
|
|
||||||
];
|
|
||||||
|
|
||||||
serverConfig.forEach(server => {
|
|
||||||
const serverName = server.name;
|
|
||||||
const serverNumber = server.number;
|
|
||||||
|
|
||||||
if (servers[serverName]) {
|
|
||||||
const baseServer = servers[serverName];
|
|
||||||
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
|
||||||
|
|
||||||
playlist.forEach(item => {
|
|
||||||
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
|
|
||||||
|
|
||||||
// Convert to standard stream format
|
|
||||||
streams.push({
|
|
||||||
name: `${details.original_title}`,
|
|
||||||
title: `${details.original_title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
|
||||||
url: processedItem.videoUrl,
|
|
||||||
quality: processedItem.quality
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
|
||||||
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
|
|
||||||
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
|
|
||||||
url: sub.url,
|
|
||||||
lang: sub.label
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return streams;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[MP4Hydra] Error with alternative title:`, error.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to try without year
|
|
||||||
async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) {
|
|
||||||
try {
|
|
||||||
console.log(`[MP4Hydra] Retrying with title only (without year): ${details.title}`);
|
|
||||||
const titleOnlySlug = details.slug;
|
|
||||||
|
|
||||||
// Create new form data with title only
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('v', '8');
|
|
||||||
formData.append('z', JSON.stringify([{
|
|
||||||
s: titleOnlySlug,
|
|
||||||
t: mediaType,
|
|
||||||
se: seasonNum,
|
|
||||||
ep: episodeNum
|
|
||||||
}]));
|
|
||||||
|
|
||||||
const response = await axios({
|
|
||||||
method: 'post',
|
|
||||||
url: 'https://mp4hydra.org/info2?v=8',
|
|
||||||
data: formData,
|
|
||||||
headers: {
|
|
||||||
...formData.getHeaders(),
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36',
|
|
||||||
'Accept': '*/*',
|
|
||||||
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
|
|
||||||
'Origin': 'https://mp4hydra.org',
|
|
||||||
'Referer': `https://mp4hydra.org/${mediaType}/${titleOnlySlug}`,
|
|
||||||
'Sec-Fetch-Dest': 'empty',
|
|
||||||
'Sec-Fetch-Mode': 'cors',
|
|
||||||
'Sec-Fetch-Site': 'same-origin'
|
|
||||||
},
|
|
||||||
timeout: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process response
|
|
||||||
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
|
|
||||||
const playlist = response.data.playlist;
|
|
||||||
const servers = response.data.servers;
|
|
||||||
|
|
||||||
console.log(`[MP4Hydra] Found ${playlist.length} videos with title only`);
|
|
||||||
|
|
||||||
// For movies, process all videos
|
|
||||||
const streams = [];
|
|
||||||
const serverConfig = [
|
|
||||||
{ name: 'Beta', number: '#1' },
|
|
||||||
{ name: 'Beta#3', number: '#2' }
|
|
||||||
];
|
|
||||||
|
|
||||||
serverConfig.forEach(server => {
|
|
||||||
const serverName = server.name;
|
|
||||||
const serverNumber = server.number;
|
|
||||||
|
|
||||||
if (servers[serverName]) {
|
|
||||||
const baseServer = servers[serverName];
|
|
||||||
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
|
||||||
|
|
||||||
playlist.forEach(item => {
|
|
||||||
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
|
|
||||||
|
|
||||||
// Convert to standard stream format
|
|
||||||
streams.push({
|
|
||||||
name: `${details.title}`,
|
|
||||||
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
|
||||||
url: processedItem.videoUrl,
|
|
||||||
quality: processedItem.quality,
|
|
||||||
provider: "mp4hydra",
|
|
||||||
headers: {
|
|
||||||
'Referer': 'https://mp4hydra.org/'
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
|
||||||
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
|
|
||||||
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
|
|
||||||
url: sub.url,
|
|
||||||
lang: sub.label
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return streams;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[MP4Hydra] Error with title-only:`, error.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to get TMDB details
|
|
||||||
async function getTMDBDetails(tmdbId, mediaType) {
|
|
||||||
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
|
||||||
const TMDB_API_KEY = getTmdbApiKey();
|
|
||||||
if (!TMDB_API_KEY) {
|
|
||||||
console.warn('[MP4Hydra] TMDB API key missing; skipping TMDB metadata fetch');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(`[MP4Hydra] Fetching ${mediaType} details for TMDB ID: ${tmdbId}`);
|
|
||||||
const response = await axios.get(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${TMDB_API_KEY}`);
|
|
||||||
|
|
||||||
if (mediaType === 'movie') {
|
|
||||||
return {
|
|
||||||
title: response.data.title,
|
|
||||||
original_title: response.data.original_title,
|
|
||||||
year: response.data.release_date ? response.data.release_date.split('-')[0] : null,
|
|
||||||
slug: generateSlug(response.data.title)
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
title: response.data.name,
|
|
||||||
original_title: response.data.original_name,
|
|
||||||
year: response.data.first_air_date ? response.data.first_air_date.split('-')[0] : null,
|
|
||||||
slug: generateSlug(response.data.name)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[MP4Hydra] Error fetching details from TMDB:`, error.message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { getMP4HydraStreams };
|
|
||||||
+10
-4
@@ -62,8 +62,9 @@ const getCookieForRequest = async (regionPreference = null, userCookie = null) =
|
|||||||
try {
|
try {
|
||||||
const array = Array.isArray(global.currentRequestConfig.cookies) ? global.currentRequestConfig.cookies.filter(Boolean) : [];
|
const array = Array.isArray(global.currentRequestConfig.cookies) ? global.currentRequestConfig.cookies.filter(Boolean) : [];
|
||||||
if (array.length > 0) {
|
if (array.length > 0) {
|
||||||
// Just pick first for now (quota logic removed here if previously elsewhere)
|
// Strip 'ui=' prefix if present — callers always prepend 'ui=' themselves
|
||||||
baseCookieToUse = array[0];
|
const raw = array[0];
|
||||||
|
baseCookieToUse = raw.startsWith('ui=') ? raw.slice(3) : raw;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`[CookieManager] Cookie array selection error: ${e.message}`);
|
console.warn(`[CookieManager] Cookie array selection error: ${e.message}`);
|
||||||
@@ -1211,7 +1212,12 @@ const fetchSourcesForSingleFid = async (fidToProcess, shareKey, regionPreference
|
|||||||
|
|
||||||
const baseHeaders = {
|
const baseHeaders = {
|
||||||
'Cookie': `ui=${cookieForRequest}`,
|
'Cookie': `ui=${cookieForRequest}`,
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'Origin': 'https://www.febbox.com',
|
||||||
|
'Referer': `https://www.febbox.com/share/${shareKey}`,
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||||
|
'Accept': '*/*',
|
||||||
};
|
};
|
||||||
|
|
||||||
const finalPostUrl = FEBBOX_PLAYER_URL;
|
const finalPostUrl = FEBBOX_PLAYER_URL;
|
||||||
@@ -1221,7 +1227,7 @@ const fetchSourcesForSingleFid = async (fidToProcess, shareKey, regionPreference
|
|||||||
console.log(` Fetching fresh player data for video FID: ${fidToProcess} (Share: ${shareKey}) directly to ${FEBBOX_PLAYER_URL}`);
|
console.log(` Fetching fresh player data for video FID: ${fidToProcess} (Share: ${shareKey}) directly to ${FEBBOX_PLAYER_URL}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(finalPostUrl, postDataForAxios, axiosConfig);
|
const response = await axios.post(finalPostUrl, postDataForAxios, { ...axiosConfig, responseType: 'text' });
|
||||||
const playerContent = response.data;
|
const playerContent = response.data;
|
||||||
|
|
||||||
// Mark the region as available if the request succeeded
|
// Mark the region as available if the request succeeded
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
const axios = require('axios');
|
|
||||||
|
|
||||||
// Confirmed AES-CBC decoder for VidZee encrypted link format.
|
|
||||||
// Format (after initial atob in site script): ivBase64:cipherBase64
|
|
||||||
// ivBase64 -> CryptoJS Base64 parsed to WordArray (IV)
|
|
||||||
// Key: UTF-8 bytes of "qrincywincyspider" padded with nulls to 32 bytes (AES-256-CBC)
|
|
||||||
// Cipher: base64 ciphertext, PKCS7 padding, mode CBC.
|
|
||||||
let cryptoJs; // lazy load to avoid cost if not needed
|
|
||||||
function decodeVidZeeToken(token, debug) {
|
|
||||||
try {
|
|
||||||
if (typeof token !== 'string') return null;
|
|
||||||
if (/^https?:\/\//i.test(token)) return null; // already a URL
|
|
||||||
// Expect pattern base64:base64 (iv:cipher)
|
|
||||||
const raw = Buffer.from(token, 'base64').toString('utf8');
|
|
||||||
if (!raw.includes(':')) return null;
|
|
||||||
const [ivB64, cipherB64] = raw.split(':');
|
|
||||||
if (!ivB64 || !cipherB64) return null;
|
|
||||||
if (!cryptoJs) cryptoJs = require('crypto-js');
|
|
||||||
const iv = cryptoJs.enc.Base64.parse(ivB64.trim());
|
|
||||||
const keyStr = 'qrincywincyspider';
|
|
||||||
// Pad key with nulls to 32 bytes
|
|
||||||
const keyUtf8 = cryptoJs.enc.Utf8.parse(keyStr.padEnd(32, '\0'));
|
|
||||||
const decrypted = cryptoJs.AES.decrypt(cipherB64.trim(), keyUtf8, {
|
|
||||||
iv,
|
|
||||||
mode: cryptoJs.mode.CBC,
|
|
||||||
padding: cryptoJs.pad.Pkcs7
|
|
||||||
}).toString(cryptoJs.enc.Utf8);
|
|
||||||
if (!decrypted) return null;
|
|
||||||
if (!/^https?:\/\//i.test(decrypted)) {
|
|
||||||
if (debug) console.log('[VidZee] decrypted but not a URL', decrypted.slice(0,80));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (debug) console.log('[VidZee] AES decoded token', { tokenSnippet: token.slice(0, 24)+'...', url: decrypted.slice(0,120) });
|
|
||||||
return decrypted.trim();
|
|
||||||
} catch (e) {
|
|
||||||
if (debug) console.log('[VidZee] AES decode error', e.message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removed unused parseArgs helper
|
|
||||||
|
|
||||||
const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
|
|
||||||
if (!tmdbId) {
|
|
||||||
console.error('[VidZee] Error: TMDB ID (tmdbId) is required.');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mediaType || (mediaType !== 'movie' && mediaType !== 'tv')) {
|
|
||||||
console.error('[VidZee] Error: mediaType is required and must be either "movie" or "tv".');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaType === 'tv') {
|
|
||||||
if (!seasonNum) {
|
|
||||||
console.error('[VidZee] Error: Season (seasonNum) is required for TV shows.');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
if (!episodeNum) {
|
|
||||||
console.error('[VidZee] Error: Episode (episodeNum) is required for TV shows.');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const servers = [1,2,3,4,5,6,7,8,9,10]; // VidZee server identifiers
|
|
||||||
|
|
||||||
const streamPromises = servers.map(async (sr) => {
|
|
||||||
let targetApiUrl = `https://player.vidzee.wtf/api/server?id=${tmdbId}&sr=${sr}`;
|
|
||||||
|
|
||||||
if (mediaType === 'tv') {
|
|
||||||
targetApiUrl += `&ss=${seasonNum}&ep=${episodeNum}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let finalApiUrl;
|
|
||||||
const headers = {
|
|
||||||
'Referer': `https://player.vidzee.wtf/embed/movie/${tmdbId}`
|
|
||||||
};
|
|
||||||
const timeout = 7000; // Reduced timeout
|
|
||||||
|
|
||||||
finalApiUrl = targetApiUrl;
|
|
||||||
|
|
||||||
console.log(`[VidZee] Fetching from server ${sr}: ${targetApiUrl}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.get(finalApiUrl, {
|
|
||||||
headers: headers,
|
|
||||||
timeout: timeout
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseData = response.data;
|
|
||||||
|
|
||||||
if (!responseData || typeof responseData !== 'object') {
|
|
||||||
console.error(`[VidZee S${sr}] Error: Invalid response data from API.`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (responseData.tracks) {
|
|
||||||
delete responseData.tracks;
|
|
||||||
}
|
|
||||||
|
|
||||||
let apiSources = [];
|
|
||||||
if (responseData.url && Array.isArray(responseData.url)) {
|
|
||||||
apiSources = responseData.url;
|
|
||||||
} else if (responseData.link && typeof responseData.link === 'string') {
|
|
||||||
apiSources = [responseData];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!apiSources || apiSources.length === 0) {
|
|
||||||
console.log(`[VidZee S${sr}] No stream sources found in API response.`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const streams = apiSources.map(sourceItem => {
|
|
||||||
const label = sourceItem.name || sourceItem.type || 'VidZee';
|
|
||||||
let quality = String(label).match(/^\d+$/) ? `${label}p` : label;
|
|
||||||
if (!/(\d{3,4})p/.test(quality.toLowerCase())) {
|
|
||||||
quality = '720p';
|
|
||||||
}
|
|
||||||
const language = sourceItem.language || sourceItem.lang;
|
|
||||||
let rawLink = sourceItem.link;
|
|
||||||
const debug = process.env.VIDZEE_DEBUG === '1';
|
|
||||||
const decoded = decodeVidZeeToken(rawLink, debug);
|
|
||||||
if (decoded && /^https?:\/\//i.test(decoded)) {
|
|
||||||
if (debug) console.log('[VidZee] decoded link', { beforeSample: rawLink.slice(0,40)+'...', after: decoded.slice(0,80) });
|
|
||||||
rawLink = decoded;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
name: `VidZee Server${sr} - ${quality} - ${language}`,
|
|
||||||
title: `VidZee Server${sr} - ${quality} - ${language}`,
|
|
||||||
url: rawLink,
|
|
||||||
quality: quality,
|
|
||||||
provider: "VidZee",
|
|
||||||
headers: {
|
|
||||||
'Referer': 'https://core.vidzee.wtf/'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}).filter(stream => stream.url);
|
|
||||||
|
|
||||||
console.log(`[VidZee S${sr}] Successfully extracted ${streams.length} streams. Qualities: ${streams.map(s=>s.quality).join(', ')}`);
|
|
||||||
return streams;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response) {
|
|
||||||
console.error(`[VidZee S${sr}] Error fetching: ${error.response.status} ${error.response.statusText}`);
|
|
||||||
} else if (error.request) {
|
|
||||||
console.error(`[VidZee S${sr}] Error fetching: No response received.`);
|
|
||||||
} else {
|
|
||||||
console.error(`[VidZee S${sr}] Error fetching:`, error.message);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const allStreamsNested = await Promise.all(streamPromises);
|
|
||||||
const allStreams = allStreamsNested.flat();
|
|
||||||
|
|
||||||
console.log(`[VidZee] Found a total of ${allStreams.length} streams from servers ${servers.join(', ')}.`);
|
|
||||||
return allStreams;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = { getVidZeeStreams };
|
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
||||||
|
|
||||||
|
const DAHMER_MOVIES_API = 'https://a.111477.xyz';
|
||||||
|
const DAHMER_WORKER_API = 'https://p.111477.xyz/bulk?u=';
|
||||||
|
|
||||||
|
const REQUEST_HEADERS = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
||||||
|
'Referer': `${DAHMER_MOVIES_API}/`
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseLinks(html) {
|
||||||
|
const links = [];
|
||||||
|
const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
|
||||||
|
let match;
|
||||||
|
while ((match = rowRegex.exec(html)) !== null) {
|
||||||
|
const rowContent = match[1];
|
||||||
|
const linkMatch = rowContent.match(/<a[^>]*href=["']([^"']*)["'][^>]*>([^<]*)<\/a>/i);
|
||||||
|
const sizeMatch = rowContent.match(/<td[^>]*>(\d+(?:\.\d+)?\s?[KMGT]B)<\/td>/i);
|
||||||
|
if (linkMatch) {
|
||||||
|
const href = linkMatch[1];
|
||||||
|
const text = linkMatch[2].trim();
|
||||||
|
const size = sizeMatch ? sizeMatch[1].trim() : 'N/A';
|
||||||
|
if (text && href !== '../' && /\.(mkv|mp4|avi|webm|m3u8)$/i.test(text)) {
|
||||||
|
links.push({ text, href, size });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return links;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDirectory(title, year, season) {
|
||||||
|
const cleanTitle = title.replace(/:/g, '');
|
||||||
|
const variants = season !== null ? [
|
||||||
|
`/tvs/${encodeURIComponent(cleanTitle)}/Season%20${season < 10 ? '0' + season : season}/`,
|
||||||
|
`/tvs/${encodeURIComponent(cleanTitle)}/Season%20${season}/`
|
||||||
|
] : [
|
||||||
|
`/movies/${encodeURIComponent(`${cleanTitle} (${year})`)}/`
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const variant of variants) {
|
||||||
|
try {
|
||||||
|
const res = await axios.get(DAHMER_MOVIES_API + variant, {
|
||||||
|
headers: REQUEST_HEADERS,
|
||||||
|
timeout: 10000,
|
||||||
|
responseType: 'text'
|
||||||
|
});
|
||||||
|
if (res.data) return { html: res.data, dirUrl: DAHMER_MOVIES_API + variant };
|
||||||
|
} catch {
|
||||||
|
// try next variant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDahmermoviesStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
|
||||||
|
console.log(`[DahmerMovies] Searching for ${mediaType} ${tmdbId}`);
|
||||||
|
|
||||||
|
const tmdbKey = getTmdbApiKey();
|
||||||
|
if (!tmdbKey) {
|
||||||
|
console.error('[DahmerMovies] No TMDB API key configured.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let title, year;
|
||||||
|
try {
|
||||||
|
const type = mediaType === 'tv' ? 'tv' : 'movie';
|
||||||
|
const { data } = await axios.get(
|
||||||
|
`https://api.themoviedb.org/3/${type}/${tmdbId}?api_key=${tmdbKey}`,
|
||||||
|
{ timeout: 8000 }
|
||||||
|
);
|
||||||
|
title = data.title || data.name || '';
|
||||||
|
year = (data.release_date || data.first_air_date || '').substring(0, 4);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[DahmerMovies] TMDB lookup failed: ${err.message}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!title) return [];
|
||||||
|
|
||||||
|
const dir = await fetchDirectory(title, year, seasonNum);
|
||||||
|
if (!dir) {
|
||||||
|
console.log(`[DahmerMovies] Directory not found for "${title}"`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let paths = parseLinks(dir.html);
|
||||||
|
|
||||||
|
// For TV, filter to the specific episode when possible
|
||||||
|
if (seasonNum !== null && episodeNum !== null) {
|
||||||
|
const epStr = String(episodeNum).padStart(2, '0');
|
||||||
|
const seStr = String(seasonNum).padStart(2, '0');
|
||||||
|
const epFiltered = paths.filter(p => {
|
||||||
|
const name = p.text.toLowerCase();
|
||||||
|
return name.includes(`s${seStr}e${epStr}`) || name.includes(`e${epStr}`);
|
||||||
|
});
|
||||||
|
if (epFiltered.length > 0) paths = epFiltered;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort 4K first
|
||||||
|
paths.sort((a, b) => (/2160p|4k/i.test(b.text) ? 1 : 0) - (/2160p|4k/i.test(a.text) ? 1 : 0));
|
||||||
|
|
||||||
|
const streams = [];
|
||||||
|
for (const path of paths.slice(0, 5)) {
|
||||||
|
let directUrl;
|
||||||
|
if (path.href.startsWith('http')) {
|
||||||
|
directUrl = path.href;
|
||||||
|
} else if (path.href.includes('/movies/') || path.href.includes('/tvs/')) {
|
||||||
|
directUrl = DAHMER_MOVIES_API + (path.href.startsWith('/') ? '' : '/') + path.href;
|
||||||
|
} else {
|
||||||
|
directUrl = dir.dirUrl + path.href;
|
||||||
|
}
|
||||||
|
directUrl = decodeURI(directUrl.replace(/([^:]\/)\/+/g, '$1'));
|
||||||
|
|
||||||
|
const fileName = path.text;
|
||||||
|
const isMulti = /\b(HIN|TAM|TEL|Multi|Dual|DUB|Multi-Audio|MULTI)\b/i.test(fileName);
|
||||||
|
const hasEngTag = /\b(Eng|English)\b/i.test(fileName);
|
||||||
|
const isEnglishTitle = /^[a-zA-Z0-9\s?!\-:]+$/.test(title);
|
||||||
|
let language = 'Original';
|
||||||
|
if (isMulti) language = 'Multi Audio';
|
||||||
|
else if (isEnglishTitle && hasEngTag) language = 'English';
|
||||||
|
|
||||||
|
const formatMatch = fileName.match(/\.(mkv|mp4|m3u8|avi|webm)$/i);
|
||||||
|
const fileFormat = formatMatch ? formatMatch[1].toUpperCase() : 'LINK';
|
||||||
|
const resolution = fileName.match(/\b(2160p|1080p|720p|4[Kk])\b/)?.[0] || '1080p';
|
||||||
|
|
||||||
|
const info = fileName
|
||||||
|
.replace(/\.(mkv|mp4|avi|webm|m3u8)$/i, '')
|
||||||
|
.replace(/[\[\]()._-]/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
streams.push({
|
||||||
|
name: 'DahmerMovies',
|
||||||
|
title: `${resolution} | ${language} | ${path.size} | ${fileFormat} | ${info}`,
|
||||||
|
url: DAHMER_WORKER_API + encodeURI(directUrl),
|
||||||
|
quality: resolution,
|
||||||
|
provider: 'DahmerMovies',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': REQUEST_HEADERS['User-Agent'],
|
||||||
|
'Referer': `${DAHMER_MOVIES_API}/`,
|
||||||
|
'Accept': '*/*',
|
||||||
|
'Range': 'bytes=0-'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[DahmerMovies] Total results found: ${streams.length}`);
|
||||||
|
return streams;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getDahmermoviesStreams };
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
||||||
|
|
||||||
|
const LORDFLIX_HEADERS = {
|
||||||
|
'Accept': '*/*',
|
||||||
|
'Origin': 'https://lordflix.org',
|
||||||
|
'Referer': 'https://lordflix.org/',
|
||||||
|
'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 LORDFLIX_API = 'https://snowhouse.lordflix.club';
|
||||||
|
const MULTI_DECRYPT_API = 'https://enc-dec.app/api';
|
||||||
|
const SERVERS = ['Berlin', 'Marseille', 'Backrooms', 'Phoenix', 'Oslo', 'Luna', 'Sakura', 'Rio', 'Ativa', 'Moscow'];
|
||||||
|
|
||||||
|
// Matches the reference project's encodeQuote: encodeURIComponent with + encoded as %20
|
||||||
|
function encodeQuote(str) {
|
||||||
|
return encodeURIComponent(str).replace(/%20/g, '+').replace(/\+/g, '%20');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLordflixStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
|
||||||
|
console.log(`[Lordflix] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
|
||||||
|
|
||||||
|
const tmdbKey = getTmdbApiKey();
|
||||||
|
if (!tmdbKey) {
|
||||||
|
console.error('[Lordflix] No TMDB API key configured.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: TMDB lookup for title, year, imdbId
|
||||||
|
let info;
|
||||||
|
try {
|
||||||
|
const type = mediaType === 'tv' ? 'tv' : 'movie';
|
||||||
|
const { data } = await axios.get(
|
||||||
|
`https://api.themoviedb.org/3/${type}/${tmdbId}?api_key=${tmdbKey}&append_to_response=external_ids`,
|
||||||
|
{ timeout: 8000 }
|
||||||
|
);
|
||||||
|
info = {
|
||||||
|
title: data.title || data.name || '',
|
||||||
|
year: (data.release_date || data.first_air_date || '').split('-')[0],
|
||||||
|
imdbId: (data.external_ids && data.external_ids.imdb_id) || ''
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[Lordflix] TMDB lookup failed: ${err.message}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!info.title || !info.imdbId) {
|
||||||
|
console.error('[Lordflix] Missing title or IMDb ID from TMDB.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeParam = mediaType === 'tv' ? 'series' : 'movie';
|
||||||
|
const titleEnc = encodeQuote(info.title);
|
||||||
|
const streams = [];
|
||||||
|
|
||||||
|
// Step 2: Query each server in parallel
|
||||||
|
await Promise.all(SERVERS.map(async (server) => {
|
||||||
|
try {
|
||||||
|
let serverUrl = `${LORDFLIX_API}/?title=${titleEnc}&type=${typeParam}&year=${info.year || ''}`
|
||||||
|
+ `&imdb=${info.imdbId}&tmdb=${tmdbId}&server=${server}`;
|
||||||
|
if (mediaType === 'tv') {
|
||||||
|
serverUrl += `&season=${seasonNum}&episode=${episodeNum}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Get signed proxy URL from enc-dec.app
|
||||||
|
const encBridgeRes = await axios.get(
|
||||||
|
`${MULTI_DECRYPT_API}/enc-lordflix?url=${encodeQuote(serverUrl)}`,
|
||||||
|
{ timeout: 8000 }
|
||||||
|
);
|
||||||
|
const encBridgeJson = encBridgeRes.data;
|
||||||
|
if (!encBridgeJson || encBridgeJson.status !== 200 || !encBridgeJson.result) return;
|
||||||
|
|
||||||
|
const { url: proxyEncUrl, sign: signature } = encBridgeJson.result;
|
||||||
|
if (!proxyEncUrl || !signature) return;
|
||||||
|
|
||||||
|
// Step 4: Fetch encrypted stream data from proxy URL
|
||||||
|
const remoteEncRes = await axios.get(proxyEncUrl, {
|
||||||
|
headers: LORDFLIX_HEADERS,
|
||||||
|
timeout: 8000,
|
||||||
|
responseType: 'text'
|
||||||
|
});
|
||||||
|
const remoteEncData = typeof remoteEncRes.data === 'string'
|
||||||
|
? remoteEncRes.data
|
||||||
|
: JSON.stringify(remoteEncRes.data);
|
||||||
|
|
||||||
|
// Step 5: Decrypt via enc-dec.app
|
||||||
|
const decRes = await axios.post(`${MULTI_DECRYPT_API}/dec-lordflix`,
|
||||||
|
{ text: remoteEncData, sign: signature },
|
||||||
|
{ headers: { 'Content-Type': 'application/json' }, timeout: 8000 }
|
||||||
|
);
|
||||||
|
const finalJson = decRes.data;
|
||||||
|
if (!finalJson || finalJson.status !== 200 || !finalJson.result || finalJson.result.error) return;
|
||||||
|
|
||||||
|
const streamList = finalJson.result.stream;
|
||||||
|
if (!Array.isArray(streamList) || streamList.length === 0) return;
|
||||||
|
|
||||||
|
const topStream = streamList[0];
|
||||||
|
if (topStream.type === 'hls' && topStream.playlist) {
|
||||||
|
streams.push({
|
||||||
|
name: `Lordflix[${server}]`,
|
||||||
|
title: `Lordflix[${server}]`,
|
||||||
|
url: topStream.playlist,
|
||||||
|
quality: 'Auto',
|
||||||
|
provider: 'Lordflix',
|
||||||
|
headers: LORDFLIX_HEADERS
|
||||||
|
});
|
||||||
|
console.log(`[Lordflix] Server ${server}: got stream.`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[Lordflix] Server ${server} error: ${err.message}`);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log(`[Lordflix] Total streams: ${streams.length}`);
|
||||||
|
return streams;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getLordflixStreams };
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,92 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
||||||
|
|
||||||
|
const NOTORRENT_API = 'https://addon-osvh.onrender.com';
|
||||||
|
|
||||||
|
function cleanText(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
return str.replace(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]/gu, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractQuality(titleText) {
|
||||||
|
const raw = titleText || '';
|
||||||
|
const match = raw.match(/(\d{3,4}p)/);
|
||||||
|
if (match) return match[0];
|
||||||
|
if (raw.toUpperCase().includes('FREE')) return 'Auto';
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getNotorrentStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
|
||||||
|
console.log(`[NoTorrent] Searching for ${mediaType} ${tmdbId}`);
|
||||||
|
|
||||||
|
const tmdbKey = getTmdbApiKey();
|
||||||
|
if (!tmdbKey) {
|
||||||
|
console.error('[NoTorrent] No TMDB API key configured.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: TMDB → IMDB ID
|
||||||
|
let imdbId;
|
||||||
|
try {
|
||||||
|
const type = mediaType === 'tv' ? 'tv' : 'movie';
|
||||||
|
const { data } = await axios.get(
|
||||||
|
`https://api.themoviedb.org/3/${type}/${tmdbId}?api_key=${tmdbKey}&append_to_response=external_ids`,
|
||||||
|
{ timeout: 8000 }
|
||||||
|
);
|
||||||
|
imdbId = (data.external_ids && data.external_ids.imdb_id) || null;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[NoTorrent] TMDB lookup failed: ${err.message}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!imdbId) {
|
||||||
|
console.warn('[NoTorrent] Failed to map IMDB ID from TMDB.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Fetch streams from NoTorrent Stremio addon API
|
||||||
|
const apiUrl = (mediaType === 'tv' && seasonNum != null)
|
||||||
|
? `${NOTORRENT_API}/stream/series/${imdbId}:${seasonNum}:${episodeNum}.json`
|
||||||
|
: `${NOTORRENT_API}/stream/movie/${imdbId}.json`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(apiUrl, { timeout: 20000 });
|
||||||
|
const rawList = data.streams || [];
|
||||||
|
const streams = [];
|
||||||
|
|
||||||
|
for (const item of rawList) {
|
||||||
|
if (item.externalUrl || !item.url) continue;
|
||||||
|
if (item.url.includes('github.com') || item.url.includes('googleusercontent')) continue;
|
||||||
|
|
||||||
|
const cleanTitleStr = cleanText(item.title || '');
|
||||||
|
const quality = extractQuality(cleanTitleStr);
|
||||||
|
|
||||||
|
let language = 'Default';
|
||||||
|
const langMatch = cleanTitleStr.match(/\(([^)]+)\)/);
|
||||||
|
if (langMatch) {
|
||||||
|
language = langMatch[1].charAt(0).toUpperCase() + langMatch[1].slice(1).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyHeaders = (item.behaviorHints?.proxyHeaders?.request) || {};
|
||||||
|
const headers = { ...(item.behaviorHints?.headers || {}), ...proxyHeaders };
|
||||||
|
|
||||||
|
const nameParts = ['NoTorrent', language !== 'Default' ? language : ''].filter(p => p.trim() !== '');
|
||||||
|
streams.push({
|
||||||
|
name: nameParts.join(' \u2022 '),
|
||||||
|
title: quality,
|
||||||
|
url: item.url,
|
||||||
|
quality,
|
||||||
|
provider: 'NoTorrent',
|
||||||
|
headers: Object.keys(headers).length > 0 ? headers : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[NoTorrent] Total results found: ${streams.length}`);
|
||||||
|
return streams;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[NoTorrent] Fetch failed: ${err.message}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getNotorrentStreams };
|
||||||
+23
-21
@@ -9,11 +9,12 @@ const providerCache = new Map();
|
|||||||
const providerFunctionMap = {
|
const providerFunctionMap = {
|
||||||
'Showbox.js': 'getStreamsFromTmdbId',
|
'Showbox.js': 'getStreamsFromTmdbId',
|
||||||
'4khdhub.js': 'get4KHDHubStreams',
|
'4khdhub.js': 'get4KHDHubStreams',
|
||||||
'moviesmod.js': 'getMoviesModStreams',
|
|
||||||
'MP4Hydra.js': 'getMP4HydraStreams',
|
|
||||||
'VidZee.js': 'getVidZeeStreams',
|
|
||||||
'vixsrc.js': 'getVixsrcStreams',
|
'vixsrc.js': 'getVixsrcStreams',
|
||||||
'uhdmovies.js': 'getUHDMoviesStreams',
|
'videasy.js': 'getVideasyStreams',
|
||||||
|
'vidlink.js': 'getVidlinkStreams',
|
||||||
|
'lordflix.js': 'getLordflixStreams',
|
||||||
|
'notorrent.js': 'getNotorrentStreams',
|
||||||
|
'dahmermovies.js': 'getDahmermoviesStreams',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Stats for debug endpoint
|
// Stats for debug endpoint
|
||||||
@@ -27,7 +28,7 @@ async function getEffectiveCookies() {
|
|||||||
function getProviderFiles() {
|
function getProviderFiles() {
|
||||||
const providersDir = path.join(__dirname);
|
const providersDir = path.join(__dirname);
|
||||||
return fs.readdirSync(providersDir)
|
return fs.readdirSync(providersDir)
|
||||||
.filter(file => file.endsWith('.js') && file !== 'registry.js' && file !== 'vidsrcextractor.js')
|
.filter(file => file.endsWith('.js') && file !== 'registry.js')
|
||||||
.map(file => ({
|
.map(file => ({
|
||||||
name: path.parse(file).name.toLowerCase(),
|
name: path.parse(file).name.toLowerCase(),
|
||||||
file: file,
|
file: file,
|
||||||
@@ -110,28 +111,29 @@ function createFetchFunction(providerInfo) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize providers
|
// Initialize providers - always load all; enabled state is checked dynamically from live config
|
||||||
const providerFiles = getProviderFiles();
|
const providerFiles = getProviderFiles();
|
||||||
const providers = [];
|
const providers = [];
|
||||||
|
|
||||||
for (const providerInfo of providerFiles) {
|
for (const providerInfo of providerFiles) {
|
||||||
const enableFlag = `enable${providerInfo.name.charAt(0).toUpperCase() + providerInfo.name.slice(1)}Provider`;
|
providers.push({
|
||||||
const enabled = config[enableFlag] !== false;
|
name: providerInfo.name,
|
||||||
|
fetch: createFetchFunction(providerInfo)
|
||||||
if (enabled) {
|
});
|
||||||
providers.push({
|
console.log(`[registry] ${providerInfo.name} provider loaded`);
|
||||||
name: providerInfo.name,
|
|
||||||
enabled: true,
|
|
||||||
fetch: createFetchFunction(providerInfo)
|
|
||||||
});
|
|
||||||
console.log(`[registry] ${providerInfo.name} provider enabled`);
|
|
||||||
} else {
|
|
||||||
console.log(`[registry] ${providerInfo.name} disabled via config`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function listProviders() { return providers.map(p => ({ name: p.name, enabled: p.enabled })); }
|
function isProviderEnabled(name) {
|
||||||
function getProvider(name) { return providers.find(p => p.name === name.toLowerCase()) || null; }
|
const flag = `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`;
|
||||||
|
return config[flag] !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function listProviders() { return providers.map(p => ({ name: p.name, enabled: isProviderEnabled(p.name) })); }
|
||||||
|
function getProvider(name) {
|
||||||
|
const p = providers.find(p => p.name === name.toLowerCase());
|
||||||
|
if (!p) return null;
|
||||||
|
return { ...p, enabled: isProviderEnabled(p.name) };
|
||||||
|
}
|
||||||
|
|
||||||
function getCookieStats() { return lastCookieStats; }
|
function getCookieStats() { return lastCookieStats; }
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
const { getTmdbApiKey } = require('../utils/tmdbKey');
|
||||||
|
|
||||||
|
const VIDEASY_HEADERS = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'application/json, text/plain, */*',
|
||||||
|
'Origin': 'https://player.videasy.net',
|
||||||
|
'Referer': 'https://player.videasy.net/'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Each server maps a name to its API URL. moviesOnly:true skips TV requests.
|
||||||
|
const SERVERS = {
|
||||||
|
'Neon': { url: 'https://api.videasy.net/myflixerzupcloud/sources-with-title' },
|
||||||
|
'Yoru': { url: 'https://api.videasy.net/cdn/sources-with-title', moviesOnly: true },
|
||||||
|
'Cypher': { url: 'https://api.videasy.net/moviebox/sources-with-title' },
|
||||||
|
'Reyna': { url: 'https://api.videasy.net/primewire/sources-with-title' },
|
||||||
|
'Omen': { url: 'https://api.videasy.net/onionplay/sources-with-title' },
|
||||||
|
'Breach': { url: 'https://api.videasy.net/m4uhd/sources-with-title' },
|
||||||
|
'Ghost': { url: 'https://api.videasy.net/primesrcme/sources-with-title' },
|
||||||
|
'Sage': { url: 'https://api.videasy.net/1movies/sources-with-title' },
|
||||||
|
'Vyse': { url: 'https://api.videasy.net/hdmovie/sources-with-title' },
|
||||||
|
'Raze': { url: 'https://api.videasy.net/superflix/sources-with-title' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const DECRYPT_URL = 'https://enc-dec.app/api/dec-videasy';
|
||||||
|
|
||||||
|
async function getVideasyStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
|
||||||
|
console.log(`[Videasy] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
|
||||||
|
|
||||||
|
const tmdbKey = getTmdbApiKey();
|
||||||
|
if (!tmdbKey) {
|
||||||
|
console.error('[Videasy] No TMDB API key configured.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Resolve title/year/imdbId from TMDB
|
||||||
|
let details;
|
||||||
|
try {
|
||||||
|
const type = mediaType === 'tv' ? 'tv' : 'movie';
|
||||||
|
const { data } = await axios.get(
|
||||||
|
`https://api.themoviedb.org/3/${type}/${tmdbId}?api_key=${tmdbKey}&append_to_response=external_ids`,
|
||||||
|
{ timeout: 8000 }
|
||||||
|
);
|
||||||
|
details = {
|
||||||
|
title: data.title || data.name || '',
|
||||||
|
year: (data.release_date || data.first_air_date || '').split('-')[0],
|
||||||
|
imdbId: (data.external_ids && data.external_ids.imdb_id) || '',
|
||||||
|
type
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[Videasy] TMDB lookup failed: ${err.message}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!details.title) {
|
||||||
|
console.error('[Videasy] No title returned from TMDB.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const allStreams = [];
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
// Step 2: Query each server in parallel
|
||||||
|
await Promise.all(Object.entries(SERVERS).map(async ([name, server]) => {
|
||||||
|
if (server.moviesOnly && mediaType === 'tv') return;
|
||||||
|
|
||||||
|
let apiUrl = `${server.url}?title=${encodeURIComponent(details.title)}`
|
||||||
|
+ `&mediaType=${details.type}&year=${details.year}`
|
||||||
|
+ `&tmdbId=${tmdbId}&imdbId=${details.imdbId || ''}`;
|
||||||
|
if (mediaType === 'tv') apiUrl += `&seasonId=${seasonNum}&episodeId=${episodeNum}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const encRes = await axios.get(apiUrl, {
|
||||||
|
headers: VIDEASY_HEADERS,
|
||||||
|
timeout: 8000,
|
||||||
|
responseType: 'text'
|
||||||
|
});
|
||||||
|
|
||||||
|
const encryptedText = typeof encRes.data === 'string' ? encRes.data : JSON.stringify(encRes.data);
|
||||||
|
if (!encryptedText || encryptedText.length < 20 || encryptedText.startsWith('<')) return;
|
||||||
|
|
||||||
|
// Step 3: Decrypt via enc-dec.app
|
||||||
|
const decRes = await axios.post(DECRYPT_URL,
|
||||||
|
{ text: encryptedText, id: String(tmdbId) },
|
||||||
|
{ headers: { 'Content-Type': 'application/json' }, timeout: 8000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const resData = (decRes.data && decRes.data.result) || decRes.data;
|
||||||
|
if (!resData || !Array.isArray(resData.sources)) return;
|
||||||
|
|
||||||
|
for (const s of resData.sources) {
|
||||||
|
if (!s.url || seen.has(s.url)) continue;
|
||||||
|
seen.add(s.url);
|
||||||
|
allStreams.push({
|
||||||
|
name: `Videasy ${name}`,
|
||||||
|
title: `Videasy ${name} - ${s.quality || 'Auto'}`,
|
||||||
|
url: s.url,
|
||||||
|
quality: s.quality || 'Auto',
|
||||||
|
provider: 'Videasy',
|
||||||
|
headers: {
|
||||||
|
'Referer': 'https://player.videasy.net/',
|
||||||
|
'Origin': 'https://player.videasy.net'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(`[Videasy] Server ${name}: ${resData.sources.length} source(s)`);
|
||||||
|
} catch {
|
||||||
|
// server unreachable or returned no data — skip silently
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log(`[Videasy] Total streams: ${allStreams.length}`);
|
||||||
|
return allStreams;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getVideasyStreams };
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const VIDLINK_HEADERS = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
||||||
|
'Referer': 'https://vidlink.pro'
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getVidlinkStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
|
||||||
|
console.log(`[Vidlink] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1: Encrypt the TMDB ID via enc-dec.app
|
||||||
|
const encRes = await axios.get(
|
||||||
|
`https://enc-dec.app/api/enc-vidlink?text=${encodeURIComponent(String(tmdbId))}`,
|
||||||
|
{ timeout: 8000 }
|
||||||
|
);
|
||||||
|
const encodedTmdb = encRes.data && encRes.data.result;
|
||||||
|
if (!encodedTmdb) {
|
||||||
|
console.log('[Vidlink] Encryption step returned no result.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Fetch stream playlist from Vidlink API
|
||||||
|
const apiUrl = mediaType === 'tv'
|
||||||
|
? `https://vidlink.pro/api/b/tv/${encodedTmdb}/${seasonNum}/${episodeNum}?multiLang=0`
|
||||||
|
: `https://vidlink.pro/api/b/movie/${encodedTmdb}?multiLang=0`;
|
||||||
|
|
||||||
|
const apiRes = await axios.get(apiUrl, { headers: VIDLINK_HEADERS, timeout: 8000 });
|
||||||
|
|
||||||
|
const playlist = apiRes.data && apiRes.data.stream && apiRes.data.stream.playlist;
|
||||||
|
if (!playlist) {
|
||||||
|
console.log('[Vidlink] No playlist URL in response.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Vidlink] Got stream.`);
|
||||||
|
return [{
|
||||||
|
name: 'Vidlink',
|
||||||
|
title: 'Vidlink',
|
||||||
|
url: playlist,
|
||||||
|
quality: 'Auto',
|
||||||
|
provider: 'Vidlink',
|
||||||
|
headers: { 'Referer': 'https://vidlink.pro' }
|
||||||
|
}];
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[Vidlink] Error: ${err.message}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getVidlinkStreams };
|
||||||
@@ -1,411 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
const cheerio = require('cheerio');
|
|
||||||
// --- Constants ---
|
|
||||||
let BASEDOM = "https://cloudnestra.com"; // This can be updated by serversLoad
|
|
||||||
const SOURCE_URL = "https://vidsrc.xyz/embed";
|
|
||||||
// --- Helper: Direct Fetch ---
|
|
||||||
async function fetchWrapper(url, options) {
|
|
||||||
console.log(`[VidSrc Direct] Fetching: ${url}`);
|
|
||||||
return fetch(url, options);
|
|
||||||
}
|
|
||||||
// --- Helper Functions (copied and adapted from src/extractor.ts) ---
|
|
||||||
async function serversLoad(html) {
|
|
||||||
const $ = cheerio.load(html);
|
|
||||||
const servers = [];
|
|
||||||
const title = $("title").text() ?? "";
|
|
||||||
const baseFrameSrc = $("iframe").attr("src") ?? "";
|
|
||||||
if (baseFrameSrc) {
|
|
||||||
try {
|
|
||||||
const fullUrl = baseFrameSrc.startsWith("//") ? "https:" + baseFrameSrc : baseFrameSrc;
|
|
||||||
BASEDOM = new URL(fullUrl).origin;
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.warn(`(Attempt 1) Failed to parse base URL from iframe src: ${baseFrameSrc} using new URL(), error: ${e.message}`);
|
|
||||||
// Attempt 2: Regex fallback for origin
|
|
||||||
const originMatch = (baseFrameSrc.startsWith("//") ? "https:" + baseFrameSrc : baseFrameSrc).match(/^(https?:\/\/[^/]+)/);
|
|
||||||
if (originMatch && originMatch[1]) {
|
|
||||||
BASEDOM = originMatch[1];
|
|
||||||
console.log(`(Attempt 2) Successfully extracted origin using regex: ${BASEDOM}`);
|
|
||||||
} else {
|
|
||||||
console.error(`(Attempt 2) Failed to extract origin using regex from: ${baseFrameSrc}. Using default: ${BASEDOM}`);
|
|
||||||
// Keep the default BASEDOM = "https://cloudnestra.com" if all fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$(".serversList .server").each((index, element) => {
|
|
||||||
const server = $(element);
|
|
||||||
servers.push({
|
|
||||||
name: server.text().trim(),
|
|
||||||
dataHash: server.attr("data-hash") ?? null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
servers: servers,
|
|
||||||
title: title,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
async function parseMasterM3U8(m3u8Content, masterM3U8Url) {
|
|
||||||
const lines = m3u8Content.split('\n').map(line => line.trim());
|
|
||||||
const streams = [];
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
if (lines[i].startsWith("#EXT-X-STREAM-INF:")) {
|
|
||||||
const infoLine = lines[i];
|
|
||||||
let quality = "unknown";
|
|
||||||
const resolutionMatch = infoLine.match(/RESOLUTION=(\d+x\d+)/);
|
|
||||||
if (resolutionMatch && resolutionMatch[1]) {
|
|
||||||
quality = resolutionMatch[1];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const bandwidthMatch = infoLine.match(/BANDWIDTH=(\d+)/);
|
|
||||||
if (bandwidthMatch && bandwidthMatch[1]) {
|
|
||||||
quality = `${Math.round(parseInt(bandwidthMatch[1]) / 1000)}kbps`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (i + 1 < lines.length && lines[i + 1] && !lines[i + 1].startsWith("#")) {
|
|
||||||
const streamUrlPart = lines[i + 1];
|
|
||||||
try {
|
|
||||||
const fullStreamUrl = new URL(streamUrlPart, masterM3U8Url).href;
|
|
||||||
streams.push({ quality: quality, url: fullStreamUrl });
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error(`Error constructing URL for stream part: ${streamUrlPart} with base: ${masterM3U8Url}`, e);
|
|
||||||
streams.push({ quality: quality, url: streamUrlPart }); // Store partial URL as a fallback
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort streams by quality (highest first)
|
|
||||||
streams.sort((a, b) => {
|
|
||||||
// Extract resolution height from quality (e.g., "1280x720" -> 720)
|
|
||||||
const getHeight = (quality) => {
|
|
||||||
const match = quality.match(/(\d+)x(\d+)/);
|
|
||||||
return match ? parseInt(match[2], 10) : 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const heightA = getHeight(a.quality);
|
|
||||||
const heightB = getHeight(b.quality);
|
|
||||||
|
|
||||||
// Higher resolution comes first
|
|
||||||
return heightB - heightA;
|
|
||||||
});
|
|
||||||
|
|
||||||
return streams;
|
|
||||||
}
|
|
||||||
async function PRORCPhandler(prorcp) {
|
|
||||||
try {
|
|
||||||
const prorcpUrl = `${BASEDOM}/prorcp/${prorcp}`;
|
|
||||||
const prorcpFetch = await fetchWrapper(prorcpUrl, {
|
|
||||||
headers: {
|
|
||||||
"accept": "*/*", "accept-language": "en-US,en;q=0.9", "priority": "u=1",
|
|
||||||
"sec-ch-ua": "\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"",
|
|
||||||
"sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"",
|
|
||||||
"sec-fetch-dest": "script", "sec-fetch-mode": "no-cors", "sec-fetch-site": "same-origin",
|
|
||||||
'Sec-Fetch-Dest': 'iframe', "Referer": `${BASEDOM}/`, "Referrer-Policy": "origin",
|
|
||||||
},
|
|
||||||
timeout: 10000
|
|
||||||
});
|
|
||||||
if (!prorcpFetch.ok) {
|
|
||||||
console.error(`Failed to fetch prorcp: ${prorcpUrl}, status: ${prorcpFetch.status}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const prorcpResponse = await prorcpFetch.text();
|
|
||||||
const regex = /file:\s*'([^']*)'/gm;
|
|
||||||
const match = regex.exec(prorcpResponse);
|
|
||||||
if (match && match[1]) {
|
|
||||||
const masterM3U8Url = match[1];
|
|
||||||
const m3u8FileFetch = await fetchWrapper(masterM3U8Url, {
|
|
||||||
headers: { "Referer": prorcpUrl, "Accept": "*/*" },
|
|
||||||
timeout: 10000
|
|
||||||
});
|
|
||||||
if (!m3u8FileFetch.ok) {
|
|
||||||
console.error(`Failed to fetch master M3U8: ${masterM3U8Url}, status: ${m3u8FileFetch.status}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const m3u8Content = await m3u8FileFetch.text();
|
|
||||||
return parseMasterM3U8(m3u8Content, masterM3U8Url);
|
|
||||||
}
|
|
||||||
console.warn("No master M3U8 URL found in prorcp response for:", prorcpUrl);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error(`Error in PRORCPhandler for ${BASEDOM}/prorcp/${prorcp}:`, error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function SRCRCPhandler(srcrcpPath, refererForSrcrcp) {
|
|
||||||
try {
|
|
||||||
const srcrcpUrl = BASEDOM + srcrcpPath;
|
|
||||||
console.log(`[VidSrc - SRCRCP] Fetching: ${srcrcpUrl} (Referer: ${refererForSrcrcp})`);
|
|
||||||
const response = await fetchWrapper(srcrcpUrl, {
|
|
||||||
headers: {
|
|
||||||
"accept": "*/*",
|
|
||||||
"accept-language": "en-US,en;q=0.9",
|
|
||||||
"sec-ch-ua": "\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"",
|
|
||||||
"sec-ch-ua-mobile": "?0",
|
|
||||||
"sec-ch-ua-platform": "\"Windows\"",
|
|
||||||
"sec-fetch-dest": "iframe",
|
|
||||||
"sec-fetch-mode": "navigate",
|
|
||||||
"sec-fetch-site": "same-origin", // This might need to be 'cross-site' if BASEDOM's origin differs from srcrcp link's origin
|
|
||||||
"Referer": refererForSrcrcp,
|
|
||||||
"Referrer-Policy": "origin",
|
|
||||||
},
|
|
||||||
timeout: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(`[VidSrc - SRCRCP] Failed to fetch ${srcrcpUrl}, status: ${response.status}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseText = await response.text();
|
|
||||||
console.log(`[VidSrc - SRCRCP] Response from ${srcrcpUrl} (first 500 chars): ${responseText.substring(0, 500)}`);
|
|
||||||
|
|
||||||
// Attempt 1: Check for "file: '...'" like in PRORCP
|
|
||||||
const fileRegex = /file:\s*'([^']*)'/gm;
|
|
||||||
const fileMatch = fileRegex.exec(responseText);
|
|
||||||
if (fileMatch && fileMatch[1]) {
|
|
||||||
const masterM3U8Url = fileMatch[1];
|
|
||||||
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (via fileMatch): ${masterM3U8Url}`);
|
|
||||||
const m3u8FileFetch = await fetchWrapper(masterM3U8Url, {
|
|
||||||
headers: { "Referer": srcrcpUrl, "Accept": "*/*" },
|
|
||||||
timeout: 10000
|
|
||||||
});
|
|
||||||
if (!m3u8FileFetch.ok) {
|
|
||||||
console.error(`[VidSrc - SRCRCP] Failed to fetch master M3U8: ${masterM3U8Url}, status: ${m3u8FileFetch.status}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const m3u8Content = await m3u8FileFetch.text();
|
|
||||||
return parseMasterM3U8(m3u8Content, masterM3U8Url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt 2: Check if the responseText itself is an M3U8 playlist
|
|
||||||
if (responseText.trim().startsWith("#EXTM3U")) {
|
|
||||||
console.log(`[VidSrc - SRCRCP] Response from ${srcrcpUrl} appears to be an M3U8 playlist directly.`);
|
|
||||||
return parseMasterM3U8(responseText, srcrcpUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt 3: Look for sources = [...] or sources: [...] in script tags or in JSON-like structures
|
|
||||||
const $ = cheerio.load(responseText);
|
|
||||||
let sourcesFound = null;
|
|
||||||
$('script').each((i, script) => {
|
|
||||||
const scriptContent = $(script).html();
|
|
||||||
if (scriptContent) {
|
|
||||||
// Regex for various ways sources might be defined
|
|
||||||
const sourcesRegexes = [
|
|
||||||
/sources\s*[:=]\s*(\[[^\]]*\{(?:\s*|.*?)file\s*:\s*['"]([^'"]+)['"](?:\s*|.*?)\}[^\]]*\])/si, // extracts the URL from sources: [{file: "URL"}]
|
|
||||||
/playerInstance\.setup\s*\(\s*\{\s*sources\s*:\s*(\[[^\]]*\{(?:\s*|.*?)file\s*:\s*['"]([^'"]+)['"](?:\s*|.*?)\}[^\]]*\])/si, // for playerInstance.setup({sources: [{file: "URL"}]})
|
|
||||||
/file\s*:\s*['"]([^'"]+\.m3u8[^'"]*)['"]/i, // Direct M3U8 link in a var or object e.g. file: "URL.m3u8"
|
|
||||||
/src\s*:\s*['"]([^'"]+\.m3u8[^'"]*)['"]/i, // Direct M3U8 link e.g. src: "URL.m3u8"
|
|
||||||
/loadSource\(['"]([^'"]+\.m3u8[^'"]*)['"]\)/i, // For .loadSource("URL.m3u8")
|
|
||||||
/new\s+Player\([^)]*\{\s*src\s*:\s*['"]([^'"]+)['"]\s*\}\s*\)/i // For new Player({src: "URL"})
|
|
||||||
];
|
|
||||||
for (const regex of sourcesRegexes) {
|
|
||||||
const sourcesMatch = scriptContent.match(regex);
|
|
||||||
// For regexes that capture a JSON array-like structure in group 1 and then the URL in group 2 (first two regexes)
|
|
||||||
if (regex.source.includes('file') && regex.source.includes('sources')) { // Heuristic to identify complex sources regex capturing group 2
|
|
||||||
if (sourcesMatch && sourcesMatch[2]) { // URL is in group 2
|
|
||||||
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (script complex): ${sourcesMatch[2]}`);
|
|
||||||
sourcesFound = [{ quality: 'default', url: sourcesMatch[2] }];
|
|
||||||
return false; // break cheerio loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// For simpler regexes where URL is in group 1
|
|
||||||
else if (sourcesMatch && sourcesMatch[1]) {
|
|
||||||
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (script simple): ${sourcesMatch[1]}`);
|
|
||||||
sourcesFound = [{ quality: 'default', url: sourcesMatch[1] }];
|
|
||||||
return false; // break cheerio loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fallback: Look for any absolute .m3u8 URL within the script tag
|
|
||||||
if (!sourcesFound) {
|
|
||||||
const m3u8GenericMatch = scriptContent.match(/['"](https?:\/\/[^'"\s]+\.m3u8[^'"\s]*)['"]/i);
|
|
||||||
if (m3u8GenericMatch && m3u8GenericMatch[1]) {
|
|
||||||
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (script generic fallback): ${m3u8GenericMatch[1]}`);
|
|
||||||
sourcesFound = [{ quality: 'default', url: m3u8GenericMatch[1] }];
|
|
||||||
return false; // break cheerio loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (sourcesFound && sourcesFound.length > 0) {
|
|
||||||
// Process the first valid M3U8 URL found, or return direct links
|
|
||||||
const m3u8Source = sourcesFound.find(s => s.url && s.url.includes('.m3u8'));
|
|
||||||
if (m3u8Source) {
|
|
||||||
console.log(`[VidSrc - SRCRCP] First M3U8 source from script: ${m3u8Source.url}`);
|
|
||||||
// Ensure URL is absolute
|
|
||||||
const absoluteM3u8Url = m3u8Source.url.startsWith('http') ? m3u8Source.url : new URL(m3u8Source.url, srcrcpUrl).href;
|
|
||||||
const m3u8FileFetch = await fetchWrapper(absoluteM3u8Url, {
|
|
||||||
headers: { "Referer": srcrcpUrl, "Accept": "*/*" },
|
|
||||||
timeout: 10000
|
|
||||||
});
|
|
||||||
if (!m3u8FileFetch.ok) {
|
|
||||||
console.error(`[VidSrc - SRCRCP] Failed to fetch M3U8 from script source: ${absoluteM3u8Url}, status: ${m3u8FileFetch.status}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const m3u8Content = await m3u8FileFetch.text();
|
|
||||||
return parseMasterM3U8(m3u8Content, absoluteM3u8Url);
|
|
||||||
} else {
|
|
||||||
// Assuming direct links if no .m3u8 found in the sources array
|
|
||||||
console.log(`[VidSrc - SRCRCP] Assuming direct links from script sources:`, sourcesFound);
|
|
||||||
return sourcesFound.map(s => ({
|
|
||||||
quality: s.quality || s.label || 'auto',
|
|
||||||
url: s.url.startsWith('http') ? s.url : new URL(s.url, srcrcpUrl).href
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn(`[VidSrc - SRCRCP] No stream extraction method succeeded for ${srcrcpUrl}`);
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[VidSrc - SRCRCP] Error in SRCRCPhandler for ${srcrcpPath}:`, error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function rcpGrabber(html) {
|
|
||||||
const regex = /src:\s*'([^']*)'/;
|
|
||||||
const match = html.match(regex);
|
|
||||||
if (!match || !match[1])
|
|
||||||
return null;
|
|
||||||
return { metadata: { image: "" }, data: match[1] };
|
|
||||||
}
|
|
||||||
function getObject(id) {
|
|
||||||
const arr = id.split(':');
|
|
||||||
return { id: arr[0], season: arr[1], episode: arr[2] };
|
|
||||||
}
|
|
||||||
function getUrl(id, type) {
|
|
||||||
if (type === "movie") {
|
|
||||||
return `${SOURCE_URL}/movie/${id}`;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const obj = getObject(id);
|
|
||||||
return `${SOURCE_URL}/tv/${obj.id}/${obj.season}-${obj.episode}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function getStreamContent(id, type) {
|
|
||||||
const url = getUrl(id, type);
|
|
||||||
const embedRes = await fetchWrapper(url, { headers: { "Referer": SOURCE_URL } });
|
|
||||||
if (!embedRes.ok) {
|
|
||||||
console.error(`Failed to fetch embed page ${url}: ${embedRes.status}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const embedResp = await embedRes.text();
|
|
||||||
const { servers, title } = await serversLoad(embedResp);
|
|
||||||
const apiResponse = [];
|
|
||||||
|
|
||||||
// MODIFIED: Process servers in parallel
|
|
||||||
const serverPromises = servers.map(async (server) => {
|
|
||||||
if (!server.dataHash) return null; // Skip servers without dataHash
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rcpUrl = `${BASEDOM}/rcp/${server.dataHash}`;
|
|
||||||
const rcpRes = await fetchWrapper(rcpUrl, {
|
|
||||||
headers: { 'Sec-Fetch-Dest': 'iframe', "Referer": url }
|
|
||||||
});
|
|
||||||
if (!rcpRes.ok) {
|
|
||||||
console.warn(`RCP fetch failed for server ${server.name}: ${rcpRes.status}`);
|
|
||||||
return null; // Failed to fetch RCP
|
|
||||||
}
|
|
||||||
const rcpHtml = await rcpRes.text();
|
|
||||||
const rcpData = await rcpGrabber(rcpHtml);
|
|
||||||
|
|
||||||
if (!rcpData || !rcpData.data) {
|
|
||||||
console.warn(`Skipping server ${server.name} due to missing rcp data.`);
|
|
||||||
return null; // Missing RCP data
|
|
||||||
}
|
|
||||||
|
|
||||||
let streamDetails = null;
|
|
||||||
if (rcpData.data.startsWith("/prorcp/")) {
|
|
||||||
streamDetails = await PRORCPhandler(rcpData.data.replace("/prorcp/", ""));
|
|
||||||
} else if (rcpData.data.startsWith("/srcrcp/")) {
|
|
||||||
if (server.name === "Superembed" || server.name === "2Embed") {
|
|
||||||
console.warn(`[VidSrc] Skipping SRCRCP for known problematic server: ${server.name}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
streamDetails = await SRCRCPhandler(rcpData.data, rcpUrl); // Pass rcpUrl as referer
|
|
||||||
} else {
|
|
||||||
console.warn(`Unhandled rcp data type for server ${server.name}: ${rcpData.data.substring(0, 50)}`);
|
|
||||||
return null; // Unhandled type
|
|
||||||
}
|
|
||||||
|
|
||||||
if (streamDetails && streamDetails.length > 0) {
|
|
||||||
return {
|
|
||||||
name: title,
|
|
||||||
image: rcpData.metadata.image,
|
|
||||||
mediaId: id,
|
|
||||||
streams: streamDetails,
|
|
||||||
referer: BASEDOM,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
console.warn(`No stream details from handler for server ${server.name} (${rcpData.data})`);
|
|
||||||
return null; // No stream details found by the handler
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Error processing server ${server.name} (${server.dataHash}):`, e);
|
|
||||||
return null; // Error during server processing
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = await Promise.all(serverPromises);
|
|
||||||
// Filter out null results (failed servers or no streams) and add valid results to apiResponse
|
|
||||||
results.forEach(result => {
|
|
||||||
if (result) {
|
|
||||||
apiResponse.push(result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// END MODIFICATION
|
|
||||||
|
|
||||||
return apiResponse;
|
|
||||||
}
|
|
||||||
// --- Main execution logic (conditional) ---
|
|
||||||
async function main() {
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
if (args.length < 2) {
|
|
||||||
console.error("Usage: node vidsrcextractor.js <id> <type>");
|
|
||||||
console.error("Example (movie): node vidsrcextractor.js tt0111161 movie");
|
|
||||||
console.error("Example (series): node vidsrcextractor.js tt0944947:1:1 series"); // Game of Thrones S1E1
|
|
||||||
return 1; // error code
|
|
||||||
}
|
|
||||||
const id = args[0];
|
|
||||||
const type = args[1];
|
|
||||||
if (type !== "movie" && type !== "series") {
|
|
||||||
console.error("Invalid type. Must be 'movie' or 'series'.");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (type === "series" && id.split(':').length < 3) {
|
|
||||||
console.error("Invalid series ID format. Expected 'ttID:season:episode' (e.g., tt0944947:1:1).");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
console.log(`Fetching streams for ID: ${id}, Type: ${type}`);
|
|
||||||
try {
|
|
||||||
const results = await getStreamContent(id, type);
|
|
||||||
if (results && results.length > 0) {
|
|
||||||
console.log("Extracted Data:");
|
|
||||||
console.log(JSON.stringify(results, null, 2));
|
|
||||||
} else {
|
|
||||||
console.log("No streams found or an error occurred.");
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error during extraction process:", error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export the function for use in other modules
|
|
||||||
module.exports = { getStreamContent };
|
|
||||||
|
|
||||||
// Run main only if the script is executed directly
|
|
||||||
if (require.main === module) {
|
|
||||||
main()
|
|
||||||
.then(code => {
|
|
||||||
if (code !== 0) {
|
|
||||||
console.error(`[vidsrcextractor] Finished with non-zero code ${code}`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error("Unhandled error in main execution:", error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
+155
-189
@@ -1,214 +1,180 @@
|
|||||||
/**
|
const axios = require('axios');
|
||||||
* Vixsrc streaming provider integration (standalone)
|
|
||||||
* React Native compatible variant - no external addon framework
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Constants
|
|
||||||
const TMDB_API_KEY = "68e094699525b18a70bab2f86b1fa706";
|
|
||||||
const BASE_URL = 'https://vixsrc.to';
|
const BASE_URL = 'https://vixsrc.to';
|
||||||
let vixsrcUrl;
|
const VIXSRC_HEADERS = {
|
||||||
let subtitleApiUrl;
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150 Safari/537.36',
|
||||||
// Helper function to make HTTP requests with default headers
|
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||||
function makeRequest(url, options = {}) {
|
'Accept-Language': 'en-US,en;q=0.9',
|
||||||
const defaultHeaders = {
|
'Referer': BASE_URL,
|
||||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
'Origin': BASE_URL
|
||||||
'Accept': 'application/json,*/*',
|
};
|
||||||
'Accept-Language': 'en-US,en;q=0.5',
|
|
||||||
'Accept-Encoding': 'gzip, deflate',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
...options.headers
|
|
||||||
};
|
|
||||||
|
|
||||||
return fetch(url, {
|
// Step 1: GET /api/movie/{id} or /api/tv/{id}/{s}/{e} → { src: "/embed/..." }
|
||||||
method: options.method || 'GET',
|
async function fetchApi(url) {
|
||||||
headers: defaultHeaders,
|
try {
|
||||||
...options
|
const response = await axios.get(url, { headers: VIXSRC_HEADERS, timeout: 10000 });
|
||||||
})
|
if (response.status !== 200 || !response.data) return null;
|
||||||
.then(response => {
|
return response.data;
|
||||||
if (!response.ok) {
|
} catch {
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
return null;
|
||||||
}
|
}
|
||||||
return response;
|
}
|
||||||
})
|
|
||||||
.catch(error => {
|
// Step 2: GET BASE_URL + sublink.src → HTML embed page
|
||||||
console.error(`[Vixsrc] Request failed for ${url}: ${error.message}`);
|
async function fetchEmbedPage(suburl) {
|
||||||
throw error;
|
try {
|
||||||
|
const response = await axios.get(BASE_URL + suburl, {
|
||||||
|
headers: { ...VIXSRC_HEADERS, Accept: 'text/html,application/xhtml+xml,*/*' },
|
||||||
|
timeout: 10000,
|
||||||
|
responseType: 'text'
|
||||||
});
|
});
|
||||||
|
if (response.status !== 200) return null;
|
||||||
|
return response.data;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get TMDB info
|
// Step 3: Extract token, expires, playlist URL from embed HTML
|
||||||
function getTmdbInfo(tmdbId, mediaType) {
|
function extractTokenData(html) {
|
||||||
const url = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`;
|
const token = html.match(/token["']\s*:\s*["']([^"']+)/)?.[1];
|
||||||
|
const expires = html.match(/expires["']\s*:\s*["']([^"']+)/)?.[1];
|
||||||
|
const playlist = html.match(/url\s*:\s*["']([^"']+)/)?.[1];
|
||||||
|
|
||||||
return makeRequest(url)
|
if (!token || !expires || !playlist) return null;
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => ({ data }));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to parse M3U8 playlist
|
// Reject expired tokens (with 60s grace period)
|
||||||
// Removed obsolete parseM3U8Playlist implementation
|
if (parseInt(expires, 10) * 1000 - 60_000 < Date.now()) {
|
||||||
|
console.log('[Vixsrc] Token is expired');
|
||||||
// Removed unused helpers getQualityFromResolution and resolveUrl
|
return null;
|
||||||
|
|
||||||
// Helper function to extract stream URL from Vixsrc page
|
|
||||||
function extractStreamFromPage(url, contentType, contentId, seasonNum, episodeNum) {
|
|
||||||
|
|
||||||
if (contentType === 'movie') {
|
|
||||||
vixsrcUrl = `${BASE_URL}/movie/${contentId}`;
|
|
||||||
subtitleApiUrl = `https://sub.wyzie.ru/search?id=${contentId}`;
|
|
||||||
} else {
|
|
||||||
vixsrcUrl = `${BASE_URL}/tv/${contentId}/${seasonNum}/${episodeNum}`;
|
|
||||||
subtitleApiUrl = `https://sub.wyzie.ru/search?id=${contentId}&season=${seasonNum}&episode=${episodeNum}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[Vixsrc] Fetching: ${vixsrcUrl}`);
|
return { token, expires, playlist };
|
||||||
|
}
|
||||||
|
|
||||||
return makeRequest(vixsrcUrl, {
|
// Step 4: Append token params to master URL
|
||||||
|
function buildMasterUrl(tokenData) {
|
||||||
|
const { token, expires, playlist } = tokenData;
|
||||||
|
const sep = playlist.includes('?') ? '&' : '?';
|
||||||
|
return `${playlist}${sep}token=${token}&expires=${expires}&h=1`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: Fetch the master HLS playlist
|
||||||
|
async function fetchPlaylist(masterUrl, pageApiUrl) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(masterUrl, {
|
||||||
|
headers: { ...VIXSRC_HEADERS, Referer: pageApiUrl },
|
||||||
|
timeout: 10000,
|
||||||
|
responseType: 'text'
|
||||||
|
});
|
||||||
|
if (response.status !== 200) return null;
|
||||||
|
return response.data;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6: Parse HLS manifest for quality variants, audio tracks, subtitles
|
||||||
|
function parsePlaylist(content, masterUrl, pageApiUrl) {
|
||||||
|
const sources = [];
|
||||||
|
const subtitles = [];
|
||||||
|
const audioTracks = [];
|
||||||
|
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
// Audio tracks
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.startsWith('#EXT-X-MEDIA:TYPE=AUDIO')) continue;
|
||||||
|
const language = line.match(/LANGUAGE="([^"]+)"/)?.[1] ?? 'unknown';
|
||||||
|
const label = line.match(/NAME="([^"]+)"/)?.[1] ?? 'Audio';
|
||||||
|
audioTracks.push({ language, label });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtitles
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.startsWith('#EXT-X-MEDIA:TYPE=SUBTITLES')) continue;
|
||||||
|
const url = line.match(/URI="([^"]+)"/)?.[1];
|
||||||
|
if (!url) continue;
|
||||||
|
const label = line.match(/NAME="([^"]+)"/)?.[1] ?? 'unknown';
|
||||||
|
subtitles.push({ url, label, format: 'vtt' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quality variants — find the highest resolution
|
||||||
|
const variantRegex = /#EXT-X-STREAM-INF:[^\n]*RESOLUTION=\d+x(\d+)[^\n]*\n([^\n]+)/g;
|
||||||
|
let match;
|
||||||
|
let bestResolution = 0;
|
||||||
|
while ((match = variantRegex.exec(content)) !== null) {
|
||||||
|
const res = parseInt(match[1], 10);
|
||||||
|
if (res > bestResolution) bestResolution = res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestResolution === 0) return { sources: [], subtitles: [] };
|
||||||
|
|
||||||
|
sources.push({
|
||||||
|
name: `Vixsrc - ${bestResolution}p`,
|
||||||
|
title: `Vixsrc - ${bestResolution}p`,
|
||||||
|
url: masterUrl,
|
||||||
|
quality: `${bestResolution}p`,
|
||||||
|
provider: 'Vixsrc',
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
|
'Referer': pageApiUrl,
|
||||||
|
'User-Agent': VIXSRC_HEADERS['User-Agent']
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.then(response => response.text())
|
|
||||||
.then(html => {
|
|
||||||
console.log(`[Vixsrc] HTML length: ${html.length} characters`);
|
|
||||||
|
|
||||||
let masterPlaylistUrl = null;
|
return { sources, subtitles };
|
||||||
|
|
||||||
// Method 1: Look for window.masterPlaylist (primary method)
|
|
||||||
if (html.includes('window.masterPlaylist')) {
|
|
||||||
console.log('[Vixsrc] Found window.masterPlaylist');
|
|
||||||
|
|
||||||
const urlMatch = html.match(/url:\s*['"]([^'"]+)['"]/);
|
|
||||||
const tokenMatch = html.match(/['"]?token['"]?\s*:\s*['"]([^'"]+)['"]/);
|
|
||||||
const expiresMatch = html.match(/['"]?expires['"]?\s*:\s*['"]([^'"]+)['"]/);
|
|
||||||
|
|
||||||
if (urlMatch && tokenMatch && expiresMatch) {
|
|
||||||
const baseUrl = urlMatch[1];
|
|
||||||
const token = tokenMatch[1];
|
|
||||||
const expires = expiresMatch[1];
|
|
||||||
|
|
||||||
console.log('[Vixsrc] Extracted tokens:');
|
|
||||||
console.log(` - Base URL: ${baseUrl}`);
|
|
||||||
console.log(` - Token: ${token.substring(0, 20)}...`);
|
|
||||||
console.log(` - Expires: ${expires}`);
|
|
||||||
|
|
||||||
// Construct the master playlist URL
|
|
||||||
if (baseUrl.includes('?b=1')) {
|
|
||||||
masterPlaylistUrl = `${baseUrl}&token=${token}&expires=${expires}&h=1&lang=en`;
|
|
||||||
} else {
|
|
||||||
masterPlaylistUrl = `${baseUrl}?token=${token}&expires=${expires}&h=1&lang=en`;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Vixsrc] Constructed master playlist URL: ${masterPlaylistUrl}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 2: Look for direct .m3u8 URLs
|
|
||||||
if (!masterPlaylistUrl) {
|
|
||||||
const m3u8Match = html.match(/(https?:\/\/[^'"\s]+\.m3u8[^'"\s]*)/);
|
|
||||||
if (m3u8Match) {
|
|
||||||
masterPlaylistUrl = m3u8Match[1];
|
|
||||||
console.log('[Vixsrc] Found direct .m3u8 URL:', masterPlaylistUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 3: Look for stream URLs in script tags
|
|
||||||
if (!masterPlaylistUrl) {
|
|
||||||
const scriptMatches = html.match(/<script[^>]*>(.*?)<\/script>/gs);
|
|
||||||
if (scriptMatches) {
|
|
||||||
for (const script of scriptMatches) {
|
|
||||||
const streamMatch = script.match(/['"]?(https?:\/\/[^'"\s]+(?:\.m3u8|playlist)[^'"\s]*)/);
|
|
||||||
if (streamMatch) {
|
|
||||||
masterPlaylistUrl = streamMatch[1];
|
|
||||||
console.log('[Vixsrc] Found stream in script:', masterPlaylistUrl);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!masterPlaylistUrl) {
|
|
||||||
console.log('[Vixsrc] No master playlist URL found');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { masterPlaylistUrl, subtitleApiUrl };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get subtitles
|
async function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
|
||||||
function getSubtitles(subtitleApiUrl) {
|
|
||||||
return makeRequest(subtitleApiUrl)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(subtitleData => {
|
|
||||||
// Find English subtitle track (same logic as original)
|
|
||||||
let subtitleTrack = subtitleData.find(track =>
|
|
||||||
track.display.includes('English') && (track.encoding === 'ASCII' || track.encoding === 'UTF-8')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!subtitleTrack) {
|
|
||||||
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1252');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subtitleTrack) {
|
|
||||||
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1250');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subtitleTrack) {
|
|
||||||
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP850');
|
|
||||||
}
|
|
||||||
|
|
||||||
const subtitles = subtitleTrack ? subtitleTrack.url : '';
|
|
||||||
console.log(subtitles ? `[Vixsrc] Found subtitles: ${subtitles}` : '[Vixsrc] No English subtitles found');
|
|
||||||
return subtitles;
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.log('[Vixsrc] Subtitle fetch failed:', error.message);
|
|
||||||
return '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main function to get streams
|
|
||||||
function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
|
|
||||||
console.log(`[Vixsrc] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
|
console.log(`[Vixsrc] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
|
||||||
|
|
||||||
return getTmdbInfo(tmdbId, mediaType)
|
// Build API URL
|
||||||
.then(() => extractStreamFromPage(null, mediaType, tmdbId, seasonNum, episodeNum))
|
let apiUrl;
|
||||||
.then(streamData => {
|
if (mediaType === 'movie') {
|
||||||
if (!streamData) {
|
apiUrl = `${BASE_URL}/api/movie/${tmdbId}`;
|
||||||
console.log('[Vixsrc] No stream data found');
|
} else {
|
||||||
return [];
|
apiUrl = `${BASE_URL}/api/tv/${tmdbId}/${seasonNum}/${episodeNum}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { masterPlaylistUrl, subtitleApiUrl } = streamData;
|
console.log(`[Vixsrc] Step 1 - Calling API: ${apiUrl}`);
|
||||||
|
const apiData = await fetchApi(apiUrl);
|
||||||
|
if (!apiData || !apiData.src) {
|
||||||
|
console.log('[Vixsrc] No src returned from API');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
console.log(`[Vixsrc] Step 2 - Fetching embed page: ${BASE_URL}${apiData.src}`);
|
||||||
|
|
||||||
// Return single master playlist with Auto quality
|
const html = await fetchEmbedPage(apiData.src);
|
||||||
console.log('[Vixsrc] Returning master playlist with Auto quality...');
|
if (!html) {
|
||||||
|
console.log('[Vixsrc] Failed to fetch embed page');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
console.log(`[Vixsrc] Embed HTML length: ${html.length} characters`);
|
||||||
|
|
||||||
// Get subtitles
|
const tokenData = extractTokenData(html);
|
||||||
return getSubtitles(subtitleApiUrl)
|
if (!tokenData) {
|
||||||
.then(subtitles => {
|
console.log('[Vixsrc] Could not extract token/expires/playlist from embed HTML');
|
||||||
// Return single stream with master playlist
|
return [];
|
||||||
const tmdbStreams = [{
|
}
|
||||||
name: "Vixsrc",
|
|
||||||
title: "Auto Quality Stream",
|
|
||||||
url: masterPlaylistUrl,
|
|
||||||
quality: '1080p',
|
|
||||||
provider: 'Vidsrc',
|
|
||||||
headers: {
|
|
||||||
'Referer': vixsrcUrl,
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
|
|
||||||
console.log('[Vixsrc] Successfully processed 1 stream with Auto quality');
|
const masterUrl = buildMasterUrl(tokenData);
|
||||||
return tmdbStreams;
|
console.log(`[Vixsrc] Step 3 - Master URL: ${masterUrl}`);
|
||||||
});
|
|
||||||
})
|
const playlistContent = await fetchPlaylist(masterUrl, apiUrl);
|
||||||
.catch(error => {
|
if (!playlistContent) {
|
||||||
console.error(`[Vixsrc] Error in getVixsrcStreams: ${error.message}`);
|
console.log('[Vixsrc] Failed to fetch HLS playlist');
|
||||||
return [];
|
return [];
|
||||||
});
|
}
|
||||||
|
|
||||||
|
const { sources, subtitles } = parsePlaylist(playlistContent, masterUrl, apiUrl);
|
||||||
|
|
||||||
|
if (sources.length === 0) {
|
||||||
|
console.log('[Vixsrc] No streams found in HLS playlist');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Vixsrc] Successfully extracted ${sources.length} stream(s). Subtitles: ${subtitles.length}`);
|
||||||
|
return sources;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { getVixsrcStreams };
|
module.exports = { getVixsrcStreams };
|
||||||
|
|||||||
+3
-3
@@ -29,15 +29,15 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<footer class="side-footer">Credits to :<a class="hint" style="margin-top: .5rem; color: var(--primary-fg);" href="https://github.com/tapframe/NuvioStreamsAddon" target="_blank">NuvioStreamsAddon for Stremio</a></footer>
|
<footer class="side-footer">Credits to :<a class="hint" style="margin-top: .5rem; color: var(--primary-fg);" href="https://github.com/tapframe/NuvioStreamsAddon" target="_blank">NuvioStreamsAddon for Stremio</a></footer>
|
||||||
<div class="bmc-widget-container">
|
<div class="bmc-widget-container" style="text-align: center;">
|
||||||
<script type="text/javascript" src="https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js" data-name="bmc-button" data-slug="K3QSoR2" data-color="#5F7FFF" data-emoji="☕" data-font="Bree" data-text="Buy me a coffee" data-outline-color="#000000" data-font-color="#ffffff" data-coffee-color="#FFDD00" ></script>
|
<iframe src="https://github.com/sponsors/Inside4ndroid/button" title="Sponsor Inside4ndroid" height="32" width="114" style="border:0;border-radius:6px;margin-top:.5rem;"></iframe>
|
||||||
</div>
|
</div>
|
||||||
<footer class="side-footer">Made with ❤️ by Inside4ndroid</footer>
|
<footer class="side-footer">Made with ❤️ by Inside4ndroid</footer>
|
||||||
</aside>
|
</aside>
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<div id="bootWarn" class="alert warning" style="display:none;">No FEBBOX_COOKIES configured. Showbox / PStream streams may be unavailable.</div>
|
<div id="bootWarn" class="alert warning" style="display:none;">No FEBBOX_COOKIES configured. Showbox / PStream streams may be unavailable.</div>
|
||||||
<div id="cacheDisabledBanner" class="alert info" style="display:none;">Cache is <strong>DISABLED</strong>; expect slower responses & higher source load.</div>
|
<div id="cacheDisabledBanner" class="alert info" style="display:none;">Cache is <strong>DISABLED</strong>; expect slower responses & higher source load.</div>
|
||||||
<div id="tmdbMissingBanner" class="alert warning" style="display:none;">No TMDB API Keys configured. Metadata-dependent providers (Showbox, 4khdhub, MoviesMod, MP4Hydra) may degrade.</div>
|
<div id="tmdbMissingBanner" class="alert warning" style="display:none;">No TMDB API Keys configured. Metadata-dependent providers (Showbox, 4khdhub) may degrade.</div>
|
||||||
<form id="cfgForm" onsubmit="return false;" class="panels">
|
<form id="cfgForm" onsubmit="return false;" class="panels">
|
||||||
<section id="panel-status" class="panel">
|
<section id="panel-status" class="panel">
|
||||||
<h1>Server Status<button style="margin-left: 1rem;" id="refreshStatusBtn" type="button">Refresh Status</button></h1>
|
<h1>Server Status<button style="margin-left: 1rem;" id="refreshStatusBtn" type="button">Refresh Status</button></h1>
|
||||||
|
|||||||
+2
-13
@@ -153,17 +153,12 @@ function fillForm(data){
|
|||||||
const tmdbMissing = !((override.tmdbApiKeys && override.tmdbApiKeys.length) || (merged.tmdbApiKeys && merged.tmdbApiKeys.length) || override.tmdbApiKey || merged.tmdbApiKey);
|
const tmdbMissing = !((override.tmdbApiKeys && override.tmdbApiKeys.length) || (merged.tmdbApiKeys && merged.tmdbApiKeys.length) || override.tmdbApiKey || merged.tmdbApiKey);
|
||||||
const tmdbProvidersEnabled = [
|
const tmdbProvidersEnabled = [
|
||||||
merged.enableShowboxProvider !== false,
|
merged.enableShowboxProvider !== false,
|
||||||
merged.enable4khdhubProvider !== false,
|
merged.enable4khdhubProvider !== false
|
||||||
merged.enableMoviesmodProvider !== false,
|
|
||||||
merged.enableMp4hydraProvider !== false
|
|
||||||
].some(Boolean);
|
].some(Boolean);
|
||||||
tmdbBanner.style.display = (tmdbMissing && tmdbProvidersEnabled)? 'block':'none';
|
tmdbBanner.style.display = (tmdbMissing && tmdbProvidersEnabled)? 'block':'none';
|
||||||
}
|
}
|
||||||
const advBools = [
|
const advBools = [
|
||||||
['enable4khdhubProvider','adv_enable4khdhub'],
|
['enable4khdhubProvider','adv_enable4khdhub'],
|
||||||
['enableMoviesmodProvider','adv_enableMoviesmod'],
|
|
||||||
['enableMp4hydraProvider','adv_enableMp4hydra'],
|
|
||||||
['enableVidzeeProvider','adv_enableVidzee'],
|
|
||||||
['enableVixsrcProvider','adv_enableVixsrc'],
|
['enableVixsrcProvider','adv_enableVixsrc'],
|
||||||
['enableProxy','adv_enableProxy'],
|
['enableProxy','adv_enableProxy'],
|
||||||
['disableCache','adv_disableCache'],
|
['disableCache','adv_disableCache'],
|
||||||
@@ -245,9 +240,6 @@ async function save(){
|
|||||||
}
|
}
|
||||||
const boolMap = {
|
const boolMap = {
|
||||||
adv_enable4khdhub:'enable4khdhubProvider',
|
adv_enable4khdhub:'enable4khdhubProvider',
|
||||||
adv_enableMoviesmod:'enableMoviesmodProvider',
|
|
||||||
adv_enableMp4hydra:'enableMp4hydraProvider',
|
|
||||||
adv_enableVidzee:'enableVidzeeProvider',
|
|
||||||
adv_enableVixsrc:'enableVixsrcProvider',
|
adv_enableVixsrc:'enableVixsrcProvider',
|
||||||
adv_enableProxy:'enableProxy',
|
adv_enableProxy:'enableProxy',
|
||||||
adv_disableCache:'disableCache',
|
adv_disableCache:'disableCache',
|
||||||
@@ -338,9 +330,6 @@ async function clearAll(){
|
|||||||
// Provider enable flags default to true
|
// Provider enable flags default to true
|
||||||
enableShowboxProvider: true,
|
enableShowboxProvider: true,
|
||||||
enable4khdhubProvider: true,
|
enable4khdhubProvider: true,
|
||||||
enableMoviesmodProvider: true,
|
|
||||||
enableMp4hydraProvider: true,
|
|
||||||
enableVidzeeProvider: true,
|
|
||||||
enableVixsrcProvider: true,
|
enableVixsrcProvider: true,
|
||||||
enableProxy: false,
|
enableProxy: false,
|
||||||
// Other defaults
|
// Other defaults
|
||||||
@@ -449,7 +438,7 @@ async function loadServerStatus(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function runProviderFunctionalChecks(){
|
async function runProviderFunctionalChecks(){
|
||||||
const targetTmdb = '293660'; // TMDB ID provided
|
const targetTmdb = '550'; // TMDB ID provided
|
||||||
const provEnhanced = document.getElementById('statusProvidersEnhanced');
|
const provEnhanced = document.getElementById('statusProvidersEnhanced');
|
||||||
const summary = document.getElementById('providerCheckSummary');
|
const summary = document.getElementById('providerCheckSummary');
|
||||||
if(!provEnhanced) return;
|
if(!provEnhanced) return;
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
1758225404146
|
1778930133625
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"users": [
|
"users": [
|
||||||
{
|
{
|
||||||
"username": "admin",
|
"username": "admin",
|
||||||
"hash": "7fffd5f131a733035eaf443cd99905c0:f1cfa876f9b84fede40cd618a4352221f7ab36e4f8bead432ba5d866b06cb9fcc2903ce623d8286bb541f8d28490fc692d0bf3fadda08002265374915cdb27b5"
|
"hash": "3c2efa431b6de0bee19847236e2c296b:d88ccc2ee950eda254a03d073bc55bbc8d0efc263c93c4e20307154dc90fadbc3141dcc9556a150b39b52088bb8ad06b120bc8832d7d8dc35918e710803d207c"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+1
-1
@@ -57,7 +57,7 @@ function getProviderNames() {
|
|||||||
const providersDir = path.join(process.cwd(), 'providers');
|
const providersDir = path.join(process.cwd(), 'providers');
|
||||||
if (!fs.existsSync(providersDir)) return [];
|
if (!fs.existsSync(providersDir)) return [];
|
||||||
return fs.readdirSync(providersDir)
|
return fs.readdirSync(providersDir)
|
||||||
.filter(file => file.endsWith('.js') && file !== 'registry.js' && file !== 'vidsrcextractor.js')
|
.filter(file => file.endsWith('.js') && file !== 'registry.js')
|
||||||
.map(file => path.parse(file).name.toLowerCase());
|
.map(file => path.parse(file).name.toLowerCase());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[config] Failed to scan providers directory:', e.message);
|
console.warn('[config] Failed to scan providers directory:', e.message);
|
||||||
|
|||||||
@@ -5,9 +5,6 @@
|
|||||||
"enableShowboxProvider": true,
|
"enableShowboxProvider": true,
|
||||||
"febboxCookies": [],
|
"febboxCookies": [],
|
||||||
"enable4khdhubProvider": true,
|
"enable4khdhubProvider": true,
|
||||||
"enableMoviesmodProvider": true,
|
|
||||||
"enableMp4hydraProvider": true,
|
|
||||||
"enableVidzeeProvider": true,
|
|
||||||
"enableVixsrcProvider": true,
|
"enableVixsrcProvider": true,
|
||||||
"enableProxy": false,
|
"enableProxy": false,
|
||||||
"disableCache": false,
|
"disableCache": false,
|
||||||
@@ -15,5 +12,10 @@
|
|||||||
"disableUrlValidation": false,
|
"disableUrlValidation": false,
|
||||||
"disable4khdhubUrlValidation": false,
|
"disable4khdhubUrlValidation": false,
|
||||||
"excludeCodecsRaw": "{\"excludeDV\":false,\"excludeHDR\":false}",
|
"excludeCodecsRaw": "{\"excludeDV\":false,\"excludeHDR\":false}",
|
||||||
"minQualitiesRaw": "all"
|
"minQualitiesRaw": "all",
|
||||||
}
|
"enableUhdmoviesProvider": false,
|
||||||
|
"enableVidlinkProvider": true,
|
||||||
|
"enableVideasyProvider": true,
|
||||||
|
"enableLordflixProvider": true,
|
||||||
|
"enableNotorrentProvider": true
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user