diff --git a/CHANGELOG.md b/CHANGELOG.md
index c57aa0a..565e457 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,33 @@
All notable changes to this project will be documented in this file.
+## [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
### Removed
diff --git a/README.md b/README.md
index 0079507..6ec1940 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
-
+
@@ -34,7 +34,7 @@
## ✨ Features
- **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, MoviesMod, VidZee, 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`).
- **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`.
@@ -225,12 +225,16 @@ 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`.
### Current Built-in Providers
+- `showbox` - Showbox/FebBox streams (requires FebBox JWT cookie)
- `4khdhub` - 4KHDHub streams
-- `moviesmod` - MoviesMod streams
-- `mp4hydra` - MP4Hydra streams
+- `moviesmod` - MoviesMod streams
- `vidzee` - VidZee streams
-- `vixsrc` - Vixsrc streams
-- `uhdmovies` - UHD Movies streams
+- `vixsrc` - VixSrc streams
+- `videasy` - Videasy streams (10 servers via enc-dec.app)
+- `vidlink` - Vidlink streams (via enc-dec.app)
+- `lordflix` - LordFlix streams (9 servers via enc-dec.app)
+- `notorrent` - NoTorrent streams (Stremio addon API)
+- `dahmermovies` - DahmerMovies streams (direct file links)
### Adding a New Provider
@@ -240,12 +244,16 @@ The API supports a plugin system. Drop a new provider file in the `providers/` f
```js
// providers/registry.js
const providerFunctionMap = {
+ 'Showbox.js': 'getStreamsFromTmdbId',
'4khdhub.js': 'get4KHDHubStreams',
'moviesmod.js': 'getMoviesModStreams',
- 'MP4Hydra.js': 'getMP4HydraStreams',
'VidZee.js': 'getVidZeeStreams',
'vixsrc.js': 'getVixsrcStreams',
- 'uhdmovies.js': 'getUHDMoviesStreams',
+ 'videasy.js': 'getVideasyStreams',
+ 'vidlink.js': 'getVidlinkStreams',
+ 'lordflix.js': 'getLordflixStreams',
+ 'notorrent.js': 'getNotorrentStreams',
+ 'dahmermovies.js': 'getDahmermoviesStreams',
'yourprovider.js': 'getYourproviderStreams'
};
```
@@ -378,7 +386,11 @@ PRs welcome. Keep changes focused and avoid unrelated formatting churn. For new
## ❤️ Sponsorship
If this project helps you, consider sponsoring to support continued development & maintenance:
-➡️ **GitHub Sponsors:** https://github.com/sponsors/Inside4ndroid
+]*>([\s\S]*?)<\/tr>/gi;
+ let match;
+ while ((match = rowRegex.exec(html)) !== null) {
+ const rowContent = match[1];
+ const linkMatch = rowContent.match(/]*href=["']([^"']*)["'][^>]*>([^<]*)<\/a>/i);
+ const sizeMatch = rowContent.match(/]*>(\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 };
diff --git a/providers/lordflix.js b/providers/lordflix.js
new file mode 100644
index 0000000..6b92f27
--- /dev/null
+++ b/providers/lordflix.js
@@ -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://network.hasta-la-vista.site';
+const MULTI_DECRYPT_API = 'https://enc-dec.app/api';
+const SERVERS = ['Berlin', 'Tokyo', 'Bogota', 'Oslo', 'Luna', 'LordFlix', 'Sakura', 'Rio', 'Ativa'];
+
+// 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 };
diff --git a/providers/moviesmod.js b/providers/moviesmod.js
index a0cb631..d3d054e 100644
--- a/providers/moviesmod.js
+++ b/providers/moviesmod.js
@@ -202,7 +202,7 @@ async function extractDownloadLinks(moviePageUrl) {
if (header.is('h3') && headerText.toLowerCase().includes('season')) {
// TV Show Logic
- const linkElements = blockContent.find('a.maxbutton-episode-links, a.maxbutton-batch-zip');
+ const linkElements = blockContent.find('a.maxbutton-episode-links, a.maxbutton-batch-zip, a.maxbutton-download-links, a[href*="links.modpro.blog"], a[href*="posts.modpro.blog"]');
linkElements.each((j, linkEl) => {
const buttonText = $(linkEl).text().trim();
const linkUrl = $(linkEl).attr('href');
@@ -214,15 +214,17 @@ async function extractDownloadLinks(moviePageUrl) {
}
});
} else if (header.is('h4')) {
- // Movie Logic
- const linkElement = blockContent.find('a[href*="modrefer.in"]').first();
+ // Movie Logic - site changed from modrefer.in to links.modpro.blog
+ const linkElement = blockContent.find('a.maxbutton-download-links, a[href*="links.modpro.blog"], a[href*="posts.modpro.blog"], a[href*="modrefer.in"]').first();
if (linkElement.length > 0) {
const link = linkElement.attr('href');
const cleanQuality = extractQuality(headerText);
- links.push({
- quality: cleanQuality,
- url: link
- });
+ if (link && cleanQuality && cleanQuality !== 'Unknown') {
+ links.push({
+ quality: cleanQuality,
+ url: link
+ });
+ }
}
}
});
@@ -239,7 +241,35 @@ async function resolveIntermediateLink(initialUrl, refererUrl, quality) {
try {
const urlObject = new URL(initialUrl);
- if (urlObject.hostname.includes('dramadrip.com')) {
+ if (urlObject.hostname.includes('links.modpro.blog') || urlObject.hostname.includes('posts.modpro.blog')) {
+ const { data } = await makeRequest(initialUrl, { headers: { 'Referer': refererUrl } });
+ const $ = cheerio.load(data);
+ const finalLinks = [];
+
+ // Primary: scoped to .entry-content
+ $('.entry-content a[href*="driveseed.org"], .entry-content a[href*="cloud.unblockedgames.world"], .entry-content a[href*="tech.creativeexpressionsblog.com"], .entry-content a[href*="tech.examzculture.in"], .entry-content a[href*="tech.unblockedgames.world"], .entry-content a[href*="tech.examdegree.site"]').each((i, el) => {
+ const link = $(el).attr('href');
+ const text = $(el).text().trim();
+ if (link && text && !text.toLowerCase().includes('batch')) {
+ finalLinks.push({ server: text.replace(/\s+/g, ' '), url: link });
+ }
+ });
+
+ // Fallback: page-wide search
+ if (finalLinks.length === 0) {
+ $('a[href*="driveseed.org"], a[href*="cloud.unblockedgames.world"], a[href*="tech.creativeexpressionsblog.com"], a[href*="tech.examzculture.in"], a[href*="tech.unblockedgames.world"], a[href*="tech.examdegree.site"]').each((i, el) => {
+ const link = $(el).attr('href');
+ const text = $(el).text().trim();
+ if (link && text && !text.toLowerCase().includes('batch')) {
+ finalLinks.push({ server: text.replace(/\s+/g, ' ') || 'Download Link', url: link });
+ }
+ });
+ }
+
+ console.log(`[MoviesMod] Found ${finalLinks.length} links from ${urlObject.hostname}`);
+ return finalLinks;
+
+ } else if (urlObject.hostname.includes('dramadrip.com')) {
const { data: dramaData } = await makeRequest(initialUrl, { headers: { 'Referer': refererUrl } });
const $$ = cheerio.load(dramaData);
@@ -779,18 +809,38 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN
}
const { default: fetch } = await import('node-fetch');
- const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}&language=en-US`;
+ const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}&language=en-US&append_to_response=external_ids`;
const tmdbDetails = await (await fetch(tmdbUrl)).json();
const title = mediaType === 'tv' ? tmdbDetails.name : tmdbDetails.title;
const year = mediaType === 'tv' ? tmdbDetails.first_air_date?.substring(0, 4) : tmdbDetails.release_date?.substring(0, 4);
+ const imdbId = tmdbDetails.external_ids?.imdb_id || null;
if (!title) throw new Error('Could not get title from TMDB');
- console.log(`[MoviesMod] Found metadata: ${title} (${year})`);
- const searchResults = await searchMoviesMod(title);
+ console.log(`[MoviesMod] Found metadata: ${title} (${year})${imdbId ? ` [IMDB: ${imdbId}]` : ''}`);
+
+ // Try IMDb ID search first — more reliable than title matching
+ let searchResults = [];
+ if (imdbId) {
+ const imdbQuery = (mediaType === 'tv' && seasonNum) ? `${imdbId} Season ${seasonNum}` : imdbId;
+ console.log(`[MoviesMod] Trying IMDB ID search: ${imdbQuery}`);
+ searchResults = await searchMoviesMod(imdbQuery);
+ if (searchResults.length > 0) {
+ console.log(`[MoviesMod] Found ${searchResults.length} results via IMDB ID`);
+ }
+ }
+
+ if (searchResults.length === 0) {
+ console.log(`[MoviesMod] Falling back to title search: ${title}`);
+ searchResults = await searchMoviesMod(title);
+ if (searchResults.length === 0 && mediaType === 'tv' && seasonNum) {
+ searchResults = await searchMoviesMod(`${title} Season ${seasonNum}`);
+ }
+ }
+
if (searchResults.length === 0) throw new Error(`No search results found for "${title}"`);
- // --- NEW: Use string similarity to find the best match ---
+ // --- Use string similarity to find the best match ---
const titles = searchResults.map(r => r.title);
const bestMatch = findBestMatch(title, titles);
@@ -853,21 +903,27 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN
const finalLinks = await resolveIntermediateLink(link.url, selectedResult.url, link.quality);
if (!finalLinks || finalLinks.length === 0) return null;
- // Resolve to driveseed redirect URLs (intermediate step) for caching
+ // Resolve to host redirect URLs (intermediate step) for caching
const driveseedPromises = finalLinks.map(async (targetLink) => {
try {
let currentUrl = targetLink.url;
- // Handle SID links if they appear
- if (currentUrl.includes('tech.unblockedgames.world') || currentUrl.includes('tech.creativeexpressionsblog.com') || currentUrl.includes('tech.examzculture.in')) {
+ // Handle SID links (all known tech-unblock domains)
+ if (
+ currentUrl.includes('tech.unblockedgames.world') ||
+ currentUrl.includes('cloud.unblockedgames.world') ||
+ currentUrl.includes('tech.creativeexpressionsblog.com') ||
+ currentUrl.includes('tech.examzculture.in') ||
+ currentUrl.includes('tech.examdegree.site')
+ ) {
const resolvedUrl = await resolveTechUnblockedLink(currentUrl);
if (!resolvedUrl) return null;
currentUrl = resolvedUrl;
}
- // Only process if it's a driveseed URL
- if (currentUrl && currentUrl.includes('driveseed.org')) {
- console.log(`[MoviesMod] Caching driveseed redirect URL for ${targetLink.server}: ${currentUrl}`);
+ // Accept driveseed.org or driveleech.net (same operator, same page structure)
+ if (currentUrl && (currentUrl.includes('driveseed.org') || currentUrl.includes('driveleech.net'))) {
+ console.log(`[MoviesMod] Resolved host URL for ${targetLink.server}: ${currentUrl}`);
return { ...targetLink, driveseedRedirectUrl: currentUrl };
}
return null;
@@ -932,9 +988,11 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN
const { driveseedRedirectUrl } = targetLink;
if (!driveseedRedirectUrl) return null;
- // Process the cached driveseed redirect URL
- if (driveseedRedirectUrl.includes('driveseed.org')) {
- // First, resolve the driveseed redirect URL to get the final file page URL
+ // Process the cached host redirect URL (driveseed.org or driveleech.net)
+ if (driveseedRedirectUrl.includes('driveseed.org') || driveseedRedirectUrl.includes('driveleech.net')) {
+ const hostOrigin = new URL(driveseedRedirectUrl).origin;
+
+ // Resolve the redirect URL to get the final file page URL
const response = await makeRequest(driveseedRedirectUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
@@ -949,7 +1007,8 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN
let $; // single declaration for cheerio instance
if (redirectMatch && redirectMatch[1]) {
const finalPath = redirectMatch[1];
- finalFilePageUrl = `https://driveseed.org${finalPath}`;
+ // Use the dynamic host origin — works for both driveseed.org and driveleech.net
+ finalFilePageUrl = finalPath.startsWith('http') ? finalPath : `${hostOrigin}${finalPath}`;
console.log(`[MoviesMod] Resolved redirect to final file page: ${finalFilePageUrl}`);
// Load the final file page
@@ -996,7 +1055,7 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN
downloadOptions.push({
title: 'Resume Cloud',
type: 'resume',
- url: `https://driveseed.org${resumeCloudLink}`,
+ url: resumeCloudLink.startsWith('http') ? resumeCloudLink : `${hostOrigin}${resumeCloudLink}`,
priority: 1
});
}
diff --git a/providers/notorrent.js b/providers/notorrent.js
new file mode 100644
index 0000000..a6356ca
--- /dev/null
+++ b/providers/notorrent.js
@@ -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 };
diff --git a/providers/registry.js b/providers/registry.js
index f6e0804..c976763 100644
--- a/providers/registry.js
+++ b/providers/registry.js
@@ -10,10 +10,13 @@ const providerFunctionMap = {
'Showbox.js': 'getStreamsFromTmdbId',
'4khdhub.js': 'get4KHDHubStreams',
'moviesmod.js': 'getMoviesModStreams',
- 'MP4Hydra.js': 'getMP4HydraStreams',
'VidZee.js': 'getVidZeeStreams',
'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
@@ -27,7 +30,7 @@ async function getEffectiveCookies() {
function getProviderFiles() {
const providersDir = path.join(__dirname);
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 => ({
name: path.parse(file).name.toLowerCase(),
file: file,
@@ -110,28 +113,29 @@ function createFetchFunction(providerInfo) {
};
}
-// Initialize providers
+// Initialize providers - always load all; enabled state is checked dynamically from live config
const providerFiles = getProviderFiles();
const providers = [];
for (const providerInfo of providerFiles) {
- const enableFlag = `enable${providerInfo.name.charAt(0).toUpperCase() + providerInfo.name.slice(1)}Provider`;
- const enabled = config[enableFlag] !== false;
-
- if (enabled) {
- providers.push({
- name: providerInfo.name,
- enabled: true,
- fetch: createFetchFunction(providerInfo)
- });
- console.log(`[registry] ${providerInfo.name} provider enabled`);
- } else {
- console.log(`[registry] ${providerInfo.name} disabled via config`);
- }
+ providers.push({
+ name: providerInfo.name,
+ fetch: createFetchFunction(providerInfo)
+ });
+ console.log(`[registry] ${providerInfo.name} provider loaded`);
}
-function listProviders() { return providers.map(p => ({ name: p.name, enabled: p.enabled })); }
-function getProvider(name) { return providers.find(p => p.name === name.toLowerCase()) || null; }
+function isProviderEnabled(name) {
+ 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; }
diff --git a/providers/uhdmovies.js b/providers/uhdmovies.js
deleted file mode 100644
index d2c7ff2..0000000
--- a/providers/uhdmovies.js
+++ /dev/null
@@ -1,1418 +0,0 @@
-const axios = require('axios');
-const cheerio = require('cheerio');
-const { URLSearchParams, URL } = require('url');
-const FormData = require('form-data');
-const { CookieJar } = require('tough-cookie');
-const fs = require('fs').promises;
-const path = require('path');
-
-// Dynamic import for axios-cookiejar-support
-let axiosCookieJarSupport = null;
-const getAxiosCookieJarSupport = async () => {
- if (!axiosCookieJarSupport) {
- axiosCookieJarSupport = await import('axios-cookiejar-support');
- }
- return axiosCookieJarSupport;
-};
-
-// All requests are direct
-
-// --- Domain Fetching ---
-let uhdMoviesDomain = 'https://uhdmovies.email'; // Fallback domain
-let domainCacheTimestamp = 0;
-const DOMAIN_CACHE_TTL = 4 * 60 * 60 * 1000; // 4 hours
-
-async function getUHDMoviesDomain() {
- const now = Date.now();
- if (now - domainCacheTimestamp < DOMAIN_CACHE_TTL) {
- return uhdMoviesDomain;
- }
-
- try {
- console.log('[UHDMovies] Fetching latest domain...');
- const response = await makeRequest('https://raw.githubusercontent.com/phisher98/TVVVV/refs/heads/main/domains.json', { timeout: 10000 });
- if (response.data && response.data.UHDMovies) {
- uhdMoviesDomain = response.data.UHDMovies;
- domainCacheTimestamp = now;
- console.log(`[UHDMovies] Updated domain to: ${uhdMoviesDomain}`);
- } else {
- console.warn('[UHDMovies] Domain JSON fetched, but "UHDMovies" key was not found. Using fallback.');
- }
- } catch (error) {
- console.error(`[UHDMovies] Failed to fetch latest domain, using fallback. Error: ${error.message}`);
- }
- return uhdMoviesDomain;
-}
-
-// Constants
-const { getTmdbApiKey } = require('../utils/tmdbKey');
-function getUhdMoviesKey() {
- const k = getTmdbApiKey();
- if (!k) console.warn('[UHDMovies] TMDB API key missing; certain metadata features skipped.');
- return k;
-}
-
-// --- Caching Configuration ---
-const CACHE_ENABLED = process.env.DISABLE_CACHE !== 'true'; // Set to true to disable caching for this provider
-console.log(`[UHDMovies] Internal cache is ${CACHE_ENABLED ? 'enabled' : 'disabled'}.`);
-const CACHE_DIR = process.env.VERCEL ? path.join('/tmp', '.uhd_cache') : path.join(__dirname, '.cache', 'uhdmovies'); // Cache directory inside providers/uhdmovies
-
-// Caching layer simplified: placeholders only (extend for FS persistence if needed)
-
-// --- Caching Helper Functions ---
-const ensureCacheDir = async () => {
- if (!CACHE_ENABLED) return;
- try {
- await fs.mkdir(CACHE_DIR, { recursive: true });
- } catch (error) {
- if (error.code !== 'EEXIST') {
- console.error(`[UHDMovies Cache] Error creating cache directory: ${error.message}`);
- }
- }
-};
-
-const getFromCache = async (key) => {
- if (!CACHE_ENABLED) return null;
- return null; // no-op
-};
-
-const saveToCache = async (key, data) => {
- if (!CACHE_ENABLED) return;
- // no-op (placeholder for future FS caching)
-};
-
-// Initialize cache directory on startup
-ensureCacheDir();
-
-// Configure axios with headers to mimic a browser
-const createAxiosInstance = () => {
- const config = {
- headers: {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
- 'Accept-Language': 'en-US,en;q=0.5',
- 'Connection': 'keep-alive',
- 'Upgrade-Insecure-Requests': '1',
- 'Cache-Control': 'max-age=0'
- },
- timeout: 30000
- };
-
- return axios.create(config);
-};
-
-const axiosInstance = createAxiosInstance();
-
-// Request helper
-const makeRequest = async (url, options = {}) => {
- console.log(`[UHDMovies] Making direct request to: ${url}`);
- return axiosInstance.get(url, options);
-};
-
-// Removed unused in-memory cache structure (uhdMoviesCache)
-
-// Function to search for movies
-async function searchMovies(query) {
- try {
- const baseUrl = await getUHDMoviesDomain();
- console.log(`[UHDMovies] Searching for: ${query}`);
- const searchUrl = `${baseUrl}/search/${encodeURIComponent(query)}`;
-
- const response = await makeRequest(searchUrl);
- const $ = cheerio.load(response.data);
-
- const searchResults = [];
-
- // New logic for grid-based search results
- $('article.gridlove-post').each((index, element) => {
- const linkElement = $(element).find('a[href*="/download-"]');
- if (linkElement.length > 0) {
- const link = linkElement.first().attr('href');
- // Prefer the 'title' attribute, fallback to h1 text
- const title = linkElement.first().attr('title') || $(element).find('h1.sanket').text().trim();
-
- if (link && title && !searchResults.some(item => item.link === link)) {
- searchResults.push({
- title,
- link: link.startsWith('http') ? link : `${baseUrl}${link}`
- });
- }
- }
- });
-
- // Fallback for original list-based search if new logic fails
- if (searchResults.length === 0) {
- console.log('[UHDMovies] Grid search logic found no results, trying original list-based logic...');
- $('a[href*="/download-"]').each((index, element) => {
- const link = $(element).attr('href');
- // Avoid duplicates by checking if link already exists in results
- if (link && !searchResults.some(item => item.link === link)) {
- const title = $(element).text().trim();
- if (title) {
- searchResults.push({
- title,
- link: link.startsWith('http') ? link : `${baseUrl}${link}`
- });
- }
- }
- });
- }
-
- console.log(`[UHDMovies] Found ${searchResults.length} results`);
- return searchResults;
- } catch (error) {
- console.error(`[UHDMovies] Error searching movies: ${error.message}`);
- return [];
- }
-}
-
-// Function to extract clean quality information from verbose text
-function extractCleanQuality(fullQualityText) {
- if (!fullQualityText || fullQualityText === 'Unknown Quality') {
- return 'Unknown Quality';
- }
-
- const cleanedFullQualityText = fullQualityText.replace(/(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g, '').trim();
- const text = cleanedFullQualityText.toLowerCase();
- const quality = [];
-
- // Extract resolution
- if (text.includes('2160p') || text.includes('4k')) {
- quality.push('4K');
- } else if (text.includes('1080p')) {
- quality.push('1080p');
- } else if (text.includes('720p')) {
- quality.push('720p');
- } else if (text.includes('480p')) {
- quality.push('480p');
- }
-
- // Extract special features
- if (text.includes('hdr')) {
- quality.push('HDR');
- }
- if (text.includes('dolby vision') || text.includes('dovi') || /\bdv\b/.test(text)) {
- quality.push('DV');
- }
- if (text.includes('imax')) {
- quality.push('IMAX');
- }
- if (text.includes('bluray') || text.includes('blu-ray')) {
- quality.push('BluRay');
- }
-
- // If we found any quality indicators, join them
- if (quality.length > 0) {
- return quality.join(' | ');
- }
-
- // Fallback: try to extract a shorter version of the original text
- // Look for patterns like "Movie Name (Year) Resolution ..."
- const patterns = [
- /(\d{3,4}p.*?(?:x264|x265|hevc).*?)[[(]/i,
- /(\d{3,4}p.*?)[[(]/i,
- /((?:720p|1080p|2160p|4k).*?)$/i
- ];
-
- for (const pattern of patterns) {
- const match = cleanedFullQualityText.match(pattern);
- if (match && match[1].trim().length < 100) {
- return match[1].trim().replace(/x265/ig, 'HEVC');
- }
- }
-
- // Final fallback: truncate if too long
- if (cleanedFullQualityText.length > 80) {
- return cleanedFullQualityText.substring(0, 77).replace(/x265/ig, 'HEVC') + '...';
- }
-
- return cleanedFullQualityText.replace(/x265/ig, 'HEVC');
-}
-
-// Function to extract download links for TV shows from a page
-async function extractTvShowDownloadLinks(showPageUrl, season, episode) {
- try {
- console.log(`[UHDMovies] Extracting TV show links from: ${showPageUrl} for S${season}E${episode}`);
- const response = await makeRequest(showPageUrl);
- const $ = cheerio.load(response.data);
-
- const showTitle = $('h1').first().text().trim();
- const downloadLinks = [];
-
- // --- NEW LOGIC TO SCOPE SEARCH TO THE CORRECT SEASON ---
- let inTargetSeason = false;
- let qualityText = '';
-
- $('.entry-content').find('*').each((index, element) => {
- const $el = $(element);
- const text = $el.text().trim();
- const seasonMatch = text.match(/^SEASON\s+(\d+)/i);
-
- // Check if we are entering a new season block
- if (seasonMatch) {
- const currentSeasonNum = parseInt(seasonMatch[1], 10);
- if (currentSeasonNum === season) {
- inTargetSeason = true;
- console.log(`[UHDMovies] Entering Season ${season} block.`);
- } else if (inTargetSeason) {
- // We've hit the next season, so we stop.
- console.log(`[UHDMovies] Exiting Season ${season} block, now in Season ${currentSeasonNum}.`);
- inTargetSeason = false;
- return false; // Exit .each() loop
- }
- }
-
- if (inTargetSeason) {
- // This element is within the correct season's block.
-
- // Is this a quality header? (e.g., a or a with )
- // It often contains resolution, release group, etc.
- const isQualityHeader = $el.is('pre, p:has(strong), p:has(b), h3, h4');
- if (isQualityHeader) {
- const headerText = $el.text().trim();
- // Filter out irrelevant headers. We can be more aggressive here.
- if (headerText.length > 5 && !/plot|download|screenshot|trailer|join|powered by|season/i.test(headerText) && !($el.find('a').length > 0)) {
- qualityText = headerText; // Store the most recent quality header
- }
- }
-
- // Is this a paragraph with episode links?
- if ($el.is('p') && $el.find('a[href*="tech.unblockedgames.world"], a[href*="tech.examzculture.in"]').length > 0) {
- const linksParagraph = $el;
- const episodeRegex = new RegExp(`^Episode\\s+0*${episode}(?!\\d)`, 'i');
- const targetEpisodeLink = linksParagraph.find('a').filter((i, el) => {
- return episodeRegex.test($(el).text().trim());
- }).first();
-
- if (targetEpisodeLink.length > 0) {
- const link = targetEpisodeLink.attr('href');
- if (link && !downloadLinks.some(item => item.link === link)) {
- const sizeMatch = qualityText.match(/\[\s*([0-9.,]+\s*[KMGT]B)/i);
- const size = sizeMatch ? sizeMatch[1] : 'Unknown';
-
- const cleanQuality = extractCleanQuality(qualityText);
- const rawQuality = qualityText.replace(/(\r\n|\n|\r)/gm, " ").replace(/\s+/g, ' ').trim();
-
- console.log(`[UHDMovies] Found match: Quality='${qualityText}', Link='${link}'`);
- downloadLinks.push({ quality: cleanQuality, size: size, link: link, rawQuality: rawQuality });
- }
- }
- }
-
- // --- ENHANCED: Check for maxbutton-gdrive-episode structure ---
- if ($el.is('p') && $el.find('a.maxbutton-gdrive-episode').length > 0) {
- const episodeRegex = new RegExp(`^Episode\\s+0*${episode}(?!\\d)`, 'i');
- const targetEpisodeLink = $el.find('a.maxbutton-gdrive-episode').filter((i, el) => {
- const episodeText = $(el).find('.mb-text').text().trim();
- return episodeRegex.test(episodeText);
- }).first();
-
- if (targetEpisodeLink.length > 0) {
- const link = targetEpisodeLink.attr('href');
- if (link && !downloadLinks.some(item => item.link === link)) {
- const sizeMatch = qualityText.match(/\[\s*([0-9.,]+\s*[KMGT]B)/i);
- const size = sizeMatch ? sizeMatch[1] : 'Unknown';
-
- const cleanQuality = extractCleanQuality(qualityText);
- const rawQuality = qualityText.replace(/(\r\n|\n|\r)/gm, " ").replace(/\s+/g, ' ').trim();
-
- console.log(`[UHDMovies] Found match (maxbutton): Quality='${qualityText}', Link='${link}'`);
- downloadLinks.push({ quality: cleanQuality, size: size, link: link, rawQuality: rawQuality });
- }
- }
- }
- }
- });
-
- if (downloadLinks.length === 0) {
- console.log('[UHDMovies] Main extraction logic failed. Checking if requested season exists on page before fallback.');
-
- // Check if the requested season exists on the page at all
- let seasonExists = false;
- const actualSeasonsOnPage = new Set(); // Track what seasons actually have content
-
- // First pass: Look for actual episode content to see what seasons are available
- $('.entry-content').find('a[href*="tech.unblockedgames.world"], a[href*="tech.examzculture.in"], a.maxbutton-gdrive-episode').each((index, element) => {
- const $el = $(element);
- const linkText = $el.text().trim();
- const episodeText = $el.find('.mb-text').text().trim() || linkText;
-
- // Look for season indicators in episode links
- const seasonMatches = [
- episodeText.match(/S(\d{1,2})/i), // S01, S02, etc.
- episodeText.match(/Season\s+(\d+)/i), // Season 1, Season 2, etc.
- episodeText.match(/S(\d{1,2})E(\d{1,3})/i) // S01E01 format
- ];
-
- for (const match of seasonMatches) {
- if (match && match[1]) {
- const foundSeason = parseInt(match[1], 10);
- actualSeasonsOnPage.add(foundSeason);
- }
- }
- });
-
- console.log(`[UHDMovies] Actual seasons found on page: ${Array.from(actualSeasonsOnPage).sort((a, b) => a - b).join(', ')}`);
-
- // Check if requested season is in the actual content
- if (actualSeasonsOnPage.has(season)) {
- seasonExists = true;
- console.log(`[UHDMovies] Season ${season} confirmed to exist in actual episode content`);
- } else {
- // Fallback: Check page descriptions/titles for season mentions
- $('.entry-content').find('*').each((index, element) => {
- const $el = $(element);
- const text = $el.text().trim();
- // Match various season formats: "SEASON 2", "Season 2", "(Season 1 – 2)", "Season 1-2", etc.
- const seasonMatches = [
- text.match(/^SEASON\s+(\d+)/i),
- text.match(/\bSeason\s+(\d+)/i),
- text.match(/\(Season\s+\d+\s*[–-]\s*(\d+)\)/i), // Matches "(Season 1 – 2)"
- text.match(/Season\s+\d+\s*[–-]\s*(\d+)/i), // Matches "Season 1-2"
- text.match(/\bS(\d+)/i) // Matches "S2", "S02", etc.
- ];
-
- for (const match of seasonMatches) {
- if (match) {
- const currentSeasonNum = parseInt(match[1], 10);
- if (currentSeasonNum === season) {
- seasonExists = true;
- console.log(`[UHDMovies] Season ${season} found in page description: "${text.substring(0, 100)}..."`);
- return false; // Exit .each() loop
- }
- // For range formats like "Season 1 – 2", check if requested season is in range
- if (match[0].includes('–') || match[0].includes('-')) {
- const rangeMatch = match[0].match(/Season\s+(\d+)\s*[–-]\s*(\d+)/i);
- if (rangeMatch) {
- const startSeason = parseInt(rangeMatch[1], 10);
- const endSeason = parseInt(rangeMatch[2], 10);
- if (season >= startSeason && season <= endSeason) {
- seasonExists = true;
- console.log(`[UHDMovies] Season ${season} found in range ${startSeason}-${endSeason} in page description`);
- return false; // Exit .each() loop
- }
- }
- }
- }
- }
- });
- }
-
- if (!seasonExists) {
- console.log(`[UHDMovies] Season ${season} not found on page. Available seasons may not include the requested season.`);
- // Don't use fallback if the season doesn't exist to avoid wrong episodes
- return { title: showTitle, links: [], seasonNotFound: true };
- }
-
- console.log(`[UHDMovies] Season ${season} exists on page but episode extraction failed. Trying fallback method with season filtering.`);
-
- // --- ENHANCED FALLBACK LOGIC FOR NEW HTML STRUCTURE ---
- // Try the new maxbutton-gdrive-episode structure first
- $('.entry-content').find('a.maxbutton-gdrive-episode').each((i, el) => {
- const linkElement = $(el);
- const episodeText = linkElement.find('.mb-text').text().trim();
- const episodeRegex = new RegExp(`^Episode\\s+0*${episode}(?!\\d)`, 'i');
-
- if (episodeRegex.test(episodeText)) {
- const link = linkElement.attr('href');
- if (link && !downloadLinks.some(item => item.link === link)) {
- let qualityText = 'Unknown Quality';
-
- // Look for quality info in the preceding paragraph or heading
- const parentP = linkElement.closest('p, div');
- const prevElement = parentP.prev();
- if (prevElement.length > 0) {
- const prevText = prevElement.text().trim();
- if (prevText && prevText.length > 5 && !prevText.toLowerCase().includes('download')) {
- qualityText = prevText;
- }
- }
-
- // Check if this episode belongs to the correct season
- // Enhanced season check - look for various season formats
- const seasonCheckRegexes = [
- new RegExp(`\\.S0*${season}[\\.]`, 'i'), // .S01.
- new RegExp(`S0*${season}[\\.]`, 'i'), // S01.
- new RegExp(`S0*${season}\\b`, 'i'), // S01 (word boundary)
- new RegExp(`Season\\s+0*${season}\\b`, 'i'), // Season 1
- new RegExp(`S0*${season}`, 'i') // S01 anywhere
- ];
-
- const seasonMatch = seasonCheckRegexes.some(regex => regex.test(qualityText));
- if (!seasonMatch) {
- console.log(`[UHDMovies] Skipping episode from different season: Quality='${qualityText}'`);
- return; // Skip this episode as it's from a different season
- }
-
- const sizeMatch = qualityText.match(/\[([0-9.,]+[KMGT]B[^\]]*)\]/i);
- const size = sizeMatch ? sizeMatch[1] : 'Unknown';
- const cleanQuality = extractCleanQuality(qualityText);
- const rawQuality = qualityText.replace(/(\r\n|\n|\r)/gm, " ").replace(/\s+/g, ' ').trim();
-
- console.log(`[UHDMovies] Found match via enhanced fallback (maxbutton): Quality='${qualityText}', Link='${link}'`);
- downloadLinks.push({ quality: cleanQuality, size: size, link: link, rawQuality: rawQuality });
- }
- }
- });
-
- // If still no results, try the original fallback logic
- if (downloadLinks.length === 0) {
- console.log(`[UHDMovies] Enhanced fallback failed, trying original fallback logic.`);
- $('.entry-content').find('a[href*="tech.unblockedgames.world"], a[href*="tech.examzculture.in"]').each((i, el) => {
- const linkElement = $(el);
- const episodeRegex = new RegExp(`^Episode\\s+0*${episode}(?!\\d)`, 'i');
-
- if (episodeRegex.test(linkElement.text().trim())) {
- const link = linkElement.attr('href');
- if (link && !downloadLinks.some(item => item.link === link)) {
- let qualityText = 'Unknown Quality';
- const parentP = linkElement.closest('p, div');
- const prevElement = parentP.prev();
- if (prevElement.length > 0) {
- const prevText = prevElement.text().trim();
- if (prevText && prevText.length > 5 && !prevText.toLowerCase().includes('download')) {
- qualityText = prevText;
- }
- }
-
- // Check if this episode belongs to the correct season
- // Enhanced season check - look for various season formats
- const seasonCheckRegexes = [
- new RegExp(`\\.S0*${season}[\\.]`, 'i'), // .S01.
- new RegExp(`S0*${season}[\\.]`, 'i'), // S01.
- new RegExp(`S0*${season}\\b`, 'i'), // S01 (word boundary)
- new RegExp(`Season\\s+0*${season}\\b`, 'i'), // Season 1
- new RegExp(`S0*${season}`, 'i') // S01 anywhere
- ];
-
- const seasonMatch = seasonCheckRegexes.some(regex => regex.test(qualityText));
- if (!seasonMatch) {
- console.log(`[UHDMovies] Skipping episode from different season: Quality='${qualityText}'`);
- return; // Skip this episode as it's from a different season
- }
-
- const sizeMatch = qualityText.match(/\[([0-9.,]+[KMGT]B[^\]]*)\]/i);
- const size = sizeMatch ? sizeMatch[1] : 'Unknown';
- const cleanQuality = extractCleanQuality(qualityText);
- const rawQuality = qualityText.replace(/(\r\n|\n|\r)/gm, " ").replace(/\s+/g, ' ').trim();
-
- console.log(`[UHDMovies] Found match via original fallback: Quality='${qualityText}', Link='${link}'`);
- downloadLinks.push({ quality: cleanQuality, size: size, link: link, rawQuality: rawQuality });
- }
- }
- });
- }
- }
-
- if (downloadLinks.length > 0) {
- console.log(`[UHDMovies] Found ${downloadLinks.length} links for S${season}E${episode}.`);
- } else {
- console.log(`[UHDMovies] Could not find links for S${season}E${episode}. It's possible the logic needs adjustment or the links aren't on the page.`);
- }
-
- return { title: showTitle, links: downloadLinks };
-
- } catch (error) {
- console.error(`[UHDMovies] Error extracting TV show download links: ${error.message}`);
- return { title: 'Unknown', links: [] };
- }
-}
-
-// Function to extract download links from a movie page
-async function extractDownloadLinks(moviePageUrl, targetYear = null) {
- try {
- console.log(`[UHDMovies] Extracting links from: ${moviePageUrl}`);
- const response = await makeRequest(moviePageUrl);
- const $ = cheerio.load(response.data);
-
- const movieTitle = $('h1').first().text().trim();
- const downloadLinks = [];
-
- // Find all download links (the new SID links) and their associated quality information
- $('a[href*="tech.unblockedgames.world"], a[href*="tech.examzculture.in"]').each((index, element) => {
- const link = $(element).attr('href');
-
- if (link && !downloadLinks.some(item => item.link === link)) {
- let quality = 'Unknown Quality';
- let size = 'Unknown';
-
- // Method 1: Look for quality in the closest preceding paragraph or heading
- const prevElement = $(element).closest('p').prev();
- if (prevElement.length > 0) {
- const prevText = prevElement.text().trim();
- if (prevText && prevText.length > 20 && !prevText.includes('Download')) {
- quality = prevText;
- }
- }
-
- // Method 2: Look for quality in parent's siblings
- if (quality === 'Unknown Quality') {
- const parentSiblings = $(element).parent().prevAll().first().text().trim();
- if (parentSiblings && parentSiblings.length > 20) {
- quality = parentSiblings;
- }
- }
-
- // Method 3: Look for bold/strong text above the link
- if (quality === 'Unknown Quality') {
- const strongText = $(element).closest('p').prevAll().find('strong, b').last().text().trim();
- if (strongText && strongText.length > 20) {
- quality = strongText;
- }
- }
-
- // Method 4: Look for the entire paragraph containing quality info
- if (quality === 'Unknown Quality') {
- let currentElement = $(element).parent();
- for (let i = 0; i < 5; i++) {
- currentElement = currentElement.prev();
- if (currentElement.length === 0) break;
-
- const text = currentElement.text().trim();
- if (text && text.length > 30 &&
- (text.includes('1080p') || text.includes('720p') || text.includes('2160p') ||
- text.includes('4K') || text.includes('HEVC') || text.includes('x264') || text.includes('x265'))) {
- quality = text;
- break;
- }
- }
- }
-
- // Year-based filtering for collections
- if (targetYear && quality !== 'Unknown Quality') {
- // Check for years in quality text
- const yearMatches = quality.match(/\((\d{4})\)/g);
- let hasMatchingYear = false;
-
- if (yearMatches && yearMatches.length > 0) {
- for (const yearMatch of yearMatches) {
- const year = parseInt(yearMatch.replace(/[()]/g, ''));
- if (year === targetYear) {
- hasMatchingYear = true;
- break;
- }
- }
- if (!hasMatchingYear) {
- console.log(`[UHDMovies] Skipping link due to year mismatch. Target: ${targetYear}, Found: ${yearMatches.join(', ')} in "${quality}"`);
- return; // Skip this link
- }
- } else {
- // If no year in quality text, check filename and other indicators
- const linkText = $(element).text().trim();
- const parentText = $(element).parent().text().trim();
- const combinedText = `${quality} ${linkText} ${parentText}`;
-
- // Look for years in combined text
- const allYearMatches = combinedText.match(/\((\d{4})\)/g) || combinedText.match(/(\d{4})/g);
- if (allYearMatches) {
- let foundTargetYear = false;
- for (const yearMatch of allYearMatches) {
- const year = parseInt(yearMatch.replace(/[()]/g, ''));
- if (year >= 1900 && year <= 2030) { // Valid movie year range
- if (year === targetYear) {
- foundTargetYear = true;
- break;
- }
- }
- }
- if (!foundTargetYear && allYearMatches.length > 0) {
- console.log(`[UHDMovies] Skipping link due to no matching year found. Target: ${targetYear}, Found years: ${allYearMatches.join(', ')} in combined text`);
- return; // Skip this link
- }
- }
-
- // Additional check: if quality contains movie names that don't match target year
- const lowerQuality = quality.toLowerCase();
- if (targetYear === 2015) {
- if (lowerQuality.includes('wasp') || lowerQuality.includes('quantumania')) {
- console.log(`[UHDMovies] Skipping link for 2015 target as it contains 'wasp' or 'quantumania': "${quality}"`);
- return; // Skip this link
- }
- }
- }
- }
-
- // Extract size from quality text if present
- const sizeMatch = quality.match(/\[([0-9.,]+\s*[KMGT]B[^\]]*)\]/);
- if (sizeMatch) {
- size = sizeMatch[1];
- }
-
- // Clean up the quality information
- const cleanQuality = extractCleanQuality(quality);
-
- downloadLinks.push({
- quality: cleanQuality,
- size: size,
- link: link,
- rawQuality: quality.replace(/(\r\n|\n|\r)/gm, " ").replace(/\s+/g, ' ').trim()
- });
- }
- });
-
- return {
- title: movieTitle,
- links: downloadLinks
- };
-
- } catch (error) {
- console.error(`[UHDMovies] Error extracting download links: ${error.message}`);
- return { title: 'Unknown', links: [] };
- }
-}
-
-function extractCodecs(rawQuality) {
- const codecs = [];
- const text = rawQuality.toLowerCase();
-
- if (text.includes('hevc') || text.includes('x265')) {
- codecs.push('H.265');
- } else if (text.includes('x264')) {
- codecs.push('H.264');
- }
-
- if (text.includes('10bit') || text.includes('10-bit')) {
- codecs.push('10-bit');
- }
-
- if (text.includes('atmos')) {
- codecs.push('Atmos');
- } else if (text.includes('dts-hd')) {
- codecs.push('DTS-HD');
- } else if (text.includes('dts')) {
- codecs.push('DTS');
- } else if (text.includes('ddp5.1') || text.includes('dd+ 5.1') || text.includes('eac3')) {
- codecs.push('EAC3');
- } else if (text.includes('ac3')) {
- codecs.push('AC3');
- }
-
- if (text.includes('dovi') || text.includes('dolby vision') || /\bdv\b/.test(text)) {
- codecs.push('DV');
- } else if (text.includes('hdr')) {
- codecs.push('HDR');
- }
-
- return codecs;
-}
-
-// Function to try Instant Download method
-async function tryInstantDownload($) {
- const instantDownloadLink = $('a:contains("Instant Download")').attr('href');
- if (!instantDownloadLink) {
- return null;
- }
-
- console.log('[UHDMovies] Found "Instant Download" link, attempting to extract final URL...');
-
- try {
- const urlParams = new URLSearchParams(new URL(instantDownloadLink).search);
- const keys = urlParams.get('url');
-
- if (keys) {
- const apiUrl = `${new URL(instantDownloadLink).origin}/api`;
- const formData = new FormData();
- formData.append('keys', keys);
-
- const apiResponse = await axiosInstance.post(apiUrl, formData, {
- headers: {
- ...formData.getHeaders(),
- 'x-token': new URL(instantDownloadLink).hostname
- }
- });
-
- if (apiResponse.data && apiResponse.data.url) {
- let finalUrl = apiResponse.data.url;
- // Fix spaces in workers.dev URLs by encoding them properly
- if (finalUrl.includes('workers.dev')) {
- const urlParts = finalUrl.split('/');
- const filename = urlParts[urlParts.length - 1];
- const encodedFilename = filename.replace(/ /g, '%20');
- urlParts[urlParts.length - 1] = encodedFilename;
- finalUrl = urlParts.join('/');
- }
- console.log('[UHDMovies] Extracted final link from API:', finalUrl);
- return finalUrl;
- }
- }
-
- console.log('[UHDMovies] Could not find a valid final download link from Instant Download.');
- return null;
- } catch (error) {
- console.log(`[UHDMovies] Error processing "Instant Download": ${error.message}`);
- return null;
- }
-}
-
-// Function to try Resume Cloud method
-async function tryResumeCloud($) {
- // Look for both "Resume Cloud" and "Cloud Resume Download" buttons
- const resumeCloudButton = $('a:contains("Resume Cloud"), a:contains("Cloud Resume Download")');
-
- if (resumeCloudButton.length === 0) {
- return null;
- }
-
- const resumeLink = resumeCloudButton.attr('href');
- if (!resumeLink) {
- return null;
- }
-
- // Check if it's already a direct download link (workers.dev)
- if (resumeLink.includes('workers.dev') || resumeLink.startsWith('http')) {
- let directLink = resumeLink;
- // Fix spaces in workers.dev URLs by encoding them properly
- if (directLink.includes('workers.dev')) {
- const urlParts = directLink.split('/');
- const filename = urlParts[urlParts.length - 1];
- const encodedFilename = filename.replace(/ /g, '%20');
- urlParts[urlParts.length - 1] = encodedFilename;
- directLink = urlParts.join('/');
- }
- console.log(`[UHDMovies] Found direct "Cloud Resume Download" link: ${directLink}`);
- return directLink;
- }
-
- // Otherwise, follow the link to get the final download
- try {
- const resumeUrl = new URL(resumeLink, 'https://driveleech.net').href;
- console.log(`[UHDMovies] Found 'Resume Cloud' page link. Following to: ${resumeUrl}`);
-
- // "Click" the link by making another request
- const finalPageResponse = await makeRequest(resumeUrl, { maxRedirects: 10 });
- const $$ = cheerio.load(finalPageResponse.data);
-
- // Look for direct download links
- let finalDownloadLink = $$('a.btn-success[href*="workers.dev"], a[href*="driveleech.net/d/"]').attr('href');
-
- if (finalDownloadLink) {
- // Fix spaces in workers.dev URLs by encoding them properly
- if (finalDownloadLink.includes('workers.dev')) {
- // Split the URL at the last slash to separate the base URL from the filename
- const urlParts = finalDownloadLink.split('/');
- const filename = urlParts[urlParts.length - 1];
- // Encode spaces in the filename part only
- const encodedFilename = filename.replace(/ /g, '%20');
- urlParts[urlParts.length - 1] = encodedFilename;
- finalDownloadLink = urlParts.join('/');
- }
- console.log(`[UHDMovies] Extracted final Resume Cloud link: ${finalDownloadLink}`);
- return finalDownloadLink;
- } else {
- console.log('[UHDMovies] Could not find the final download link on the "Resume Cloud" page.');
- return null;
- }
- } catch (error) {
- console.log(`[UHDMovies] Error processing "Resume Cloud": ${error.message}`);
- return null;
- }
-}
-
-// Environment variable to control URL validation
-const URL_VALIDATION_ENABLED = process.env.DISABLE_URL_VALIDATION !== 'true';
-console.log(`[UHDMovies] URL validation is ${URL_VALIDATION_ENABLED ? 'enabled' : 'disabled'}.`);
-
-// Validate if a video URL is working (not 404 or broken)
-async function validateVideoUrl(url, timeout = 10000) {
- // Skip validation if disabled via environment variable
- if (!URL_VALIDATION_ENABLED) {
- console.log(`[UHDMovies] URL validation disabled, skipping validation for: ${url.substring(0, 100)}...`);
- return true;
- }
-
- try {
- console.log(`[UHDMovies] Validating URL: ${url.substring(0, 100)}...`);
-
- const response = await axiosInstance.head(url, {
- timeout,
- headers: {
- 'Range': 'bytes=0-1'
- }
- });
-
- // Check if status is OK (200-299) or partial content (206)
- if (response.status >= 200 && response.status < 400) {
- console.log(`[UHDMovies] ✓ URL validation successful (${response.status})`);
- return true;
- } else {
- console.log(`[UHDMovies] ✗ URL validation failed with status: ${response.status}`);
- return false;
- }
- } catch (error) {
- console.log(`[UHDMovies] ✗ URL validation failed: ${error.message}`);
- return false;
- }
-}
-
-
-// Compare media to find matching result
-function compareMedia(mediaInfo, searchResult) {
- const normalizeString = (str) => String(str || '').toLowerCase().replace(/[^a-zA-Z0-9]/g, '');
-
- const titleWithAnd = mediaInfo.title.replace(/\s*&\s*/g, ' and ');
- const normalizedMediaTitle = normalizeString(titleWithAnd);
- const normalizedResultTitle = normalizeString(searchResult.title);
-
- console.log(`[UHDMovies] Comparing: "${mediaInfo.title}" (${mediaInfo.year}) vs "${searchResult.title}"`);
- console.log(`[UHDMovies] Normalized: "${normalizedMediaTitle}" vs "${normalizedResultTitle}"`);
-
- // Check if titles match or result title contains media title
- let titleMatches = normalizedResultTitle.includes(normalizedMediaTitle);
-
- // If direct match fails, try checking for franchise/collection matches
- if (!titleMatches) {
- const mainTitle = normalizedMediaTitle.split('and')[0];
- const isCollection = normalizedResultTitle.includes('duology') ||
- normalizedResultTitle.includes('trilogy') ||
- normalizedResultTitle.includes('quadrilogy') ||
- normalizedResultTitle.includes('collection') ||
- normalizedResultTitle.includes('saga');
-
- if (isCollection && normalizedResultTitle.includes(mainTitle)) {
- console.log(`[UHDMovies] Found collection match: "${mainTitle}" in collection "${searchResult.title}"`);
- titleMatches = true;
- }
- }
-
- if (!titleMatches) {
- console.log(`[UHDMovies] Title mismatch: "${normalizedResultTitle}" does not contain "${normalizedMediaTitle}"`);
- return false;
- }
-
- // NEW: Negative keyword check for spinoffs
- const negativeKeywords = ['challenge', 'conversation', 'story', 'in conversation'];
- const originalTitleLower = mediaInfo.title.toLowerCase();
- for (const keyword of negativeKeywords) {
- if (normalizedResultTitle.includes(keyword.replace(/\s/g, '')) && !originalTitleLower.includes(keyword)) {
- console.log(`[UHDMovies] Rejecting spinoff due to keyword: "${keyword}"`);
- return false; // It's a spinoff, reject it.
- }
- }
-
- // Check year if both are available
- if (mediaInfo.year && searchResult.title) {
- const yearRegex = /\b(19[89]\d|20\d{2})\b/g; // Look for years 1980-2099
- const yearMatchesInResult = searchResult.title.match(yearRegex);
- const yearRangeMatch = searchResult.title.match(/\((\d{4})\s*-\s*(\d{4})\)/);
-
- let hasMatchingYear = false;
-
- if (yearMatchesInResult) {
- console.log(`[UHDMovies] Found years in result: ${yearMatchesInResult.join(', ')}`);
- if (yearMatchesInResult.some(yearStr => Math.abs(parseInt(yearStr) - mediaInfo.year) <= 1)) {
- hasMatchingYear = true;
- }
- }
-
- if (!hasMatchingYear && yearRangeMatch) {
- console.log(`[UHDMovies] Found year range in result: ${yearRangeMatch[0]}`);
- const startYear = parseInt(yearRangeMatch[1]);
- const endYear = parseInt(yearRangeMatch[2]);
- if (mediaInfo.year >= startYear - 1 && mediaInfo.year <= endYear + 1) {
- hasMatchingYear = true;
- }
- }
-
- // If there are any years found in the title, one of them MUST match.
- if ((yearMatchesInResult || yearRangeMatch) && !hasMatchingYear) {
- console.log(`[UHDMovies] Year mismatch. Target: ${mediaInfo.year}, but no matching year found in result.`);
- return false;
- }
- }
-
- console.log(`[UHDMovies] Match successful!`);
- return true;
-}
-
-// Function to score search results based on quality keywords and season coverage
-function scoreResult(title, requestedSeason = null) {
- let score = 0;
- const lowerTitle = title.toLowerCase();
-
- // Quality scoring
- if (lowerTitle.includes('remux')) score += 10;
- if (lowerTitle.includes('bluray') || lowerTitle.includes('blu-ray')) score += 8;
- if (lowerTitle.includes('imax')) score += 6;
- if (lowerTitle.includes('4k') || lowerTitle.includes('2160p')) score += 5;
- if (lowerTitle.includes('dovi') || lowerTitle.includes('dolby vision') || /\\bdv\\b/.test(lowerTitle)) score += 4;
- if (lowerTitle.includes('hdr')) score += 3;
- if (lowerTitle.includes('1080p')) score += 2;
- if (lowerTitle.includes('hevc') || lowerTitle.includes('x265')) score += 1;
-
- // Season coverage scoring (for TV shows)
- if (requestedSeason !== null) {
- // Check for season range formats like "Season 1 – 2" or "Season 1-2"
- const seasonRangeMatch = lowerTitle.match(/season\s+(\d+)\s*[–-]\s*(\d+)/i);
- if (seasonRangeMatch) {
- const startSeason = parseInt(seasonRangeMatch[1], 10);
- const endSeason = parseInt(seasonRangeMatch[2], 10);
- if (requestedSeason >= startSeason && requestedSeason <= endSeason) {
- score += 50; // High bonus for season range that includes requested season
- console.log(`[UHDMovies] Season range bonus (+50): ${startSeason}-${endSeason} includes requested season ${requestedSeason}`);
- }
- }
-
- // Check for specific season mentions
- const specificSeasonMatch = lowerTitle.match(/season\s+(\d+)/i);
- if (specificSeasonMatch) {
- const mentionedSeason = parseInt(specificSeasonMatch[1], 10);
- if (mentionedSeason === requestedSeason) {
- score += 30; // Good bonus for exact season match
- console.log(`[UHDMovies] Exact season bonus (+30): Season ${mentionedSeason} matches requested season ${requestedSeason}`);
- } else if (mentionedSeason < requestedSeason) {
- score -= 20; // Penalty for older season when requesting newer season
- console.log(`[UHDMovies] Season penalty (-20): Season ${mentionedSeason} is older than requested season ${requestedSeason}`);
- }
- }
-
- // Check for "Season X Added" or similar indicators
- if (lowerTitle.includes('season') && lowerTitle.includes('added')) {
- const addedSeasonMatch = lowerTitle.match(/season\s+(\d+)\s+added/i);
- if (addedSeasonMatch) {
- const addedSeason = parseInt(addedSeasonMatch[1], 10);
- if (addedSeason === requestedSeason) {
- score += 40; // High bonus for newly added season
- console.log(`[UHDMovies] Added season bonus (+40): Season ${addedSeason} was recently added`);
- }
- }
- }
- }
-
- return score;
-}
-
-// Function to parse size string into MB
-function parseSize(sizeString) {
- if (!sizeString || typeof sizeString !== 'string') {
- return 0;
- }
-
- const upperCaseSizeString = sizeString.toUpperCase();
-
- // Regex to find a number (integer or float) followed by GB, MB, or KB
- const match = upperCaseSizeString.match(/([0-9.,]+)\s*(GB|MB|KB)/);
-
- if (!match) {
- return 0;
- }
-
- const sizeValue = parseFloat(match[1].replace(/,/g, ''));
- if (isNaN(sizeValue)) {
- return 0;
- }
-
- const unit = match[2];
-
- if (unit === 'GB') {
- return sizeValue * 1024;
- } else if (unit === 'MB') {
- return sizeValue;
- } else if (unit === 'KB') {
- return sizeValue / 1024;
- }
-
- return 0;
-}
-
-// Helper function to extract cookies from jar for a specific URL
-const getCookiesForUrl = async (jar, url) => {
- try {
- const cookies = await jar.getCookies(url);
- if (cookies && cookies.length > 0) {
- return cookies.map(cookie => cookie.toString()).join('; ');
- }
- } catch (error) {
- console.log(`[UHDMovies] Error extracting cookies for ${url}: ${error.message}`);
- }
- return null;
-};
-
-// Helper function to create a session for SID resolution
-const createSession = async (jar) => {
- const { wrapper } = await getAxiosCookieJarSupport();
-
- const sessionConfig = {
- jar,
- headers: {
- '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',
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
- 'Accept-Language': 'en-US,en;q=0.5',
- 'Connection': 'keep-alive',
- 'Upgrade-Insecure-Requests': '1'
- }
- };
-
- const session = wrapper(axios.create(sessionConfig));
-
-
-
- return session;
-};
-
-// New function to resolve the tech.unblockedgames.world links
-async function resolveSidToDriveleech(sidUrl) {
- console.log(`[UHDMovies] Resolving SID link: ${sidUrl}`);
- const { origin } = new URL(sidUrl);
- const jar = new CookieJar();
-
- // Create session
- const session = await createSession(jar);
-
- try {
- // Step 0: Get the _wp_http value
- console.log(" [SID] Step 0: Fetching initial page...");
- const responseStep0 = await session.get(sidUrl);
- let $ = cheerio.load(responseStep0.data);
- const initialForm = $('#landing');
- const wp_http_step1 = initialForm.find('input[name="_wp_http"]').val();
- const action_url_step1 = initialForm.attr('action');
-
- if (!wp_http_step1 || !action_url_step1) {
- console.error(" [SID] Error: Could not find _wp_http in initial form.");
- return null;
- }
-
- // Step 1: POST to the first form's action URL
- console.log(" [SID] Step 1: Submitting initial form...");
- const step1Data = new URLSearchParams({ '_wp_http': wp_http_step1 });
- const responseStep1 = await session.post(action_url_step1, step1Data, {
- headers: { 'Referer': sidUrl, 'Content-Type': 'application/x-www-form-urlencoded' }
- });
-
- // Step 2: Parse verification page for second form
- console.log(" [SID] Step 2: Parsing verification page...");
- $ = cheerio.load(responseStep1.data);
- const verificationForm = $('#landing');
- const action_url_step2 = verificationForm.attr('action');
- const wp_http2 = verificationForm.find('input[name="_wp_http2"]').val();
- const token = verificationForm.find('input[name="token"]').val();
-
- if (!action_url_step2) {
- console.error(" [SID] Error: Could not find verification form.");
- return null;
- }
-
- // Step 3: POST to the verification URL
- console.log(" [SID] Step 3: Submitting verification...");
- const step2Data = new URLSearchParams({ '_wp_http2': wp_http2, 'token': token });
- const responseStep2 = await session.post(action_url_step2, step2Data, {
- headers: { 'Referer': responseStep1.request.res.responseUrl, 'Content-Type': 'application/x-www-form-urlencoded' }
- });
-
- // Step 4: Find dynamic cookie and link from JavaScript
- console.log(" [SID] Step 4: Parsing final page for JS data...");
- let finalLinkPath = null;
- let cookieName = null;
- let cookieValue = null;
-
- const scriptContent = responseStep2.data;
- const cookieMatch = scriptContent.match(/s_343\('([^']+)',\s*'([^']+)'/);
- const linkMatch = scriptContent.match(/c\.setAttribute\("href",\s*"([^"]+)"\)/);
-
- if (cookieMatch) {
- cookieName = cookieMatch[1].trim();
- cookieValue = cookieMatch[2].trim();
- }
- if (linkMatch) {
- finalLinkPath = linkMatch[1].trim();
- }
-
- if (!finalLinkPath || !cookieName || !cookieValue) {
- console.error(" [SID] Error: Could not extract dynamic cookie/link from JS.");
- return null;
- }
-
- const finalUrl = new URL(finalLinkPath, origin).href;
- console.log(` [SID] Dynamic link found: ${finalUrl}`);
- console.log(` [SID] Dynamic cookie found: ${cookieName}`);
-
- // Step 5: Set cookie and make final request
- console.log(" [SID] Step 5: Setting cookie and making final request...");
- await jar.setCookie(`${cookieName}=${cookieValue}`, origin);
-
- const finalResponse = await session.get(finalUrl, {
- headers: { 'Referer': responseStep2.request.res.responseUrl }
- });
-
- // Step 6: Extract driveleech URL from meta refresh tag
- $ = cheerio.load(finalResponse.data);
- const metaRefresh = $('meta[http-equiv="refresh"]');
- if (metaRefresh.length > 0) {
- const content = metaRefresh.attr('content');
- const urlMatch = content.match(/url=(.*)/i);
- if (urlMatch && urlMatch[1]) {
- const driveleechUrl = urlMatch[1].replace(/"/g, "").replace(/'/g, "");
- console.log(` [SID] SUCCESS! Resolved Driveleech URL: ${driveleechUrl}`);
- return driveleechUrl;
- }
- }
-
- console.error(" [SID] Error: Could not find meta refresh tag with Driveleech URL.");
- return null;
-
- } catch (error) {
- console.error(` [SID] Error during SID resolution: ${error.message}`);
- if (error.response) {
- console.error(` [SID] Status: ${error.response.status}`);
- }
- return null;
- }
-}
-
-// Main function to get streams for TMDB content
-async function getUHDMoviesStreams(tmdbId, mediaType = 'movie', season = null, episode = null) {
- console.log(`[UHDMovies] Attempting to fetch streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${mediaType === 'tv' ? `, S:${season}E:${episode}` : ''}`);
-
- const cacheKey = `uhd_final_v18_${tmdbId}_${mediaType}${season ? `_s${season}e${episode}` : ''}`;
-
- try {
- // 1. Check cache first
- let cachedLinks = await getFromCache(cacheKey);
- if (cachedLinks && cachedLinks.length > 0) {
- console.log(`[UHDMovies] Cache HIT for ${cacheKey}. Using ${cachedLinks.length} cached Driveleech links.`);
- } else {
- if (cachedLinks && cachedLinks.length === 0) {
- console.log(`[UHDMovies] Cache contains empty data for ${cacheKey}. Refetching from source.`);
- } else {
- console.log(`[UHDMovies] Cache MISS for ${cacheKey}. Fetching from source.`);
- }
- console.log(`[UHDMovies] Cache MISS for ${cacheKey}. Fetching from source.`);
- // 2. If cache miss, get TMDB info to perform search
- const TMDB_API_KEY_UHDMOVIES = getUhdMoviesKey();
- const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY_UHDMOVIES}`;
- const tmdbResponse = await axios.get(tmdbUrl);
- const tmdbData = tmdbResponse.data;
- const mediaInfo = {
- title: mediaType === 'tv' ? tmdbData.name : tmdbData.title,
- year: parseInt(((mediaType === 'tv' ? tmdbData.first_air_date : tmdbData.release_date) || '').split('-')[0], 10)
- };
-
- if (!mediaInfo.title) throw new Error('Could not extract title from TMDB response.');
- console.log(`[UHDMovies] TMDB Info: "${mediaInfo.title}" (${mediaInfo.year || 'N/A'})`);
-
- // 3. Search for the media on UHDMovies
- const searchTitle = mediaInfo.title.replace(/:/g, '').replace(/\s*&\s*/g, ' and ');
- let searchResults = await searchMovies(searchTitle);
-
- // If no results or only wrong year results, try fallback search with just main title
- if (searchResults.length === 0 || !searchResults.some(result => compareMedia(mediaInfo, result))) {
- console.log(`[UHDMovies] Primary search failed or no matches. Trying fallback search...`);
-
- // Extract main title (remove subtitles after colon, "and the", etc.)
- let fallbackTitle = mediaInfo.title.split(':')[0].trim();
- if (fallbackTitle.includes('and the')) {
- fallbackTitle = fallbackTitle.split('and the')[0].trim();
- }
- if (fallbackTitle !== searchTitle) {
- console.log(`[UHDMovies] Fallback search with: "${fallbackTitle}"`);
- const fallbackResults = await searchMovies(fallbackTitle);
- if (fallbackResults.length > 0) {
- searchResults = fallbackResults;
- }
- }
- }
-
- if (searchResults.length === 0) {
- console.log(`[UHDMovies] No search results found for "${mediaInfo.title}".`);
- // Don't cache empty results to allow retrying later
- return [];
- }
-
- // 4. Find the best matching result
- const matchingResults = searchResults.filter(result => compareMedia(mediaInfo, result));
-
- if (matchingResults.length === 0) {
- console.log(`[UHDMovies] No matching content found for "${mediaInfo.title}" (${mediaInfo.year}).`);
- // Don't cache empty results to allow retrying later
- return [];
- }
-
- let matchingResult;
- let scoredResults = null; // Declare outside the conditional
-
- if (matchingResults.length === 1) {
- matchingResult = matchingResults[0];
- } else {
- console.log(`[UHDMovies] Found ${matchingResults.length} matching results. Scoring to find the best...`);
-
- scoredResults = matchingResults.map(result => {
- const score = scoreResult(result.title, mediaType === 'tv' ? season : null);
- console.log(` - Score ${score}: ${result.title}`);
- return { ...result, score };
- }).sort((a, b) => b.score - a.score);
-
- matchingResult = scoredResults[0];
- console.log(`[UHDMovies] Best match selected with score ${matchingResult.score}: "${matchingResult.title}"`);
- }
-
- console.log(`[UHDMovies] Found matching content: "${matchingResult.title}"`);
-
- // 5. Extract SID links from the movie/show page
- let downloadInfo = await (mediaType === 'tv' ? extractTvShowDownloadLinks(matchingResult.link, season, episode) : extractDownloadLinks(matchingResult.link, mediaInfo.year));
-
- // Check if season was not found or episode extraction failed, and we have multiple results to try
- if (downloadInfo.links.length === 0 && matchingResults.length > 1 && scoredResults &&
- (downloadInfo.seasonNotFound || (mediaType === 'tv' && downloadInfo.title))) {
- console.log(`[UHDMovies] Season ${season} not found or episode extraction failed on best match. Trying next best match...`);
-
- // Try the next best match
- const nextBestMatch = scoredResults[1];
- console.log(`[UHDMovies] Trying next best match: "${nextBestMatch.title}"`);
-
- downloadInfo = await (mediaType === 'tv' ? extractTvShowDownloadLinks(nextBestMatch.link, season, episode) : extractDownloadLinks(nextBestMatch.link, mediaInfo.year));
-
- if (downloadInfo.links.length > 0) {
- console.log(`[UHDMovies] Successfully found links on next best match!`);
- } else {
- console.log(`[UHDMovies] Next best match also failed. No download links found.`);
- }
- }
-
- if (downloadInfo.links.length === 0) {
- console.log('[UHDMovies] No download links found on page.');
- // Don't cache empty results to allow retrying later
- return [];
- }
-
- // 6. Resolve all SID links to driveleech redirect URLs (intermediate step)
- console.log(`[UHDMovies] Resolving ${downloadInfo.links.length} SID link(s) to driveleech redirect URLs...`);
- const resolutionPromises = downloadInfo.links.map(async (linkInfo) => {
- try {
- let driveleechUrl = null;
-
- if (linkInfo.link && (linkInfo.link.includes('tech.unblockedgames.world') || linkInfo.link.includes('tech.creativeexpressionsblog.com') || linkInfo.link.includes('tech.examzculture.in'))) {
- driveleechUrl = await resolveSidToDriveleech(linkInfo.link);
- } else if (linkInfo.link && (linkInfo.link.includes('driveseed.org') || linkInfo.link.includes('driveleech.net'))) {
- // If it's already a direct driveseed/driveleech link, use it
- driveleechUrl = linkInfo.link;
- }
-
- if (!driveleechUrl) return null;
-
- console.log(`[UHDMovies] Caching driveleech redirect URL for ${linkInfo.quality}: ${driveleechUrl}`);
- return { ...linkInfo, driveleechRedirectUrl: driveleechUrl };
- } catch (error) {
- console.error(`[UHDMovies] Error resolving ${linkInfo.quality}: ${error.message}`);
- return null;
- }
- });
-
- cachedLinks = (await Promise.all(resolutionPromises)).filter(Boolean);
-
- // 7. Save the successfully resolved driveleech redirect URLs to the cache
- if (cachedLinks.length > 0) {
- console.log(`[UHDMovies] Caching ${cachedLinks.length} resolved driveleech redirect URLs for key: ${cacheKey}`);
- await saveToCache(cacheKey, cachedLinks);
- } else {
- console.log(`[UHDMovies] No driveleech redirect URLs could be resolved. Not caching to allow retrying later.`);
- return [];
- }
- }
-
- if (!cachedLinks || cachedLinks.length === 0) {
- console.log('[UHDMovies] No final file page URLs found after scraping/cache check.');
- return [];
- }
-
- // 8. Process all cached driveleech redirect URLs to get streaming links
- console.log(`[UHDMovies] Processing ${cachedLinks.length} cached driveleech redirect URL(s) to get streaming links.`);
- const streamPromises = cachedLinks.map(async (linkInfo) => {
- try {
- // First, resolve the driveleech redirect URL to get the final file page URL
- const response = await makeRequest(linkInfo.driveleechRedirectUrl, { maxRedirects: 10 });
- let $ = cheerio.load(response.data);
-
- // Check for JavaScript redirect (window.location.replace)
- const scriptContent = $('script').html();
- const redirectMatch = scriptContent && scriptContent.match(/window\.location\.replace\("([^"]+)"\)/);
-
- let finalFilePageUrl = linkInfo.driveleechRedirectUrl;
- if (redirectMatch && redirectMatch[1]) {
- finalFilePageUrl = new URL(redirectMatch[1], 'https://driveleech.net/').href;
- console.log(`[UHDMovies] Resolved redirect to final file page: ${finalFilePageUrl}`);
-
- // Load the final file page
- const finalResponse = await makeRequest(finalFilePageUrl, { maxRedirects: 10 });
- $ = cheerio.load(finalResponse.data);
- }
-
- // Extract file size and name information
- let sizeInfo = 'Unknown';
- let fileName = null;
-
- const sizeElement = $('li.list-group-item:contains("Size :")').text();
- if (sizeElement) {
- const sizeMatch = sizeElement.match(/Size\s*:\s*([0-9.,]+\s*[KMGT]B)/);
- if (sizeMatch) {
- sizeInfo = sizeMatch[1];
- }
- }
-
- const nameElement = $('li.list-group-item:contains("Name :")');
- if (nameElement.length > 0) {
- fileName = nameElement.text().replace('Name :', '').trim();
- } else {
- const h5Title = $('div.card-header h5').clone().children().remove().end().text().trim();
- if (h5Title) {
- fileName = h5Title.replace(/\[.*\]/, '').trim();
- }
- }
-
- // Try download methods to get final streaming URL
- const downloadMethods = [
- { name: 'Resume Cloud', func: tryResumeCloud },
- { name: 'Instant Download', func: tryInstantDownload }
- ];
-
- for (const method of downloadMethods) {
- try {
- const finalUrl = await method.func($);
-
- if (finalUrl) {
- const isValid = await validateVideoUrl(finalUrl);
- if (isValid) {
- const rawQuality = linkInfo.rawQuality || '';
- const codecs = extractCodecs(rawQuality);
- const cleanFileName = fileName ? fileName.replace(/\.[^/.]+$/, "").replace(/[._]/g, ' ') : (linkInfo.quality || 'Unknown');
-
- return {
- name: `UHDMovies`,
- title: `${cleanFileName}\n${sizeInfo}`,
- url: finalUrl,
- quality: linkInfo.quality,
- provider: 'uhdmovies',
- headers: {}
- };
- }
- }
- } catch (error) {
- console.log(`[UHDMovies] ${method.name} failed: ${error.message}`);
- }
- }
-
- return null;
- } catch (error) {
- console.error(`[UHDMovies] Error processing cached driveleech redirect ${linkInfo.driveleechRedirectUrl}: ${error.message}`);
- return null;
- }
- });
-
- const streams = (await Promise.all(streamPromises)).filter(Boolean);
- console.log(`[UHDMovies] Successfully processed ${streams.length} final stream links.`);
-
- // Sort final streams by size
- streams.sort((a, b) => {
- const sizeA = parseSize(a.size);
- const sizeB = parseSize(b.size);
- return sizeB - sizeA;
- });
-
- return streams;
- } catch (error) {
- console.error(`[UHDMovies] A critical error occurred in getUHDMoviesStreams for ${tmdbId}: ${error.message}`);
- if (error.stack) console.error(error.stack);
- return [];
- }
-}
-
-module.exports = { getUHDMoviesStreams };
\ No newline at end of file
diff --git a/providers/videasy.js b/providers/videasy.js
new file mode 100644
index 0000000..c36a26f
--- /dev/null
+++ b/providers/videasy.js
@@ -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 };
diff --git a/providers/vidlink.js b/providers/vidlink.js
new file mode 100644
index 0000000..352bfe8
--- /dev/null
+++ b/providers/vidlink.js
@@ -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 };
diff --git a/providers/vidsrcextractor.js b/providers/vidsrcextractor.js
deleted file mode 100644
index 81473f3..0000000
--- a/providers/vidsrcextractor.js
+++ /dev/null
@@ -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 ");
- 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);
- });
-}
diff --git a/providers/vixsrc.js b/providers/vixsrc.js
index d7435ca..b93b9f1 100644
--- a/providers/vixsrc.js
+++ b/providers/vixsrc.js
@@ -1,214 +1,180 @@
-/**
- * Vixsrc streaming provider integration (standalone)
- * React Native compatible variant - no external addon framework
- */
+const axios = require('axios');
-// Constants
-const TMDB_API_KEY = "68e094699525b18a70bab2f86b1fa706";
const BASE_URL = 'https://vixsrc.to';
-let vixsrcUrl;
-let subtitleApiUrl;
-// Helper function to make HTTP requests with default headers
-function makeRequest(url, options = {}) {
- const defaultHeaders = {
- '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',
- 'Accept': 'application/json,*/*',
- 'Accept-Language': 'en-US,en;q=0.5',
- 'Accept-Encoding': 'gzip, deflate',
- 'Connection': 'keep-alive',
- ...options.headers
- };
+const VIXSRC_HEADERS = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150 Safari/537.36',
+ 'Accept': 'application/json, text/javascript, */*; q=0.01',
+ 'Accept-Language': 'en-US,en;q=0.9',
+ 'Referer': BASE_URL,
+ 'Origin': BASE_URL
+};
- return fetch(url, {
- method: options.method || 'GET',
- headers: defaultHeaders,
- ...options
- })
- .then(response => {
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
- }
- return response;
- })
- .catch(error => {
- console.error(`[Vixsrc] Request failed for ${url}: ${error.message}`);
- throw error;
+// Step 1: GET /api/movie/{id} or /api/tv/{id}/{s}/{e} → { src: "/embed/..." }
+async function fetchApi(url) {
+ try {
+ const response = await axios.get(url, { headers: VIXSRC_HEADERS, timeout: 10000 });
+ if (response.status !== 200 || !response.data) return null;
+ return response.data;
+ } catch {
+ return null;
+ }
+}
+
+// Step 2: GET BASE_URL + sublink.src → HTML embed page
+async function fetchEmbedPage(suburl) {
+ 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
-function getTmdbInfo(tmdbId, mediaType) {
- const url = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`;
+// Step 3: Extract token, expires, playlist URL from embed HTML
+function extractTokenData(html) {
+ 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)
- .then(response => response.json())
- .then(data => ({ data }));
-}
+ if (!token || !expires || !playlist) return null;
-// Helper function to parse M3U8 playlist
-// Removed obsolete parseM3U8Playlist implementation
-
-// Removed unused helpers getQualityFromResolution and resolveUrl
-
-// 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}`;
+ // Reject expired tokens (with 60s grace period)
+ if (parseInt(expires, 10) * 1000 - 60_000 < Date.now()) {
+ console.log('[Vixsrc] Token is expired');
+ return null;
}
- 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: {
- '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;
-
- // 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(/
+
+
No FEBBOX_COOKIES configured. Showbox / PStream streams may be unavailable.
Cache is DISABLED; expect slower responses & higher source load.
- No TMDB API Keys configured. Metadata-dependent providers (Showbox, 4khdhub, MoviesMod, MP4Hydra) may degrade.
+ No TMDB API Keys configured. Metadata-dependent providers (Showbox, 4khdhub, MoviesMod) may degrade.
|