20 Commits
Author SHA1 Message Date
Inside4ndroid aacac098f2 Update CHANGELOG for version 1.3.0: add new providers, utilities, and documentation improvements; fix various bugs and enhance configuration options. 2026-08-19 23:39:49 +01:00
Inside4ndroid 2c9a2cabbf Add new providers for streaming services and utility functions
- Implement StreamFlix provider for fetching movie and episode streams.
- Add VaPlayer provider to retrieve streaming URLs for movies and TV shows.
- Introduce ZXCStreams provider for accessing various streaming links.
- Create utility functions for fetching episode counts from Cinemeta.
- Develop a title matching system for improved search results.
- Implement TMDB title to IMDB ID resolution for better integration with TMDB API.
2026-08-19 23:35:13 +01:00
Inside4ndroid 01f06f807e Merge pull request #57 from venxm14/main
Fix bugs related to ENABLE_PROXY and Proxy URL handling
2026-06-21 20:20:31 +01:00
venxm14 74bf677f83 Fixed Proxy URL stripper bug and HTTP protocol bug 2026-06-21 21:13:45 +05:30
venxm14 d03b047e89 Fixed ENABLE_PROXY environment variable bug 2026-06-21 21:08:43 +05:30
Inside4ndroid 671bbb6159 Clear user config 2026-05-29 20:31:50 +01:00
Inside4ndroid 47d2886fbe chore: bump to 1.1.1, remove MoviesMod & VidZee providers, fix LordFlix domain, fix Docker proxy crash 2026-05-29 20:29:01 +01:00
Inside4ndroid d0419321a4 Huge update see changelog for details. 2026-05-16 12:29:45 +01:00
Inside4ndroid 35b95809de Release v1.0.9: Remove Showbox provider, cleanup dependencies 2025-11-19 13:11:30 +00:00
Inside4ndroid 6ebb0d32d5 chore(release): 1.0.8 2025-10-03 16:27:54 +01:00
Inside4ndroid c8ef89398f chore(release): bump to 1.0.7 (VidZee AES decoder, tail prefetch instrumentation) 2025-09-21 14:53:13 +01:00
Inside4ndroid 9714ad51c8 docs: update README and CHANGELOG for v1.0.6 proxy features 2025-09-20 15:31:55 +01:00
Inside4ndroid 17079e1646 chore: provider updates (4khdhub permanent r2.dev block + zip strip, moviesclub vidora headers) 2025-09-19 10:51:38 +01:00
Inside4ndroid 25cc59659d docs: update README (unified stream schema, MoviesClub, registry mapping, docker run)\nchore(release): 1.0.4 tag prep\nchore: ignore restart.trigger and .vscode 2025-09-18 21:00:21 +01:00
Inside4ndroid ab18e06a94 chore(release): 1.0.3 bind to 0.0.0.0 (Docker host access), add BIND_HOST env; update CHANGELOG and README badge 2025-09-17 12:01:35 +01:00
Inside4ndroid 25acff24a7 fix(server): bind to 0.0.0.0; set BIND_HOST for Docker/runtime to expose port 2025-09-17 12:00:13 +01:00
Inside4ndroid 0e9f6d92e4 chore(release): bump version to 1.0.2, standardize Docker port 8787, update CHANGELOG and README badge 2025-09-17 11:51:02 +01:00
Inside4ndroid 3433c093bd Update user-config.json 2025-09-17 11:40:53 +01:00
Inside4ndroid e489a1b613 chore: bump version to 1.0.1, add CHANGELOG, restart UX (UI modal + nodemon trigger), config/filters fixes, Docker ownership and compose docs 2025-09-17 11:35:15 +01:00
Inside4ndroid 484ea4bb3f update changelog. 2025-09-16 18:57:38 +01:00
102 changed files with 5377 additions and 17680 deletions
+5
View File
@@ -0,0 +1,5 @@
TMDB_API_KEY=your_tmdb_api_key
# Optional: JSON array for multi-key rotation
# TMDB_API_KEYS=["key1","key2"]
# Optional: server port (defaults to 8787)
# API_PORT=8787
+4
View File
@@ -1 +1,5 @@
/node_modules /node_modules
restart.trigger
.vscode/
.env
/tmp_context
+268 -2
View File
@@ -1,11 +1,263 @@
# Changelog # Changelog
All notable changes to this project will be documented in this file.
## [1.3.0] - 2026-08-19
### Added
- **7 new providers ported from the Infinite-streams v5.0.0 Stremio addon** (CommonJS ports, registered in `providers/registry.js`):
- `streamflix` - StreamFlix direct MP4 links (30-min `data.json` cache + Firebase episode lookup).
- `vaplayer` - VaPlayer HLS streams resolved by IMDb ID.
- `castletv` - CastleTV streams via AES-128-CBC encrypted API (`api.hlowb.com`), per-track + shared fallback, subtitle support.
- `hdghartv` - HDGharTV title search with fuzzy match + IMDb ID verification, quality-sorted links.
- `netmirror` - Netflix direct (embed-tmdb) plus NewTV platform fallback (Netflix/Prime Video/Hotstar/Disney) with rotating discovery domains.
- `onetouchtv` - OneTouchTV streams via AES-256-CBC custom-base64 API (title/season matching + IMDb verification + absolute-episode resolution).
- `zxcstreams` - ZXCStreams multi-server backend (icarus/berkas/orion/athena) with dynamic domain discovery and shared-token flow.
- **New shared utilities**: `utils/titleMatch.js` (fuzzy title matching port), `utils/tmdbTitleToImdb.js` (TMDB title→IMDb resolution with cache), `utils/cinemetaEpisodes.js` (episode counts per season).
- **Configurable provider-check TMDB ID**: the title used by dashboard functional checks is now editable in the Server Status panel (default `278`, env `PROVIDER_CHECK_TMDB_ID`). Persisted via `POST /api/config`, reset via Clear All, and exposed through `/api/status` as `providerCheckTmdbId`.
### Fixed
- **Vidlink provider**: Rewritten for the new API format — parses `stream.qualities` into one stream per quality (best-first), no longer relies on the removed HLS proxy format. Requires a working CDN (some CDNs 429 server-side requests from blocked IPs; browsers play direct URLs fine).
- **`utils/tmdb.js`**: Now reads the TMDB API key via `utils/tmdbKey` (normalized `tmdbApiKeys`) instead of the deleted legacy `config.tmdbApiKey` field, so `getDetails`/`resolveImdbId` work with the current config schema.
- **castletv quality parsing**: Quality is now parsed from `resolutionDescription` (SD 480P / HD 720P / FHD 1080P); streams are deduped per URL keeping the highest resolution (eliminates the "2024p" year-match bug).
- **docker-compose.yml**: Fixed broken YAML indentation on the `BIND_HOST` environment entry that prevented `docker compose up` from parsing.
### Changed
- Version bumped to **1.3.0** across `package.json` and `package-lock.json`.
- `.env.example` refreshed to the current schema (`API_PORT` instead of legacy `PORT`, placeholder TMDB key, documented multi-key JSON array).
### Documentation
- **README fully rewritten** to match the current project state: 13-provider list, accurate authentication docs (`utils/auth-users.json`, default `admin` / `change-me`), corrected environment variable table, complete endpoint + proxy route reference, plugin example mirroring the real `providerFunctionMap`, and a new "Server Status & Functional Checks" section.
---
## [1.2.0] - 2026-08-19
### Removed
- **LordFlix provider**: Permanently removed — upstream API `snowhouse.lordflix.club` (and `lordflix.org`) no longer resolves in DNS. `enc-dec.app` is a passthrough signer and returns signed URLs for the dead host; no compatible successor API exists (`lordflix.gd` / `lordflix.app` are unrelated clones using TMDB-ID + iframe embeds, not the snowhouse API).
- **NoTorrent provider**: Permanently removed — taken down by its developer; the Stremio addon API bridge (`addon-osvh.onrender.com`) is no longer available.
---
## [1.1.1] - 2026-05-29
### Removed
- **MoviesMod provider**: Permanently removed — `moviesmod.farm` returns HTTP 403 with Cloudflare managed challenge (`cf-mitigated: challenge`). No server-side workaround exists without a browser runtime.
- **VidZee provider**: Permanently removed — hardcoded AES-256-GCM salt is no longer valid against VidZee's current API key blob, causing all decryption to fail.
### Fixed
- **LordFlix provider**: Updated upstream domain (`network.hasta-la-vista.site` → `snowhouse.lordflix.club`) and refreshed server list (10 servers).
- **Docker startup crash**: Added missing `COPY proxy ./proxy` to both build and runtime stages in `Dockerfile` — `apiServer.js` unconditionally requires `./proxy/proxyServer` at startup.
---
## [1.1.0] - 2026-05-16
### Dependency Security
- **npm vulnerabilities fixed**: Upgraded `cheerio` (1.0.0 → 1.2.0) and `axios-cookiejar-support` (6.0.2 → 7.0.0) to eliminate 14 vulnerabilities (including `undici <=6.23.0` high/critical CVEs). Result: 0 vulnerabilities.
### Added
- **Videasy provider**: 10-server parallel scraper via enc-dec.app decrypt relay.
- **Vidlink provider**: Single-stream HLS source via enc-dec.app encode relay.
- **LordFlix provider**: 9-server scraper via enc-dec.app (replaces defunct Vidsync).
- **NoTorrent provider**: Stremio addon API bridge (`addon-osvh.onrender.com`).
- **DahmerMovies provider**: Open-directory direct file link scraper with proxy rewrite.
- **GitHub Sponsors button**: Official iframe embed in admin sidebar and README badge.
### Removed
- **Vidsync provider**: All 6 servers returned HTTP 401 — API locked. Replaced by LordFlix.
- **MP4Hydra provider**: Dead upstream, removed entirely.
- **UHDMovies provider**: Dead upstream, removed entirely.
- **vidsrcextractor.js**: Unused legacy file removed.
### Fixed
- **Provider enable/disable not working at runtime**: Removed the startup-time enabled/disabled filter. All providers are now always loaded into the registry array. `listProviders()` and `getProvider()` now call `isProviderEnabled(name)` which reads the live `config` object on every call. Changes take effect immediately without a server restart.
- **Showbox/FebBox provider**: Restored with correct `responseType:'text'`, AJAX headers, and double `ui=` cookie bug fix.
- **MoviesMod provider**: Updated class-based selectors and added support for `links.modpro.blog`, `cloud.unblockedgames.world`, `tech.examdegree.site`, and `driveleech.net` domains.
- **VixSrc provider**: Corrected to proper 2-step API flow.
---
## [1.0.9] - 2025-11-19
### Removed
- **Showbox/FebBox/PStream provider**: Permanently removed due to PStream API being protected by Cloudflare bot detection, making it inaccessible without complex proxy infrastructure
- Deleted `providers/Showbox.js` and backup files
- Removed FebBox cookie management from configuration panel
- Removed `FEBBOX_COOKIES`, `SHOWBOX_CACHE_DIR`, and related environment variables
- Cleaned up all Showbox-specific logic from provider registry
- Removed FebBox/PStream configuration UI panel and related functions
- **Unused dependencies**: Removed 121 packages including puppeteer, patchright, puppeteer-extra, puppeteer-extra-plugin-stealth, and vm2 (~200MB+ saved)
- **Unused utility files**: Removed `cloudflareBypasser.js` and `jsunpack.js`
### Fixed
- **VidZee provider**: Updated to support two-stage AES decryption with dynamic API keys. The provider now fetches an encrypted key from `https://core.vidzee.wtf/api-key`, decrypts it using AES-256-GCM with a hardcoded key, then uses the result to decrypt video URLs with AES-256-CBC. Includes 1-hour API key caching for performance.
- **4khdhub provider**: Added support for `links.modpro.blog` domain (site migrated from `modrefer.in`).
- **moviesmod provider**: Added support for `links.modpro.blog` domain alongside existing `modrefer.in`.
- **uhdmovies provider**: Fixed domain to use `uhdmovies.rip` instead of outdated `uhdmovies.mov`. Added automatic domain replacement for stale scraped URLs. Created new `utils/linkResolver.js` utility to handle driveseed/driveleech download button extraction (supports Instant Download, Resume Cloud, Resume Worker Bot, Direct Links CF Type 1).
- **Provider registry**: Fixed `listProviders()` to return all available providers with their enabled status, not just enabled ones. Config panel now shows all 6 providers correctly.
---
## [1.0.8] - 2025-10-03
### Added
- Added `processStreamsForProxy` so the aggregate and provider-specific endpoints automatically rewrite stream URLs through the internal `/m3u8-proxy`/`/ts-proxy` layer whenever `enableProxy` is turned on, stripping provider headers safely.
### Changed
- Showbox provider overhaul: filesystem-only caching (Redis removed), smarter FebBox cookie selection with region fallbacks, TMDB title/image validation that recognises romanized names, plus cached HEAD size lookups for faster listings.
- Vixsrc provider rewritten to parse the `window.masterPlaylist` payload (token + expiry), returning a single master playlist with English subtitle lookup and correct Referer headers.
- Config loader now prefers `utils/user-config.json`, dedupes TMDB keys, defaults provider enable flags to `true`, and scrubs legacy proxy env values when mirroring overrides back to `process.env`.
- 4KHDHub provider permanently drops `.zip` archive links instead of trimming extensions and improves host distribution logging.
- Provider registry dynamically enumerates available modules (removing MoviesClub/Xprime remnants) while keeping per-request cookie stats for the admin debug panel.
### Removed
- Deleted legacy `providers/moviesclub.js`, `providers/xprime.js`, and other unused proxy/auth remnants that were no longer referenced.
### Fixed
- Aggregated responses now obey the proxy flag without client changes—streams returned from `/api/streams/...` are proxy-wrapped and omit upstream header hints when `enableProxy` is active.
---
## [1.0.7] - 2025-09-21
### Added
- VidZee provider: definitive AES-256-CBC decoder (replaced heuristic token attempts). Decodes newly obfuscated `atob(token) => ivBase64:cipherBase64` structure using padded key `qrincywincyspider` and PKCS7.
- Tail segment acceleration: internal tail prefetch map (`tailPrefetchMap`) with TTL cleanup and configurable `tailPrefetchKB` (default 256 KB) now documented and instrumented.
### Changed
- Proxy range negotiation: clarified interaction between `progressiveOpen` and synthetic initial partial (auto-suppression when progressive active). Additional debug logging around tail cache hits and forced host overrides.
- VidZee streams now always return direct decrypted URLs (per-stream `originalToken` retained for debugging when `VIDZEE_DEBUG=1`).
- Updated README version badge to 1.0.7.
### Fixed
- Eliminated edge cases where encoded VidZee tokens were leaking through without decoding.
- Reduced VLC initial loop behavior via tail prefetch serving last-byte probes from cache faster (combined with earlier 1.0.6 range logic).
### Notes
- Future proxy tuning items (size meta map, dynamic progressive growth) tracked but not part of this release.
---
## [1.0.6] - 2025-09-20
### Removed
- MoviesClub provider (multi-server scraping complexities & Turnstile challenge; deprecated permanently)
- Xprime provider (upstream Xprime.tv offline due to security changes)
- 4khdhub provider: all `.zip` archive links are now omitted entirely (previous releases experimented with stripping the extension which produced non-playable pseudo-MKV links)
### Added
- Structured multi-server debug instrumentation (session summaries, per-fetch metrics, pattern counters)
- Turnstile challenge detection & bypass attempt scaffold (synthetic `/rcp_verify` token posting)
- Optional internal stream proxy (`enableProxy` flag): mounts `/m3u8-proxy`, `/ts-proxy`, `/sub-proxy` with playlist + segment + subtitle handling and segment prefetch cache.
- Proxy range management features:
- `clampOpen` (default on) – caps ambiguous open-ended `bytes=0-` requests to a bounded initial window (`openChunkKB`, default 4096 KB)
- `progressiveOpen` (default on) – incremental expansion of the head range on successive `bytes=0-` requests instead of a single huge span
- `initChunkKB` (default 512 KB) – size of the synthetic initial 206 response when neither clamp/progressive produce a range and `noSynth` is not set
- `tailPrefetch` (default on) + `tailPrefetchKB` (default 256 KB) – asynchronous fetch & in-memory cache of the file tail to satisfy rapid VLC tail probes
- `force200` (opt-in) – normalizes upstream 206 responses to 200 for diagnostics
- `noSynth` (opt-in) – disables synthetic initial partial response generation
- Tail prefetch TTL cleanup task (30 min window) and in-memory maps for: segment cache, open range clamp, progressive growth, and tail buffers
- Host routing overrides: `pixeldrain.*` & `video-downloads.googleusercontent.com` are forced through `/ts-proxy` (extensionless or ambiguous content)
### Changed
- Centralized multi-server request headers with realistic `sec-ch-ua*` & `Sec-Fetch-*` values
- Added retry, rotating User-Agent, and cookie jar logic to multi-server fetch pipeline
- Showbox provider priority map updated after Xprime removal
- README/Docs trimmed to reflect current active providers only
- When `enableProxy` is active, stream response objects have their original `headers` field removed (proxy handles all required headers internally)
- 4khdhub provider now filters out archive endpoints instead of attempting extension normalization (prevents feeding ZIP files to players)
- Open-ended range handling improved to reduce VLC negotiation loops by throttling first-pass read size and growing progressively
- Synthetic initial partial response is automatically suppressed when `progressiveOpen` is active (real range growth preferred)
### Fixed
- Ensured multi-server fallback attempts (direct rcp player/m3u8 extraction) operate with improved diagnostics
- Eliminated repeated VLC tail probe stalls caused by archive masquerading as video content (root cause was filtered by dropping `.zip` URLs)
### Documentation
- Updated README version badge to 1.0.6 and provider list (removed MoviesClub & Xprime, clarified active providers list)
- Added proxy tuning parameter reference (clamp/progressive/tail prefetch, synthetic partial, force200) and host override notes
- Expanded explanation that per-stream headers are stripped when proxying is enabled
---
## [1.0.5] - 2025-09-19
### Improved
- 4khdhub provider: Permanently block `r2.dev` FSL links (previous optional flag removed).
- Added Referer/Origin headers automatically for FSL Server links during validation (prior to block enforcement ensured proper behavior).
- Tightened URL validation: removed unconditional trust for `r2.dev`; validation logic now consistent across hosts.
- Host distribution instrumentation logs final hostname counts for easier diagnostics.
- Preserved HubCloud worker `.zip` links by stripping the `.zip` extension instead of discarding them (enables direct playback attempts).
### Notes
- `r2.dev` links are always removed from final output; no env flag required.
---
## [1.0.4] - 2025-09-18
### Changed
- Unified stream object schema standardized:
```json
{ "title": "…", "url": "…", "quality": "…", "provider": "…", "headers": { } }
```
---
## [1.0.3] - 2025-09-17
### Fixed
- Server now binds to `0.0.0.0` by default so Docker port publishing works correctly from the host. Added `BIND_HOST=0.0.0.0` in Dockerfile and compose.
### Notes
- If you were seeing `ERR_CONNECTION_REFUSED` on `http://localhost:8787`, pull the latest image or rebuild, then re-run with `-p 8787:8787`.
---
## [1.0.2] - 2025-09-17
### Fixed
- Standardized Docker port to `8787` everywhere (Dockerfile `EXPOSE`, compose `ports`, healthchecks, and README examples). Previous release notes mentioned 8787 but some environments weren't reset; this release ensures consistency.
### Documentation
- Updated README version badge to 1.0.2 and verified Docker commands use `-p 8787:8787`.
---
## [1.0.1] - 2025-09-17
### Added
- Admin UI: Restart Server button (below Logout) with a themed confirmation modal. The UI polls `/api/health` and auto-reloads when the server is back.
- Sidebar divider under Logout/Restart for clarity.
### Changed
- Exclude Codecs presets:
- Introduced "None" (default) → `{ "excludeDV": false, "excludeHDR": false }`.
- "All" → `{ "excludeDV": true, "excludeHDR": true }`.
- Persist and render presets reliably after Save / Reload.
- Minimum Quality: Clear All now resets to `"all"` and selects the All preset in the UI.
- Clear All: Now fully resets TMDB keys, FebBox cookies, providers, and filters; added a themed confirmation modal with optional "Don't ask again" preference.
- Live Config: Hide legacy `tmdbApiKey` (only show `tmdbApiKeys`).
- Restart behavior:
- Local dev: Nodemon watches `restart.trigger`; backend writes it before a clean exit to force a restart.
- Docker: Compose uses `restart: unless-stopped`; Docker restarts the container after restart endpoint triggers exit.
- Dockerfile: Ensure non-root `app` user owns `/app` for writing overrides and restart marker.
- package.json scripts: Simplified to `start`, `start:dev`, and `lint`; both start scripts are nodemon-based and watch `restart.trigger`.
### Fixed
- Provider matrix re-renders immediately after Clear All (no page refresh needed).
- Handling of Exclude Codecs "ALL" previously not persisting correctly.
---
## [1.0.0] - 2025-09-16 ## [1.0.0] - 2025-09-16
Initial stable release.
### Added ### Added
- Comprehensive `README.md` (features, endpoints, admin UI overview, screenshots gallery, Docker usage, troubleshooting). - Comprehensive `README.md` (features, endpoints, admin UI overview, screenshots gallery, Docker usage, troubleshooting).
- `LICENSE` (MIT) file. - `LICENSE` (MIT) file.
- Multi‑TMDB key rotation support (array of keys; random selection per request). - Multi-TMDB key rotation support (array of keys; random selection per request).
- Config override system writing to `utils/user-config.json` with live merged view. - Config override system writing to `utils/user-config.json` with live merged view.
- Session-based authentication (login, logout, session check, password change) with brute-force mitigation. - Session-based authentication (login, logout, session check, password change) with brute-force mitigation.
- Rate limiting + exponential lockouts for failed login attempts. - Rate limiting + exponential lockouts for failed login attempts.
@@ -42,9 +294,23 @@
- Heartbeat diagnostic interval to aid investigation of unexpected exits. - Heartbeat diagnostic interval to aid investigation of unexpected exits.
- Intercepted premature `process.exit` calls to avoid silent shutdowns during debugging. - Intercepted premature `process.exit` calls to avoid silent shutdowns during debugging.
---
## Historical Context ## Historical Context
This 1.0.0 release consolidates modernization work: provider cleanup, configuration clarity, deployment ergonomics (Docker + CI), security hardening, and observability. This 1.0.0 release consolidates modernization work: provider cleanup, configuration clarity, deployment ergonomics (Docker + CI), security hardening, and observability.
--- ---
[1.0.0]: https://github.com/Inside4ndroid/TMDB-Embed-API/releases/1.0.0 [1.3.0]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.1.1...v1.2.0
[1.1.1]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.9...v1.1.0
[1.0.9]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.8...v1.0.9
[1.0.8]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.7...v1.0.8
[1.0.7]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.6...v1.0.7
[1.0.6]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.5...v1.0.6
[1.0.5]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.4...v1.0.5
[1.0.4]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.3...v1.0.4
[1.0.3]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.2...v1.0.3
[1.0.2]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.1...v1.0.2
[1.0.1]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.0...v1.0.1
+5
View File
@@ -12,6 +12,7 @@ RUN npm install --omit=dev
# Copy only required source (avoid sending screenshots, node_modules already installed) # Copy only required source (avoid sending screenshots, node_modules already installed)
COPY apiServer.js ./ COPY apiServer.js ./
COPY providers ./providers COPY providers ./providers
COPY proxy ./proxy
COPY public ./public COPY public ./public
COPY utils ./utils COPY utils ./utils
COPY README.md ./ COPY README.md ./
@@ -22,6 +23,7 @@ ARG VERSION=dev
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production \ ENV NODE_ENV=production \
API_PORT=8787 \ API_PORT=8787 \
BIND_HOST=0.0.0.0 \
APP_VERSION=${VERSION} APP_VERSION=${VERSION}
# Create non-root user # Create non-root user
@@ -32,6 +34,7 @@ COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/apiServer.js ./ COPY --from=build /app/apiServer.js ./
COPY --from=build /app/public ./public COPY --from=build /app/public ./public
COPY --from=build /app/providers ./providers COPY --from=build /app/providers ./providers
COPY --from=build /app/proxy ./proxy
COPY --from=build /app/utils ./utils COPY --from=build /app/utils ./utils
COPY --from=build /app/package.json ./ COPY --from=build /app/package.json ./
COPY --from=build /app/README.md ./ COPY --from=build /app/README.md ./
@@ -39,6 +42,8 @@ COPY --from=build /app/README.md ./
# Expose port (documentational; runtime can override) # Expose port (documentational; runtime can override)
EXPOSE 8787 EXPOSE 8787
# Ensure runtime user owns app directory for writes (overrides, restart marker)
RUN chown -R app:app /app
USER app USER app
# Labels / metadata # Labels / metadata
+206 -66
View File
@@ -6,7 +6,7 @@
<img src="https://img.shields.io/badge/Node.js-18%2B-brightgreen?style=flat" /> <img src="https://img.shields.io/badge/Node.js-18%2B-brightgreen?style=flat" />
<img src="https://img.shields.io/badge/Status-Active-success?style=flat" /> <img src="https://img.shields.io/badge/Status-Active-success?style=flat" />
<img src="https://img.shields.io/badge/License-MIT-blue?style=flat" /> <img src="https://img.shields.io/badge/License-MIT-blue?style=flat" />
<img src="https://img.shields.io/badge/Version-1.0.0-informational?style=flat" /> <img src="https://img.shields.io/badge/Version-1.3.0-informational?style=flat" />
<img src="https://img.shields.io/docker/pulls/inside4ndroid/tmdb-embed-api?label=Docker%20Pulls&style=flat" /> <img src="https://img.shields.io/docker/pulls/inside4ndroid/tmdb-embed-api?label=Docker%20Pulls&style=flat" />
</p> </p>
@@ -33,15 +33,32 @@
--- ---
## ✨ Features ## ✨ Features
- **Multi‑TMDB Key Rotation** – Supply multiple API keys; one is chosen randomly per request. - **13 Built-in Providers** – Showbox/FebBox, 4KHDHub, VixSrc, Videasy, Vidlink, DahmerMovies, StreamFlix, VaPlayer, CastleTV, HDGharTV, NetMirror, OneTouchTV, ZXCStreams — with per-provider enable toggles + default selection.
- **Provider Aggregation** – Pluggable providers (Showbox, 4khdhub, MoviesMod, MP4Hydra, VidZee, Vixsrc, Xprime) with per‑provider enable toggles + default selection. - **Multi-TMDB Key Rotation** – Supply multiple API keys; one is chosen randomly per request.
- **🔥 Plugin System** – Drop new provider files in `providers/` and add its exported function to the registry map (`providers/registry.js` → `providerFunctionMap`). No core file edits required.
- **Dynamic Filtering** – Minimum quality presets, custom JSON quality map, codec exclusion rules (presets + JSON). - **Dynamic Filtering** – Minimum quality presets, custom JSON quality map, codec exclusion rules (presets + JSON).
- **Runtime Overrides UI** – Fully interactive web admin at `/` (login protected) writing to `utils/user-config.json`. - **Runtime Overrides UI** – Fully interactive web admin at `/config.html` (login protected) writing to `utils/user-config.json`.
- **Session Auth + Rate Limiting** – Login system with brute‑force lockouts, logout, password change. - **Session Auth + Rate Limiting** – Login system with brute-force lockouts, logout, and password change.
- **Status & Health Panel** – Live metrics, provider status, endpoint list, functional provider checks. - **Status & Health Panel** – Live metrics, provider status, endpoint list, and per-provider functional checks (configurable check title via TMDB ID).
- **Config Propagation** – Overrides mirrored to `process.env` for legacy compatibility (no `.env` required after first save). - **Config Propagation** – Overrides mirrored to `process.env` for legacy compatibility (no `.env` required after first save).
- **Back‑Navigation Safe** – Cache-control + visibility/session revalidation. - **Back-Navigation Safe** – Cache-control + visibility/session revalidation.
- **Extensible** – Simple provider registry pattern. - **Optional Stream Proxy Layer** – When enabled, rewrites returned stream URLs so HLS playlists, TS segments, and subtitles are served through internal endpoints (`/m3u8-proxy`, `/ts-proxy`, `/sub-proxy`) allowing uniform headers, origin shielding, and optional segment caching.
When active the API omits per-stream `headers` objects from responses (they're no longer needed by clients) to avoid leaking upstream header requirements.
Proxy Tuning Parameters (query flags accepted by `/ts-proxy` – defaults shown):
- `clampOpen` (on) – If a client sends an ambiguous `Range: bytes=0-`, constrain it to an initial window of `openChunkKB` (default 4096 KB) to avoid huge first reads.
- `openChunkKB=4096` – Size (KB) used for both clamp window and each progressive expansion increment.
- `progressiveOpen` (on) – Grow successive ambiguous head requests (`bytes=0-`) incrementally instead of one large span. Maintains a per-URL expansion map.
- `initChunkKB=512` – Size used for a synthetic initial partial (206) when no client range is provided and progressive growth is disabled. Capped 64–2048 KB.
- `noSynth=1` – Disable synthetic initial partial generation (forces pass-through behavior).
- `force200=1` – Normalize upstream 206 responses to 200 (diagnostics / edge player testing).
- `tailPrefetch` (on) – Enable asynchronous tail fetch of the file's last bytes to satisfy rapid player tail probes.
- `tailPrefetchKB=256` – Tail window size (64–2048 KB). Cached in memory with TTL cleanup.
Behavior Notes:
- Synthetic partials auto-disable when `progressiveOpen` is active (real progressive ranges preferred).
- Player tail probes (e.g., VLC metadata scans) are accelerated by the cached tail window.
- Forced 200 mode strips `Content-Range` to emulate full responses for troubleshooting.
- Host Overrides: `pixeldrain.*` and `video-downloads.googleusercontent.com` URLs are routed through `/ts-proxy` regardless of extension to ensure correct range + MIME handling.
--- ---
@@ -53,11 +70,11 @@ npm install
# 2. (Optional) Copy example env if you want an initial TMDB key # 2. (Optional) Copy example env if you want an initial TMDB key
cp .env.example .env # then edit TMDB_API_KEY= cp .env.example .env # then edit TMDB_API_KEY=
# 3. Start API (production style) # 3. Start API with automatic restarts (recommended for local dev)
node apiServer.js npm start
# Or with automatic restarts (if you add a dev script) # Or production-style single run
# npm run dev:api # node apiServer.js
# 4. Open the Admin UI (login page) in browser # 4. Open the Admin UI (login page) in browser
http://localhost:8787/ http://localhost:8787/
@@ -66,6 +83,8 @@ http://localhost:8787/
curl http://localhost:8787/api/health curl http://localhost:8787/api/health
``` ```
**Default credentials (first run):** `admin` / `change-me` — **change them immediately** from the dashboard or via `POST /auth/change-password`.
--- ---
## 🐳 Docker Usage ## 🐳 Docker Usage
@@ -79,6 +98,11 @@ docker run --name tmdb-embed-api -p 8787:8787 \
inside4ndroid/tmdb-embed-api:latest inside4ndroid/tmdb-embed-api:latest
``` ```
Or the minimal quick-test run:
```bash
docker run -it -p 8787:8787 inside4ndroid/tmdb-embed-api:latest
```
Persist overrides (Windows PowerShell example) by mounting a local file: Persist overrides (Windows PowerShell example) by mounting a local file:
```powershell ```powershell
New-Item -ItemType File -Path .\utils\user-config.json -Force | Out-Null New-Item -ItemType File -Path .\utils\user-config.json -Force | Out-Null
@@ -100,14 +124,13 @@ docker run --name tmdb-embed -p 8787:8787 \
After first login + save, the UI writes overrides into the mounted `user-config.json` so they persist across container restarts. After first login + save, the UI writes overrides into the mounted `user-config.json` so they persist across container restarts.
### docker-compose ### docker-compose
An example `docker-compose.yml` is included. Start with: An example `docker-compose.yml` is included (healthcheck + `restart: unless-stopped`). Start with:
```bash ```bash
docker compose up -d --build docker compose up -d --build
``` ```
Environment variables can be supplied via a `.env` file in the same directory (Compose automatically loads it). Example `.env`: Environment variables can be supplied via a `.env` file in the same directory (Compose automatically loads it). Example `.env`:
``` ```
TMDB_API_KEY=first_key TMDB_API_KEY=first_key
FEBBOX_COOKIES=cookie1,cookie2
``` ```
To stop & remove: To stop & remove:
@@ -124,19 +147,30 @@ docker run -p 8787:8787 \
``` ```
or add / remove keys inside the Admin UI (Keys panel) and save. or add / remove keys inside the Admin UI (Keys panel) and save.
If both `TMDB_API_KEY` and `TMDB_API_KEYS` are provided, rotation uses the array. Clearing the array in the UI also clears the legacy key.
### Key Environment Variables ### Key Environment Variables
| Variable | Purpose | Notes | | Variable | Purpose | Notes |
|----------|---------|-------| |----------|---------|-------|
| `API_PORT` | Port the server listens on | Defaults to 8787 | | `API_PORT` | Port the server listens on | Defaults to `8787` |
| `BIND_HOST` | Interface to bind | Defaults to `0.0.0.0` |
| `TMDB_API_KEY` | Single TMDB key (legacy) | Use if you only have one key | | `TMDB_API_KEY` | Single TMDB key (legacy) | Use if you only have one key |
| `TMDB_API_KEYS` | JSON array of keys | Overrides single key when present | | `TMDB_API_KEYS` | JSON array of keys | Overrides single key when present |
| `FEBBOX_COOKIES` | Comma separated FebBox cookies | Enables Showbox provider immediately | | `DEFAULT_PROVIDERS` | Comma-separated default providers | Used by the aggregate endpoint |
| `PASSWORD_HASH` | Pre-seed admin password hash | Optional (UI can set) | | `DEFAULT_REGION` / `FEBBOX_REGION` | Default region | Passed through for legacy compat |
| `ADMIN_USERNAME` | Override default username | Default: `admin` | | `MIN_QUALITIES` | Min quality setting / JSON | Mirrored to the config model |
| `EXCLUDE_CODECS` | Codec exclusion JSON | e.g. `{"excludeDV":true}` |
If both `TMDB_API_KEY` and `TMDB_API_KEYS` are provided, rotation uses the array. Clearing the array in the UI also clears the legacy key. | `FEBBOX_COOKIES` | FebBox JWT cookie(s) | Comma-separated; required for Showbox |
| `ENABLE_<PROVIDER>_PROVIDER` | Per-provider enable flags | e.g. `ENABLE_4KHDHUB_PROVIDER=true` |
--- | `DISABLE_CACHE` | Disable internal caches | `true`/`false` |
| `ENABLE_PSTREAM_API` | PStream API flag | Default `true` |
| `DISABLE_URL_VALIDATION` | Skip general URL checks | Default `false` |
| `DISABLE_4KHDHUB_URL_VALIDATION` | Skip 4khdhub URL checks | Default `false` |
| `ENABLE_PROXY` | Mount proxy routes | Default `false` |
| `PROVIDER_CHECK_TMDB_ID` | Title for dashboard functional checks | Default `278` |
| `SHOWBOX_CACHE_DIR` | Custom Showbox cache directory | Optional |
| `NETMIRROR_API_BASE` | NetMirror embed base URL | Optional override |
| `NETMIRROR_STREAM_REFERER` | NetMirror stream Referer header | Optional override |
### Updating the Image ### Updating the Image
```bash ```bash
@@ -144,14 +178,21 @@ docker compose pull # if using an external registry (future)
docker compose up -d --build docker compose up -d --build
``` ```
### Restart from Admin UI
The Admin panel includes a Restart Server control.
- Local (nodemon): the backend writes a `restart.trigger` file and exits; nodemon detects the change and restarts automatically.
- Docker Compose: the container exits and is restarted by `restart: unless-stopped`.
### Healthcheck ### Healthcheck
Container health relies on `GET /api/health`. If you disable or modify that route, adjust the Dockerfile / compose healthcheck accordingly. Container health relies on `GET /api/health`. If you disable or modify that route, adjust the Dockerfile / compose healthcheck accordingly.
--- ---
## �🔐 Authentication ## 🔐 Authentication
The root (`/`) serves the login page. After successful login a session cookie (`session`) is issued (HttpOnly; 12h lifetime). All admin pages (e.g. `config.html`) require an active session. The root (`/`) serves the login page. After successful login a session cookie (`session`) is issued (HttpOnly; 12h lifetime). All admin pages (e.g. `config.html`) require an active session.
Credentials are stored in `utils/auth-users.json` (auto-created on first run with `admin` / `change-me`). Passwords are hashed with PBKDF2-SHA512 (100,000 iterations, random salt). There is no `PASSWORD_HASH` / `ADMIN_USERNAME` environment override — manage users directly in this file or via the change-password endpoint.
| Endpoint | Method | Purpose | | Endpoint | Method | Purpose |
|----------|--------|---------| |----------|--------|---------|
| `/auth/login` | POST | Authenticate (JSON: `{ username, password }`) | | `/auth/login` | POST | Authenticate (JSON: `{ username, password }`) |
@@ -159,7 +200,7 @@ The root (`/`) serves the login page. After successful login a session cookie (`
| `/auth/session` | GET | Check session status | | `/auth/session` | GET | Check session status |
| `/auth/change-password` | POST | Update password (requires session) | | `/auth/change-password` | POST | Update password (requires session) |
Repeated failed logins trigger exponential lockouts (Retry-After header emitted). Repeated failed logins trigger escalating lockouts (`Retry-After` header emitted; 5 attempts per window).
--- ---
@@ -173,9 +214,10 @@ Saving in the UI writes only changed keys. Setting a field to empty removes the
**Override File:** `utils/user-config.json` **Override File:** `utils/user-config.json`
```json ```json
{ {
"defaultProviders": ["showbox","4khdhub"], "defaultProviders": ["4khdhub"],
"tmdbApiKeys": ["KEY_A","KEY_B"], "tmdbApiKeys": ["KEY_A","KEY_B"],
"enableShowboxProvider": true "enable4khdhubProvider": true,
"providerCheckTmdbId": "278"
} }
``` ```
@@ -187,8 +229,7 @@ Saving in the UI writes only changed keys. Setting a field to empty removes the
| Core | Port, default providers, default region | | Core | Port, default providers, default region |
| Quality / Filters | Min quality presets & codec exclusion JSON | | Quality / Filters | Min quality presets & codec exclusion JSON |
| Keys | Add/remove TMDB API keys (rotated randomly) | | Keys | Add/remove TMDB API keys (rotated randomly) |
| FebBox / PStream | Manage FebBox cookies powering Showbox provider | | Advanced | Provider toggles, cache & validation flags |
| Advanced | Provider toggles, cache & validation flags, proxy paths |
| Server Status | Live metrics, provider functional checks | | Server Status | Live metrics, provider functional checks |
| Live Config | View merged + override JSON snapshots | | Live Config | View merged + override JSON snapshots |
@@ -197,22 +238,75 @@ Session is revalidated on visibility and back/forward navigation to prevent stal
--- ---
## 🔌 Providers ## 🔌 Providers
Current built‑in providers: 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`.
- `showbox`
- `4khdhub`
- `moviesmod`
- `mp4hydra`
- `vidzee`
- `vixsrc`
- `xprime`
You can enable/disable and mark defaults via the Provider Matrix in the UI. If **no defaults** are selected, all enabled providers participate in aggregation. ### Current Built-in Providers
- `showbox` - Showbox/FebBox streams (requires FebBox JWT cookie)
- `4khdhub` - 4KHDHub streams
- `vixsrc` - VixSrc streams
- `videasy` - Videasy streams (10 servers via enc-dec.app)
- `vidlink` - Vidlink streams
- `dahmermovies` - DahmerMovies streams (direct file links)
- `streamflix` - StreamFlix streams (direct MP4 links)
- `vaplayer` - VaPlayer streams (HLS via IMDb ID)
- `castletv` - CastleTV streams (AES-128 encrypted API)
- `hdghartv` - HDGharTV streams (title-matched search + IMDb verification)
- `netmirror` - NetMirror streams (Netflix direct + NewTV platform fallback)
- `onetouchtv` - OneTouchTV streams (AES-256 encrypted API)
- `zxcstreams` - ZXCStreams streams (multi-server backend, dynamic domain discovery)
### Adding a New Provider (Outline) Providers that rely on title resolution against TMDB (`castletv`, `hdghartv`, `onetouchtv`, `zxcstreams`, `vaplayer`, `netmirror`) require at least one TMDB API key configured.
1. Create `providers/myprovider.js` exporting an async fetch function (see existing files for patterns).
2. Lazy‑load it in `providers/registry.js` similar to others. ### Adding a New Provider
3. Add a config flag (`enableMyproviderProvider`) if you want toggle support. 1. **Create** `providers/yourprovider.js` with your stream fetching logic
4. Update the provider matrix arrays in `public/index.js`. 2. **Export** a function like `getYourproviderStreams(tmdbId, mediaType, season, episode)`
3. **Register** it in `providers/registry.js` → `providerFunctionMap`:
```js
// providers/registry.js
const providerFunctionMap = {
'Showbox.js': 'getStreamsFromTmdbId',
'4khdhub.js': 'get4KHDHubStreams',
'vixsrc.js': 'getVixsrcStreams',
'videasy.js': 'getVideasyStreams',
'vidlink.js': 'getVidlinkStreams',
'dahmermovies.js': 'getDahmermoviesStreams',
'streamflix.js': 'getStreamflixStreams',
'vaplayer.js': 'getVaplayerStreams',
'castletv.js': 'getCastletvStreams',
'hdghartv.js': 'getHdghartvStreams',
'netmirror.js': 'getNetmirrorStreams',
'onetouchtv.js': 'getOnetouchtvStreams',
'zxcstreams.js': 'getZxcstreamsStreams',
'yourprovider.js': 'getYourproviderStreams'
};
```
4. The provider will appear in the admin UI with an enable/disable toggle.
**Example Provider (Unified Output):**
```javascript
async function getYourproviderStreams(tmdbId, mediaType, season, episode) {
// Your scraping/API logic here
return [{
name: "YourProvider",
title: "Fight Club - 1080p [YourProvider #1]",
url: "https://stream.url/video.mp4",
quality: "1080p",
provider: "yourprovider",
headers: { "User-Agent": "Mozilla/5.0" }
}];
}
module.exports = { getYourproviderStreams };
```
> **⚠️ Important**: All providers must return streams in the unified JSON format to ensure compatibility with filtering and aggregation.
The system automatically:
- ✅ Detects new provider files
- ✅ Adds enable/disable toggles in the admin UI
- ✅ Includes them in stream aggregation
- ✅ Applies filtering and quality controls
- ✅ No core file edits required!
--- ---
@@ -221,43 +315,74 @@ You can enable/disable and mark defaults via the Provider Matrix in the UI. If *
|----------|-------------| |----------|-------------|
| `GET /api/health` | Basic heartbeat | | `GET /api/health` | Basic heartbeat |
| `GET /api/metrics` | Runtime counters & summary | | `GET /api/metrics` | Runtime counters & summary |
| `GET /api/status` | Metrics + providers + endpoints list | | `GET /api/status` | Metrics + providers + endpoints + `providerCheckTmdbId` |
| `GET /api/providers` | Enabled providers summary | | `GET /api/providers` | All providers with enabled status |
| `GET /api/providers/:name` | Single provider status | | `GET /api/providers/:name` | Single provider status |
| `GET /api/streams/:type/:tmdbId` | Aggregate streams (`type` = movie|series) | | `GET /api/streams/:type/:tmdbId` | Aggregate streams (`type` = movie\|series; supports `?season=&episode=`) |
| `GET /api/streams/:provider/:type/:tmdbId` | Provider-specific streams | | `GET /api/streams/:provider/:type/:tmdbId` | Provider-specific streams (same query params) |
| `GET /api/config` | `{ merged, override }` | | `GET /api/config` | `{ merged, override, overridePath }` |
| `POST /api/config` | Apply override patch | | `POST /api/config` | Apply override patch (persisted to `utils/user-config.json`) |
| `POST /api/restart` | Graceful restart (writes `restart.trigger` + exits) |
| `GET /api/debug/env` | Debug environment/config snapshot |
Aggregate endpoint auto-resolves IMDb when needed and merges provider output before filtering. Proxy routes (mounted only when `enableProxy` is on):
| Endpoint | Description |
|----------|-------------|
| `GET /m3u8-proxy?url=...&headers=...` | Rewrites & serves HLS playlists/segments |
| `GET /ts-proxy?url=...&headers=...` | Range-aware file/segment proxy |
| `GET /sub-proxy?url=...` | Subtitle passthrough proxy |
The aggregate endpoint auto-resolves IMDb when needed, merges all enabled (or `defaultProviders`) provider output, then applies filtering. Provider timing is returned per provider (`providerTimings`).
--- ---
## 🧪 Stream Object Shape (Typical) ## 🧪 Stream Object Schema (Unified)
```json ```json
{ {
"title": "Example Title", "name": "ProviderDisplay",
"url": "https://...", "title": "Fight Club - 1080p [YourProvider #1]",
"url": "https://stream.url/video.mp4",
"quality": "1080p", "quality": "1080p",
"provider": "showbox", "provider": "yourprovider",
"size": null, "headers": { "User-Agent": "Mozilla/5.0" },
"languages": ["en"], "subtitles": [ { "url": "https://.../en.srt", "lang": "English" } ]
"subtitles": [],
"codecs": ["H264"],
"headers": { "Referer": "..." }
} }
``` ```
- `name` / `title` – display strings (often include quality/source hints).
- `url` – direct upstream URL (or proxied URL when `enableProxy` is on).
- `quality` – e.g. `Auto`, `1080p`, `720p`, `480p`, `4K`. Missing/unknown qualities parse as `0`.
- `headers` – optional upstream request headers (stripped when proxying).
- `subtitles` – optional embedded subtitle tracks (CastleTV, NetMirror, OneTouchTV, Showbox).
Filtering passes through `applyFilters` to enforce min quality + codec exclusions. Filtering passes through `applyFilters` to enforce min quality + codec exclusions (see below).
> Note: When the `enableProxy` flag is turned on, provider-specific request headers are stripped from each stream object before responding. Clients should use the proxied URL directly without adding custom Referer/Origin headers.
---
## ⚙️ Configuration Flags (Advanced Panel)
| Flag | Default | Purpose |
|------|---------|---------|
| `enable<Name>Provider` | true | Enable/disable a provider (e.g. `enableCastletvProvider`) |
| `disableCache` | false | Disables internal caches |
| `enablePStreamApi` | true | PStream API flag |
| `disableUrlValidation` | false | Skip general URL pattern validation checks |
| `disable4khdhubUrlValidation` | false | Skip 4khdhub-specific URL validation |
| `enableProxy` | false | Mounts proxy routes and rewrites stream URLs through them |
| `providerCheckTmdbId` | `278` | TMDB ID used by dashboard functional checks |
| `showboxCacheDir` | — | Custom Showbox cache directory |
Toggle `enableProxy` to activate the internal proxy. This adds lightweight playlist/segment/subtitle rewriting without modifying provider code. Disable it to return direct upstream URLs.
--- ---
## 🧩 Quality & Codec Filtering ## 🧩 Quality & Codec Filtering
- Presets: `all`, `480p`, `720p`, `1080p`, `1440p`, `2160p`. - Presets: `all`, `480p`, `720p`, `1080p`, `1440p`, `2160p`.
- Custom quality JSON example: - Custom quality JSON example (runtime applies the `default` entry; per-provider keys are preserved for future use):
```json ```json
{ "default": "900p", "showbox": "1080p" } { "default": "900p" }
``` ```
- Quality strings are normalized numerically — `Auto` ≈ 1080, `HD` ≈ 720, `SD` ≈ 480, plus `4K`/`2160`, `1440`, `1080`, `720`, `576`, `480`, `360`, `240`.
- Codec exclusion JSON example: - Codec exclusion JSON example:
```json ```json
{ "excludeDV": true, "excludeHDR": false } { "excludeDV": true, "excludeHDR": false }
@@ -265,11 +390,20 @@ Filtering passes through `applyFilters` to enforce min quality + codec exclusion
--- ---
## 📊 Server Status & Functional Checks
The **Server Status** panel shows live metrics, the endpoint list, and a per-provider functional check table:
- **Run Provider Functional Checks** – hits `/api/streams/:provider/movie/:tmdbId` for each enabled provider and reports pass/fail with stream counts.
- **Provider check TMDB ID** – the title used for the checks is configurable in the dashboard (default `278` = *The Shawshank Redemption*). The value is persisted to `utils/user-config.json` and exposed via `/api/status` as `providerCheckTmdbId`.
---
## 🔐 Security Notes ## 🔐 Security Notes
- Admin UI requires login; session cookie is HttpOnly. - Admin UI requires login; session cookie is HttpOnly.
- Cache-control headers disable storing sensitive pages. - Cache-control headers disable storing sensitive pages.
- Login is rate limited with escalating lockouts. - Login is rate limited with escalating lockouts (5 attempts per window).
- Password change endpoint enforces minimum length. - Password change endpoint enforces minimum length (8+).
- Default credentials (`admin` / `change-me`) are generated on first run — change them immediately.
--- ---
@@ -279,6 +413,7 @@ Filtering passes through `applyFilters` to enforce min quality + codec exclusion
| Node Version | 18+ LTS | | Node Version | 18+ LTS |
| Reverse Proxy | Terminate TLS (e.g., Nginx) and forward to API port | | Reverse Proxy | Terminate TLS (e.g., Nginx) and forward to API port |
| Persistent Config | Mount / persist `utils/user-config.json` | | Persistent Config | Mount / persist `utils/user-config.json` |
| Auth Users | Persist `utils/auth-users.json` so credentials survive restarts |
| Logs | Pipe stdout to centralized logger | | Logs | Pipe stdout to centralized logger |
| Scaling | Use a single instance unless providers are CPU bound | | Scaling | Use a single instance unless providers are CPU bound |
@@ -289,11 +424,12 @@ For ephemeral platforms (e.g., Vercel) note that some providers use temporary di
## 💡 Troubleshooting ## 💡 Troubleshooting
| Symptom | Cause / Fix | | Symptom | Cause / Fix |
|---------|------------| |---------|------------|
| No streams from Showbox | Missing FebBox cookies |
| TMDB quota issues | Add more keys under Keys panel | | TMDB quota issues | Add more keys under Keys panel |
| Provider missing in matrix | Ensure its enable flag exists & UI updated | | Provider missing in matrix | Ensure its enable flag exists & UI updated |
| Empty merged config after restart | `user-config.json` deleted or unreadable | | Empty merged config after restart | `user-config.json` deleted or unreadable |
| Streams low quality | Adjust min quality preset or custom JSON | | Streams low quality | Adjust min quality preset or custom JSON |
| Showbox shows nothing | Provide `FEBBOX_COOKIES` (comma-separated) under Keys |
| Functional checks fail | Some providers 0-stream legit titles; try changing the *Provider check TMDB ID* |
--- ---
@@ -308,14 +444,18 @@ PRs welcome. Keep changes focused and avoid unrelated formatting churn. For new
## ❤️ Sponsorship ## ❤️ Sponsorship
If this project helps you, consider sponsoring to support continued development & maintenance: If this project helps you, consider sponsoring to support continued development & maintenance:
➡️ **GitHub Sponsors:** https://github.com/sponsors/Inside4ndroid <p align="center">
<a href="https://github.com/sponsors/Inside4ndroid">
<img src="https://img.shields.io/badge/Sponsor-GitHub%20Sponsors-ea4aaa?style=for-the-badge&logo=github-sponsors&logoColor=white" alt="Sponsor on GitHub" />
</a>
</p>
Every contribution accelerates feature delivery & sustainability. Every contribution accelerates feature delivery & sustainability.
--- ---
## 📜 License ## 📜 License
MIT. See `LICENSE`. MIT.
--- ---
+55 -5
View File
@@ -3,13 +3,23 @@ const express = require('express');
const cors = require('cors'); const cors = require('cors');
const os = require('os'); const os = require('os');
const { config, saveConfigPatch, OVERRIDE_PATH } = require('./utils/config'); const { config, saveConfigPatch, OVERRIDE_PATH } = require('./utils/config');
const { authenticate, issueSession, requireAuth, getSession, updatePassword, AUTH_FILE } = require('./utils/auth'); const { authenticate, issueSession, requireAuth, getSession, updatePassword } = require('./utils/auth');
const path = require('path'); const path = require('path');
const { listProviders, getProvider, getCookieStats } = require('./providers/registry'); const { listProviders, getProvider, getCookieStats } = require('./providers/registry');
const { createProxyRoutes, processStreamsForProxy } = require('./proxy/proxyServer');
const { resolveImdbId } = require('./utils/tmdb'); const { resolveImdbId } = require('./utils/tmdb');
const { applyFilters } = require('./utils/streamFilters'); const { applyFilters } = require('./utils/streamFilters');
const app = express(); const app = express();
app.set('trust proxy', 1);
// Conditionally mount proxy routes early so downstream handlers can use them
if (config.enableProxy) {
console.log('[startup] enableProxy flag active: mounting proxy routes');
createProxyRoutes(app);
} else {
console.log('[startup] enableProxy flag disabled: proxy routes not mounted');
}
// --- Simple In-Memory Rate Limiting for /auth/login --- // --- Simple In-Memory Rate Limiting for /auth/login ---
const loginAttempts = new Map(); // key: ip, value: { count, first, last, lockedUntil } const loginAttempts = new Map(); // key: ip, value: { count, first, last, lockedUntil }
@@ -66,8 +76,11 @@ function recordLoginSuccess(ip){
loginAttempts.delete(ip); loginAttempts.delete(ip);
} }
// Guard against premature process.exit from imported legacy modules // Guard against premature process.exit from imported legacy modules, but allow controlled restarts
const realProcessExit = process.exit.bind(process);
let allowControlledExit = false;
process.exit = function(code){ process.exit = function(code){
if (allowControlledExit) return realProcessExit(code);
console.warn('[diagnostic] Intercepted process.exit with code', code, new Error('exit trace').stack); console.warn('[diagnostic] Intercepted process.exit with code', code, new Error('exit trace').stack);
// keep process alive for debugging // keep process alive for debugging
}; };
@@ -198,6 +211,28 @@ app.post('/api/config', (req,res) => {
res.json({ success: ok, merged: config }); res.json({ success: ok, merged: config });
}); });
// Restart endpoint (requires auth via session cookie on /config.html UI)
app.post('/api/restart', (req,res) => {
const sess = getSession(req);
if(!sess) return res.status(401).json({ success:false, error:'UNAUTHORIZED' });
res.json({ success:true, message:'RESTARTING' });
// Give the response a moment to flush
setTimeout(()=>{
try {
const fs = require('fs');
const restartMarker = require('path').join(process.cwd(), 'restart.trigger');
fs.writeFileSync(restartMarker, String(Date.now()));
console.warn('[control] wrote restart.trigger to notify nodemon');
} catch (e) {
console.warn('[control] failed to write restart marker:', e.message);
}
console.warn('[control] restarting process by exit(0)');
// Let nodemon detect the file change and restart the app
allowControlledExit = true;
realProcessExit(0);
}, 300);
});
// --- Basic informational endpoints --- // --- Basic informational endpoints ---
app.get('/api/health', (req, res) => { app.get('/api/health', (req, res) => {
res.json({ ok: true, service: 'tmdb-embed-api', time: new Date().toISOString() }); res.json({ ok: true, service: 'tmdb-embed-api', time: new Date().toISOString() });
@@ -246,7 +281,7 @@ app.get('/api/status', (req,res) => {
const cookieOk = !cookieRequired || (config.febboxCookies && config.febboxCookies.length > 0); const cookieOk = !cookieRequired || (config.febboxCookies && config.febboxCookies.length > 0);
return { name: p.name, enabled: p.enabled, cookieRequired, cookieOk }; return { name: p.name, enabled: p.enabled, cookieRequired, cookieOk };
}); });
res.json({ success:true, metrics: { res.json({ success:true, providerCheckTmdbId: config.providerCheckTmdbId, metrics: {
uptimeSeconds: Math.round((Date.now()-metrics.startTime)/1000), uptimeSeconds: Math.round((Date.now()-metrics.startTime)/1000),
requestsTotal: metrics.requestsTotal, requestsTotal: metrics.requestsTotal,
streamRequests: metrics.streamRequests, streamRequests: metrics.streamRequests,
@@ -315,6 +350,12 @@ app.get('/api/streams/:type/:tmdbId', async (req,res) => {
let streams = results.flat(); let streams = results.flat();
streams = applyFilters(streams, 'aggregate', config.minQualities, config.excludeCodecs); streams = applyFilters(streams, 'aggregate', config.minQualities, config.excludeCodecs);
metrics.streamsReturned += streams.length; metrics.streamsReturned += streams.length;
if (config.enableProxy) {
const serverUrl = `${req.protocol}://${req.get('host')}`;
streams = processStreamsForProxy(streams, serverUrl);
// Omit original headers when proxying to avoid leaking upstream requirements
streams = streams.map(s => { if (s && typeof s === 'object') { const { headers, ...rest } = s; return rest; } return s; });
}
res.json({ success:true, tmdbId, imdbId, count: streams.length, providerTimings, streams }); res.json({ success:true, tmdbId, imdbId, count: streams.length, providerTimings, streams });
} catch (e) { } catch (e) {
metrics.lastError = e.message; metrics.lastError = e.message;
@@ -341,6 +382,11 @@ app.get('/api/streams/:provider/:type/:tmdbId', async (req,res) => {
const providerTimings = { [prov.name]: Date.now()-t0 }; const providerTimings = { [prov.name]: Date.now()-t0 };
streams = applyFilters(streams, prov.name, config.minQualities, config.excludeCodecs); streams = applyFilters(streams, prov.name, config.minQualities, config.excludeCodecs);
metrics.streamsReturned += streams.length; metrics.streamsReturned += streams.length;
if (config.enableProxy) {
const serverUrl = `${req.protocol}://${req.get('host')}`;
streams = processStreamsForProxy(streams, serverUrl);
streams = streams.map(s => { if (s && typeof s === 'object') { const { headers, ...rest } = s; return rest; } return s; });
}
res.json({ success:true, provider: prov.name, tmdbId, imdbId, count: streams.length, providerTimings, streams }); res.json({ success:true, provider: prov.name, tmdbId, imdbId, count: streams.length, providerTimings, streams });
} catch (e) { } catch (e) {
metrics.lastError = e.message; metrics.lastError = e.message;
@@ -349,8 +395,12 @@ app.get('/api/streams/:provider/:type/:tmdbId', async (req,res) => {
}); });
const PORT = config.port; const PORT = config.port;
const server = app.listen(PORT, () => { const HOST = process.env.BIND_HOST || '0.0.0.0';
console.log(`TMDB Embed REST API listening on http://localhost:${PORT}`); const server = app.listen(PORT, HOST, () => {
console.log(`TMDB Embed REST API listening on http://${HOST}:${PORT}`);
if (HOST !== 'localhost') {
console.log(`Local access (if running on your machine): http://localhost:${PORT}`);
}
console.log('Endpoints:'); console.log('Endpoints:');
console.log(' GET /api/health'); console.log(' GET /api/health');
console.log(' GET /api/metrics'); console.log(' GET /api/metrics');
+4
View File
@@ -9,6 +9,7 @@ services:
# Alternative: supply JSON array # Alternative: supply JSON array
# - TMDB_API_KEYS=["key1","key2"] # - TMDB_API_KEYS=["key1","key2"]
- API_PORT=8787 - API_PORT=8787
- BIND_HOST=0.0.0.0
# FebBox cookies (comma separated) if you want Showbox immediately functional # FebBox cookies (comma separated) if you want Showbox immediately functional
- FEBBOX_COOKIES=${FEBBOX_COOKIES:-} - FEBBOX_COOKIES=${FEBBOX_COOKIES:-}
ports: ports:
@@ -16,6 +17,8 @@ services:
volumes: volumes:
# Persist runtime overrides (created by the admin UI) # Persist runtime overrides (created by the admin UI)
- user_config_data:/app/utils/user-config.json - user_config_data:/app/utils/user-config.json
# Allow restart trigger file (optional; stored in container FS)
# - restart_marker:/app/restart.trigger
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8787/api/health || exit 1"] test: ["CMD-SHELL", "wget -qO- http://localhost:8787/api/health || exit 1"]
@@ -25,3 +28,4 @@ services:
retries: 3 retries: 3
volumes: volumes:
user_config_data: user_config_data:
# restart_marker:
+402 -1093
View File
File diff suppressed because it is too large Load Diff
+7 -10
View File
@@ -1,20 +1,19 @@
{ {
"name": "tmdb-embed-api", "name": "tmdb-embed-api",
"version": "1.0.0", "version": "1.3.0",
"description": "Standalone streaming embed source aggregation API (TMDB ID in, streams out)", "description": "Standalone streaming embed source aggregation API (TMDB ID in, streams out)",
"main": "apiServer.js", "main": "apiServer.js",
"scripts": { "scripts": {
"start": "node apiServer.js", "start": "nodemon --watch restart.trigger apiServer.js",
"start:api": "node --watch apiServer.js", "start:dev": "nodemon --watch restart.trigger --ignore providers/.cache --ignore **/cache apiServer.js",
"dev:api": "nodemon --ignore providers/.cache --ignore **/tmdb-embed-showbox-cache apiServer.js",
"lint": "eslint . --ext .js" "lint": "eslint . --ext .js"
}, },
"dependencies": { "dependencies": {
"aromanize": "^0.1.5", "aromanize": "^0.1.5",
"axios": "^1.9.0", "axios": "^1.9.0",
"axios-cookiejar-support": "^6.0.2", "axios-cookiejar-support": "^7.0.0",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",
"cheerio": "^1.0.0", "cheerio": "^1.2.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
@@ -24,18 +23,16 @@
"jsdom": "^26.1.0", "jsdom": "^26.1.0",
"node-fetch": "^2.7.0", "node-fetch": "^2.7.0",
"p-limit": "^6.2.0", "p-limit": "^6.2.0",
"puppeteer": "^24.11.0",
"string-similarity": "^4.0.4", "string-similarity": "^4.0.4",
"tough-cookie": "^5.1.2", "tough-cookie": "^5.1.2",
"ttl-cache": "^1.0.2", "ttl-cache": "^1.0.2",
"vm2": "^3.9.19",
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.10", "eslint": "^8.57.0",
"eslint": "^8.57.0" "nodemon": "^3.1.10"
} }
} }
@@ -1,14 +0,0 @@
{
"data": [
"https://hubcloud.one/drive/eeltgk7tefggdkw",
"https://hubdrive.space/file/2839373447",
"https://hubcloud.one/drive/zm1uzufz8wh1zmv",
"https://hubdrive.space/file/6441436585",
"https://hubcloud.one/drive/dm1sbdubufkkdsv",
"https://hubdrive.space/file/3875373190",
"https://hubcloud.one/drive/wev1xt17dypotb1",
"https://hubdrive.space/file/4216265302",
"https://hubcloud.one/drive/mrhza1jqmajrjko",
"https://hubdrive.space/file/2736804525"
]
}
@@ -1,14 +0,0 @@
{
"data": [
"https://hubcloud.one/drive/2rc17eosl11jalt",
"https://hubdrive.space/file/30320495116",
"https://hubcloud.one/drive/lb82vul8vwzw8lz",
"https://hubdrive.space/file/24320017865",
"https://hubcloud.one/drive/ls4j4srjsh228ks",
"https://hubdrive.space/file/8303893568",
"https://hubcloud.one/drive/cds5rdggp8pwxds",
"https://hubdrive.space/file/8032803119",
"https://hubcloud.one/drive/gb655ffb5nmzg3g",
"https://hubdrive.space/file/2836399263"
]
}
@@ -1,20 +0,0 @@
{
"data": [
"https://hubdrive.space/file/5855904942",
"https://hubdrive.space/file/5855904942",
"https://hubdrive.space/file/27692865336",
"https://hubdrive.space/file/27692865336",
"https://hubdrive.space/file/6521988586",
"https://hubdrive.space/file/6521988586",
"https://hubdrive.space/file/9974112091",
"https://hubdrive.space/file/9974112091",
"https://hubdrive.space/file/8806133211",
"https://hubdrive.space/file/8806133211",
"https://hubdrive.space/file/6167785635",
"https://hubdrive.space/file/6167785635",
"https://hubdrive.space/file/10054935010",
"https://hubdrive.space/file/10054935010",
"https://hubdrive.space/file/4750964205",
"https://hubdrive.space/file/4750964205"
]
}
@@ -1 +0,0 @@
[]
@@ -1,3 +0,0 @@
{
"data": null
}
@@ -1,64 +0,0 @@
{
"data": [
{
"quality": "720p",
"driveseedRedirectLinks": [
{
"server": "✅ Fast Server (G-Drive)",
"url": "https://tech.unblockedgames.world/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEtlZnl5UDZCUGpyd1B0aW1WK01GOERRK0V6MDJ5MFNGbGh1Q1h4Y3VYckNXSDZ2MlIxMDVYMnRyeDMydFBNdWNEajRSbDZXMWlzRjNMaVhMdnhDZkVqK1lkeEFrTFVKcnpBemFRSUJ4NStWTE51aHJIM1pOSUlMK3NxMDMwcUVpZXlPR0dPL2tzQmlWUDFmNkhGR0RSbg==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=Z1JOMXJJbTVlVFFIdXcvQ2xRMFZ5ZEtGekMrZk1ISkY1b3d4MUhray8yM2xPVHRGK00zTWtmekhYcjJRQXBLYw=="
},
{
"server": "🚀 Google Drive (Server 2)",
"url": "https://tech.unblockedgames.world/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEpaWllxaHVJQ3JtNmZUb1cyMy92NkpPRWdIekdtNjNwTDJRTEh0Z3Mva1JYTUthaStDUVp3UzlRa0hqVVlyQjZWdExtZG1wTitoMmRzVXpyWHZRZ0tPaFpVR3lQcmFzUVBSb2hkdE1wWlB6d0s5ZGsyb2UwOCtGRWZKQS9wKzhaZVRrTjBCendhZVV3YksrOGNXZkdJdg==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=UkZYTyswOUlYWnRkd3c4UFg5TWEyNkxMRXdFakZNL05CMjYxcElpOGxOZWxJUEVMQXo4b2V6alh0RWJTemJEcg=="
}
]
},
{
"quality": "1080p",
"driveseedRedirectLinks": [
{
"server": "✅ Fast Server (G-Drive)",
"url": "https://tech.unblockedgames.world/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEtYaWxrRmU5aEVETnNSaE0veXJXN1VnYXJjZTVyQ2hVNUtzbk85MDI0OC9Edm9EUWVpWHNMYU0rRXp0ZGpUWTdFZG5VQzR6T0x3UmgvUEY5R1lFc2lrRFkxK3B3N1A1V0htN3h3eDJyaTY5REJSK1RjVm00MkNnRk9XQW12eCtRWE5seWxFZC9uelNXWUNOSXpBbllkaw==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=QXhGREliQjNpZitEWFJZY01qdXc4cmt4SnRMNGlGcit0eGNkbDNySHUybnVWZjIycElLZ0VTNGdMV3A1S1lKVw=="
},
{
"server": "🚀 Google Drive (Server 2)",
"url": "https://tech.unblockedgames.world/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZElqd1lDTmJPOWRrK2tMc0ZTMENiQTFtRkZkUVBZVjArQ1d4NFhvdzhNcnhsalBHbm82dnVTY3ppS1Jla3RWTHVxMmxuemhzWUdEczdnMElYS3lEN1Y4WEp2bXdxb3FndWFJKzVHRWE1Y1ljR3dkamZQR0U4czV3Y0ZKcUVvSFVRVVdIQ1RITnBtTGIvelVIeXljQWd5Sg==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=ZXV0eEdJYlhJcFhad0FSeDQxWFRGajZGZ3NDZG9BQTQ1c0JvWVUyMnFBMjJIam5zdUE4UnE2dlIwRjVVYVBERg=="
}
]
},
{
"quality": "720p",
"driveseedRedirectLinks": [
{
"server": "✅ Fast Server (G-Drive)",
"url": "https://tech.unblockedgames.world/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZExIOWo1MEtyUTN4L3dUeW8wbzc3VUFXODNBRFZSaFBpUHNwdTZ4aEF2YVJUdVlodzI0Q0pFU1hnQ0ZaUUFHOVNsaEsrWFI0RkY0SFFhYTEyWjFBdFNwNGlHczdNaXpEOS9qeXdVT2dsMU10anpscjVnN21IdmpCcVoyY2x4djJzVzZXb3ZkM0FxM2Q1SXdvR0o5Q3cvNg==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=K1U1RnhlTVdFc25JakJLVFZlTzlaN0w3V2VFRnVlelBLQWRaOVVpVXNBZWFwWksvREw0QmxWQjFCS0pDYWlXOQ=="
},
{
"server": "🚀 Google Drive (Server 2)",
"url": "https://tech.unblockedgames.world/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZExqUmh2aXFIeEJDWjQwZkpVVUpZWWZUQzZzeVh4L3VMblgyR1F4eGJjSDc1OVBRQ2VxeHZyb21acHh5SzNvRkhneXBPTmdhSGgvTEh4QnNKMlJoVUJSUnpvempYdExaZUZkcWQ5VnN2TXFqRzJwYXRvSDF4bk1iTXZ1UlZ0R0JtQjhXYmNEQVE0emhmVlFOR1NLODh1eA==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=aExxTjRHZk4wY1ZjNDJ2VHhzcjdrcVBweHBJbWhGNitYN1ZlUGR4Z0gxalR1T2orZFBFdTRLMnRuUndiVE5IQg=="
}
]
},
{
"quality": "1080p",
"driveseedRedirectLinks": [
{
"server": "✅ Fast Server (G-Drive)",
"url": "https://tech.unblockedgames.world/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEtPNXdWSEEvK1RsbzRDdDJlY0xTd0xlY1FDbHJhK0RjdUlWKzlmZkl6dG1IRWFkTStReXlSUGtXNHhGWG5jZEJDc2ZnUytzK281d3E4VXJVcFdNN0lxZVVjK2F4Y2hhVDAvL2pJRXVHNGMybk5tQW1uSkh5d2p5VlFqTDdaWGdLcUwzMGF6aFNibjkwZTVHd2l5eXRPTg==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=Y08zbk11amZCd3NacmxONVJZYlNNN3FIaklVU0lSSXp6VWhVUTkxVzlpWEJWNVFSOXQ5MTFUWmQvUFNCSzQ0WA=="
},
{
"server": "🚀 Google Drive (Server 2)",
"url": "https://tech.unblockedgames.world/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEozOWlUeUxnd2JuT1l5WHNvQlNVUWpNeGNzTEplSExmN2R4ZDNQVm55Y243VHFicHBVREVZUHQraVZJUmxLeVFYVFArb0Y0ejc2MEUzRE1sL2tsRStKdUNwMW1GSEdkZ0ZrR09Bdzl0ZzN5M3ErU1RCTGdFV1F5UVNiOEtqRVlRcmNNckFZQ0xSbW5vbTRMdW1TOVg5UQ==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=Vk5abDBBaEhiaE9rNE8xTHNwR1BZQVZZUTJ0Qjc2bzFzdHVhVUV5VWdPVnNHOWl0ek40S1FxZ1U3YmUwWTdVNg=="
}
]
}
]
}
@@ -1,224 +0,0 @@
{
"data": [
{
"quality": "Season 1 {Hindi-English} 720p 10Bit Esubs [340MB] - Episode Links",
"driveseedRedirectLinks": [
{
"server": "Episode 1",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEw0Y0tZeDhyUmtkbTFtM0tEay90aGM0TmVJNFdxTGk2MVpORURBQUNNaHRKOGJKZlRkdXNnZzMwb1JPaXRrN0VoV1hReFZyNzNUWFBsQ2R6aGkzKy80VXloNGtQZmllUkhMbGdmcXd0Y01LbVRPd1Foc283eFhQL1VKR3pocGZWZEp0bUJTM1VYRzNGTzYxU0VSd0hwbg==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=UnRjNmRSQ3pLV29ONnRUS0kvYmY3RjV6OTRGUCtpbzRLWE81ejgvM0hrYUMzMjM0cEpnSEh3OGVUTGxXY3BrRQ=="
},
{
"server": "Episode 2",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZElRaTlRd1loWlRLNFhDL3UxeEZxRFl5VW93a3h4eTVyYWNRUFJMYnJoTnFzbHZpQ2hFejZEc1N3RHJYd05UODlnN29MZURUN2tRTTdUQVg2cEZGcVBuUHJLNEpPaVpzREd6d0ZyNzBHRzFrc3F0aktLYW1zaEhTM3FaSnRBTjF6b2x0aU5NU2FMdU40aTA0dkxMdzVQTg==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=bkd6amRlRWE0UlhxV0dqRlo4b1B3TmVCZklXTWRMWnNRbXlDYWdFTnJYWHFIQ2lqOWl5K0pCT2pMaWdXQzdpbw=="
},
{
"server": "Episode 3",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEtLYmtEYjdWVllPYjZLMm1TMXNZaFl2WXdKUk1kbkR5d0Z3Ylo3TTRNSEo4VWxrZnFQVEMvTzduTDhjNG1ITm4zTVdHc3pzdlhMVFVOOHpOSnNWLzMyOHg0SDdFNi9nSThJaFczRkFMV21NVWZ6Nk0ybUNvejQxanF2OWZZUHhBREY2cWNVcCtyMm1XeDBBdFpYWFFlRQ==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=azkzOHJGMHR2T2YzRUhwWU8raUJuam9MVzZ2UWNXdUNkVVNMdjgrN3FBM1JEVkhJZFlZajcrd1JERENQQ3pCRA=="
},
{
"server": "Episode 4",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEw5L1JZVWZxK09QMWtxbEFWU2ppdUx2SmhKQVFrK0FNNEY0NkJsUDJjZlprQTI3eTlBRW5kaEpQSlN1Vks5RzRUMFBVL0FvQ1hJa2J2R2JIV2ovYUNNVkdZYm9hQjNPY2hLTVZyZml4SUQ0YmROUDFOZm5HbVc1SWpvaXRPZHM0d1ZFMDFUOFBPR0VGRW5qdy9RbTNDSw==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=QVdhWE5rVkhmV3B3dHJKTUQzNEd2VDM3cVJNMW9pN05qQUNDTnJ3ZGR2WmVKR2FVS25heUlob0ZOSnlUUS9TUA=="
},
{
"server": "Episode 5",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZExsVjhoaC83WDZCSDZvUlo2bEtOOTNmUmNzUjlmaHlKS01GK2VmcHVTOGQ5RUlEOFhLTnBtdk9LTHRmSTJjU3NLZHdyZEg0a0Z1ZjFCUGxHbzJ2WGo5TDhmNnhyUTR4WkR4MG9hc3EyOTZadkYzN00xYXRqTGt5YnQrZGFVTUdkWktxV3JJcDdubGpoSVN0SkZzdWZ3dQ==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=Qy9GNFN3U2loNWsyRFVGUUh3RnpmbSs1UURJTVllOEJzNDdwOVhHcUxUci9rZWdOS1JIS0xmVGw5L3QxWGZzSg=="
},
{
"server": "Episode 6",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEtGTjA3WlR0MUwvODZqckhBQWFOVUNCUWtQaUVZbzFxTmx6UWpOUFJxMnVCcitiWEtpdGtiSEJSNW5iUG5XSkxwWFZ6a1g2b3BkbFJmN1JYblIweW1xY2R6ZjEvSW9xRU5xZnZTQlRpS3lnU0d1NThhdTZhMFAvUGdYQzl0TWprSkRLa1RwTzVJd1N0N1p0NkhLdkp0cQ==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=WjhBd0J6a09PUWVYeDlaRG5yNU5qcE5KM09ITks2d2NqTkM1UGxtR0c2T2VBTEhnanByRVdBanN0MzJyNDk4UQ=="
},
{
"server": "Episode 7",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZExRVlYyVkpzdWxBWUo3bzBzcWs5d3R3dHNXOWxqUHVlWFdkTlgxSGhaUS9FU1E4NXdCS3laSVlSZUlQTDBFWkxnMGkxOUkzdklmellSR2RyTk9uSXpTWk0vWC9jYkVHdFlQOThjZUs5ckNTNGRraGlFYnRmQ3FhcDdIdEF4ZFdEdjQ0MHJPTytZVUFLRTZVdkhMb2ZUUQ==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=ZjJqSGRoWUtpMW56VlhwTUpzWFVWSTdMazRDSnZJQlQzb0p6dXdTbU1xc2ZrK0FxMzNHSUQzS0hIMDhuS1NUOQ=="
},
{
"server": "Episode 8",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEkzdVZ2UWpRV01CWWJhczc1VUZiM1A0aWlGSHNPbzFnNmNudkdnUXU2QVVUUTBDUGJjTEJIQ2tMSHAzeExiR2dEbXVYaWNZdWtuUFVFUklFbWFLeWNNb2VDRXJ3aVdubmN0UE14bG5NbTV5TkF3VWtWS2RFU2s3WkszRVJpVEVuY055VjNtNWJLVzhSUHV4U1BDMVJnVQ==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=Tng3MmRaUWFERkFaR3ZnR0ZaSzJDaklkMVZudWFFSkE2TGxEREpDYVFUV2J2M1FpNjc1ZHVQajJtT21QZ2t1TA=="
},
{
"server": "Episode 9",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEp6eS9jcnJib1hHeGRwY0RYbHJoMGhSMTlDNUZvSSt0RUlvK2tjRloraWt4Mm5MYUl1MktxbHU5Ry9pS0FuL21TYkphTnZEaXJGdkx6RG9YR3VwaVVpT2g3UkFhUTZjS1Z1eGZaLzhiWjBtUmlYeWFQL1JWbUMwQmdBRnRkYkRhanhqSjJ0aXNJUTRGQy9LRmk4a3V6WA==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=bDZiZXNON0lNTjRCS0tDbDRlb1dRRTk5NCt1V2ZUV1piNGZ3eExZQVFPRVYweEVkWHMzOEJUUGxlOXRLa0RpbA=="
},
{
"server": "Episode 10",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEpFZ1Q4elA2S2x2aTZzQ2tvNXBiMVBHclRJbFMraTBPRHVjQUt0SU8raEg2RWlpZ25leFhlQnNialIyQUJWWWlQMEtWUFJsdTM0Z2NmNGJWOGNLcXRDSzhKNlNUa01WK1grTTRBaGdVT1I2VTE3UEZ6ZDVLVjFkVjdVZFZQU1AxaUtqdVRnNnhNT2tZL1JMMExTcEY0Kw==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=OWRIYzFabCtGM1gzQ3RPRlJreFo5Ykt4MHVKeFYwTW5SRndaQ3NqOEg0UzRiQ2FDZlJ0d0ZEcDA3cTQxSHZzNA=="
}
]
},
{
"quality": "Season 1 {Hindi-English} 720p x264 Esubs [500MB] - Episode Links",
"driveseedRedirectLinks": [
{
"server": "Episode 1",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZElNMHVBSFVUOGhqOUdXMGtDeDRzeVMrSHlPblJSSFlEMi9ZY2R3OFNXNGJRRk1MelNBRTR6NGlPU2ozVXd4cFFRaFM1MlV4QXVlTWhTK2F2akZNaTBoYmc2ZFRoRmYvcnJDdUJZdDY2RGlFZFM5ZzdRNmF5WUZRU0xVYjNTS1dIUWZ1S1JXcS8rQXBMTGNnVTRmSE9MMg==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=c2RhVDROL3JOZG5GV3RoWndtbi9RMHFnN1YrSDF6eUtuSlA4WWR5ZXdOV0pIaGcvNC9BTWN4cURMSTlRRzR4WA=="
},
{
"server": "Episode 2",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZElzOHFpOW9YeGlEenVtQkpZUjhkaGFobmhVYnZRLzJDRTQ5SHNBRlowTis1QnpTOXFub1E3ZHE5cGpoUFhDVU1hbHNCbms5YXprWTh0N05UR09vOUN3TWI2WXFGUGh3SkdGUUFzSURPZlFMU3lvbk9LRW91T0xKZ1U5SGMyOUxsSFkySmlyZzBSYVBkbHBiS2dsblpMLw==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=WjJOc2dvVG8yTTlocmRzTXdEZ3NqWTNQa1hOd1ZnRFU1RnV5aWRpR3djaXFZK1IwVXYzYWxLa3lBUDRpSmkrQQ=="
},
{
"server": "Episode 3",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEp3RHNXZUxnK3l2VVVoL2hpLzdBckpCRTF2b1k2ZldidEhJMlpGc213Q0dUb3BIUlMwSW9ibmFtalBKSk9PQXJSRWZ3L3E3Yk1QdVA3NGloN21KVWo2L3hIaWV3QStmWGk3c0tWMS9neVRTU2swalV1bHhFM0ROeVFudTR4T00wSWVPNmpjR0dQK0U5NFl3SWNGRHZHRA==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=YXN0eGVCaVRhL0hndU1JVTFqa3NRQlR0UE9YdEdEY1ZMOGxzRG8wbjQybldkWFc2MkR5MjR3WE9XS3QzZ2tZUA=="
},
{
"server": "Episode 4",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEtlSkVSTnJrbWVpZU9GK0lYTXVwV0xBMEtPaU9FTFNwaFJVNlBYWG4zMzYxazRqSkV2c2I2Q2FaSnBWVXNEZFRjMEg5bnVubEFDbnk3VUhxVXh6Sk9lWWhWejFvWmlUeXhrNjBXZ24vcU5pNVdMZ0JvNDZvUHZMS0hid3JYOVJYSENzem9BVUI0dHdCc2RpVjZUTXB1Rg==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=Q3IydEdlS0NXbUdkeSttRXdqWk9aZFZGcFVpME9wQXVFMTRKTGpKci9qR2lIKzVOSmlzNHV2VUF1NFF1ekJKZA=="
},
{
"server": "Episode 5",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEtZMi8rMURQS1IydSs2UzRmZzBxbTV6cUR5NGxXN0pxVzFzZHhteTN4dG92MUlpZlU0dFBOTTgyRGRiSS9wZStzdmR4SngyVHNkV3kwZkVuS1phTDNBNzZJR2VRQWJ4NmM5MkxISHdvRkZXd3FwcVF6NjhJY2RwR0FRMzlsb2c4WkFaN1BLcUFKcldwVFVnNmpvVDF1cw==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=OWhTdjhaaS9uU2hSSzJTc2V1c0lGQmtFZHZ6eHRjKzFLQmhNNmlUSFJtVEc5dGVEdEFYQkF2aE1weFQrNTU5Rw=="
},
{
"server": "Episode 6",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZExFVkQ1Wm5Uc3pRcElVTW9rUE5IVXNHU3Ura0dXRVUzMHNrOWYreFU1WGRxd2wxd1RYYnJ5VnJadjRlM2tPcFVYdmE2dEVkZTRpU1h2TGVBVWhJQ1ZHaDZ4WnNDbVBFNVliWVNwQytPeGF5OEZLWndaZFNTbm1ORFVhZzY1NjhpZitIZ2NLZmYweVFwUnFGTyt4dkFaSA==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=TTlaUm9vQXRjMVg2SXprZlFISDRrVDBpdzNwTUxyeG5RekVoWUp3Y3FGK214SFdTVTdkTzJBMDFHMU8xQ1VXdw=="
},
{
"server": "Episode 7",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEplNERKdG0wYk1xSzRGdVY2cFNQYmN1RTJCTU5BMXUrdFcvN3NsNG9ieWtUMjZaU2FDMytPZWp5V0lkeTBzZkhiZVhaRitwOVB5RWl2R3ZDT3BNTGNBOHFLSzZSN2c0ZmkrVWpQZlhVTWlncXBWVHE5MlpFMmI2a3BKSWhTVVdYeHYzRzJpdllXM0tHU2xRQ2tkb2VqWA==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=SFNGTGpTZE1wTU1DSUwrK3FiOStyU2pzV1V2MWY5NGNyVmVnQ1FRZWRmSGxuQUFVSElGNGVXbU9NNHdTR3hRUw=="
},
{
"server": "Episode 8",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEs2SHp3TDFJcmxUQjBUNGlNYUorck5VMGdqbFlhZFhVRHk3YlRYdlRaVXJWZWtMenEybW8xWU1oNjgrNmJZNXp1cUVDd1BTVjBkbDJKZzh5bzgzamVITGF5Y0szamJwbW9paVYvSTA4OVpWVnV1V2dTZHpoVFJCbWg3R3UrUnd5VWx6VXhOYk95SEE1MXhPMHBnT2xVcQ==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=SmFzM2NEaTBkSjBBNW1MQkJEYlArdUJSUldGYWhxUlJoL1hQOVNPNUR2NnkycTl2aXhSN3FrMGhDcVJZTW4vVg=="
},
{
"server": "Episode 9",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEl3TVBneUl4Nnc4MDIvQUFCSWk5R1RzSklPS2hjMFZXVncyZGFMYUlZNEFZQzhTNkxFVVhzNDFKREVkQ01TaGJGZWlMNEhZY21vMUVNN0U5cVpicXpaV0g3RG1rUi93clhWcmlubWgvWlBLRGJQdUpOOUlYUWhXMkFubU9OMjRBVk9lMnp2YUtBQlNobDRNWVdBOTdmRQ==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=djU0OCtyWkdrMC9vRmNnbHdGektUVG5PdUlEaXNKUG9jL0JrV0t3ZzZnUklKUWQ2M3pkYXAxOS9oaU9tYkpmRQ=="
},
{
"server": "Episode 10",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZExySXZEUkJUaktxSWlWd3BXOHp1d0tvMlZGekxHa2ZCK3lpYkpzTVVEc2FKcTZjbXl6L2ljaGlCNThSRzVFUEZnUmg5ejd0cm1rZTh4QkRSVmNJd2hVS0wwRW9vYmJicXNVamZRTldDU0V0VGZBTjhmUzhVUm85TnNhbTcrQTRWalVBdUpWbkIwSnhmOHRWTzl4ZVN1bA==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=aEZoSFlxZHRiK0F0OWtCSmxDOVRZdFE1VnlwaUZnOWozYUg2WDNNd0lzcnJ4bjNzVlY5akZ6TmI2NmNNNURrNw=="
}
]
},
{
"quality": "Season 1 {Hindi-English} 1080p 10Bit Esubs [970MB] - Episode Links",
"driveseedRedirectLinks": [
{
"server": "Episode 1",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEptWXBMMVZiYzVaZFYwYXp5emZBUjlkcmp5eG9PT1hYQjR2czJ1ME5MVmRyK2JtT0ZXci9uY1NVVEIvTFZ0WjVtUTR3ZzRqSFgwS3hhaWhseHpYWVFzOXpENS9ySlJ3U3AwbkRQdGR5cTVML1p0dzRVcys1V2oxUllZNU1CUXgxN3QxeUgvN01PYjNTN0JncE1nMmhxTA==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=VlFVOVp3RThlc1phanBHdURQdm9HVVo2ZytmT1JqVGxkR0lqUS9ZUXBaLzE3RlhaL2JNbkxQcVV4SkJvNWY2dQ=="
},
{
"server": "Episode 2",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZExrc3ZYTXpxNWJoRFJFSWVjTFFTWXB4TWdaTUlhbFJUTW5MVEJhSm1oWDA5SmJLNGhxcjNnbFlHS1o3ZUZZVlE1U1BGaC9CWUlOTmhyWjZYazNsOHZudHkvQTBmNzFXd2NWcnpaUHlrQy96RWYrQ1dBVUVJdmtXelRnNis2ZFM3ZFI5N1FTdVd6VnlPZUF3Znp4ckZJaA==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=czB5SG9hL0ZLaXY4dEJMdy9yQVZvckZTUmNaOU5MWW0yZjgxR3BZeUpIWUFxWHhNOGFwcDU0K1ZJYVdQL0ViUg=="
},
{
"server": "Episode 3",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZExFWGtXbkhCbUFBN1Z1ZEZJRXEvQ1BNVUZuZkczWXFVVjVFWjBoc3FqTDlrVklPT0VMNkRTbGpOenA2K3pzTWV2cnpqeUNBb2hsT0FvVmJmZDlSdFYxZkhaZGhPTldVakpBVzlQeXJsVTRMWXZDaHJIdzU2N3RPMDV5M1ZDaCtGQkFneE56QWp4Sk5QWndBUDY1QmZ6Zw==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=K1VYdWloUnluSThFeFJiQkxkWS85VFJvT1JkMyt1MUNDYUZTUkthYXdzMDdDUS83WEZ3VXdiOU9SakRTODg0OQ=="
},
{
"server": "Episode 4",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEtlV0Z0ejRaQS9vT0dwMWNGZzhQR2RlSnR2Y09PQy9BTlc3RGVSMjUwMEVJT0lVV2oremZsV0JSd0s1SThqTFJIbEk0SERxM2pDQUZTTEV5R3dsVXk0WnNUbXIrNHl5YmdhS00yRWJRSUpnbDllUzRDWVlZalRjcEZZcGd3UmdvRitabnNTcHlZVWJBZWtTYzlTcjNxMQ==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=dXZEZFVPeXNnbzkzNjJPMFM5K1dkbGZGNXY5RzZZODYrRTlsZ2lCYzl5UkhWRTZXYTlBbjYvV0RiQUFLNFhoUA=="
},
{
"server": "Episode 5",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEsrVkd1SkIyNnBnTVp2ZEJTcGJTMGNLem1Ja0JuMHlDUHQ1QTJYa20zVWdwL0I4VFhkaVNEdWNDZ2l2RkphWG1OQmYrWmhnRVhVL0V0RDBNRHZhNFNyU0JqWlV6K1FiMmxFVCs5N2p2YlV4eEh2RHUra0lrOGtkY3hRL1k2Sm1qZmlxRkROWEJ5cWw5bVMzYjMrbWt0Yw==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=TUNyZGxZb1NRN3lseTZGRjFFRnhuY0dLUEhNL3VBN0txMElqVWF3eDJrS3ZXUy9GUHIxTXB6ZUduRlB5eHRITQ=="
},
{
"server": "Episode 6",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEwxdklkdSttMFBENTBOTGlkOHE2Qm5zbU1VUlM1WFFqSnB6NkRRd2xTY2F2WVpaNGkzY0xzelZ1Wk9RNXZRTUR0MTJWWDRCZm82bUd2T1RDUllzZzcxNER0cXQyTzlaTlhNWmVCaENhcnRqeDRCM01rbU5UdmpweVl4cnVuWWFjK3NncnBlVlRiTHEvbHZYL3JzSTEwaQ==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=bzJpL3ptZ1V3UTJEMHRrcGJqVlVEeDJOdFp6S2pGY3VFSjd6ZUNXSmZKT0N5L1pGZFFkMGtnbW0vZlRsN2swbg=="
},
{
"server": "Episode 7",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZElVS0FUQWdMcm5hVTE4Njh5NlVBMTFzMjJxcEsya0ZzT1pRMkZ6N092bWZUbTg5UHJPV1pPdm9YQ0tpNnNJNldnU01BOWppaUdTaW5zWVhQaU9qMXFiMVVISzZZNG8rTzY3WjI4Y2JQZStudzRGa3MxSmtKbWJXRTg0dmp2M0NENTNJaGtYZlUvNzhNemt2RFFsTEhlKw==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=S213TDNGWVduQzE5SnVvS3Y2VWtLeWlkTmpNMlEzMUswb3NiQ1ZUL3h2RGorcXNmQm0xTjBWRzJwSTdxVWkzVQ=="
},
{
"server": "Episode 8",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEl4bG05d2IzR05OLzNhY3VXYjVwL2xtZWNvanhmYVVCVk9mcklVdi96RW5xUW8wODMwZ1YwOEJibjgwaVZreU1ndGgxbzhJUUF4TTNXN2kvRVR5Sm9kY0VDdTlhenp0OGk5M1d1UzhhQTRhWHRNOUo0djNiQzFzNDExMXRuL05zb1Q2Y3ZhckJmcTV6QmhqOGxDRkUvOA==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=SmJudHFnUjBpTnFUUjhDYWZuaU4vSXBrN2xzUUYyb0ZiN3ZTeGlTS1N4UzF4Q0xLc2VHK1FoK1ZGSkQ5dXU5TQ=="
},
{
"server": "Episode 9",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEx1akRqKzRTOXIzNUVzbE1VVEtNSGt4OVJDV0xnTTkyVlo0S09udFcrbmU0cko4R1NWM2hGWmV6UXRvV01lQ3hxN1V3L3VKY3Q3YlMxazRZRHBNRSs4Y1RRQ2owZ1VjelJESGp3WXRiRGdmcGVOc21YVlhuT21QQW94MFhONy9KWTcyVnVtNnExTUlKTHlKNlQ0d1NIUQ==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=dHNReW9XQTM2emQra05ET3FZbmFrNEN4RzloUkdxQW9sYW16K0NTU2MvTWg2VUxpZTBnYTZNYW92bmdEZmtEdA=="
},
{
"server": "Episode 10",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEtpYTJKdlJ3c1F4R3grd1FDV3JpWnhPbEdyWjV2RnhQT2VEQWY4SXE1K0NVajI1aFgvK011am5JUWdPaXo5K0gvRVZGWDJiaGZUZDJPVEZKRWpOOE1oUE03NlgveFlLamNLMXA2UE9seWJma3cwanZmWDFtTy9XVnZyck9DbHRPdVczSVB6K3g2TEI5aCtEa0RnM1pjQg==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=Q3Y5TXdaMVpSNHpsVTRnWGpDMncxeVIrNXMxOVVmOGdNUXJadFcyb05GL2FPRFdweWlDNUZiS2srcGxTL2x5aw=="
}
]
},
{
"quality": "Season 1 {Hindi-English} 1080p x264 Esubs [1.2GB] - Episode Links",
"driveseedRedirectLinks": [
{
"server": "Episode 1",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZExZWXo0Mzh6anRBc1k2OWljeXFiYkdXSHEyV1lYMFdDVk10VTNkNkJCanZoSEtWd2g3aFYxdERITkx6OUlsalErYWRKcGtCVFNUbjFGVWEyLzNva251a2tmODhYZUZOallmN21TVFBVaTEzSWhwbGlVV2ZvSGtSTTBqZk5uMm9hdENIZmI5VjBwRXVSY2xrajQ5Ni9ZVQ==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=KytGSDUxVkV2Y2RSRUxoTXhwWnNYNlJBa3JWU29EeEs3cFFCZkxOTWQ4Vy9rVkZXV0pOMHU1NEJ5S2JvbVY2VA=="
},
{
"server": "Episode 2",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEx5ME9yM1lWdHIvTGNRejJnSDB1Y0hGVXg5UXMxblU4VEh4aVFBUVFVNk5US3FEYXVEdW1iaEN3NkdyN2FyaDNucGxGbThtQ3VSa0IxN1hmWElsditkRUJpei9aWVA0RWpHd1dOQWQyOVM4Sk1FTzQyYlRYZ2V6eHUrTjVDWVZlTmFmdG5yMjAwdnoyUGs1VHhwWGZYMA==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=VEdRMXJPeHVSZXJTckY0eUxjdDJ1TThKQVNvVUUySWRxdjBwUjNDRGU5SUc5TnlHZzRobWhmay9DOG9Bdnd6OQ=="
},
{
"server": "Episode 3",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEpwRStQMTBhangzQkU1MW9lcXB1RUlzMHh1WTBrdmVjaHpqMlpuYkEraDlUQ0gybmV1WlVjQ3cwd3p1ZDM5MmZhV0NIUEpoWWM2dGZxczRIZnZNQkV5VmVZMVF6VWd2STFUM3E1WWdBb2swUCtUUmZzMEoxSTY5OG1Xa29RRlR4bjF4RzBBeHlsSkxGY3hrM0s2WHBEdQ==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=NDRCaWx1RTM3SWNmN3QrSE9iQ0pKQUJoNzh6VGVlYVBKdmZVUDcwVXJGOGJ6TzIycDI5cnVNY0NzU1lwc1Znbg=="
},
{
"server": "Episode 4",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEwrN2w0MHdjTHpTenJzRjByVlU5eUh1WEhCbDhaYzc5M2I0S1lsS3Z6Qy94OEtkaU1ybnlmNFV1VkFaVVUwMHUwSmkzZzJ1ZjNydkRYQTNFdWpMUWI0cit0S0JHT1Fwd1VibFNxRUNoL0NJVjBBTGpiTGFBUFhvcGQxY2xJT0VnU1o3ejQrQnNzZFVqdHR0MGhHTjRpNA==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=a21MUG5FTFFlZURiZXRRaU5kS0pZYWNodFlMWUl0K09CU080WDhhMnFWbEp0YnF5d0lSaGRJRGw2U2d0bDIxNw=="
},
{
"server": "Episode 5",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEpDaTVoNkQvOVI4UnRNRzF3S3BYOWgrV25md0xnUzhINHZQbDZyV0xOallqb1FOeVRWaVpjWExvY0hFejhKVjFxVjJvb3Z2SUc2ZWNiaVowYWdxZVFOVWl3SXV6UlIyaWloOGFmTGx6SGdLWjNBWGZ6N2JRVGVPNDlWSlNpcHN3YkhuMEFpM3hzdjU3cHE1TTd5NzJqRQ==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=MUMzWENQeGFWUzhlOXpWNkQ0ekNqR0RuQUJPNG5xZktiSlZnbFFodkJpczY4Mm4vbTB0dUU1ZERWcjhvSVhWRg=="
},
{
"server": "Episode 6",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEpnd2pDOTBlVWhhUTZzOEJRTWhERVlTWkJ2Z0J1MTRFRisxdGczQ2VXSTY4dmNyeDRxMi9UU1Y2ckRsRllnL0wzN0MrZFVKZWVsQmYxdjFMYmhhVjd5SnplL003bnFlbXRFLzJZSjJSWjR4SVNPUlpULzZNYWd0TG9NWWhmemk2MzZqcHd5Qk9wVENRd0JxeHRzRlRmQg==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=WHByMlZFUCtUVkhMSkFEaTY5UmtmNWQzRWxuRVVsZWZWNzJmVEE5dlg5NjgzUXlKeDNVbnZmVCt2VjZFNnZ0dw=="
},
{
"server": "Episode 7",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEowUFJJRWdlWHl6TGZnUXJuczZhL3RPdVV4R1ZzTEJydE1xTUJxTXFSSmtaWkoxd2lReEl5bER1cmRBQ0ZETisrSHZ3MEp3QXRnRVZCd1dPdEMyWHltUGgwUURMbnhvbEFzY0xZMjhLUWIvMi9FOGNkR20vdnhDOFd4U0s4dXdzTkJxODBJbi9IRzJmVitPTXYxZUZGQQ==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=VXQzT3VWZ3R3NStPYXlOTDBNSjNEYkpVYmJnM1k1VTg1WXpldUEzMEZoWWtJWXdVUncrUEduWCtWeEp1Qi9nUA=="
},
{
"server": "Episode 8",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEpJNzhNdUxFQmM4bXA5Qm51eU14RDdFZzVhSEwwNE1weHZPajRzSEJwQk51djJsVnZJZGZQY1JZQmRvdHdjZ1RyS1RMUVRGZEVwYzdndUE3dUpudDAvMkV2bEo3NW0xRmRLMjVjWHJRR2I1Z2NyVVZPSTQvWkxZQi9neEtPZG4ybXRNdk9xaGUxNVdSNUpyMUhLNmVPUw==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=dTJYSExoSXBjTGZkTTBjZWNINUNuVFVLMlZEazR2SWE2cjdpWFVzd09VY1FpRFFjTytOWVV6MGt4a2pCazgxZg=="
},
{
"server": "Episode 9",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEpJSTROcHA5UmV4cXhjYTFmQVMvZUNKZmNtV2lHYnpHakFiaEJLSDRseUdYa3BNdmN6R3loOUEvYlViNlExS3JnM0ZPK1JpSzY1SHpuN2s2MDZuYkh0a256YnhyUnQxU2VaWlVDV1Y5cVo1VUlnMGxIWmRaY2I0TUR1SHJUQ0pvUUNrdys4VWp0TWhha0p2bSs5TjQ2WQ==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=enB3enJJeE9HeG04cXBoVkJhL1V3ZWQyVlpPU0Q3NHRXUWJiWmlFVGdCZlN0UGtaalFwYnNsMkh5Mm0xT29jRg=="
},
{
"server": "Episode 10",
"url": "https://tech.examzculture.in/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEtraXJtMzcwWWYzL3F5M2YzcjRFOUxIenVQY3dvTld6Sk9pakxocHpaSDEyNDNOVHZ2WUVUNnRCZTIxVEgwbTBQZzRJSU1BbkozSEpJa0FXbzdPSFlNZzREUVlsaUNDa0Nxak1CSlVlSUN4ZE1kWUNvU2hwVjVwdVJvSjFqcGNPaXplakE5c1lxM0pnNXh5OFEvc09ZVg==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=QmpLVFI1c3U2bnM3bTEwQU4wV3E1WnFMa3RxMVhscS9la2NqQUhjTmhjdXZXVEx5Mm9Xa1lsNjlSVVpZajdvYQ=="
}
]
}
]
}
@@ -1,64 +0,0 @@
{
"data": [
{
"quality": "720p",
"driveseedRedirectLinks": [
{
"server": "✅ Fast Server (G-Drive)",
"url": "https://tech.unblockedgames.world/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEtpNjQ1ZFhZeDl4NlZVcm9rQ2lsL0hCM0k3VytEUzRpWWMvZ1FBZlNqRDhrTE5wQVdjeHNQYzJiRG5FMDJWd3lGSXc1LzFPT0hVWFlaWTBZOGw3aDN0V1lkNDVpK0NZVjFXLzh0UGZLSzUydUU2VW83U1YwRWJvdkhVMnh1MVk0Sk9tUm9Ra2FHWVpCQXZpS3R1RGdlNA==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=MkR2QTY2K0pua0xqOS9xa0pmNVIycmNiTndtQzBxbGJ5VU5vVnU0eTh3RU9ML3AydUlJU214bUV5cGx6Rlc2cw=="
},
{
"server": "🚀 Google Drive (Server 2)",
"url": "https://tech.unblockedgames.world/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEo1Qm4wbW1FSnJlWkpuWWoya0JZU3VPbXVCMTdlOFNlTEpycW9nUUpPN1Ric0lnbm5mVGQxS1FGdGFjbHZ5WHN1VllwMS93QXdoNzdYdE15Z1VuQkl5TStmTUpaVzA2dWdxSEZ2WmhxaFZaS3JML0RxQ1pTNlY2eUdzbjcrMFBBSit3TnlGRmNlamxvQXJISTNNS1Y2RQ==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=RURQY0lpbE1IWlNaZDhkNld1MFFnSEQ4V0xXNFJEeHZNNGFSZ3lnUW54aDZTLzUrRXdhVVRLaEhITno1ejFYdw=="
}
]
},
{
"quality": "1080p",
"driveseedRedirectLinks": [
{
"server": "✅ Fast Server (G-Drive)",
"url": "https://tech.unblockedgames.world/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZElqenM4UmhUeEJKTlpncjJnbVR1aldlTmZIb0kzcStJdm9vMGx6UmtGWUtKMjBWMXdYM1o5U3pseEJJMEMvN29McW9qeXZrZTVEa1F1ejlFbUgwRFBJb3dDTTV5TVoyVktJOGk3eEZYUys3Y29sQW5GcmtqaXZEMk9MSk1JWm9JSVlVTWdoSWdVQWNiQnpEYXp6Q054Vw==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=dHV1dW9KeTlVMEM0UERzaVFaRWVTelN2TlVEUG5IUFdsSFdDYk4vZ29WamhPZThPcTBRRk9oc0lhTnppSlN0ag=="
},
{
"server": "🚀 Google Drive (Server 2)",
"url": "https://tech.unblockedgames.world/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEtSYVErY0dNVXJDenhFZzFHZ05mR0ZOWWJ1eWJCNEgvdWtFRTlwY3orekF4QS84LzR1enRuTnpxMFZLT0ZGYkxVdXNuTHNWUklUUDh0SEkrYlYrbHI5MUFRcUo1UWNRSm93b2FScnA0YVU5T3dFeENHa2xHcjhYSXNBcXpodkE0alpOMW85L0ZoVS9MSzRFY2trS0NnTQ==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=dVNjL3hNRXhseGRrenJhQzFwQ24yekphRXhyL1JtUzBxUXlCTVgzUVNOSHRmbzhKK05BU3FjT0Q3MFhrMzlRYw=="
}
]
},
{
"quality": "720p",
"driveseedRedirectLinks": [
{
"server": "✅ Fast Server (G-Drive)",
"url": "https://tech.unblockedgames.world/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEsrRk5KdUcxZG1IblE5T2d2cGowNW9mT2dLd3ZHK0tzeXp1TnJVZGs0cGs1Mmp6d1kyK1lMY1lERitNL2dlNUtlMGJxa0JaYncrRFNzeVlZOEo2QUJLc0RWTGtuRVI2TTZ4Mk1hUVZCQTVlTUo2d3FmcUVrbUFOQnY1RUNFd3J1TmxHTnQvVE5yL1BoNkkwaHVrdVBiZA==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=ZjU4U21qeFVnN2x1RDhVbm55VHFJUWU1b2JHVEljRm96QzcySzV0NFZqRU1HU05uSlN5ck00TUs3bnFRS1ljWg=="
},
{
"server": "🔗 Gdrive Links (Login)",
"url": "https://tech.unblockedgames.world/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZExBSThhUFBxd1BlaE81V0UwOCtVUGphK2lFL00xNXJ1OEQyaGJHckRxSi9SelBiNTFMQ3IzNENPWXhoeDg0SjRGMmo4WTFLMVYyOGhRWXVjSDNkajAramZtMHF6dHFpd2NqZGYwWTE3aUxqcGxYbHJyc2xHWUw0anNXUmpUS3E0SFVZc2cyU0RJVEl0Ky92SThreVVLdg==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=Z2luaGp1aXZ4WXkwSnFBU3pyZWxiU21Mb015bldhdTkzd1JTMkY5bU1CVVB3Q05sUTd1NlRKbHZJSUNnanREUA=="
}
]
},
{
"quality": "1080p",
"driveseedRedirectLinks": [
{
"server": "✅ Fast Server (G-Drive)",
"url": "https://tech.unblockedgames.world/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEpxbCtGRHB6dmUrTUp2ZUl2R3pHVGhyaDFTVUJCTDhsTnBTbXBmSFRwZ3BVUk5tS0xGRVJJd245eFgzanlKZ0Q4SzlPakFDQzNZZHhBdFVleFNCL1p4UGpxd1hFWUdXNXBWSm5XQ1FQWERXcDE1RSsrTjBVNGVmeHR6aDFwbGNrbkJSL004T1d2dTJ2MHlrYkhGSmlIQg==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=alBtU2J0bGprbXYxbzhJcmwrTXc3ZjhDMFhFV0xxZ25ocUliR2NZbkcxMkp5RER2QmlMNjNBWmxEZXhoeUpaRA=="
},
{
"server": "🔗 Gdrive Links (Login)",
"url": "https://tech.unblockedgames.world/?sid=VWNwbWlPV1lMWFcwa1ZDb1dxQVBZcExWbFhIM3l3TExmMjg5TGlNcDcrSmhRZmlMWEs1b0pXRUlnRllnckQ5dWdJTDRiOGNLM3N1WWx5d0Y5Vk16SGl1MHN3ZDkzUnpPVW1vRUM3OFNRZEpPSnl1Ri9xazhmcVUwakZydFg2NFZVUzJRbUxqdjkvSktycFBKWmhML2F5VXpYMCt6c1draXQxVGYvWEhLK0xwNVJVTVZQSlRiWUtHTUd2TmNuSytybHd0czhlcFdwM3cvZnZnZ1YrcmNhTUdDSUxabU1nZ04veXlCYUIrSFIvbkJmSWpmSkFkOFVHTDJMVk5ibkw4Lw==",
"driveseedRedirectUrl": "https://driveseed.org/r?key=K09xNUEvbnBzaGh3Ym94RVQ2QW5aakdHV240a1M3RjhuRUViSHlYenpPVT0=&id=Q0o2RUdsdE9iZDhIYlQwMXlRdjEyMlA1NEVKZkRmRUNYZTcwdjNGa3RPa0xHZ29Na0taTkM4T2ZjWGZmRWxkQw=="
}
]
}
]
}
@@ -1,4 +0,0 @@
{
"showboxUrl": "NO_URL_FOUND",
"timestamp": 1757925353144
}
@@ -1,4 +0,0 @@
{
"showboxUrl": "NO_URL_FOUND",
"timestamp": 1757926777078
}
@@ -1,4 +0,0 @@
{
"showboxUrl": "NO_URL_FOUND",
"timestamp": 1757926668402
}
@@ -1,4 +0,0 @@
{
"showboxUrl": "NO_URL_FOUND",
"timestamp": 1757950186614
}
@@ -1,4 +0,0 @@
{
"result": "NO_MATCH_FOUND",
"timestamp": 1757950186613
}
@@ -1,4 +0,0 @@
{
"result": "NO_MATCH_FOUND",
"timestamp": 1757926668401
}
@@ -1,4 +0,0 @@
{
"result": "NO_MATCH_FOUND",
"timestamp": 1757926777077
}
@@ -1,4 +0,0 @@
{
"result": "NO_MATCH_FOUND",
"timestamp": 1757925353143
}
@@ -1,10 +0,0 @@
{
"id": 1263256,
"titles": [
{
"iso_3166_1": "VN",
"title": "Chàng Happy Gilmore 2",
"type": ""
}
]
}
@@ -1,8 +0,0 @@
{
"id": 1263256,
"imdb_id": "tt31868189",
"wikidata_id": "Q130269576",
"facebook_id": null,
"instagram_id": null,
"twitter_id": null
}
File diff suppressed because it is too large Load Diff
@@ -1,58 +0,0 @@
{
"adult": false,
"backdrop_path": "/x5dVPttNDZaVRTvbk7pYrtGZoZN.jpg",
"belongs_to_collection": {
"id": 1263259,
"name": "Happy Gilmore Collection",
"poster_path": "/7nUOrnZHSgnhagLtydIYbD4rrDY.jpg",
"backdrop_path": "/mhrE9ij30cIvPFVFRXZSwwTchUg.jpg"
},
"budget": 152000000,
"genres": [
{
"id": 35,
"name": "Comedy"
}
],
"homepage": "https://www.netflix.com/title/81696722",
"id": 1263256,
"imdb_id": "tt31868189",
"origin_country": [
"US"
],
"original_language": "en",
"original_title": "Happy Gilmore 2",
"overview": "Happy Gilmore isn't done with golf — not by a long shot. Since his retirement after his first Tour Championship win, Gilmore returns to finance his daughter's ballet classes.",
"popularity": 24.4786,
"poster_path": "/ynT06XivgBDkg7AtbDbX1dJeBGY.jpg",
"production_companies": [
{
"id": 878,
"logo_path": "/s5j0xnTXvglZoz49CUaQclVv3s9.png",
"name": "Happy Madison Productions",
"origin_country": "US"
}
],
"production_countries": [
{
"iso_3166_1": "US",
"name": "United States of America"
}
],
"release_date": "2025-07-25",
"revenue": 0,
"runtime": 118,
"spoken_languages": [
{
"english_name": "English",
"iso_639_1": "en",
"name": "English"
}
],
"status": "Released",
"tagline": "Happy Gilmore returns!",
"title": "Happy Gilmore 2",
"video": false,
"vote_average": 6.673,
"vote_count": 712
}
@@ -1,305 +0,0 @@
{
"id": 1893,
"titles": [
{
"iso_3166_1": "US",
"title": "Star Wars: Episode I",
"type": "working title"
},
{
"iso_3166_1": "US",
"title": "Star Wars I: The Phantom Menace",
"type": "video box title"
},
{
"iso_3166_1": "CZ",
"title": "Skrytá hrozba",
"type": "short title"
},
{
"iso_3166_1": "FR",
"title": "La menace fantôme",
"type": "short title"
},
{
"iso_3166_1": "FI",
"title": "Tähtien sota - episodi I: Pimeä uhka",
"type": "TV title"
},
{
"iso_3166_1": "DK",
"title": "Den usynlige fjende",
"type": "short title"
},
{
"iso_3166_1": "RU",
"title": "Скрытая угроза",
"type": "short title"
},
{
"iso_3166_1": "ES",
"title": "Star Wars: Episodio I - La amenaza fantasma",
"type": ""
},
{
"iso_3166_1": "US",
"title": "The Phantom Menace",
"type": "short title"
},
{
"iso_3166_1": "HK",
"title": "星球大戰前傳:魅影危機",
"type": ""
},
{
"iso_3166_1": "US",
"title": "Star Wars: The Phantom Menace",
"type": "iTunes Title"
},
{
"iso_3166_1": "TW",
"title": "星際大戰首部曲:威脅潛伏",
"type": ""
},
{
"iso_3166_1": "KR",
"title": "스타워즈 에피소드1 보이지 않는 위험",
"type": ""
},
{
"iso_3166_1": "HU",
"title": "Bajlós Árnyak",
"type": ""
},
{
"iso_3166_1": "BR",
"title": "Star Wars: A Ameaça Fantasma",
"type": "short title"
},
{
"iso_3166_1": "NO",
"title": "Star Wars: Episode I - Den skjulte trussel",
"type": ""
},
{
"iso_3166_1": "FR",
"title": "Star Wars - Episode I - La Menace Fantôme",
"type": ""
},
{
"iso_3166_1": "GR",
"title": "Ο Πόλεμος Των Άστρων: Επεισόδιο 1 - Η Αόρατη Απειλή",
"type": ""
},
{
"iso_3166_1": "CA",
"title": "Star Wars : épisode I - La menace fantôme",
"type": ""
},
{
"iso_3166_1": "CZ",
"title": "Hvězdné války: Epizoda I - Skrytá hrozba",
"type": ""
},
{
"iso_3166_1": "KR",
"title": "스타 워즈 에피소드 1, 보이지 않는 위험",
"type": ""
},
{
"iso_3166_1": "FR",
"title": "Star Wars : La menace fantôme",
"type": ""
},
{
"iso_3166_1": "SK",
"title": "Star Wars: Skrytá hrozba",
"type": ""
},
{
"iso_3166_1": "SK",
"title": "Hviezdné vojny: Epizoda I - Skrytá hrozba",
"type": ""
},
{
"iso_3166_1": "RU",
"title": "Звёздные войны: Эпизод I - Скрытая угроза",
"type": ""
},
{
"iso_3166_1": "RU",
"title": "Звёздные войны: Скрытая угроза",
"type": ""
},
{
"iso_3166_1": "UA",
"title": "Зоряні війни: Прихована загроза",
"type": ""
},
{
"iso_3166_1": "IT",
"title": "Star Wars - Episodio I - La minaccia fantasma",
"type": ""
},
{
"iso_3166_1": "DE",
"title": "Star Wars Episode 1 - Die dunkle Bedrohung",
"type": ""
},
{
"iso_3166_1": "JP",
"title": "スター・ウォーズ/ファントム・メナス",
"type": "iTunes title"
},
{
"iso_3166_1": "KR",
"title": "스타워즈 에피소드 1 보이지 않는 위험",
"type": ""
},
{
"iso_3166_1": "US",
"title": "Star Wars: The Phantom Menace (Episode I)",
"type": "Disney+"
},
{
"iso_3166_1": "CN",
"title": "星球大战前传:魅影危机",
"type": ""
},
{
"iso_3166_1": "PL",
"title": "Gwiezdne Wojny I - Mroczne Widmo",
"type": ""
},
{
"iso_3166_1": "TW",
"title": "星球大戰前傳:魅影危機",
"type": ""
},
{
"iso_3166_1": "FR",
"title": "Star Wars, Episode I - La Menace fantôme",
"type": ""
},
{
"iso_3166_1": "CN",
"title": "星球大战1:幽灵的威胁",
"type": ""
},
{
"iso_3166_1": "CN",
"title": "星球大战前传1:幽灵的威胁",
"type": ""
},
{
"iso_3166_1": "CN",
"title": "星球大战前传:幽灵的威胁",
"type": ""
},
{
"iso_3166_1": "CN",
"title": "星球大战:幽灵的威胁",
"type": ""
},
{
"iso_3166_1": "CN",
"title": "星球大战前传1:魅影危机",
"type": ""
},
{
"iso_3166_1": "CN",
"title": "星球大战:魅影危机",
"type": ""
},
{
"iso_3166_1": "FR",
"title": "Star Wars, épisode I : La Menace fantôme",
"type": ""
},
{
"iso_3166_1": "HU",
"title": "Csillagok Háborúja I. - Baljós Árnyak",
"type": ""
},
{
"iso_3166_1": "GE",
"title": "ვარსკვლავური ომები: ფარული საფრთხე (ეპიზოდი I)",
"type": "Disney+"
},
{
"iso_3166_1": "GE",
"title": "ვარსკვლავური ომები: ეპიზოდი I",
"type": ""
},
{
"iso_3166_1": "GE",
"title": "ვარსკვლავური ომები I",
"type": ""
},
{
"iso_3166_1": "GE",
"title": "ვარსკვლავური ომები 1",
"type": "short title"
},
{
"iso_3166_1": "GE",
"title": "ვარსკვლავური ომები: ეპიზოდი I - ფარული საფრთხე",
"type": ""
},
{
"iso_3166_1": "GE",
"title": "ვარსკვლავური ომები: ეპიზოდი 1",
"type": ""
},
{
"iso_3166_1": "IL",
"title": "סטאר וורס פרק I: אימת הפאנטום",
"type": ""
},
{
"iso_3166_1": "VN",
"title": "Chiến Tranh Giữa Các Vì Sao 1: Hiểm Họa Bóng Ma",
"type": ""
},
{
"iso_3166_1": "ES",
"title": "La amenaza fantasma",
"type": "Titulo abreviado"
},
{
"iso_3166_1": "MX",
"title": "Star Wars: La amenaza fantasma",
"type": ""
},
{
"iso_3166_1": "JP",
"title": "スター・ウォーズ/ファントム・メナス (エピソード1)",
"type": ""
},
{
"iso_3166_1": "US",
"title": "Star Wars Episode I - The Phantom Menace",
"type": ""
},
{
"iso_3166_1": "DE",
"title": "Star Wars - Episode 1 - Die dunkle Bedrohung",
"type": ""
},
{
"iso_3166_1": "IL",
"title": "מלחמת הכוכבים 1: אימת הפנטום",
"type": ""
},
{
"iso_3166_1": "SG",
"title": "Star Wars Episode I:The Phantom Menace",
"type": "HK & Singapore Style English Asian Font Set"
},
{
"iso_3166_1": "UZ",
"title": "Yulduzlar jangi-1: Yashirin tahdid",
"type": ""
}
]
}
@@ -1,8 +0,0 @@
{
"id": 1893,
"imdb_id": "tt0120915",
"wikidata_id": "Q165713",
"facebook_id": "StarWars",
"instagram_id": "starwars",
"twitter_id": "starwars"
}
File diff suppressed because it is too large Load Diff
@@ -1,66 +0,0 @@
{
"adult": false,
"backdrop_path": "/wDe8LzwuvHYYiuwyNfxdYQq8ti4.jpg",
"belongs_to_collection": {
"id": 10,
"name": "Star Wars Collection",
"poster_path": "/22dj38IckjzEEUZwN1tPU5VJ1qq.jpg",
"backdrop_path": "/4z9ijhgEthfRHShoOvMaBlpciXS.jpg"
},
"budget": 115000000,
"genres": [
{
"id": 12,
"name": "Adventure"
},
{
"id": 28,
"name": "Action"
},
{
"id": 878,
"name": "Science Fiction"
}
],
"homepage": "http://www.starwars.com/films/star-wars-episode-i-the-phantom-menace",
"id": 1893,
"imdb_id": "tt0120915",
"origin_country": [
"US"
],
"original_language": "en",
"original_title": "Star Wars: Episode I - The Phantom Menace",
"overview": "Anakin Skywalker, a young slave strong with the Force, is discovered on Tatooine. Meanwhile, the evil Sith have returned, enacting their plot for revenge against the Jedi.",
"popularity": 7.5815,
"poster_path": "/6wkfovpn7Eq8dYNKaG5PY3q2oq6.jpg",
"production_companies": [
{
"id": 1,
"logo_path": "/tlVSws0RvvtPBwViUyOFAO0vcQS.png",
"name": "Lucasfilm Ltd.",
"origin_country": "US"
}
],
"production_countries": [
{
"iso_3166_1": "US",
"name": "United States of America"
}
],
"release_date": "1999-05-19",
"revenue": 924317558,
"runtime": 136,
"spoken_languages": [
{
"english_name": "English",
"iso_639_1": "en",
"name": "English"
}
],
"status": "Released",
"tagline": "Every saga has a beginning.",
"title": "Star Wars: Episode I - The Phantom Menace",
"video": false,
"vote_average": 6.566,
"vote_count": 15219
}
@@ -1,110 +0,0 @@
{
"id": 278,
"titles": [
{
"iso_3166_1": "SE",
"title": "Rita Hayworth - nyckel till flykten",
"type": ""
},
{
"iso_3166_1": "CN",
"title": "肖申克的救赎",
"type": "Mandarin title"
},
{
"iso_3166_1": "GE",
"title": "Gaqceva shoushenkidan",
"type": ""
},
{
"iso_3166_1": "AZ",
"title": "Şouşenkdən qaçış",
"type": ""
},
{
"iso_3166_1": "RU",
"title": "Искупление Шоушенком",
"type": ""
},
{
"iso_3166_1": "AZ",
"title": "Shoushenkden Qacish",
"type": ""
},
{
"iso_3166_1": "IR",
"title": "Rastgari Dar Shawshank",
"type": ""
},
{
"iso_3166_1": "IL",
"title": "Homot Shel Tikva",
"type": ""
},
{
"iso_3166_1": "JP",
"title": "Shôshanku no sora ni",
"type": ""
},
{
"iso_3166_1": "RS",
"title": "Бекство из Шошенка",
"type": ""
},
{
"iso_3166_1": "KR",
"title": "쇼생크 탈출",
"type": ""
},
{
"iso_3166_1": "FR",
"title": "Les Évadés",
"type": ""
},
{
"iso_3166_1": "PL",
"title": "Skazani na Shawshank",
"type": ""
},
{
"iso_3166_1": "JP",
"title": "ショーシャンクの空に:1994",
"type": ""
},
{
"iso_3166_1": "IR",
"title": "رستگاری در شاوشنگ",
"type": ""
},
{
"iso_3166_1": "KR",
"title": "쇼생크탈출",
"type": ""
},
{
"iso_3166_1": "BR",
"title": "Um Sonho de Liberdade",
"type": ""
},
{
"iso_3166_1": "TW",
"title": "刺激1995",
"type": ""
},
{
"iso_3166_1": "AM",
"title": "Փախուստ Շոուշենքից",
"type": ""
},
{
"iso_3166_1": "AR",
"title": "Sueño de Libertad",
"type": ""
},
{
"iso_3166_1": "BY",
"title": "Выкупленне з Шаўшэнку",
"type": ""
}
]
}
@@ -1,8 +0,0 @@
{
"id": 278,
"imdb_id": "tt0111161",
"wikidata_id": "Q172241",
"facebook_id": null,
"instagram_id": null,
"twitter_id": null
}
File diff suppressed because it is too large Load Diff
@@ -1,57 +0,0 @@
{
"adult": false,
"backdrop_path": "/pNjh59JSxChQktamG3LMp9ZoQzp.jpg",
"belongs_to_collection": null,
"budget": 25000000,
"genres": [
{
"id": 18,
"name": "Drama"
},
{
"id": 80,
"name": "Crime"
}
],
"homepage": "",
"id": 278,
"imdb_id": "tt0111161",
"origin_country": [
"US"
],
"original_language": "en",
"original_title": "The Shawshank Redemption",
"overview": "Imprisoned in the 1940s for the double murder of his wife and her lover, upstanding banker Andy Dufresne begins a new life at the Shawshank prison, where he puts his accounting skills to work for an amoral warden. During his long stretch in prison, Dufresne comes to be admired by the other inmates -- including an older prisoner named Red -- for his integrity and unquenchable sense of hope.",
"popularity": 31.7568,
"poster_path": "/9cqNxx0GxF0bflZmeSMuL5tnGzr.jpg",
"production_companies": [
{
"id": 97,
"logo_path": "/qv3ih9pR9w2XNKZDsqDqAGuZjqc.png",
"name": "Castle Rock Entertainment",
"origin_country": "US"
}
],
"production_countries": [
{
"iso_3166_1": "US",
"name": "United States of America"
}
],
"release_date": "1994-09-23",
"revenue": 28341469,
"runtime": 142,
"spoken_languages": [
{
"english_name": "English",
"iso_639_1": "en",
"name": "English"
}
],
"status": "Released",
"tagline": "Fear can hold you prisoner. Hope can set you free.",
"title": "The Shawshank Redemption",
"video": false,
"vote_average": 8.7,
"vote_count": 28866
}
@@ -1,25 +0,0 @@
{
"id": 293660,
"titles": [
{
"iso_3166_1": "VN",
"title": "Quái Nhân Deadpool",
"type": ""
},
{
"iso_3166_1": "CN",
"title": "死侍1",
"type": ""
},
{
"iso_3166_1": "ES",
"title": "Deadpool: 1",
"type": "Castellano"
},
{
"iso_3166_1": "DE",
"title": "Deadpool 1",
"type": ""
}
]
}
@@ -1,8 +0,0 @@
{
"id": 293660,
"imdb_id": "tt1431045",
"wikidata_id": "Q19347291",
"facebook_id": "DeadpoolMovie",
"instagram_id": "deadpoolmovie",
"twitter_id": "deadpoolmovie"
}
File diff suppressed because it is too large Load Diff
@@ -1,84 +0,0 @@
{
"adult": false,
"backdrop_path": "/en971MEXui9diirXlogOrPKmsEn.jpg",
"belongs_to_collection": {
"id": 448150,
"name": "Deadpool Collection",
"poster_path": "/30c5jO7YEXuF8KiWXLg9m28GWDA.jpg",
"backdrop_path": "/dTq7mGyAR5eAydR532feWfjJjzm.jpg"
},
"budget": 58000000,
"genres": [
{
"id": 28,
"name": "Action"
},
{
"id": 12,
"name": "Adventure"
},
{
"id": 35,
"name": "Comedy"
}
],
"homepage": "https://www.20thcenturystudios.com/movies/deadpool",
"id": 293660,
"imdb_id": "tt1431045",
"origin_country": [
"US"
],
"original_language": "en",
"original_title": "Deadpool",
"overview": "The origin story of former Special Forces operative turned mercenary Wade Wilson, who, after being subjected to a rogue experiment that leaves him with accelerated healing powers, adopts the alter ego Deadpool. Armed with his new abilities and a dark, twisted sense of humor, Deadpool hunts down the man who nearly destroyed his life.",
"popularity": 15.1616,
"poster_path": "/3E53WEZJqP6aM84D8CckXx4pIHw.jpg",
"production_companies": [
{
"id": 25,
"logo_path": "/qZCc1lty5FzX30aOCVRBLzaVmcp.png",
"name": "20th Century Fox",
"origin_country": "US"
},
{
"id": 431,
"logo_path": "/6dcR1MbRqYgt3jUVYxkHe68GFnZ.png",
"name": "The Donners' Company",
"origin_country": "US"
},
{
"id": 28788,
"logo_path": "/Aqomtf9oh5dKtxBNEagkdlp3aGv.png",
"name": "Genre Films",
"origin_country": "US"
},
{
"id": 7505,
"logo_path": "/837VMM4wOkODc1idNxGT0KQJlej.png",
"name": "Marvel Entertainment",
"origin_country": "US"
}
],
"production_countries": [
{
"iso_3166_1": "US",
"name": "United States of America"
}
],
"release_date": "2016-02-09",
"revenue": 782837347,
"runtime": 108,
"spoken_languages": [
{
"english_name": "English",
"iso_639_1": "en",
"name": "English"
}
],
"status": "Released",
"tagline": "Feel the love.",
"title": "Deadpool",
"video": false,
"vote_average": 7.623,
"vote_count": 31904
}
@@ -1,5 +0,0 @@
{
"tmdbId": "278",
"tmdbType": "movie",
"title": "The Shawshank Redemption"
}
@@ -1,5 +0,0 @@
{
"tmdbId": "1893",
"tmdbType": "movie",
"title": "Star Wars: Episode I - The Phantom Menace"
}
@@ -1,5 +0,0 @@
{
"tmdbId": "1263256",
"tmdbType": "movie",
"title": "Happy Gilmore 2"
}
@@ -1,74 +0,0 @@
{
"data": [
{
"quality": "Unknown Quality",
"size": "Unknown",
"link": "https://tech.unblockedgames.world/?sid=dmpORG1pUDNaSE4zMWt4RTFDVDMzbjJiSzNaUWpVaG5QM1F2NGJZUjhRVT0=",
"rawQuality": "Unknown Quality",
"driveleechRedirectUrl": "https://uhdmovies.eu/1080p-uhd/"
},
{
"quality": "Unknown Quality",
"size": "Unknown",
"link": "https://tech.unblockedgames.world/?sid=dmpORG1pUDNaSE4zMWt4RTFDVDMzaE5qZ1JnMXNxa1hHT1JiL3ZEZ2lNcz0=",
"rawQuality": "Unknown Quality",
"driveleechRedirectUrl": "https://uhdmovies.eu/movies/"
},
{
"quality": "4K | HDR",
"size": "16.68 GB",
"link": "https://tech.unblockedgames.world/?sid=bnpxOTF5Uk9VOVFZM2lEMkxXUmp0VTdoWDBEcGZMVWlrSmRYNlplRk9lQUdoYmJPbisvYi83VjZWNEdWbXVUai8vc3hQdGRwQ3lFRWtlZVVMR3YxSnR1MGV5dE1HTWs0bEtqM0x1bHhCT2o4SUZUQjVyK3FDd1MyUXhZQlJ3TFJsdTNiUHhwd1Q4QTVGRlpUdGQxNVpPd0E2YmlWVUJ0c0lXZU9Sa0s1eXVuT2prMEd5dTVEc0xCdHl0Q0FZTTd2Nlhad1pLTnZzODg5L2tWRDR5aGp3ZEpZL014N1RRVVZRYUpWRXg5ZjRGQUtGZDZOTHJ0QVp0MitQUDY4WmVqbA==",
"rawQuality": "Happy Glimore 2 (2025).2160p NF WEB-DL HDR 10bit HEVC [Hindi DDP 5.1 + English DDP 5.1] x265 (HHWEB-UHDMovies) [16.68 GB]",
"driveleechRedirectUrl": "https://driveleech.net/r?key=UkVwSTIzU1gzQ3czMFBpQlBmeHB6NklzQjRRZFdMYStJaWI4UnZLdVd4QT0=&id=dWFxZDQrS3RwK2dZZFVoUHBydmRxQVlORUVHQVc0NkFsRm02UDk3ai80YjZHbE85N2dOWWVZcUhrMVh3U2JONA=="
},
{
"quality": "4K",
"size": "11.14 GB",
"link": "https://tech.unblockedgames.world/?sid=bnpxOTF5Uk9VOVFZM2lEMkxXUmp0VTdoWDBEcGZMVWlrSmRYNlplRk9lQUdoYmJPbisvYi83VjZWNEdWbXVUai8vc3hQdGRwQ3lFRWtlZVVMR3YxSnR1MGV5dE1HTWs0bEtqM0x1bHhCT2dBcGU3S01vY1JIWkxlYnRZUlpscVFDNFIxbEpDeGtTWVFxNCt3dlVsampCNlhrdVJlUXU3VkZCQVZPL0ttd0hPZ2Q3eU5KQ1VGZVI4cW5ZbWxmNUNXWEt0ejRiTUVBQ3FWQ1RZMjBmV2tPMWdZRVNnQURYZjZSWE56cjJUYjNHbnQ1OUZ2NG96VVZUclJlNlZKSGx6YQ==",
"rawQuality": "Happy Glimore 2 (2025).2160p NF WEB-DL SDR 10bit HEVC [Hindi DDP 5.1 + English DDP 5.1] x265 (HHWEB-UHDMovies) [11.14 GB]",
"driveleechRedirectUrl": "https://driveleech.net/r?key=UkVwSTIzU1gzQ3czMFBpQlBmeHB6NklzQjRRZFdMYStJaWI4UnZLdVd4QT0=&id=UHBTVjR3T3IybXRMS0RjY01tK3FHMVJqbGNzdWVEMFczNjkvdjdpWUh2SHl5ekwwVEFnNHFNdllKRFg2Z015TA=="
},
{
"quality": "1080p",
"size": "5.28 GB",
"link": "https://tech.unblockedgames.world/?sid=bnpxOTF5Uk9VOVFZM2lEMkxXUmp0VTdoWDBEcGZMVWlrSmRYNlplRk9lQUdoYmJPbisvYi83VjZWNEdWbXVUai8vc3hQdGRwQ3lFRWtlZVVMR3YxSnR1MGV5dE1HTWs0bEtqM0x1bHhCT2hiSUN6VHNPMnIyK1o2NHV4VkVpQmVnUmRqUmRoOFh1UkpKZkdhY3JmbWxOeXdMU1c2SVdnb0FwTnh1b2EySDVVZTVOK1hwTVk0RTIwNEgvN21BcEp4STc1OHlpbUV0Ylh6c1pyc0V5VjFQTEttS3luUXZPL0ZTQ3pycTlhYXgwcGpjWDcyZFdENmdKam5DUmxXUmFKcQ==",
"rawQuality": "Happy.Gilmore.2.2025.1080p.NF.WEB-DL.DUAL.DDP5.1.Atmos.H.264-ShiNobi [5.28 GB]",
"driveleechRedirectUrl": "https://driveleech.net/r?key=UkVwSTIzU1gzQ3czMFBpQlBmeHB6NklzQjRRZFdMYStJaWI4UnZLdVd4QT0=&id=cW5YYzgyZ2IwdW9pWmdXakRIMTRad2xkV3l2eGxkSEZtc0FwUE8vSEs4cjJTNEZLVWIvTjZvaUVPeHQrZ3p5bQ=="
},
{
"quality": "1080p",
"size": "5.28 GB",
"link": "https://tech.unblockedgames.world/?sid=dmpORG1pUDNaSE4zMWt4RTFDVDMzdldUTFJiZlVQZVg0V2JWNkRGRU9uM2RPcjB5UTRtWmxvUXF0OUVLWU8yYw==",
"rawQuality": "Happy.Gilmore.2.2025.1080p.NF.WEB-DL.DUAL.DDP5.1.Atmos.H.264-ShiNobi [5.28 GB]",
"driveleechRedirectUrl": "https://uhdmovies.eu/1080p-60fps/"
},
{
"quality": "1080p",
"size": "5.28 GB",
"link": "https://tech.unblockedgames.world/?sid=dmpORG1pUDNaSE4zMWt4RTFDVDMzcGJmM0lkMWZ4Zyt1Q2xiZ3VlQThqcXlHbHQ2ZUNaejFqbElUa1lXZm9PSg==",
"rawQuality": "Happy.Gilmore.2.2025.1080p.NF.WEB-DL.DUAL.DDP5.1.Atmos.H.264-ShiNobi [5.28 GB]",
"driveleechRedirectUrl": "https://uhdmovies.eu/1080p-10bit/"
},
{
"quality": "1080p",
"size": "5.28 GB",
"link": "https://tech.unblockedgames.world/?sid=dmpORG1pUDNaSE4zMWt4RTFDVDMzdlJ5d0FSNm5XZjFtTlkrM3FmOFpLbz0=",
"rawQuality": "Happy.Gilmore.2.2025.1080p.NF.WEB-DL.DUAL.DDP5.1.Atmos.H.264-ShiNobi [5.28 GB]",
"driveleechRedirectUrl": "https://uhdmovies.eu/4k-hdr/"
},
{
"quality": "Unknown Quality",
"size": "Unknown",
"link": "https://tech.unblockedgames.world/?sid=dmpORG1pUDNaSE4zMWt4RTFDVDMzamFGMjVxNDJ0ZTc0RFM2M0pUNFdYTW1tRlNzbVl2Uzk0N3pDSHdoTmZFUQ==",
"rawQuality": "Unknown Quality",
"driveleechRedirectUrl": "https://uhdmovies.eu/2160p-movies/"
},
{
"quality": "Unknown Quality",
"size": "Unknown",
"link": "https://tech.unblockedgames.world/?sid=dmpORG1pUDNaSE4zMWt4RTFDVDMzbDZUM21XeXB0Tzl6TTB6c1BDN0x0cz0=",
"rawQuality": "Unknown Quality",
"driveleechRedirectUrl": "https://uhdmovies.eu/3d-movies/"
}
]
}
@@ -1,25 +0,0 @@
{
"data": [
{
"quality": "4K | DV | BluRay",
"size": "Unknown",
"link": "https://tech.unblockedgames.world/?sid=bnpxOTF5Uk9VOVFZM2lEMkxXUmp0VTdoWDBEcGZMVWlrSmRYNlplRk9lQUdoYmJPbisvYi83VjZWNEdWbXVUai8vc3hQdGRwQ3lFRWtlZVVMR3YxSnR1MGV5dE1HTWs0bEtqM0x1bHhCT2pvMHJ3OElxMDl5bFcwWjdSL2cvQWZBdGdnVTNZUE5uSUllcDBlZUc0N0FWb25JS2J4bDNLQVRXMTUwYytoRzFodExZa3pDNE00NWVrejVSS1R3bmlyaDZwR2diVkVMUFhLVkRWTlBWazNGNURYNWczYVFEZ080WWNydkNqNHRxajJuUitjdXJEZkZiNnNrRnBrZDlyMg==",
"rawQuality": "Game.of.Thrones.S01.UHD.BluRay.2160p.DV.HEVC.REMUX.[Hindi DD 2.0 – Eng TrueHD.Atmos.7.1]-MSubs~(Tom) FraMeSToR [25 GB/E]",
"driveleechRedirectUrl": "https://driveleech.net/r?key=UkVwSTIzU1gzQ3czMFBpQlBmeHB6NklzQjRRZFdMYStJaWI4UnZLdVd4QT0=&id=OEcxNEtwQ3ZrMFlaSEp2eFY1RGZYa1Mwa1V0NThXVGQ3T1FjZnpOM0NBY2poZjQyRFpRNmgwQ3hhZkxzeFpjRw=="
},
{
"quality": "1080p | BluRay",
"size": "Unknown",
"link": "https://tech.unblockedgames.world/?sid=bnpxOTF5Uk9VOVFZM2lEMkxXUmp0VTdoWDBEcGZMVWlrSmRYNlplRk9lQUdoYmJPbisvYi83VjZWNEdWbXVUai8vc3hQdGRwQ3lFRWtlZVVMR3YxSnR1MGV5dE1HTWs0bEtqM0x1bHhCT2oycFVobHI5cmZ3OVNWaG5RYWZEQTFyRmVkQWZ4amdycTVjU0J6Vnl1UUl5SmxPR1ZxdmJJWlRXL1doYUVKMitqN1FWdDdKdERiTUI2U3c4TVJ1M1lYdDZpMTd3WTc1bXM1WElFYVZLTGlrODFGbVNaN20wOFYzYzBIZkUyTlQvZWFRbXEvMXVndGh6dG1Ld2FwLy82OA==",
"rawQuality": "Game of Thrones (2011) S01 1080p BluRay [Hindi DD 2.0 + English DTS 5.1] x264-LiNG[AK47] [5 GB/E]",
"driveleechRedirectUrl": "https://driveleech.net/r?key=UkVwSTIzU1gzQ3czMFBpQlBmeHB6NklzQjRRZFdMYStJaWI4UnZLdVd4QT0=&id=eUc0QUdoUmt5VkxGdTNmWk1aOVlndTFwNHZlUHl5RHNIMFBMZjFZdEdXTkc1L2lqWXlQdU96Y3ZFRXl4UTNNbw=="
},
{
"quality": "1080p | BluRay",
"size": "Unknown",
"link": "https://tech.unblockedgames.world/?sid=bnpxOTF5Uk9VOVFZM2lEMkxXUmp0VTdoWDBEcGZMVWlrSmRYNlplRk9lQUdoYmJPbisvYi83VjZWNEdWbXVUai8vc3hQdGRwQ3lFRWtlZVVMR3YxSnR1MGV5dE1HTWs0bEtqM0x1bHhCT2pSNWFKd3dwNVR2MVpRd3lqN1cwYXFxQVJiaTdHOC9rWmhqcFNVTytydVVBUFFGVEd1aUJ5YjUxLzZFaFgxU1JjU1hFTEpVdDVpMXlGTWVNMmh0aFNWb2d4TzBaTlNaOU9EM3Y4SVM3bllGSmJQR0NvZzg1TWNObEdOOWZoNFVyazhBOGk3emhxZjhGVy9RY3NpYnB6MQ==",
"rawQuality": "Game of Thrones S01 1080p BluRay 10bit HEVC [Hindi DD 2.0 + English AAC 5.1] x265-Joy[AK47] [800 MB/E] [09 GB ZIP]",
"driveleechRedirectUrl": "https://driveleech.net/r?key=UkVwSTIzU1gzQ3czMFBpQlBmeHB6NklzQjRRZFdMYStJaWI4UnZLdVd4QT0=&id=U2lzTnlYK3lhbDg3a0JNb3NsUVZ5OCtLV1FvaysrMG5jRHJOSFpSUllrWW9qNm56UCtYdkRUcmREYlNxVkp4Ng=="
}
]
}
@@ -1,123 +0,0 @@
{
"data": [
{
"quality": "Unknown Quality",
"size": "Unknown",
"link": "https://tech.unblockedgames.world/?sid=dmpORG1pUDNaSE4zMWt4RTFDVDMza2Z2VFBEdEhydU93VlhXT3JOYXFGaUhtU3FyeGkwbjh6d1RoajJEaW5UbQ==",
"rawQuality": "Unknown Quality",
"driveleechRedirectUrl": "https://uhdmovies.mov/4k-movies/"
},
{
"quality": "Unknown Quality",
"size": "Unknown",
"link": "https://tech.unblockedgames.world/?sid=dmpORG1pUDNaSE4zMWt4RTFDVDMzbUoyR2JpV2JxSXVOcjZLK0dGKzFhNDdlR09GT3JsaHBmcXR6cFVXeEdCYw==",
"rawQuality": "Unknown Quality",
"driveleechRedirectUrl": "https://uhdmovies.mov/2160p-hevc/"
},
{
"quality": "Unknown Quality",
"size": "Unknown",
"link": "https://tech.unblockedgames.world/?sid=dmpORG1pUDNaSE4zMWt4RTFDVDMza3ZQQ2I2VEZZM3p6ZWFPMDhBQWRoaEh6c21YSjRDQXV5dHNFdGhNZVF3UA==",
"rawQuality": "Unknown Quality",
"driveleechRedirectUrl": "https://uhdmovies.mov/1080p-uhd/"
},
{
"quality": "Unknown Quality",
"size": "Unknown",
"link": "https://tech.unblockedgames.world/?sid=dmpORG1pUDNaSE4zMWt4RTFDVDMzdlNJRWNQTHBvd0ZXZXQ2bXQzQ083ND0=",
"rawQuality": "Unknown Quality",
"driveleechRedirectUrl": "https://uhdmovies.mov/movies/"
},
{
"quality": "4K | HDR | DV | BluRay",
"size": "26GB",
"link": "https://tech.unblockedgames.world/?sid=bnpxOTF5Uk9VOVFZM2lEMkxXUmp0VTdoWDBEcGZMVWlrSmRYNlplRk9lQUdoYmJPbisvYi83VjZWNEdWbXVUai8vc3hQdGRwQ3lFRWtlZVVMR3YxSnR1MGV5dE1HTWs0bEtqM0x1bHhCT2hGRTRnNzZRQUIrNUIxcWpDMk9xMEVRNXV2bncyNG8reHE3YVJtOUxBbjhvVzRSYTFjd2VsN2drOUlxM2VVVHd1K3YzcmRtSDg5N3FXMVRQY2hPYjhkUUFmcGlNNnVmb1hZMnJEL2R1WHQxb1dhNEJjV0U2RERKYjVEc2ZHOUk5TzZVbXlxanZHVFhFYVRsOUp0LzdYag==",
"rawQuality": "Deadpool (2016) 2160p UHD BluRay DV HDR 10bit HEVC [Hindi DDP 5.1 + English DDP 5.1] x265 (d3g-UHDMovies [26GB]",
"driveleechRedirectUrl": "https://driveleech.net/r?key=UkVwSTIzU1gzQ3czMFBpQlBmeHB6NklzQjRRZFdMYStJaWI4UnZLdVd4QT0=&id=bFZyc1p1MTVRSU94bXo5ZmJzQlJHcDByaGYwcXZSdnZVbm84ZzVQRlFvSVlLQlFOcTAzWHFGUzB5UGh5cERDRw=="
},
{
"quality": "4K | HDR | DV | BluRay",
"size": "6GB",
"link": "https://tech.unblockedgames.world/?sid=bnpxOTF5Uk9VOVFZM2lEMkxXUmp0VTdoWDBEcGZMVWlrSmRYNlplRk9lQUdoYmJPbisvYi83VjZWNEdWbXVUai8vc3hQdGRwQ3lFRWtlZVVMR3YxSnR1MGV5dE1HTWs0bEtqM0x1bHhCT2llRS9MRS9Md3BGVWZLWDkrQzk3N05kZVd0aFJwNFNJZGJOYjJ2c3U0aXFiVkNpQkxseWhsNkNWemZwYkk2RFRyOWh5VHJiVHFMTGk3ODU0c2xrcG82Yzk3cnJlSEU5R1JwSjBkYk8wN1JkUnJ3dE0wWCtLWHJxUXc3OVBSZEMySTg0azhDMlc4cnVUc3pDUldaQlBGTg==",
"rawQuality": "Deadpool (2016) 2160p UHD BluRay DV HDR 10bit HEVC [Hindi DDP 5.1 + English DDP 5.1] x265 (RARBG-UHDMovies) [6GB]",
"driveleechRedirectUrl": "https://driveleech.net/r?key=UkVwSTIzU1gzQ3czMFBpQlBmeHB6NklzQjRRZFdMYStJaWI4UnZLdVd4QT0=&id=L0VucTlwOXNCdmRjNFJUL01XS2lTNDNuRTNQbGtnYWFpNk5QOUNQSEN1cm1kRUtmUy8rZDhReDUzVEk0N1k2Mw=="
},
{
"quality": "1080p | HDR | DV | BluRay",
"size": "12GB",
"link": "https://tech.unblockedgames.world/?sid=UldoSTVjL3N3QUU0K05LZzlkVXQxaDdNb0pJa0NJcXArK2RjL3RlQ1gvMD0=",
"rawQuality": "Deadpool (2016) 1080p UHD BluRay DV HDR 10bit HEVC [Hindi DDP 5.1 + English DDP 5.1] x265 (NMcT-UHDMovies [12GB]",
"driveleechRedirectUrl": "javascript:void(0);"
},
{
"quality": "1080p | BluRay",
"size": "11GB",
"link": "https://tech.unblockedgames.world/?sid=bnpxOTF5Uk9VOVFZM2lEMkxXUmp0VTdoWDBEcGZMVWlrSmRYNlplRk9lQUdoYmJPbisvYi83VjZWNEdWbXVUai8vc3hQdGRwQ3lFRWtlZVVMR3YxSnR1MGV5dE1HTWs0bEtqM0x1bHhCT2hBMkpOb2VJRHdGV1ROVTRVc2w5WGJERTNDVlhFUkE3TjdCUWdkcENqZXkwNXFlM1dRK0MwUjJucmpNTjJlUW5sRDZqWXlSamZRa3dHZThVbG9NelErbHA2VXp5eTBlbG1iY0g2NFNGZE1XL2lTSzlyR3VsTHd4YjVGQXF0NTBDSlpuTVhjVmpycGVKNEtGbmlJVVg2VQ==",
"rawQuality": "Deadpool (2016) 1080p UHD BluRay [Hindi DDP 5.1 + English DDP 5.1] x264 (LoRD-UHDMovies) [11GB]",
"driveleechRedirectUrl": "https://driveleech.net/r?key=UkVwSTIzU1gzQ3czMFBpQlBmeHB6NklzQjRRZFdMYStJaWI4UnZLdVd4QT0=&id=NUVMaXZ0dFJJWXd0WlJaWUZyMUpGaDFiRjhRUEt1SXRUT1JPd29JR3NMWEs1ZnNydWl1bjYxeVBCaFp0ZG52dA=="
},
{
"quality": "1080p | BluRay",
"size": "21GB",
"link": "https://tech.unblockedgames.world/?sid=bnpxOTF5Uk9VOVFZM2lEMkxXUmp0VTdoWDBEcGZMVWlrSmRYNlplRk9lQUdoYmJPbisvYi83VjZWNEdWbXVUai8vc3hQdGRwQ3lFRWtlZVVMR3YxSnR1MGV5dE1HTWs0bEtqM0x1bHhCT2pLSkd1UmpiYmdQRExHQnJZbS9UaEhmTUlYT09LQnI5QXIxR1ZGd3JzRy80SXdMbjB4OW9jR1R1S2llMkRTSk02SWdpdkJtSmVmZlVHZ3JBTHdyUkE2djg0VWdISXhiTXM3Q3NwVFg3R3E3UU5rVDJ0RDlUb290ckNuZDRWczdvVEgrSk40N3FpK3Q2UWE2TlVxa0M3ag==",
"rawQuality": "Deadpool (2016) 1080p BluRay REMUX AVC [Hindi DDP 5.1 + English DDP 5.1] x264 (CC-UHDMovies) [21GB]",
"driveleechRedirectUrl": "https://driveleech.net/r?key=UkVwSTIzU1gzQ3czMFBpQlBmeHB6NklzQjRRZFdMYStJaWI4UnZLdVd4QT0=&id=a3lXeUFGT29ZaVFZZFRua2lqOFZ3ZGF2bFRidFNaTWFqSGVYUWgrajZmMXl5TVVKclZWeHJFc1c1eDFFV2s3eQ=="
},
{
"quality": "1080p | BluRay",
"size": "11.6GB",
"link": "https://tech.unblockedgames.world/?sid=bnpxOTF5Uk9VOVFZM2lEMkxXUmp0VTdoWDBEcGZMVWlrSmRYNlplRk9lQUdoYmJPbisvYi83VjZWNEdWbXVUai8vc3hQdGRwQ3lFRWtlZVVMR3YxSnR1MGV5dE1HTWs0bEtqM0x1bHhCT2g5N3pSVmlGNnNibnMrdktudm9QWk9STklic3VrRFZBRE5PWXphOExvcUY0Z1BmRno1cFRKRXZIaHBhV3NIRjVYSWZPa0hwbnBJR1pxSEhGME52RllGcUxvdGx0ZVpqdkVyK3BUdmtlbUlJWE9LRVd2MUlQUFZhZnE5ZTErWUVtNTJnNnVKbE83UkpiTURrZ21UeHZzcQ==",
"rawQuality": "Deadpool (2016) 1080p BluRay AVC [Hindi DDP 5.1 + English DTS 5.1] x264 (HDMaNiAcS-UHDMovies) [11.6GB]",
"driveleechRedirectUrl": "https://driveleech.net/r?key=UkVwSTIzU1gzQ3czMFBpQlBmeHB6NklzQjRRZFdMYStJaWI4UnZLdVd4QT0=&id=bEtkV1ZOemMvVkFMdEVFRGtzK1ladkVndWFBYXhWMWVQSWN1aS96S0kvY2w4R0hUWm1MS25RbHIwVVk3N0RNVg=="
},
{
"quality": "1080p | BluRay",
"size": "6GB",
"link": "https://tech.unblockedgames.world/?sid=bnpxOTF5Uk9VOVFZM2lEMkxXUmp0VTdoWDBEcGZMVWlrSmRYNlplRk9lQUdoYmJPbisvYi83VjZWNEdWbXVUai8vc3hQdGRwQ3lFRWtlZVVMR3YxSnR1MGV5dE1HTWs0bEtqM0x1bHhCT2dYWHN4ODgzeXUrOURvQTVPbHhOVEZUWnJOZzRBdTd5UkhDTmV6bDIyNnFwUFd6VDBMaXdaVFh6U05ZeVl2U3lIMTE3ZjZOTWlEMVgxWFMvTVFHMzBoWHQvK0p1Y3p3UHRsREJjT0w4bVhtekduMnh1dWRKS0NMSTV4bXV6M3dtampiMXlLWDVpdVZLell1WC83b2tnLw==",
"rawQuality": "Deadpool (2016) 1080p BluRay 10bit HEVC [Hindi DDP 5.1 + English AAC 5.1] x265 (BeiTai-UHDMovies) [6GB]",
"driveleechRedirectUrl": "https://driveleech.net/r?key=UkVwSTIzU1gzQ3czMFBpQlBmeHB6NklzQjRRZFdMYStJaWI4UnZLdVd4QT0=&id=OGlEbjNEN1JGVVBueGE5elVCWjY3bTdVYUpPdm5tZGMrTnNna0lsTWIwMzVSQThUbmdQOEE5WkVVbDVRZTFLUg=="
},
{
"quality": "1080p | BluRay",
"size": "2GB",
"link": "https://tech.unblockedgames.world/?sid=bnpxOTF5Uk9VOVFZM2lEMkxXUmp0VTdoWDBEcGZMVWlrSmRYNlplRk9lQUdoYmJPbisvYi83VjZWNEdWbXVUai8vc3hQdGRwQ3lFRWtlZVVMR3YxSnR1MGV5dE1HTWs0bEtqM0x1bHhCT2dLSjBXTVNJWXVPK0FxeVZrUVZPUDhJMDJLcjRYOXZ4TVZXYlJ5dndpTDNsM0tmbkpQWVJ5em91NU41YndacERXSTNjemxCNE5GVnowM2F5QkYrVlE1SmhQVGcxelZIVjlNNXBONVJLYTcwK0dDNk42cWNMalE3Qk1WY0x1MVA1MFR0NkVGNGZkWkNRZnp6dS85Zml5Yw==",
"rawQuality": "Deadpool (2016) 1080p BluRay 10bit HEVC [Hindi DDP 5.1 + English AAC 5.1] x265 (RARBG-UHDMovies) [2GB]",
"driveleechRedirectUrl": "https://driveleech.net/r?key=UkVwSTIzU1gzQ3czMFBpQlBmeHB6NklzQjRRZFdMYStJaWI4UnZLdVd4QT0=&id=aVl6ZkFvQTZzUHdIUVE4SStONG02YmlONzkydzdVaUY0dDhIdHpxQVpjK2wrNmpUNkhCeHBwZWI3Q3Zwby9HLw=="
},
{
"quality": "1080p | BluRay",
"size": "2GB",
"link": "https://tech.unblockedgames.world/?sid=dmpORG1pUDNaSE4zMWt4RTFDVDMzbVhDeGlwbGZ5alNmd1dGSExtbllZNmlHQU1hd21Fd00xRGV5R081Ym1xSQ==",
"rawQuality": "Deadpool (2016) 1080p BluRay 10bit HEVC [Hindi DDP 5.1 + English AAC 5.1] x265 (RARBG-UHDMovies) [2GB]",
"driveleechRedirectUrl": "https://uhdmovies.mov/1080p-60fps/"
},
{
"quality": "1080p | BluRay",
"size": "2GB",
"link": "https://tech.unblockedgames.world/?sid=dmpORG1pUDNaSE4zMWt4RTFDVDMzbTlTYS9SYXVlWmVCNUpiM3VLMGY5VFpGMUMwRnBhRllXMXFWeUE3Q3dGcA==",
"rawQuality": "Deadpool (2016) 1080p BluRay 10bit HEVC [Hindi DDP 5.1 + English AAC 5.1] x265 (RARBG-UHDMovies) [2GB]",
"driveleechRedirectUrl": "https://uhdmovies.mov/1080p-10bit/"
},
{
"quality": "1080p | BluRay",
"size": "2GB",
"link": "https://tech.unblockedgames.world/?sid=dmpORG1pUDNaSE4zMWt4RTFDVDMzaEtZenp1aENhajVZeG5sTXNFcFVRTT0=",
"rawQuality": "Deadpool (2016) 1080p BluRay 10bit HEVC [Hindi DDP 5.1 + English AAC 5.1] x265 (RARBG-UHDMovies) [2GB]",
"driveleechRedirectUrl": "https://uhdmovies.mov/4k-hdr/"
},
{
"quality": "Unknown Quality",
"size": "Unknown",
"link": "https://tech.unblockedgames.world/?sid=dmpORG1pUDNaSE4zMWt4RTFDVDMzb3U1SmhRbk5yN0NDdGxPcmZNbUx4QkNPcEZ3bnQvM1R0Y1dsV2p1RFFGRQ==",
"rawQuality": "Unknown Quality",
"driveleechRedirectUrl": "https://uhdmovies.mov/2160p-movies/"
},
{
"quality": "Unknown Quality",
"size": "Unknown",
"link": "https://tech.unblockedgames.world/?sid=dmpORG1pUDNaSE4zMWt4RTFDVDMzdFBXM3RQUkFjRkF6T05icGNaL0FKejdzT0FXVGhwVVhCdVFkM294c2w4ag==",
"rawQuality": "Unknown Quality",
"driveleechRedirectUrl": "https://uhdmovies.mov/3d-movies/"
}
]
}
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
@@ -1 +0,0 @@
Unknown size
+230 -75
View File
@@ -9,6 +9,9 @@ const path = require('path');
const DOMAINS_URL = 'https://raw.githubusercontent.com/phisher98/TVVVV/refs/heads/main/domains.json'; const DOMAINS_URL = 'https://raw.githubusercontent.com/phisher98/TVVVV/refs/heads/main/domains.json';
let cachedDomains = null; let cachedDomains = null;
// Permanent policy: block r2.dev links (previously optional via env flag)
console.log('[4KHDHub] r2.dev links will be removed (permanent policy)');
// --- Caching Configuration --- // --- Caching Configuration ---
const CACHE_ENABLED = process.env.DISABLE_CACHE !== 'true'; const CACHE_ENABLED = process.env.DISABLE_CACHE !== 'true';
console.log(`[4KHDHub] Internal cache is ${CACHE_ENABLED ? 'enabled' : 'disabled'}.`); console.log(`[4KHDHub] Internal cache is ${CACHE_ENABLED ? 'enabled' : 'disabled'}.`);
@@ -61,8 +64,7 @@ function validateUrl(url) {
// Skip validation for known reliable hosting services // Skip validation for known reliable hosting services
const trustedHosts = [ const trustedHosts = [
'pixeldrain.dev', 'pixeldrain.dev'
'r2.dev'
]; ];
const isTrustedHost = trustedHosts.some(host => urlObj.hostname.includes(host)); const isTrustedHost = trustedHosts.some(host => urlObj.hostname.includes(host));
@@ -74,11 +76,21 @@ function validateUrl(url) {
const protocol = urlObj.protocol === 'https:' ? https : http; const protocol = urlObj.protocol === 'https:' ? https : http;
// Add referer/origin for r2.dev validation attempts
const extraHeaders = {};
if (/r2\.dev$/i.test(urlObj.hostname)) {
const origin = `${urlObj.protocol}//${urlObj.hostname}`;
extraHeaders['Referer'] = origin + '/';
extraHeaders['Origin'] = origin;
console.log(`[4KHDHub] Adding Referer/Origin headers for validation of r2.dev URL: ${url}`);
}
const options = { const options = {
method: 'HEAD', method: 'HEAD',
timeout: 15000, timeout: 15000,
headers: { headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
...extraHeaders
} }
}; };
@@ -112,7 +124,11 @@ function validateUrl(url) {
function makeRequest(url, options = {}) { function makeRequest(url, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const urlObj = new URL(url); const maxRedirects = options.maxRedirects || 10;
let redirectCount = 0;
const doRequest = (currentUrl) => {
const urlObj = new URL(currentUrl);
const isHttps = urlObj.protocol === 'https:'; const isHttps = urlObj.protocol === 'https:';
const httpModule = isHttps ? https : http; const httpModule = isHttps ? https : http;
@@ -129,8 +145,27 @@ function makeRequest(url, options = {}) {
}; };
const req = httpModule.request(requestOptions, (res) => { const req = httpModule.request(requestOptions, (res) => {
if (options.allowRedirects === false && (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 303 || res.statusCode === 307 || res.statusCode === 308)) { const redirectCodes = [301, 302, 303, 307, 308];
resolve({ statusCode: res.statusCode, headers: res.headers }); const isRedirect = redirectCodes.includes(res.statusCode);
const location = res.headers['location'];
if (options.allowRedirects === false && isRedirect) {
resolve({ statusCode: res.statusCode, headers: res.headers, url: currentUrl });
return;
}
if (isRedirect && location && redirectCount < maxRedirects) {
redirectCount++;
res.resume(); // Consume and discard the redirect body
let nextUrl;
try {
nextUrl = new URL(location, currentUrl).toString();
} catch (error) {
resolve({ statusCode: res.statusCode, headers: res.headers, body: '', url: currentUrl });
return;
}
console.log(`[4KHDHub] Following redirect (${redirectCount}/${maxRedirects}): ${currentUrl} -> ${nextUrl}`);
doRequest(nextUrl);
return; return;
} }
@@ -139,9 +174,9 @@ function makeRequest(url, options = {}) {
res.on('end', () => { res.on('end', () => {
if (options.parseHTML && data) { if (options.parseHTML && data) {
const $ = cheerio.load(data); const $ = cheerio.load(data);
resolve({ $: $, body: data, statusCode: res.statusCode, headers: res.headers }); resolve({ $: $, body: data, statusCode: res.statusCode, headers: res.headers, url: currentUrl });
} else { } else {
resolve({ body: data, statusCode: res.statusCode, headers: res.headers }); resolve({ body: data, statusCode: res.statusCode, headers: res.headers, url: currentUrl });
} }
}); });
}); });
@@ -149,6 +184,9 @@ function makeRequest(url, options = {}) {
req.on('error', reject); req.on('error', reject);
req.on('timeout', () => reject(new Error('Request timeout'))); req.on('timeout', () => reject(new Error('Request timeout')));
req.end(); req.end();
};
doRequest(url);
}); });
} }
@@ -397,8 +435,10 @@ function generateAlternativeQueries(title, originalTitle = null) {
return romans[num] || match; return romans[num] || match;
}), }),
title.replace(/\b(I{1,3}|IV|V|VI{0,3}|IX|X)\b/g, match => { title.replace(/\b(I{1,3}|IV|V|VI{0,3}|IX|X)\b/g, match => {
const romans = { 'I': '1', 'II': '2', 'III': '3', 'IV': '4', 'V': '5', const romans = {
'VI': '6', 'VII': '7', 'VIII': '8', 'IX': '9', 'X': '10' }; 'I': '1', 'II': '2', 'III': '3', 'IV': '4', 'V': '5',
'VI': '6', 'VII': '7', 'VIII': '8', 'IX': '9', 'X': '10'
};
return romans[match] || match; return romans[match] || match;
}) })
]; ];
@@ -657,8 +697,8 @@ function extractHubCloudLinks(url, referer) {
const altElement = $(selector).first(); const altElement = $(selector).first();
if (altElement.length > 0) { if (altElement.length > 0) {
const rawHref = altElement.attr('href'); const rawHref = altElement.attr('href');
if (rawHref) { if (rawHref && /^https?:\/\//i.test(rawHref)) {
href = rawHref.startsWith('http') ? rawHref : `${baseUrl.replace(/\/$/, '')}/${rawHref.replace(/^\//, '')}`; href = rawHref;
console.log(`[4KHDHub] Found download link with selector ${selector}: ${href}`); console.log(`[4KHDHub] Found download link with selector ${selector}: ${href}`);
found = true; found = true;
break; break;
@@ -705,6 +745,22 @@ function extractHubCloudLinks(url, referer) {
const downloadButtons = $('div.card-body h2 a.btn'); const downloadButtons = $('div.card-body h2 a.btn');
console.log(`[4KHDHub] Found ${downloadButtons.length} download buttons`); console.log(`[4KHDHub] Found ${downloadButtons.length} download buttons`);
// Apply JS-assigned pixel server hrefs (var pxl = "https://...") — the page
// overwrites the pixeldrain button href at runtime, so prefer the JS value
const pxlMatch = (response.body || '').match(/var\s+pxl\s*=\s*["']([^"']+)["']/);
if (pxlMatch && pxlMatch[1]) {
const pxlUrl = pxlMatch[1];
let applied = false;
$('a.btn[href*="pixeldrain"]').each((i, el) => {
$(el).attr('href', pxlUrl);
applied = true;
});
if (!applied) {
$('div.card-body h2').append(`<a href="${pxlUrl}" class="btn">Download [PixelServer]</a>`);
}
console.log(`[4KHDHub] Applied JS-assigned pixel server URL: ${pxlUrl}`);
}
if (downloadButtons.length === 0) { if (downloadButtons.length === 0) {
// Try alternative selectors for download buttons // Try alternative selectors for download buttons
const altSelectors = ['a.btn', '.btn', 'a[href]']; const altSelectors = ['a.btn', '.btn', 'a[href]'];
@@ -749,12 +805,24 @@ function extractHubCloudLinks(url, referer) {
if (displayFilename) titleParts.push(displayFilename); if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size); if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n'); const finalTitle = titleParts.join('\n');
let headers = {};
try {
const host = new URL(link).hostname;
if (/r2\.dev$/i.test(host)) {
headers = { Referer: baseUrl + '/', Origin: baseUrl };
console.log('[4KHDHub] Added Referer/Origin headers for r2.dev FSL link');
}
} catch {
// Ignore URL parsing errors
}
resolve({ resolve({
name: `4KHDHub - FSL Server${qualityLabel}`, name: `4KHDHub - FSL Server${qualityLabel}`,
title: finalTitle, title: finalTitle,
url: link, url: link,
quality: quality quality: quality,
provider: '4khdhub',
headers
}); });
}) })
.catch(() => { .catch(() => {
@@ -763,12 +831,24 @@ function extractHubCloudLinks(url, referer) {
if (displayFilename) titleParts.push(displayFilename); if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size); if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n'); const finalTitle = titleParts.join('\n');
let headers = {};
try {
const host = new URL(link).hostname;
if (/r2\.dev$/i.test(host)) {
headers = { Referer: baseUrl + '/', Origin: baseUrl };
console.log('[4KHDHub] Added Referer/Origin headers for r2.dev FSL link (fallback)');
}
} catch {
// Ignore URL parsing errors
}
resolve({ resolve({
name: `4KHDHub - FSL Server${qualityLabel}`, name: `4KHDHub - FSL Server${qualityLabel}`,
title: finalTitle, title: finalTitle,
url: link, url: link,
quality: quality quality: quality,
provider: '4khdhub',
headers
}); });
}); });
} else if (text.includes('Download File')) { } else if (text.includes('Download File')) {
@@ -786,7 +866,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - HubCloud${qualityLabel}`, name: `4KHDHub - HubCloud${qualityLabel}`,
title: finalTitle, title: finalTitle,
url: link, url: link,
quality: quality quality: quality,
provider: '4khdhub',
headers: {}
}); });
}) })
.catch(() => { .catch(() => {
@@ -800,7 +882,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - HubCloud${qualityLabel}`, name: `4KHDHub - HubCloud${qualityLabel}`,
title: finalTitle, title: finalTitle,
url: link, url: link,
quality: quality quality: quality,
provider: '4khdhub',
headers: {}
}); });
}); });
} else if (text.includes('BuzzServer')) { } else if (text.includes('BuzzServer')) {
@@ -829,7 +913,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - BuzzServer${qualityLabel}`, name: `4KHDHub - BuzzServer${qualityLabel}`,
title: finalTitle, title: finalTitle,
url: finalUrl, url: finalUrl,
quality: quality quality: quality,
provider: '4khdhub',
headers: {}
}); });
}) })
.catch(() => { .catch(() => {
@@ -843,7 +929,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - BuzzServer${qualityLabel}`, name: `4KHDHub - BuzzServer${qualityLabel}`,
title: finalTitle, title: finalTitle,
url: finalUrl, url: finalUrl,
quality: quality quality: quality,
provider: '4khdhub',
headers: {}
}); });
}); });
} else { } else {
@@ -858,12 +946,12 @@ function extractHubCloudLinks(url, referer) {
} else if (link.includes('pixeldra')) { } else if (link.includes('pixeldra')) {
console.log(`[4KHDHub] Button ${index + 1} is Pixeldrain`); console.log(`[4KHDHub] Button ${index + 1} is Pixeldrain`);
// Convert pixeldrain.net/u/ID format to pixeldrain.net/api/file/ID format // Convert pixeldrain /u/ID format to /api/file/ID format (any pixeldrain host)
let convertedLink = link; let convertedLink = link;
const pixeldrainMatch = link.match(/pixeldrain\.net\/u\/([a-zA-Z0-9]+)/); const pixeldrainMatch = link.match(/pixeldrain\.(?:dev|net|com)\/u\/([a-zA-Z0-9]+)/);
if (pixeldrainMatch) { if (pixeldrainMatch) {
const fileId = pixeldrainMatch[1]; const fileId = pixeldrainMatch[1];
convertedLink = `https://pixeldrain.net/api/file/${fileId}`; convertedLink = link.replace(/\/u\/([a-zA-Z0-9]+)/, `/api/file/${fileId}`);
console.log(`[4KHDHub] Converted Pixeldrain URL from ${link} to ${convertedLink}`); console.log(`[4KHDHub] Converted Pixeldrain URL from ${link} to ${convertedLink}`);
} }
@@ -880,7 +968,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - Pixeldrain${qualityLabel}`, name: `4KHDHub - Pixeldrain${qualityLabel}`,
title: finalTitle, title: finalTitle,
url: convertedLink, url: convertedLink,
quality: quality quality: quality,
provider: '4khdhub',
headers: {}
}); });
}) })
.catch(() => { .catch(() => {
@@ -894,7 +984,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - Pixeldrain${qualityLabel}`, name: `4KHDHub - Pixeldrain${qualityLabel}`,
title: finalTitle, title: finalTitle,
url: convertedLink, url: convertedLink,
quality: quality quality: quality,
provider: '4khdhub',
headers: {}
}); });
}); });
} else if (text.includes('S3 Server')) { } else if (text.includes('S3 Server')) {
@@ -912,7 +1004,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - S3 Server${qualityLabel}`, name: `4KHDHub - S3 Server${qualityLabel}`,
title: finalTitle, title: finalTitle,
url: link, url: link,
quality: quality quality: quality,
provider: '4khdhub',
headers: {}
}); });
}) })
.catch(() => { .catch(() => {
@@ -926,33 +1020,46 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - S3 Server${qualityLabel}`, name: `4KHDHub - S3 Server${qualityLabel}`,
title: finalTitle, title: finalTitle,
url: link, url: link,
quality: quality quality: quality,
provider: '4khdhub',
headers: {}
}); });
}); });
} else if (text.includes('10Gbps')) { } else if (text.includes('10Gbps') || /pixel\.hubcloud/i.test(link) || link.includes('dl.php')) {
console.log(`[4KHDHub] Button ${index + 1} is 10Gbps server, following redirects...`); console.log(`[4KHDHub] Button ${index + 1} is 10Gbps/pixel server, resolving final link...`);
// Handle 10Gbps server with multiple redirects // Follow redirects (pixel.hubcloud.cx -> worker -> gamerxyt.com/dl.php?link=<file>)
let currentLink = link; // and extract the 'link' query parameter from the final effective URL
makeRequest(link, { parseHTML: false })
const followRedirects = () => {
return makeRequest(currentLink, {
parseHTML: false,
allowRedirects: false
})
.then(response => { .then(response => {
const redirectUrl = response.headers['location']; const finalUrl = response.url || link;
if (!redirectUrl) { let decodedUrl = '';
throw new Error('No redirect found');
try {
const urlObj = new URL(finalUrl);
const linkParam = urlObj.searchParams.get('link');
if (linkParam) {
decodedUrl = linkParam;
if (decodedUrl.includes('%')) {
try { decodedUrl = decodeURIComponent(decodedUrl); } catch { /* keep as-is */ }
}
}
} catch { /* ignore */ }
if (!decodedUrl) {
// Old-style fallback: look for link= in the redirect URL
const oldMatch = finalUrl.match(/[?&]link=([^&]+)/);
if (oldMatch) {
decodedUrl = oldMatch[1];
try { decodedUrl = decodeURIComponent(decodedUrl); } catch { /* keep as-is */ }
}
} }
console.log(`[4KHDHub] 10Gbps redirect: ${redirectUrl}`); if (!decodedUrl) {
throw new Error('Final link not found');
}
console.log(`[4KHDHub] 10Gbps final link: ${decodedUrl}`);
if (redirectUrl.includes('id=')) {
// Final redirect, extract the link parameter
const finalLink = redirectUrl.split('link=')[1];
if (finalLink) {
console.log(`[4KHDHub] 10Gbps final link: ${finalLink}`);
const decodedUrl = decodeURIComponent(finalLink);
// Get actual filename from HEAD request // Get actual filename from HEAD request
return getFilenameFromUrl(decodedUrl) return getFilenameFromUrl(decodedUrl)
.then(actualFilename => { .then(actualFilename => {
@@ -966,7 +1073,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - 10Gbps Server${qualityLabel}`, name: `4KHDHub - 10Gbps Server${qualityLabel}`,
title: finalTitle, title: finalTitle,
url: decodedUrl, url: decodedUrl,
quality: quality quality: quality,
provider: '4khdhub',
headers: {}
}; };
}) })
.catch(() => { .catch(() => {
@@ -980,19 +1089,12 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - 10Gbps Server${qualityLabel}`, name: `4KHDHub - 10Gbps Server${qualityLabel}`,
title: finalTitle, title: finalTitle,
url: decodedUrl, url: decodedUrl,
quality: quality quality: quality,
provider: '4khdhub',
headers: {}
}; };
}); });
} })
throw new Error('Final link not found');
} else {
currentLink = redirectUrl;
return followRedirects();
}
});
};
followRedirects()
.then(result => { .then(result => {
console.log(`[4KHDHub] 10Gbps processing completed`); console.log(`[4KHDHub] 10Gbps processing completed`);
resolve(result); resolve(result);
@@ -1016,7 +1118,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - HubCloud${qualityLabel}`, name: `4KHDHub - HubCloud${qualityLabel}`,
title: finalTitle, title: finalTitle,
url: link, url: link,
quality: quality quality: quality,
provider: '4khdhub',
headers: {}
}); });
}) })
.catch(() => { .catch(() => {
@@ -1030,7 +1134,9 @@ function extractHubCloudLinks(url, referer) {
name: `4KHDHub - HubCloud${qualityLabel}`, name: `4KHDHub - HubCloud${qualityLabel}`,
title: finalTitle, title: finalTitle,
url: link, url: link,
quality: quality quality: quality,
provider: '4khdhub',
headers: {}
}); });
}); });
} }
@@ -1274,14 +1380,18 @@ function extractStreamingLinks(downloadLinks) {
return Promise.all(promises) return Promise.all(promises)
.then(results => { .then(results => {
const validResults = results.filter(result => result !== null); const validResults = results.filter(r => r !== null).flat();
const flatResults = validResults.flat(); const filtered = validResults.filter(link => {
// Filter out .zip files if (!link || !link.url) return false;
const filteredResults = flatResults.filter(link => { const lower = link.url.toLowerCase();
return link && link.url && !link.url.toLowerCase().endsWith('.zip'); if (lower.endsWith('.zip')) {
console.log(`[4KHDHub] Skipping archive (zip) link: ${link.url}`);
return false;
}
return true;
}); });
// Note: Link count will be logged after validation completes console.log(`[4KHDHub] Post-processing: kept ${filtered.length}, skipped ${validResults.length - filtered.length} zip archive links`);
return filteredResults; return filtered;
}); });
} }
@@ -1302,6 +1412,12 @@ function extractHubDriveLinks(url, referer) {
console.log(`[4KHDHub] HubDrive extracted info - Size: ${size}, Header: ${header}, Quality: ${quality}, HeaderDetails: ${headerDetails}`); console.log(`[4KHDHub] HubDrive extracted info - Size: ${size}, Header: ${header}, Quality: ${quality}, HeaderDetails: ${headerDetails}`);
// Guard against dead/expired HubDrive files ("File not found" page)
if (/file not found/i.test(header)) {
console.log('[4KHDHub] HubDrive file not found, skipping');
return [];
}
// Extract filename from header for title display // Extract filename from header for title display
let filename = headerDetails || header || 'Unknown'; let filename = headerDetails || header || 'Unknown';
// Clean up the filename by removing common prefixes and file extensions // Clean up the filename by removing common prefixes and file extensions
@@ -1327,9 +1443,13 @@ function extractHubDriveLinks(url, referer) {
for (const selector of alternatives) { for (const selector of alternatives) {
foundBtn = $(selector).first(); foundBtn = $(selector).first();
if (foundBtn.length > 0) { if (foundBtn.length > 0) {
const altHref = foundBtn.attr('href');
if (altHref && /^https?:\/\//i.test(altHref)) {
console.log(`[4KHDHub] Found download button with selector: ${selector}`); console.log(`[4KHDHub] Found download button with selector: ${selector}`);
break; break;
} }
foundBtn = null;
}
} }
if (!foundBtn || foundBtn.length === 0) { if (!foundBtn || foundBtn.length === 0) {
@@ -1388,7 +1508,9 @@ function processHubDriveLink(href, referer, filename = 'Unknown', size = '', qua
name: `4KHDHub - HubDrive${qualityLabel}`, name: `4KHDHub - HubDrive${qualityLabel}`,
title: finalTitle, title: finalTitle,
url: href, url: href,
quality: quality quality: quality,
provider: '4khdhub',
headers: {}
}]; }];
}) })
.catch(() => { .catch(() => {
@@ -1402,7 +1524,9 @@ function processHubDriveLink(href, referer, filename = 'Unknown', size = '', qua
name: `4KHDHub - HubDrive${qualityLabel}`, name: `4KHDHub - HubDrive${qualityLabel}`,
title: finalTitle, title: finalTitle,
url: href, url: href,
quality: quality quality: quality,
provider: '4khdhub',
headers: {}
}]; }];
}); });
} }
@@ -1463,7 +1587,9 @@ function processExtractorLink(link, resolve, linkNumber) {
name: '4KHDHub Direct Link', name: '4KHDHub Direct Link',
title: finalTitle, title: finalTitle,
url: link, url: link,
quality: 1080 quality: 1080,
provider: "4khdhub",
headers: {}
}]); }]);
}) })
.catch(() => { .catch(() => {
@@ -1477,7 +1603,9 @@ function processExtractorLink(link, resolve, linkNumber) {
name: '4KHDHub Direct Link', name: '4KHDHub Direct Link',
title: finalTitle, title: finalTitle,
url: link, url: link,
quality: 1080 quality: 1080,
provider: "4khdhub",
headers: {}
}]); }]);
}); });
} else { } else {
@@ -1696,7 +1824,9 @@ async function get4KHDHubStreams(tmdbId, type, season = null, episode = null) {
const suspiciousPatterns = [ const suspiciousPatterns = [
'www-google-com.cdn.ampproject.org', 'www-google-com.cdn.ampproject.org',
'bloggingvector.shop', 'bloggingvector.shop',
'cdn.ampproject.org' 'cdn.ampproject.org',
'hubcloud.cx/tg', // Telegram share redirects, not playable streams
'/tg/go'
]; ];
const isSuspicious = suspiciousPatterns.some(pattern => url.includes(pattern)); const isSuspicious = suspiciousPatterns.some(pattern => url.includes(pattern));
@@ -1740,16 +1870,41 @@ async function get4KHDHubStreams(tmdbId, type, season = null, episode = null) {
} }
// Normalize to generic stream object format // Normalize to generic stream object format
const streams = validatedLinks.map(link => ({ let streams = validatedLinks.map(link => ({
name: link.name, // Don't add prefix since it's already included name: link.name, // Don't add prefix since it's already included
title: link.title || link.name, title: link.title || link.name,
url: link.url, url: link.url,
quality: `${link.quality}p`, quality: `${link.quality}p`,
behaviorHints: { provider: "4khdhub",
bingeGroup: '4khdhub-streams' headers: {}
}
})); }));
// Always remove r2.dev links
{
const before = streams.length;
streams = streams.filter(s => {
try { return !new URL(s.url).hostname.endsWith('r2.dev'); } catch { return true; }
});
const removed = before - streams.length;
if (removed > 0) console.log(`[4KHDHub] Removed ${removed} r2.dev streams (permanent policy)`);
}
// Host distribution instrumentation
try {
const hostCounts = streams.reduce((acc, s) => {
try {
const h = new URL(s.url).hostname;
acc[h] = (acc[h] || 0) + 1;
} catch {
// Ignore URL parsing errors
}
return acc;
}, {});
console.log('[4KHDHub] Final host distribution:', hostCounts);
} catch {
// Ignore URL parsing errors
}
console.log(`[4KHDHub] Returning ${streams.length} streams`); console.log(`[4KHDHub] Returning ${streams.length} streams`);
return streams; return streams;
-506
View File
@@ -1,506 +0,0 @@
const axios = require('axios');
const FormData = require('form-data');
// Helper function to generate slug from title
function generateSlug(title) {
return title.toLowerCase()
.replace(/[^\w\s-]/g, '') // Remove special characters
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-'); // Replace multiple hyphens with a single hyphen
}
// Process episode data from MP4Hydra response
function processEpisode(episode, baseServer, serverName, serverNumber) {
const videoUrl = `${baseServer}${episode.src}`;
const subtitles = episode.subs ? episode.subs.map(sub => ({
label: sub.label,
url: `${baseServer}${sub.src}`
})) : [];
return {
title: episode.show_title || episode.title,
episode: episode.title,
type: episode.type,
quality: episode.quality || episode.label,
videoUrl: videoUrl,
server: serverName,
serverNumber: serverNumber,
subtitles: subtitles
};
}
// Main function to get streams from MP4Hydra
async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNum = null) {
try {
console.log(`[MP4Hydra] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}, Season: ${seasonNum}, Episode: ${episodeNum}`);
// Get TMDB details
const details = await getTMDBDetails(tmdbId, mediaType);
if (!details) {
console.log(`[MP4Hydra] Could not fetch details for TMDB ID: ${tmdbId}`);
return [];
}
console.log(`[MP4Hydra] Found title: ${details.title} (${details.year})`);
// Generate slug in the format "movie-name-year"
let slug = details.slug;
if (mediaType === 'movie' && details.year) {
slug = `${details.slug}-${details.year}`;
}
console.log(`[MP4Hydra] Using slug: ${slug}`);
// Create form data for multipart/form-data request
const formData = new FormData();
formData.append('v', '8');
formData.append('z', JSON.stringify([{
s: slug,
t: mediaType,
se: seasonNum,
ep: episodeNum
}]));
// Make request to MP4Hydra API
const response = await axios({
method: 'post',
url: 'https://mp4hydra.org/info2?v=8',
data: formData,
headers: {
...formData.getHeaders(),
'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36',
'Accept': '*/*',
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
'Origin': 'https://mp4hydra.org',
'Referer': `https://mp4hydra.org/${mediaType}/${slug}`,
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin'
},
timeout: 10000
});
// Process response
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
const playlist = response.data.playlist;
const servers = response.data.servers;
console.log(`[MP4Hydra] Found ${playlist.length} videos`);
console.log(`[MP4Hydra] Available servers: ${Object.keys(servers).join(', ')}`);
// For TV shows, find the specific episode
if (mediaType === 'tv' && seasonNum && episodeNum) {
const paddedSeason = seasonNum.toString().padStart(2, '0');
const paddedEpisode = episodeNum.toString().padStart(2, '0');
const seasonEpisode = `S${paddedSeason}E${paddedEpisode}`;
const targetEpisode = playlist.find(item =>
item.title && item.title.toUpperCase() === seasonEpisode.toUpperCase()
);
if (!targetEpisode) {
console.log(`[MP4Hydra] Could not find ${seasonEpisode}`);
return [];
}
console.log(`[MP4Hydra] Found episode: ${targetEpisode.show_title || targetEpisode.title}`);
// Process streams from main servers
const streams = [];
const serverConfig = [
{ name: 'Beta', number: '#1' },
{ name: 'Beta#3', number: '#2' }
];
serverConfig.forEach(server => {
const serverName = server.name;
const serverNumber = server.number;
if (servers[serverName]) {
const baseServer = servers[serverName];
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
const processedEpisode = processEpisode(targetEpisode, baseServer, serverName, serverNumber);
// Convert to standard stream format
streams.push({
title: `${details.title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
url: processedEpisode.videoUrl,
quality: processedEpisode.quality,
provider: "MP4Hydra",
size: "Unknown size",
behaviorHints: {
notWebReady: true,
headers: {
'Referer': 'https://mp4hydra.org/'
}
}
});
// Add subtitle tracks if available
if (processedEpisode.subtitles && processedEpisode.subtitles.length > 0) {
streams[streams.length - 1].subtitles = processedEpisode.subtitles.map(sub => ({
url: sub.url,
lang: sub.label
}));
}
}
});
return streams;
}
// For movies, process all videos
const streams = [];
const serverConfig = [
{ name: 'Beta', number: '#1' },
{ name: 'Beta#3', number: '#2' }
];
serverConfig.forEach(server => {
const serverName = server.name;
const serverNumber = server.number;
if (servers[serverName]) {
const baseServer = servers[serverName];
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
playlist.forEach(item => {
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
// Convert to standard stream format
streams.push({
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
url: processedItem.videoUrl,
quality: processedItem.quality,
provider: "MP4Hydra",
size: "Unknown size",
behaviorHints: {
notWebReady: true,
headers: {
'Referer': 'https://mp4hydra.org/'
}
}
});
// Add subtitle tracks if available
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
url: sub.url,
lang: sub.label
}));
}
});
}
});
return streams;
} else {
// Try with original title if the first attempt failed
if (details.title !== details.original_title) {
return await tryAlternativeTitle(details, mediaType, seasonNum, episodeNum);
}
// Try without year for movies
if (mediaType === 'movie' && details.year) {
return await tryWithoutYear(details, mediaType, seasonNum, episodeNum);
}
console.log('[MP4Hydra] No streaming links found');
return [];
}
} catch (error) {
console.error(`[MP4Hydra] Error fetching streams:`, error.message);
return [];
}
}
// Helper function to try with original title
async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) {
try {
console.log(`[MP4Hydra] Retrying with original title: ${details.original_title}`);
const originalSlug = generateSlug(details.original_title);
let originalFullSlug = originalSlug;
if (mediaType === 'movie' && details.year) {
originalFullSlug = `${originalSlug}-${details.year}`;
}
// Create new form data with original title
const formData = new FormData();
formData.append('v', '8');
formData.append('z', JSON.stringify([{
s: originalFullSlug,
t: mediaType,
se: seasonNum,
ep: episodeNum
}]));
const response = await axios({
method: 'post',
url: 'https://mp4hydra.org/info2?v=8',
data: formData,
headers: {
...formData.getHeaders(),
'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36',
'Accept': '*/*',
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
'Origin': 'https://mp4hydra.org',
'Referer': `https://mp4hydra.org/${mediaType}/${originalFullSlug}`,
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin'
},
timeout: 10000
});
// Process response
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
const playlist = response.data.playlist;
const servers = response.data.servers;
console.log(`[MP4Hydra] Found ${playlist.length} videos with original title`);
// For TV shows, find the specific episode
if (mediaType === 'tv' && seasonNum && episodeNum) {
const paddedSeason = seasonNum.toString().padStart(2, '0');
const paddedEpisode = episodeNum.toString().padStart(2, '0');
const seasonEpisode = `S${paddedSeason}E${paddedEpisode}`;
const targetEpisode = playlist.find(item =>
item.title && item.title.toUpperCase() === seasonEpisode.toUpperCase()
);
if (!targetEpisode) {
console.log(`[MP4Hydra] Could not find ${seasonEpisode} with original title`);
return [];
}
// Process streams from main servers
const streams = [];
const serverConfig = [
{ name: 'Beta', number: '#1' },
{ name: 'Beta#3', number: '#2' }
];
serverConfig.forEach(server => {
const serverName = server.name;
const serverNumber = server.number;
if (servers[serverName]) {
const baseServer = servers[serverName];
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
const processedEpisode = processEpisode(targetEpisode, baseServer, serverName, serverNumber);
// Convert to standard stream format
streams.push({
title: `${details.original_title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
url: processedEpisode.videoUrl,
quality: processedEpisode.quality,
provider: "MP4Hydra",
size: "Unknown size",
behaviorHints: {
notWebReady: true,
headers: {
'Referer': 'https://mp4hydra.org/'
}
}
});
// Add subtitle tracks if available
if (processedEpisode.subtitles && processedEpisode.subtitles.length > 0) {
streams[streams.length - 1].subtitles = processedEpisode.subtitles.map(sub => ({
url: sub.url,
lang: sub.label
}));
}
}
});
return streams;
}
// For movies, process all videos
const streams = [];
const serverConfig = [
{ name: 'Beta', number: '#1' },
{ name: 'Beta#3', number: '#2' }
];
serverConfig.forEach(server => {
const serverName = server.name;
const serverNumber = server.number;
if (servers[serverName]) {
const baseServer = servers[serverName];
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
playlist.forEach(item => {
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
// Convert to standard stream format
streams.push({
title: `${details.original_title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
url: processedItem.videoUrl,
quality: processedItem.quality,
provider: "MP4Hydra",
size: "Unknown size",
behaviorHints: {
notWebReady: true,
headers: {
'Referer': 'https://mp4hydra.org/'
}
}
});
// Add subtitle tracks if available
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
url: sub.url,
lang: sub.label
}));
}
});
}
});
return streams;
}
return [];
} catch (error) {
console.error(`[MP4Hydra] Error with alternative title:`, error.message);
return [];
}
}
// Helper function to try without year
async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) {
try {
console.log(`[MP4Hydra] Retrying with title only (without year): ${details.title}`);
const titleOnlySlug = details.slug;
// Create new form data with title only
const formData = new FormData();
formData.append('v', '8');
formData.append('z', JSON.stringify([{
s: titleOnlySlug,
t: mediaType,
se: seasonNum,
ep: episodeNum
}]));
const response = await axios({
method: 'post',
url: 'https://mp4hydra.org/info2?v=8',
data: formData,
headers: {
...formData.getHeaders(),
'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36',
'Accept': '*/*',
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
'Origin': 'https://mp4hydra.org',
'Referer': `https://mp4hydra.org/${mediaType}/${titleOnlySlug}`,
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin'
},
timeout: 10000
});
// Process response
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
const playlist = response.data.playlist;
const servers = response.data.servers;
console.log(`[MP4Hydra] Found ${playlist.length} videos with title only`);
// For movies, process all videos
const streams = [];
const serverConfig = [
{ name: 'Beta', number: '#1' },
{ name: 'Beta#3', number: '#2' }
];
serverConfig.forEach(server => {
const serverName = server.name;
const serverNumber = server.number;
if (servers[serverName]) {
const baseServer = servers[serverName];
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
playlist.forEach(item => {
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
// Convert to standard stream format
streams.push({
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
url: processedItem.videoUrl,
quality: processedItem.quality,
provider: "MP4Hydra",
size: "Unknown size",
behaviorHints: {
notWebReady: true,
headers: {
'Referer': 'https://mp4hydra.org/'
}
}
});
// Add subtitle tracks if available
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
url: sub.url,
lang: sub.label
}));
}
});
}
});
return streams;
}
return [];
} catch (error) {
console.error(`[MP4Hydra] Error with title-only:`, error.message);
return [];
}
}
// Helper function to get TMDB details
async function getTMDBDetails(tmdbId, mediaType) {
const { getTmdbApiKey } = require('../utils/tmdbKey');
const TMDB_API_KEY = getTmdbApiKey();
if(!TMDB_API_KEY){
console.warn('[MP4Hydra] TMDB API key missing; skipping TMDB metadata fetch');
return null;
}
try {
console.log(`[MP4Hydra] Fetching ${mediaType} details for TMDB ID: ${tmdbId}`);
const response = await axios.get(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${TMDB_API_KEY}`);
if (mediaType === 'movie') {
return {
title: response.data.title,
original_title: response.data.original_title,
year: response.data.release_date ? response.data.release_date.split('-')[0] : null,
slug: generateSlug(response.data.title)
};
} else {
return {
title: response.data.name,
original_title: response.data.original_name,
year: response.data.first_air_date ? response.data.first_air_date.split('-')[0] : null,
slug: generateSlug(response.data.name)
};
}
} catch (error) {
console.error(`[MP4Hydra] Error fetching details from TMDB:`, error.message);
return null;
}
}
module.exports = { getMP4HydraStreams };
+43 -82
View File
@@ -62,8 +62,9 @@ const getCookieForRequest = async (regionPreference = null, userCookie = null) =
try { try {
const array = Array.isArray(global.currentRequestConfig.cookies) ? global.currentRequestConfig.cookies.filter(Boolean) : []; const array = Array.isArray(global.currentRequestConfig.cookies) ? global.currentRequestConfig.cookies.filter(Boolean) : [];
if (array.length > 0) { if (array.length > 0) {
// Just pick first for now (quota logic removed here if previously elsewhere) // Strip 'ui=' prefix if present — callers always prepend 'ui=' themselves
baseCookieToUse = array[0]; const raw = array[0];
baseCookieToUse = raw.startsWith('ui=') ? raw.slice(3) : raw;
} }
} catch (e) { } catch (e) {
console.warn(`[CookieManager] Cookie array selection error: ${e.message}`); console.warn(`[CookieManager] Cookie array selection error: ${e.message}`);
@@ -169,8 +170,7 @@ const CACHE_DIR = process.env.SHOWBOX_CACHE_DIR
? process.env.SHOWBOX_CACHE_DIR ? process.env.SHOWBOX_CACHE_DIR
: (process.env.VERCEL ? path.join('/tmp', '.cache') : path.join(require('os').tmpdir(), 'tmdb-embed-showbox-cache')); : (process.env.VERCEL ? path.join('/tmp', '.cache') : path.join(require('os').tmpdir(), 'tmdb-embed-showbox-cache'));
console.log(`Using cache directory: ${CACHE_DIR}`); console.log(`Using cache directory: ${CACHE_DIR}`);
// MODIFICATION: Remove hardcoded SHOWBOX_PROXY_URL, will use environment variable
// const SHOWBOX_PROXY_URL = "https://starlit-valkyrie-39f5ab.netlify.app/?destination=";
// Ensure cache directories exist // Ensure cache directories exist
const ensureCacheDir = async (dirPath) => { const ensureCacheDir = async (dirPath) => {
@@ -1212,7 +1212,12 @@ const fetchSourcesForSingleFid = async (fidToProcess, shareKey, regionPreference
const baseHeaders = { const baseHeaders = {
'Cookie': `ui=${cookieForRequest}`, 'Cookie': `ui=${cookieForRequest}`,
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest',
'Origin': 'https://www.febbox.com',
'Referer': `https://www.febbox.com/share/${shareKey}`,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'Accept': '*/*',
}; };
const finalPostUrl = FEBBOX_PLAYER_URL; const finalPostUrl = FEBBOX_PLAYER_URL;
@@ -1222,7 +1227,7 @@ const fetchSourcesForSingleFid = async (fidToProcess, shareKey, regionPreference
console.log(` Fetching fresh player data for video FID: ${fidToProcess} (Share: ${shareKey}) directly to ${FEBBOX_PLAYER_URL}`); console.log(` Fetching fresh player data for video FID: ${fidToProcess} (Share: ${shareKey}) directly to ${FEBBOX_PLAYER_URL}`);
try { try {
const response = await axios.post(finalPostUrl, postDataForAxios, axiosConfig); const response = await axios.post(finalPostUrl, postDataForAxios, { ...axiosConfig, responseType: 'text' });
const playerContent = response.data; const playerContent = response.data;
// Mark the region as available if the request succeeded // Mark the region as available if the request succeeded
@@ -1355,8 +1360,7 @@ class ShowBoxScraper {
this.userCookie = userCookie; this.userCookie = userCookie;
// ScraperAPI key support removed // ScraperAPI key support removed
// Initialize proxy rotation counter for this instance
this.proxyCounter = Math.floor(Math.random() * 1000); // Random start to avoid patterns
this.baseHeaders = { this.baseHeaders = {
'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', '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',
@@ -1374,31 +1378,7 @@ class ShowBoxScraper {
}; };
} }
// Get the next proxy URL from the rotation
getNextProxy() {
// Read proxy configuration from environment
const useRotatingProxy = process.env.SHOWBOX_USE_ROTATING_PROXY === 'true';
const primaryProxy = process.env.SHOWBOX_PROXY_URL_VALUE;
const alternateProxy = process.env.SHOWBOX_PROXY_URL_ALTERNATE;
// Increment the counter for this instance
this.proxyCounter++;
// ScraperAPI support removed; simple proxy rotation only
// Return direct connection if both proxies are missing
if (!primaryProxy && !alternateProxy) return null;
// If rotation disabled or alternate proxy not set, just use primary proxy
if (!useRotatingProxy || !alternateProxy) return primaryProxy;
// Use modulo to alternate between available proxies
const proxyIndex = this.proxyCounter % 2;
const selectedProxy = proxyIndex === 0 ? primaryProxy : alternateProxy;
console.log(`[Rotating Proxy] Selected proxy ${proxyIndex + 1}/2 for request #${this.proxyCounter}`);
return selectedProxy;
}
async _makeRequest(url, isJsonExpected = false) { async _makeRequest(url, isJsonExpected = false) {
// Removed unused timerLabel variable (profiling optional) // Removed unused timerLabel variable (profiling optional)
@@ -1416,22 +1396,8 @@ class ShowBoxScraper {
} }
// For HTML requests, we don't cache the raw HTML anymore - it's too large and wasteful // For HTML requests, we don't cache the raw HTML anymore - it's too large and wasteful
// Get the next proxy URL from the rotation // Always make direct requests
const selectedProxy = this.getNextProxy(); const requestUrl = url;
let requestUrl = url;
// removed unused isUsingScraperApi flag
if (selectedProxy) {
if (typeof selectedProxy === 'function') {
requestUrl = selectedProxy(url); // Direct transformation
} else if (selectedProxy.trim() !== '') {
// This is a regular proxy URL
requestUrl = `${selectedProxy}${encodeURIComponent(url)}`;
console.log(`ShowBoxScraper: Making request to: ${url} via Proxy: ${selectedProxy}`);
}
} else {
console.log(`ShowBoxScraper: Making direct request to: ${url} (no proxy available)`);
}
// Removed console.time call (timerLabel removed) // Removed console.time call (timerLabel removed)
@@ -1463,7 +1429,7 @@ class ShowBoxScraper {
try { try {
const response = await axios.get(requestUrl, { const response = await axios.get(requestUrl, {
headers: currentHeaders, headers: currentHeaders,
timeout: 30000 // Consider increasing if proxy adds significant latency timeout: 30000
}); });
const responseData = response.data; const responseData = response.data;
@@ -2020,10 +1986,12 @@ const getStreamsFromTmdbId = async (tmdbType, tmdbId, seasonNum = null, episodeN
} }
} catch (e) { /* ignore if URL parsing fails */ } } catch (e) { /* ignore if URL parsing fails */ }
streamsFromThisShareInfo.push({ streamsFromThisShareInfo.push({
name: streamTitle,
title: streamTitle, title: streamTitle,
url: source.url, url: source.url,
quality: parseQualityFromLabel(source.label), quality: parseQualityFromLabel(source.label),
codecs: extractCodecDetails(key5FromDirectSource || streamTitle) provider: 'Febbox Direct',
headers: {}
}); });
} }
// If direct sources are found, original code used 'continue', // If direct sources are found, original code used 'continue',
@@ -2047,10 +2015,12 @@ const getStreamsFromTmdbId = async (tmdbType, tmdbId, seasonNum = null, episodeN
const streamTitle = `${baseStreamTitle} - ${source.label}`; const streamTitle = `${baseStreamTitle} - ${source.label}`;
streamsFromThisShareInfo.push({ streamsFromThisShareInfo.push({
name: streamTitle,
title: streamTitle, title: streamTitle,
url: source.url, url: source.url,
quality: parseQualityFromLabel(source.label), quality: parseQualityFromLabel(source.label),
codecs: extractCodecDetails(source.detailedFilename || streamTitle) provider: 'Febbox FID',
headers: {}
}); });
} }
} }
@@ -2242,10 +2212,12 @@ const processShowWithSeasonsEpisodes = async (febboxUrl, showboxTitle, seasonNum
for (const source of directSources) { for (const source of directSources) {
const streamTitle = `${showboxTitle} - ${source.label}`; const streamTitle = `${showboxTitle} - ${source.label}`;
streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall
name: streamTitle,
title: streamTitle, title: streamTitle,
url: source.url, url: source.url,
quality: parseQualityFromLabel(source.label), quality: parseQualityFromLabel(source.label),
codecs: extractCodecDetails(source.detailedFilename || streamTitle) provider: 'Febbox Direct',
headers: {}
}); });
} }
console.timeEnd(processTimerLabel); console.timeEnd(processTimerLabel);
@@ -2262,10 +2234,12 @@ const processShowWithSeasonsEpisodes = async (febboxUrl, showboxTitle, seasonNum
for (const source of sources) { for (const source of sources) {
const streamTitle = `${showboxTitle} - ${source.label}`; const streamTitle = `${showboxTitle} - ${source.label}`;
streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall
name: streamTitle,
title: streamTitle, title: streamTitle,
url: source.url, url: source.url,
quality: parseQualityFromLabel(source.label), quality: parseQualityFromLabel(source.label),
codecs: extractCodecDetails(source.detailedFilename || streamTitle) provider: 'Febbox FID',
headers: {}
}); });
} }
} }
@@ -2546,10 +2520,12 @@ const processShowWithSeasonsEpisodes = async (febboxUrl, showboxTitle, seasonNum
const streamTitle = `${showboxTitle} - S${seasonNum}${episodeNum && selectedEpisode && fid === selectedEpisode.fid ? `E${episodeNum}` : (episodeDetail ? `E${episodeDetail.episodeNum}` : '')} - ${episodeName} - ${source.label}`; const streamTitle = `${showboxTitle} - S${seasonNum}${episodeNum && selectedEpisode && fid === selectedEpisode.fid ? `E${episodeNum}` : (episodeDetail ? `E${episodeDetail.episodeNum}` : '')} - ${episodeName} - ${source.label}`;
streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall streamsForThisCall.push({ // MODIFIED: Push to streamsForThisCall
name: streamTitle,
title: streamTitle, title: streamTitle,
url: source.url, url: source.url,
quality: parseQualityFromLabel(source.label), quality: parseQualityFromLabel(source.label),
codecs: extractCodecDetails(source.detailedFilename || streamTitle) provider: 'Febbox FID',
headers: {}
}); });
} }
} else { } else {
@@ -2710,9 +2686,8 @@ const sortStreamsByQuality = (streams) => {
// Provider priority ordering (lower number = higher priority) // Provider priority ordering (lower number = higher priority)
const providerSortKeys = { const providerSortKeys = {
'ShowBox': 1, 'ShowBox': 1,
'Xprime.tv': 2, 'HollyMovieHD': 2,
'HollyMovieHD': 3, 'Soaper TV': 3,
'Soaper TV': 4,
// Default for unknown providers // Default for unknown providers
default: 99 default: 99
}; };
@@ -2851,16 +2826,8 @@ const getStreamsFromPStreamAPI = async (imdbId, tmdbType, seasonNum = null, epis
console.log(`[PStream] No ui-token available, making unauthenticated request`); console.log(`[PStream] No ui-token available, making unauthenticated request`);
} }
// Check if proxy should be used // Always use direct API URL
const useProxy = process.env.SHOWBOX_PROXY_URL_VALUE && process.env.SHOWBOX_PROXY_URL_VALUE.trim() !== ''; const finalApiUrl = apiUrl;
let finalApiUrl = apiUrl;
if (useProxy) {
finalApiUrl = `${process.env.SHOWBOX_PROXY_URL_VALUE}${encodeURIComponent(apiUrl)}`;
console.log(`[PStream] Using proxy for request: ${finalApiUrl}`);
} else {
console.log(`[PStream] Making direct request (no proxy configured)`);
}
// Make the request // Make the request
const response = await axios.get(finalApiUrl, { const response = await axios.get(finalApiUrl, {
@@ -2910,17 +2877,15 @@ const processPStreamResponse = (apiResponse, tmdbType, seasonNum, episodeNum) =>
for (const [quality, url] of Object.entries(apiResponse.streams)) { for (const [quality, url] of Object.entries(apiResponse.streams)) {
if (url && typeof url === 'string') { if (url && typeof url === 'string') {
// Use "Auto" for name and actual filename for title // Use "Auto" for name and actual filename for title
const streamName = "Auto";
const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${quality}`; const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${quality}`;
streams.push({ streams.push({
name: streamName, name: streamTitle,
title: streamTitle, title: streamTitle,
url: url, url: url,
quality: parseQualityFromLabel(quality), quality: parseQualityFromLabel(quality),
codecs: extractCodecDetails(apiResponse.name || quality), // Use filename for codec detection provider: 'PStream',
size: apiResponse.size || 'Unknown size', headers: {}
provider: 'PStream'
}); });
} }
} }
@@ -2938,17 +2903,15 @@ const processPStreamResponse = (apiResponse, tmdbType, seasonNum, episodeNum) =>
continue; continue;
} }
const streamName = "Auto";
const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${source.quality}`; const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${source.quality}`;
streams.push({ streams.push({
name: streamName, name: streamTitle,
title: streamTitle, title: streamTitle,
url: source.url, url: source.url,
quality: parseQualityFromLabel(source.quality), quality: parseQualityFromLabel(source.quality),
codecs: source.codec ? [source.codec] : [], provider: 'PStream',
size: source.size || 'Unknown size', headers: {}
provider: 'PStream'
}); });
} }
@@ -2961,17 +2924,15 @@ const processPStreamResponse = (apiResponse, tmdbType, seasonNum, episodeNum) =>
for (const item of apiResponse.data) { for (const item of apiResponse.data) {
if (item.url || item.file) { if (item.url || item.file) {
const streamName = "Auto";
const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${item.quality || item.label || 'Unknown'}`; const streamTitle = apiResponse.name || `${tmdbType === 'movie' ? 'PStream' : `PStream - S${seasonNum}E${episodeNum}`} - ${item.quality || item.label || 'Unknown'}`;
streams.push({ streams.push({
name: streamName, name: streamTitle,
title: streamTitle, title: streamTitle,
url: item.url || item.file, url: item.url || item.file,
quality: parseQualityFromLabel(item.quality || item.label), quality: parseQualityFromLabel(item.quality || item.label),
codecs: item.codec ? [item.codec] : [], provider: 'PStream',
size: item.size || 'Unknown size', headers: {}
provider: 'PStream'
}); });
} }
} }
-129
View File
@@ -1,129 +0,0 @@
const axios = require('axios');
// Removed unused parseArgs helper
const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
if (!tmdbId) {
console.error('[VidZee] Error: TMDB ID (tmdbId) is required.');
return [];
}
if (!mediaType || (mediaType !== 'movie' && mediaType !== 'tv')) {
console.error('[VidZee] Error: mediaType is required and must be either "movie" or "tv".');
return [];
}
if (mediaType === 'tv') {
if (!seasonNum) {
console.error('[VidZee] Error: Season (seasonNum) is required for TV shows.');
return [];
}
if (!episodeNum) {
console.error('[VidZee] Error: Episode (episodeNum) is required for TV shows.');
return [];
}
}
const servers = [1,2,3,4,5,6,7,8,9,10]; // VidZee server identifiers
const streamPromises = servers.map(async (sr) => {
let targetApiUrl = `https://player.vidzee.wtf/api/server?id=${tmdbId}&sr=${sr}`;
if (mediaType === 'tv') {
targetApiUrl += `&ss=${seasonNum}&ep=${episodeNum}`;
}
let finalApiUrl;
const headers = {
'Referer': `https://player.vidzee.wtf/embed/movie/${tmdbId}` // Default for proxy method
};
const timeout = 7000; // Reduced timeout
const proxyBaseUrl = process.env.VIDZEE_PROXY_URL || process.env.SHOWBOX_PROXY_URL_VALUE;
if (proxyBaseUrl) {
finalApiUrl = proxyBaseUrl + encodeURIComponent(targetApiUrl);
} else {
finalApiUrl = targetApiUrl;
}
console.log(`[VidZee] Fetching from server ${sr}: ${targetApiUrl}`);
try {
const response = await axios.get(finalApiUrl, {
headers: headers,
timeout: timeout
});
const responseData = response.data;
if (!responseData || typeof responseData !== 'object') {
console.error(`[VidZee S${sr}] Error: Invalid response data from API.`);
return [];
}
if (responseData.tracks) {
delete responseData.tracks;
}
let apiSources = [];
if (responseData.url && Array.isArray(responseData.url)) {
apiSources = responseData.url;
} else if (responseData.link && typeof responseData.link === 'string') {
apiSources = [responseData];
}
if (!apiSources || apiSources.length === 0) {
console.log(`[VidZee S${sr}] No stream sources found in API response.`);
return [];
}
const streams = apiSources.map(sourceItem => {
// Prefer sourceItem.name as label, fallback to sourceItem.type, then 'VidZee Stream'
const label = sourceItem.name || sourceItem.type || 'VidZee';
// Ensure quality has 'p' if it's a resolution, or keep it as is
let quality = String(label).match(/^\d+$/) ? `${label}p` : label;
// Normalize non-numeric or ambiguous quality labels to a baseline so they survive minQuality filter
if (!/(\d{3,4})p/.test(quality.toLowerCase())) {
quality = '720p';
}
const language = sourceItem.language || sourceItem.lang;
return {
title: `VidZee S${sr} - ${quality}`,
url: sourceItem.link, // Use sourceItem.link for the URL
quality: quality,
language: language,
provider: "VidZee",
size: "Unknown size",
behaviorHints: {
notWebReady: true,
headers: {
'Referer': 'https://core.vidzee.wtf/'
}
}
};
}).filter(stream => stream.url);
console.log(`[VidZee S${sr}] Successfully extracted ${streams.length} streams. Qualities: ${streams.map(s=>s.quality).join(', ')}`);
return streams;
} catch (error) {
if (error.response) {
console.error(`[VidZee S${sr}] Error fetching: ${error.response.status} ${error.response.statusText}`);
} else if (error.request) {
console.error(`[VidZee S${sr}] Error fetching: No response received.`);
} else {
console.error(`[VidZee S${sr}] Error fetching:`, error.message);
}
return [];
}
});
const allStreamsNested = await Promise.all(streamPromises);
const allStreams = allStreamsNested.flat();
console.log(`[VidZee] Found a total of ${allStreams.length} streams from servers ${servers.join(', ')}.`);
return allStreams;
};
module.exports = { getVidZeeStreams };
+417
View File
@@ -0,0 +1,417 @@
const { createDecipheriv } = require('crypto');
const { getDetails } = require('../utils/tmdb');
const CASTLE_BASE = 'https://api.hlowb.com';
const PKG = 'com.external.castle';
const CHANNEL = 'IndiaA';
const CLIENT = '1';
const LANG = 'en-US';
const API_HEADERS = {
'User-Agent': 'okhttp/4.9.3',
'Accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.9',
'Connection': 'Keep-Alive',
'Referer': CASTLE_BASE
};
const PLAYBACK_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': 'video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'identity',
'Connection': 'keep-alive',
'Sec-Fetch-Dest': 'video',
'Sec-Fetch-Mode': 'no-cors',
'Sec-Fetch-Site': 'cross-site',
'DNT': '1'
};
function castleSafeParse(text) {
const safe = text.replace(/([:{[,]\s*)(\d{16,})/g, '$1"$2"');
return JSON.parse(safe);
}
function deriveKey(securityKey) {
const keyBytes = Buffer.from(securityKey, 'base64');
const suffix = Buffer.from('T!BgJB', 'utf8');
const combined = Buffer.concat([keyBytes, suffix]);
if (combined.length < 16) {
return Buffer.concat([combined, Buffer.alloc(16 - combined.length, 0)]);
}
return combined.subarray(0, 16);
}
function decryptCastle(cipherText, securityKey) {
const key = deriveKey(securityKey);
const decipher = createDecipheriv('aes-128-cbc', key, key);
decipher.setAutoPadding(true);
const decrypted = Buffer.concat([
decipher.update(Buffer.from(cipherText, 'base64')),
decipher.final()
]);
return decrypted.toString('utf8');
}
async function castleRequest(url, options = {}) {
const res = await fetch(url, {
...options,
headers: { ...API_HEADERS, ...(options.headers || {}) },
signal: AbortSignal.timeout(12000)
});
if (!res.ok) {
throw new Error(`[CastleTV] HTTP ${res.status}: ${res.statusText}`);
}
return res;
}
async function extractCipher(res) {
const text = await res.text();
const trimmed = text.trim();
if (!trimmed) throw new Error('[CastleTV] Empty response body');
try {
const parsed = JSON.parse(trimmed);
if (parsed.data && typeof parsed.data === 'string') {
return parsed.data.trim();
}
} catch {
// Not JSON — raw cipher text
}
return trimmed;
}
function unwrap(obj) {
if (obj && obj.data && typeof obj.data === 'object' && !Array.isArray(obj.data)) {
return obj.data;
}
return obj;
}
async function getSecurityKey() {
const url = `${CASTLE_BASE}/v0.1/system/getSecurityKey/1?channel=${CHANNEL}&clientType=${CLIENT}&lang=${LANG}`;
const res = await castleRequest(url);
const json = await res.json();
if (json.code !== 200 || !json.data) {
throw new Error(`[CastleTV] Security key error: ${JSON.stringify(json)}`);
}
return json.data;
}
async function searchCastle(secKey, keyword) {
const params = new URLSearchParams({
channel: CHANNEL,
clientType: CLIENT,
keyword,
lang: LANG,
mode: '1',
packageName: PKG,
page: '1',
size: '30'
});
const res = await castleRequest(`${CASTLE_BASE}/film-api/v1.1.0/movie/searchByKeyword?${params}`);
const cipher = await extractCipher(res);
return castleSafeParse(decryptCastle(cipher, secKey));
}
async function getCastleDetails(secKey, movieId) {
const url = `${CASTLE_BASE}/film-api/v1.9.9/movie?channel=${CHANNEL}&clientType=${CLIENT}&lang=${LANG}&movieId=${movieId}&packageName=${PKG}`;
const res = await castleRequest(url);
const cipher = await extractCipher(res);
return castleSafeParse(decryptCastle(cipher, secKey));
}
async function getVideoByLanguage(secKey, movieId, episodeId, languageId, resolution) {
const body = {
mode: '1',
appMarket: 'GuanWang',
clientType: CLIENT,
woolUser: 'false',
apkSignKey: 'ED0955EB04E67A1D9F3305B95454FED485261475',
androidVersion: '13',
movieId,
episodeId,
languageId,
isNewUser: 'true',
resolution: resolution.toString(),
packageName: PKG
};
const url = `${CASTLE_BASE}/film-api/v2.0.1/movie/getVideo2?clientType=${CLIENT}&packageName=${PKG}&channel=${CHANNEL}&lang=${LANG}`;
const res = await castleRequest(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const cipher = await extractCipher(res);
return castleSafeParse(decryptCastle(cipher, secKey));
}
async function getVideoShared(secKey, movieId, episodeId, resolution) {
const body = {
mode: '1',
appMarket: 'GuanWang',
clientType: CLIENT,
woolUser: 'false',
apkSignKey: 'ED0955EB04E67A1D9F3305B95454FED485261475',
androidVersion: '13',
movieId,
episodeId,
isNewUser: 'true',
resolution: resolution.toString(),
packageName: PKG
};
const url = `${CASTLE_BASE}/film-api/v2.0.1/movie/getVideo2?clientType=${CLIENT}&packageName=${PKG}&channel=${CHANNEL}&lang=${LANG}`;
const res = await castleRequest(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const cipher = await extractCipher(res);
return castleSafeParse(decryptCastle(cipher, secKey));
}
function resolutionLabel(res) {
const map = { 1: '480p', 2: '720p', 3: '1080p' };
return map[res] || `${res}p`;
}
const KNOWN_HEIGHTS = new Set([240, 360, 480, 540, 576, 720, 1080, 1440, 2160]);
function knownHeight(value) {
const n = Number(value);
return Number.isFinite(n) && KNOWN_HEIGHTS.has(n);
}
function resolutionNumToLabel(num) {
const map = { 1: '480p', 2: '720p', 3: '1080p', 4: '4K' };
return map[num] || null;
}
function streamQuality(url, description, resolutionNum, defaultQual) {
if (description) {
const m = /(?:SD|HD|FHD|UHD|4K)?\s*(\d{3,4})\s*p?/i.exec(String(description).trim());
if (m && knownHeight(m[1])) {
return `${m[1]}p`;
}
if (/4k|uhd/i.test(String(description))) return '4K';
}
const numLabel = resolutionNumToLabel(Number(resolutionNum));
if (numLabel) return numLabel;
if (url) {
const tokens = url.match(/[^/a-z](?:(\d{3,4})\s*p?)[^a-z]/gi);
if (tokens) {
for (const t of tokens) {
const m = /(\d{3,4})/i.exec(t);
if (m && knownHeight(m[1])) return `${m[1]}p`;
}
}
}
return defaultQual;
}
function formatSize(bytes) {
if (typeof bytes !== 'number' || bytes <= 0) return 'Unknown';
if (bytes > 1000000000) return `${(bytes / 1000000000).toFixed(2)} GB`;
return `${(bytes / 1000000).toFixed(0)} MB`;
}
function buildCastleStreams(raw, langLabel, titleLine, resolution) {
const data = unwrap(raw);
if (!data.videoUrl && !((data.videos || []).length)) return [];
const defaultQual = resolutionLabel(resolution);
const subtitles = (data.subtitles || [])
.filter((s) => typeof s.url === 'string' && s.url.length > 0)
.map((s, i) => ({
url: s.url.replace(/ /g, '%20'),
lang: s.abbreviate || s.title || 'Unknown',
id: `castle-${s.languageId || s.abbreviate || i}`
}));
const streams = [];
if (data.videos && data.videos.length > 0) {
const bestByUrl = new Map();
for (const v of data.videos) {
const videoUrl = v.url || data.videoUrl;
if (!videoUrl) continue;
const resNum = Number(v.resolution) || 0;
const qual = streamQuality(
videoUrl,
v.resolutionDescription,
resNum,
defaultQual
);
const existing = bestByUrl.get(videoUrl);
if (existing && existing.resNum >= resNum) continue;
const nameTag = langLabel ? `CastleTV ${langLabel}` : 'CastleTV';
bestByUrl.set(videoUrl, {
resNum,
stream: {
name: `${nameTag} | ${qual}`,
title: `${titleLine}\n${qual} | ${formatSize(v.size)} | Castle`,
url: videoUrl,
quality: qual,
provider: 'CastleTV',
headers: PLAYBACK_HEADERS,
...(subtitles.length ? { subtitles } : {})
}
});
}
for (const { stream } of bestByUrl.values()) {
streams.push(stream);
}
} else {
const videoUrl = data.videoUrl;
if (!videoUrl) return [];
const qual = streamQuality(videoUrl, data.resolutionDescription, 0, defaultQual);
const nameTag = langLabel ? `CastleTV ${langLabel}` : 'CastleTV';
streams.push({
name: `${nameTag} | ${qual}`,
title: `${titleLine}\n${qual} | ${formatSize(data.size)} | Castle`,
url: videoUrl,
quality: qual,
provider: 'CastleTV',
headers: PLAYBACK_HEADERS,
...(subtitles.length ? { subtitles } : {})
});
}
return streams;
}
function pickPreferredTracks(tracks) {
if (!tracks || !tracks.length) return tracks || [];
const withVideo = tracks.filter((t) => t.existIndividualVideo === true);
return withVideo.length > 0 ? withVideo : tracks;
}
async function getCastletvStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[CastleTV] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
try {
const type = mediaType === 'tv' ? 'series' : 'movie';
const tmdbType = mediaType === 'tv' ? 'tv' : 'movie';
const details = await getDetails(tmdbType, tmdbId);
const title = (details && (details.title || details.name)) || '';
const year = (details && (details.release_date || details.first_air_date || '').slice(0, 4)) || null;
if (!title) {
console.log(`[CastleTV] No TMDB title resolved for ${tmdbId}`);
return [];
}
const season = seasonNum || 1;
const episode = episodeNum || 1;
const titleLine = type === 'series'
? `${title} S${String(season).padStart(2, '0')}E${String(episode).padStart(2, '0')}${year ? ` (${year})` : ''}`
: `${title}${year ? ` (${year})` : ''}`;
const secKey = await getSecurityKey();
const keyword = year ? `${title} ${year}` : title;
const searchResult = await searchCastle(secKey, keyword);
const rows = (unwrap(searchResult).rows || []);
if (rows.length === 0) {
console.log(`[CastleTV] No search results for "${keyword}"`);
return [];
}
const titleLc = title.toLowerCase();
const match = rows.find((r) => {
const name = (r.title || r.name || '').toLowerCase();
return name.includes(titleLc) || titleLc.includes(name);
}) || rows[0];
if (!match) return [];
const castleId = (match.id || match.redirectId || match.redirectIdStr || '').toString();
if (!castleId) return [];
let castleDetails = await getCastleDetails(secKey, castleId);
let activeId = castleId;
if (type === 'series') {
const seasons = (unwrap(castleDetails).seasons || []);
const seasonEntry = seasons.find((s) => s.number === season);
if (seasonEntry && seasonEntry.movieId && seasonEntry.movieId.toString() !== castleId) {
castleDetails = await getCastleDetails(secKey, seasonEntry.movieId.toString());
activeId = seasonEntry.movieId.toString();
}
}
const episodes = (unwrap(castleDetails).episodes || []);
let episodeId = null;
if (type === 'series') {
const ep = episodes.find((e) => e.number === episode);
episodeId = ep && ep.id ? ep.id.toString() : null;
} else {
episodeId = episodes[0] && episodes[0].id ? episodes[0].id.toString() : null;
}
if (!episodeId) {
console.log(`[CastleTV] No episode found (${season}/${episode})`);
return [];
}
const epEntry = episodes.find((e) => e.id && e.id.toString() === episodeId);
const allTracks = (epEntry && epEntry.tracks) || [];
const tracks = pickPreferredTracks(allTracks);
const streams = [];
const seenUrls = new Set();
if (tracks.length > 0) {
const langJobs = tracks.map((track) =>
Promise.allSettled(
[3, 2, 1].map((resolution) =>
getVideoByLanguage(secKey, activeId, episodeId, String(track.languageId), resolution)
.then((raw) => ({ raw, resolution, track }))
)
)
);
const allResults = await Promise.all(langJobs);
for (const trackResults of allResults) {
for (const r of trackResults) {
if (r.status !== 'fulfilled') continue;
const { raw, resolution, track } = r.value;
const langLabel = `[${track.languageName || track.abbreviate || 'Unknown'}]`;
for (const s of buildCastleStreams(raw, langLabel, titleLine, resolution)) {
if (!seenUrls.has(s.url)) {
seenUrls.add(s.url);
streams.push(s);
}
}
}
}
}
if (streams.length === 0) {
const sharedResults = await Promise.allSettled(
[3, 2, 1].map((resolution) =>
getVideoShared(secKey, activeId, episodeId, resolution)
.then((raw) => ({ raw, resolution }))
)
);
for (const r of sharedResults) {
if (r.status !== 'fulfilled') continue;
const { raw, resolution } = r.value;
for (const s of buildCastleStreams(raw, '', titleLine, resolution)) {
if (!seenUrls.has(s.url)) {
seenUrls.add(s.url);
streams.push(s);
}
}
}
}
console.log(`[CastleTV] Got ${streams.length} stream(s) for "${title}"`);
return streams;
} catch (err) {
console.error(`[CastleTV] Error: ${err.message}`);
return [];
}
}
module.exports = { getCastletvStreams };
+152
View File
@@ -0,0 +1,152 @@
const axios = require('axios');
const { getTmdbApiKey } = require('../utils/tmdbKey');
const DAHMER_MOVIES_API = 'https://a.111477.xyz';
const DAHMER_WORKER_API = 'https://p.111477.xyz/bulk?u=';
const REQUEST_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Referer': `${DAHMER_MOVIES_API}/`
};
function parseLinks(html) {
const links = [];
const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
let match;
while ((match = rowRegex.exec(html)) !== null) {
const rowContent = match[1];
const linkMatch = rowContent.match(/<a[^>]*href=["']([^"']*)["'][^>]*>([^<]*)<\/a>/i);
const sizeMatch = rowContent.match(/<td[^>]*>(\d+(?:\.\d+)?\s?[KMGT]B)<\/td>/i);
if (linkMatch) {
const href = linkMatch[1];
const text = linkMatch[2].trim();
const size = sizeMatch ? sizeMatch[1].trim() : 'N/A';
if (text && href !== '../' && /\.(mkv|mp4|avi|webm|m3u8)$/i.test(text)) {
links.push({ text, href, size });
}
}
}
return links;
}
async function fetchDirectory(title, year, season) {
const cleanTitle = title.replace(/:/g, '');
const variants = season !== null ? [
`/tvs/${encodeURIComponent(cleanTitle)}/Season%20${season < 10 ? '0' + season : season}/`,
`/tvs/${encodeURIComponent(cleanTitle)}/Season%20${season}/`
] : [
`/movies/${encodeURIComponent(`${cleanTitle} (${year})`)}/`
];
for (const variant of variants) {
try {
const res = await axios.get(DAHMER_MOVIES_API + variant, {
headers: REQUEST_HEADERS,
timeout: 10000,
responseType: 'text'
});
if (res.data) return { html: res.data, dirUrl: DAHMER_MOVIES_API + variant };
} catch {
// try next variant
}
}
return null;
}
async function getDahmermoviesStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[DahmerMovies] Searching for ${mediaType} ${tmdbId}`);
const tmdbKey = getTmdbApiKey();
if (!tmdbKey) {
console.error('[DahmerMovies] No TMDB API key configured.');
return [];
}
let title, year;
try {
const type = mediaType === 'tv' ? 'tv' : 'movie';
const { data } = await axios.get(
`https://api.themoviedb.org/3/${type}/${tmdbId}?api_key=${tmdbKey}`,
{ timeout: 8000 }
);
title = data.title || data.name || '';
year = (data.release_date || data.first_air_date || '').substring(0, 4);
} catch (err) {
console.error(`[DahmerMovies] TMDB lookup failed: ${err.message}`);
return [];
}
if (!title) return [];
const dir = await fetchDirectory(title, year, seasonNum);
if (!dir) {
console.log(`[DahmerMovies] Directory not found for "${title}"`);
return [];
}
let paths = parseLinks(dir.html);
// For TV, filter to the specific episode when possible
if (seasonNum !== null && episodeNum !== null) {
const epStr = String(episodeNum).padStart(2, '0');
const seStr = String(seasonNum).padStart(2, '0');
const epFiltered = paths.filter(p => {
const name = p.text.toLowerCase();
return name.includes(`s${seStr}e${epStr}`) || name.includes(`e${epStr}`);
});
if (epFiltered.length > 0) paths = epFiltered;
}
// Sort 4K first
paths.sort((a, b) => (/2160p|4k/i.test(b.text) ? 1 : 0) - (/2160p|4k/i.test(a.text) ? 1 : 0));
const streams = [];
for (const path of paths.slice(0, 5)) {
let directUrl;
if (path.href.startsWith('http')) {
directUrl = path.href;
} else if (path.href.includes('/movies/') || path.href.includes('/tvs/')) {
directUrl = DAHMER_MOVIES_API + (path.href.startsWith('/') ? '' : '/') + path.href;
} else {
directUrl = dir.dirUrl + path.href;
}
directUrl = decodeURI(directUrl.replace(/([^:]\/)\/+/g, '$1'));
const fileName = path.text;
const isMulti = /\b(HIN|TAM|TEL|Multi|Dual|DUB|Multi-Audio|MULTI)\b/i.test(fileName);
const hasEngTag = /\b(Eng|English)\b/i.test(fileName);
const isEnglishTitle = /^[a-zA-Z0-9\s?!\-:]+$/.test(title);
let language = 'Original';
if (isMulti) language = 'Multi Audio';
else if (isEnglishTitle && hasEngTag) language = 'English';
const formatMatch = fileName.match(/\.(mkv|mp4|m3u8|avi|webm)$/i);
const fileFormat = formatMatch ? formatMatch[1].toUpperCase() : 'LINK';
const resolution = fileName.match(/\b(2160p|1080p|720p|4[Kk])\b/)?.[0] || '1080p';
const info = fileName
.replace(/\.(mkv|mp4|avi|webm|m3u8)$/i, '')
.replace(/[\[\]()._-]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
streams.push({
name: 'DahmerMovies',
title: `${resolution} | ${language} | ${path.size} | ${fileFormat} | ${info}`,
url: DAHMER_WORKER_API + encodeURI(directUrl),
quality: resolution,
provider: 'DahmerMovies',
headers: {
'User-Agent': REQUEST_HEADERS['User-Agent'],
'Referer': `${DAHMER_MOVIES_API}/`,
'Accept': '*/*',
'Range': 'bytes=0-'
}
});
}
console.log(`[DahmerMovies] Total results found: ${streams.length}`);
return streams;
}
module.exports = { getDahmermoviesStreams };
+128
View File
@@ -0,0 +1,128 @@
const { getDetails, resolveImdbId } = require('../utils/tmdb');
const { findBestMatchWithRetry } = require('../utils/titleMatch');
const { tmdbTitleToImdbId } = require('../utils/tmdbTitleToImdb');
const HDGHARTV_API = 'https://hdghartv.cc/api';
const HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'application/json, */*',
'Referer': 'https://hdghartv.cc/'
};
async function fetchJson(url, timeoutMs = 10000) {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
const res = await fetch(url, { headers: HEADERS, signal: controller.signal });
clearTimeout(timer);
if (!res.ok) return null;
return await res.json();
} catch (err) {
console.warn(`[HDGharTV] fetch failed for ${url}: ${err.message}`);
return null;
}
}
async function searchHdghartv(title, kind, variants) {
const searchByVariant = async (variantTitle) => {
const encoded = encodeURIComponent(variantTitle);
const data = await fetchJson(`${HDGHARTV_API}/search?q=${encoded}`);
if (!data) return [];
const results = kind === 'movie' ? data.movies : data.series;
return (results || []).map((r) => ({ title: r.title, type: kind, raw: r }));
};
const { best } = await findBestMatchWithRetry(
{ title, type: kind },
variants,
searchByVariant,
{ provider: 'HDGharTV' }
);
if (!best) return null;
return { id: best.raw._id, title: best.raw.title };
}
const QUALITY_RANK = { '4K': 0, '1080p': 1, '720p': 2, '480p': 3, '360p': 4 };
function sortLinks(links) {
return [...links].sort((a, b) => (QUALITY_RANK[a.quality] || 99) - (QUALITY_RANK[b.quality] || 99));
}
function linksToStreams(links, labelSuffix = '') {
return sortLinks(links.filter((l) => l.isActive && l.url)).map((l) => ({
name: 'HDGharTV',
title: `HDGharTV${labelSuffix ? ` ${labelSuffix}` : ''} · ${l.quality}`,
url: l.url,
quality: l.quality,
provider: 'HDGharTV',
headers: HEADERS
}));
}
async function getHdghartvStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[HDGharTV] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
try {
const kind = mediaType === 'tv' ? 'series' : 'movie';
const tmdbType = mediaType === 'tv' ? 'tv' : 'movie';
const details = await getDetails(tmdbType, tmdbId);
const title = (details && (details.title || details.name)) || '';
if (!title) {
console.log(`[HDGharTV] No TMDB title resolved for ${tmdbId}`);
return [];
}
const variants = [title];
const imdbId = await resolveImdbId(tmdbType, tmdbId);
const match = await searchHdghartv(title, kind, variants);
if (!match) {
console.log(`[HDGharTV] "${title}" not found`);
return [];
}
if (imdbId && imdbId.startsWith('tt')) {
const resolvedId = await tmdbTitleToImdbId(match.title, undefined, kind).catch(() => null);
if (resolvedId && resolvedId !== imdbId) {
console.log(`[HDGharTV] IMDB ID mismatch — rejecting match: expected ${imdbId}, resolved ${resolvedId}`);
return [];
}
}
if (kind === 'movie') {
const movie = await fetchJson(`${HDGHARTV_API}/movies/public/${match.id}`);
if (!movie || !movie.streamingLinks || !movie.streamingLinks.length) {
console.log(`[HDGharTV] No streaming links for movie "${match.title}"`);
return [];
}
return linksToStreams(movie.streamingLinks);
}
const season = seasonNum || 1;
const episode = episodeNum || 1;
const series = await fetchJson(`${HDGHARTV_API}/series/public/${match.id}`);
if (!series || !series.seasons || !series.seasons.length) {
console.log(`[HDGharTV] No seasons for series "${match.title}"`);
return [];
}
const seasonData = series.seasons.find((s) => s.seasonNumber === season);
if (!seasonData) {
console.log(`[HDGharTV] Season ${season} not found`);
return [];
}
const episodeData = seasonData.episodes && seasonData.episodes.find((e) => e.episodeNumber === episode);
if (!episodeData || !episodeData.streamingLinks || !episodeData.streamingLinks.length) {
console.log(`[HDGharTV] Episode ${episode} or links not found`);
return [];
}
return linksToStreams(episodeData.streamingLinks, `S${season}E${episode}`);
} catch (err) {
console.error(`[HDGharTV] Error: ${err.message}`);
return [];
}
}
module.exports = { getHdghartvStreams };
File diff suppressed because it is too large Load Diff
+303
View File
@@ -0,0 +1,303 @@
const { getDetails } = require('../utils/tmdb');
const STREAM_CACHE_TTL = 25 * 60 * 1000;
const PLATFORM_ORDER = ['netflix', 'primevideo', 'hotstar', 'disney'];
const UA_POOL = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.82 Mobile Safari/537.36'
];
const ACCEPT_LANG_POOL = [
'en-US,en;q=0.9',
'en-GB,en;q=0.9',
'en-IN,en;q=0.9,hi;q=0.7',
'en-US,en;q=0.8,es;q=0.5'
];
let uaIndex = 0;
let langIndex = 0;
function nextUA() { return UA_POOL[uaIndex++ % UA_POOL.length]; }
function nextLang() { return ACCEPT_LANG_POOL[langIndex++ % ACCEPT_LANG_POOL.length]; }
const streamCache = new Map();
const inflight = new Map();
let resolvedApiUrl = '';
const NEW_TV_DOMAINS = [
'aHR0cHM6Ly9tb2JpbGVkZXRlY3RzLmNvbQ==',
'aHR0cHM6Ly9tb2JpbGVkZXRlY3QuYXBw',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LmFydA==',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LmNj',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LmNsaWNr',
'aHR0cHM6Ly9tb2JpZGV0ZWN0Lmluaw==',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LmxpdmU=',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LnBybw==',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LnNob3A=',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LnNpdGU=',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LnNwYWNl',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LnN0b3Jl',
'aHR0cHM6Ly9tb2JpZGV0ZWN0LnZpcA==',
'aHR0cHM6Ly9tb2JpZGV0ZWN0Lndpa2k=',
'aHR0cHM6Ly9tb2JpZGV0ZWN0Lnh5eg=='
];
const PLATFORM_MAP = { netflix: { ott: 'nf' }, primevideo: { ott: 'pv' }, hotstar: { ott: 'hs' }, disney: { ott: 'hs' } };
function apiBase() {
return (process.env.NETMIRROR_API_BASE || 'https://net27.cc').replace(/\/$/, '');
}
function streamReferer() {
return process.env.NETMIRROR_STREAM_REFERER || 'https://videodownloader.site/';
}
function newTvHeaders(ott, extra = {}) {
return {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0',
'X-Requested-With': 'NetmirrorNewTV v1.0',
'Accept': 'application/json, text/plain, */*',
'Ott': ott,
'User-Agent': nextUA(),
'Accept-Language': nextLang(),
...extra
};
}
async function resolveNewTvApi() {
if (resolvedApiUrl) return resolvedApiUrl;
const custom = (process.env.NEWTV_DOMAINS || '')
.split(',')
.map((value) => value.trim().replace(/\/$/, ''))
.filter(Boolean);
const domains = [
...custom,
...NEW_TV_DOMAINS.map((value) => Buffer.from(value, 'base64').toString('utf8'))
];
for (const domain of domains) {
try {
const response = await fetch(`${domain}/checknewtv.php`, {
headers: { ...newTvHeaders('nf') },
signal: AbortSignal.timeout(10000)
});
const data = await response.json();
if (data.token_hash) {
resolvedApiUrl = Buffer.from(data.token_hash, 'base64').toString('utf8').replace(/\/$/, '');
return resolvedApiUrl;
}
} catch {
// Try the next rotating discovery domain.
}
}
throw new Error('NetMirror NewTV API discovery failed');
}
async function fetchNetflix(tmdbId, type, title, season, episode) {
try {
const base = apiBase();
const url = type === 'series'
? `${base}/api/embed-tmdb/${tmdbId}?type=tv&se=${season}&ep=${episode}`
: `${base}/api/embed-tmdb/${tmdbId}`;
const response = await fetch(url, {
headers: {
Accept: 'application/json, text/plain, */*',
Referer: `${base}/`,
'User-Agent': nextUA(),
'Accept-Language': nextLang()
},
signal: AbortSignal.timeout(20000)
});
if (!response.ok) return [];
const data = await response.json();
if (data.ok !== true) return [];
const subtitles = (data.captions || []).flatMap((caption, index) => {
if (!caption.url) return [];
return [{
id: `netflix-${index}-${caption.lang || 'en'}`,
url: caption.url.startsWith('/') ? `${base}${caption.url}` : caption.url,
lang: caption.lang || 'en',
label: caption.name || 'English'
}];
});
const headers = { Referer: streamReferer(), 'User-Agent': nextUA() };
const streams = (data.streams || []).filter((stream) => stream.url).map((stream) => ({
name: 'NetMirror | Netflix',
title: `${title}\n${stream.resolution ? `${stream.resolution}p` : 'Auto'}`,
url: stream.url,
quality: stream.resolution ? `${stream.resolution}p` : 'Auto',
provider: 'NetMirror',
headers,
...(subtitles.length ? { subtitles } : {})
}));
if (streams.length || !data.mp4) return streams;
return [{
name: 'NetMirror | Netflix',
title: `${title}\nAuto`,
url: data.mp4,
quality: 'Auto',
provider: 'NetMirror',
headers,
...(subtitles.length ? { subtitles } : {})
}];
} catch (error) {
console.log(`[NetMirror] Netflix direct failed: ${error.message}`);
return [];
}
}
function parseNumber(value) {
if (!value) return null;
const parsed = parseInt(String(value).replace(/[^\d]/g, ''), 10);
return Number.isFinite(parsed) ? parsed : null;
}
async function getEpisodes(api, showId, postData, config) {
const result = [];
const selectedSeason = Array.isArray(postData.season) ? postData.season.findIndex((item) => item.selected) : -1;
const selectedSeasonId = selectedSeason >= 0 ? postData.season[selectedSeason].id : postData.nextPageSeason;
const add = (episode, seasonNumber) => {
result.push({
id: episode.id,
s: seasonNumber || parseNumber(episode.sNum),
ep: parseNumber(episode.ep) || parseNumber(episode.epNum)
});
};
for (const episode of postData.episodes || []) {
if (episode) add(episode, selectedSeason >= 0 ? selectedSeason + 1 : null);
}
if (postData.nextPageShow === 1 && selectedSeasonId) {
try {
const response = await fetch(
`${api}/newtv/episodes.php?id=${encodeURIComponent(selectedSeasonId)}&page=2`,
{ headers: newTvHeaders(config.ott), signal: AbortSignal.timeout(15000) }
);
const data = await response.json();
for (const episode of data.episodes || []) {
if (episode) add(episode, selectedSeason >= 0 ? selectedSeason + 1 : null);
}
} catch {
// The first page is still useful when pagination is unavailable.
}
}
return result;
}
async function fetchPlatform(platform, title, type, season, episode) {
try {
const config = PLATFORM_MAP[platform];
const api = await resolveNewTvApi();
const searchResponse = await fetch(
`${api}/newtv/search.php?s=${encodeURIComponent(title)}`,
{ headers: newTvHeaders(config.ott), signal: AbortSignal.timeout(15000) }
);
const searchData = await searchResponse.json();
const first = (searchData.searchResult || [])[0];
if (!first || !first.id) return [];
const postResponse = await fetch(
`${api}/newtv/post.php?id=${encodeURIComponent(first.id)}`,
{ headers: newTvHeaders(config.ott, { Lastep: '', Usertoken: '' }), signal: AbortSignal.timeout(15000) }
);
const postData = await postResponse.json();
let targetId = first.id;
if (type === 'series') {
const episodes = await getEpisodes(api, first.id, postData, config);
const target = episodes.find((item) => item.s === season && item.ep === episode);
if (!target) return [];
targetId = target.id;
} else if (postData.type === 't' || ((postData.episodes || []).filter(Boolean).length > 0)) {
return [];
} else {
targetId = postData.main_id || first.id;
}
const playerResponse = await fetch(
`${api}/newtv/player.php?id=${encodeURIComponent(targetId)}`,
{ headers: newTvHeaders(config.ott, { Usertoken: '' }), signal: AbortSignal.timeout(15000) }
);
const player = await playerResponse.json();
if (player.status && player.status !== 'ok') {
console.log(`[NetMirror] ${platform} player status: ${player.status} — accepting if link present`);
}
if (!player.video_link) return [];
return [{
name: `NetMirror | ${platform === 'primevideo' ? 'Prime Video' : platform.charAt(0).toUpperCase() + platform.slice(1)}`,
title: `${title}\nAuto`,
url: player.video_link,
quality: 'Auto',
provider: 'NetMirror',
headers: {
Referer: player.referer || api,
'User-Agent': nextUA()
}
}];
} catch (error) {
console.log(`[NetMirror] ${platform} failed: ${error.message}`);
return [];
}
}
function hdOnly(streams, forceHd) {
if (!forceHd) return streams;
const hd = streams.filter((stream) => {
const match = (stream.quality || '').match(/(\d+)p/i);
return !match || Number(match[1]) >= 720;
});
return hd.length ? hd : streams;
}
async function getNetmirrorStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[NetMirror] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
const type = mediaType === 'tv' ? 'series' : 'movie';
const season = mediaType === 'tv' ? (seasonNum || null) : null;
const episode = mediaType === 'tv' ? (episodeNum || null) : null;
try {
const details = await getDetails(mediaType === 'tv' ? 'tv' : 'movie', tmdbId);
const title = (details && (details.title || details.name)) || '';
if (!title) {
console.log(`[NetMirror] No TMDB title resolved for ${tmdbId}`);
return [];
}
const key = `${tmdbId}:${type}:${season || 0}:${episode || 0}`;
const cached = streamCache.get(key);
if (cached && cached.expiresAt > Date.now()) {
return hdOnly(cached.streams, true);
}
const running = inflight.get(key);
if (running) return running;
const request = (async () => {
const direct = await fetchNetflix(tmdbId, type, title, season, episode);
const fallback = await Promise.all(
PLATFORM_ORDER
.filter((platform) => platform !== 'netflix' || direct.length === 0)
.map((platform) => fetchPlatform(platform, title, type, season, episode))
);
const streams = hdOnly([...direct, ...fallback.flat()], true);
streamCache.set(key, { streams, expiresAt: Date.now() + STREAM_CACHE_TTL });
console.log(`[NetMirror] ${tmdbId} streams ready: ${streams.length}`);
return streams;
})().finally(() => inflight.delete(key));
inflight.set(key, request);
return request;
} catch (err) {
console.error(`[NetMirror] Error: ${err.message}`);
return [];
}
}
module.exports = { getNetmirrorStreams };
+334
View File
@@ -0,0 +1,334 @@
const { createDecipheriv } = require('crypto');
const { getDetails, resolveImdbId } = require('../utils/tmdb');
const { getEpisodesPerSeason } = require('../utils/cinemetaEpisodes');
const { findBestMatch } = require('../utils/titleMatch');
const { tmdbTitleToImdbId } = require('../utils/tmdbTitleToImdb');
const MAIN_URL = 'https://api3.devcorp.me';
const AES_KEY = Buffer.from('im72charPasswordofdInitVectorStm', 'utf8');
const AES_IV = Buffer.from('im72charPassword', 'utf8');
const FETCH_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': 'https://onetouchtv.xyz/'
};
const FETCH_TIMEOUT_MS = 15000;
const FETCH_RETRIES = 2;
const FETCH_RETRY_DELAY_MS = 400;
const CACHE_TTL_MS = 5 * 60 * 1000;
const cache = new Map();
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
function fromCache(key) {
const entry = cache.get(key);
if (!entry) return null;
if (Date.now() - entry.ts > CACHE_TTL_MS) {
cache.delete(key);
return null;
}
return entry.data;
}
function toCache(key, data) {
cache.set(key, { data, ts: Date.now() });
}
function decrypt(encoded) {
let s = encoded
.replace(/-_\./g, '/')
.replace(/@/g, '+')
.replace(/\s+/g, '');
const pad = s.length % 4;
if (pad !== 0) s += '='.repeat(4 - pad);
const buf = Buffer.from(s, 'base64');
const decipher = createDecipheriv('aes-256-cbc', AES_KEY, AES_IV);
const dec = Buffer.concat([decipher.update(buf), decipher.final()]);
return JSON.parse(dec.toString('utf8'));
}
async function fetchEncryptedOnce(url) {
const res = await fetch(url, {
headers: FETCH_HEADERS,
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
});
if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
const text = await res.text();
const parsed = decrypt(text);
return parsed.result;
}
async function fetchEncrypted(path, cacheTtlMs = CACHE_TTL_MS) {
const url = path.startsWith('https://') ? path : `${MAIN_URL}${path}`;
if (cacheTtlMs > 0) {
const cached = fromCache(url);
if (cached !== null) return cached;
}
let lastErr;
for (let attempt = 0; attempt <= FETCH_RETRIES; attempt++) {
try {
const data = await fetchEncryptedOnce(url);
if (cacheTtlMs > 0) toCache(url, data);
return data;
} catch (err) {
lastErr = err;
if (attempt < FETCH_RETRIES) {
console.warn(`[OneTouchTV] fetch failed (attempt ${attempt + 1}): ${err.message}`);
await sleep(FETCH_RETRY_DELAY_MS * (attempt + 1));
}
}
}
throw lastErr;
}
const TYPE_MAP = {
movie: new Set(['movie']),
series: new Set(['drama', 'variety', 'anime'])
};
function isTypeCompatible(stremioType, onetouchType) {
const allowed = TYPE_MAP[stremioType];
if (!allowed) return true;
return allowed.has(onetouchType.toLowerCase());
}
function findBestOttMatch(results, title, year, stremioType, targetSeason) {
if (results.length === 0) return null;
const filtered = results.filter((r) => isTypeCompatible(stremioType, r.type));
if (filtered.length === 0) return null;
const candidates = filtered.map((r) => ({
title: r.title,
year: parseInt(r.year, 10) || undefined,
type: stremioType,
season: targetSeason ?? undefined,
raw: r
}));
const { best } = findBestMatch(
{
title,
year: year ?? undefined,
type: stremioType,
season: targetSeason ?? undefined
},
candidates,
{ provider: 'OneTouchTV' }
);
return best ? best.raw : null;
}
async function searchContent(keyword) {
const path = `/vod/search?keyword=${encodeURIComponent(keyword)}`;
try {
const results = await fetchEncrypted(path);
return Array.isArray(results) ? results : [];
} catch (err) {
console.error(`[OneTouchTV] search failed for "${keyword}": ${err.message}`);
return [];
}
}
async function getDetail(id) {
try {
const result = await fetchEncrypted(`/vod/${id}/detail`);
return result ?? null;
} catch (err) {
console.error(`[OneTouchTV] detail failed for ${id}: ${err.message}`);
return null;
}
}
async function getEpisodeStreams(id, playId) {
try {
const result = await fetchEncrypted(`/vod/${id}/episode/${playId}`, 0);
return result ?? null;
} catch (err) {
console.error(`[OneTouchTV] episode fetch failed ${id}/${playId}: ${err.message}`);
return null;
}
}
function resolveQuality(quality) {
if (!quality) return 'Unknown';
const q = quality.toLowerCase();
if (q === 'auto') return 'Auto';
if (q.includes('1080')) return '1080p';
if (q.includes('720')) return '720p';
if (q.includes('480')) return '480p';
if (q.includes('360')) return '360p';
return quality;
}
async function resolveAbsoluteEpisode(imdbId, season, episode) {
if (season <= 1) return episode;
const episodesPerSeason = await getEpisodesPerSeason(imdbId);
if (episodesPerSeason.length < season - 1) return null;
let offset = 0;
for (let s = 0; s < season - 1; s++) {
const count = episodesPerSeason[s] ?? 0;
if (count === 0) return null;
offset += count;
}
return offset + episode;
}
async function getOnetouchtvStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[OneTouchTV] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
try {
const type = mediaType === 'tv' ? 'series' : 'movie';
const tmdbType = mediaType === 'tv' ? 'tv' : 'movie';
const details = await getDetails(tmdbType, tmdbId);
const title = (details && (details.title || details.name)) || '';
const year = (details && (details.release_date || details.first_air_date || '').slice(0, 4)) || null;
if (!title) {
console.log(`[OneTouchTV] No TMDB title resolved for ${tmdbId}`);
return [];
}
const imdbId = await resolveImdbId(tmdbType, tmdbId);
const season = seasonNum || 1;
const episode = episodeNum || 1;
const searchKeywords = [title];
if (type === 'series' && season > 1) {
searchKeywords.unshift(`${title} Season ${season}`);
}
let matchResult = null;
for (const keyword of searchKeywords) {
const results = await searchContent(keyword);
const searchYear = keyword !== title && season > 1 ? null : year;
const match = findBestOttMatch(results, keyword, searchYear, type, season);
if (match) {
matchResult = match;
break;
}
}
if (!matchResult) {
console.log(`[OneTouchTV] "${title}" not found`);
return [];
}
if (imdbId && imdbId.startsWith('tt')) {
const matchedYear = matchResult.year ? parseInt(matchResult.year, 10) : undefined;
const resolvedId = await tmdbTitleToImdbId(
matchResult.title,
Number.isFinite(matchedYear) ? matchedYear : undefined,
type
).catch(() => null);
if (resolvedId && resolvedId !== imdbId) {
console.log(`[OneTouchTV] IMDB ID mismatch — rejecting match: expected ${imdbId}, resolved ${resolvedId}`);
return [];
}
}
const detail = await getDetail(matchResult.id);
if (!detail || !detail.episodes || detail.episodes.length === 0) {
console.log(`[OneTouchTV] no episodes in detail for "${matchResult.title}"`);
return [];
}
let targetEpisode = null;
if (type === 'movie') {
targetEpisode = detail.episodes[0] ?? null;
} else {
const targetSeasonInTitle = new RegExp(`\\bseason\\s+${season}\\b`, 'i');
const anySeasonInTitle = /\bseason\s+(\d+)\b/i;
const matchedSeasonNum = anySeasonInTitle.exec(matchResult.title);
const entryIsForTargetSeason =
season === 1 || targetSeasonInTitle.test(matchResult.title);
if (
season > 1 &&
matchedSeasonNum !== null &&
parseInt(matchedSeasonNum[1], 10) !== season
) {
console.warn(`[OneTouchTV] matched title names a different season — aborting`);
return [];
}
if (entryIsForTargetSeason) {
targetEpisode = detail.episodes.find((e) => parseInt(e.episode, 10) === episode) ?? null;
} else {
let absEp = null;
if (imdbId) {
absEp = await resolveAbsoluteEpisode(imdbId, season, episode);
}
if (absEp !== null) {
targetEpisode = detail.episodes.find((e) => parseInt(e.episode, 10) === absEp) ?? null;
}
if (!targetEpisode && season === 1) {
targetEpisode = detail.episodes.find((e) => parseInt(e.episode, 10) === episode) ?? null;
}
}
}
if (!targetEpisode) {
console.log(`[OneTouchTV] episode not found (S${season}E${episode})`);
return [];
}
const streamData = await getEpisodeStreams(matchResult.id, targetEpisode.playId);
if (!streamData) return [];
const sources = streamData.sources ?? [];
if (sources.length === 0) {
console.log(`[OneTouchTV] no sources in episode for "${matchResult.title}"`);
return [];
}
const subtitles = [];
for (const track of streamData.track ?? []) {
if (track.file && track.name) {
subtitles.push({ url: track.file, lang: track.name });
}
}
const out = [];
for (const src of sources) {
if (!src.url) continue;
const quality = resolveQuality(src.quality);
const sourceName = src.name ? ` | ${src.name}` : '';
out.push({
name: `OneTouchTV${sourceName}`,
title: `${matchResult.title}\n${quality} · ${(src.type ?? 'HLS').toUpperCase()}`,
url: src.url,
quality,
provider: 'OneTouchTV',
headers: {
'User-Agent': 'Mozilla/5.0',
'Referer': `${MAIN_URL}/`
},
...(subtitles.length ? { subtitles } : {})
});
}
const seen = new Set();
const streams = out.filter((s) => {
if (seen.has(s.url)) return false;
seen.add(s.url);
return true;
});
console.log(`[OneTouchTV] Got ${streams.length} stream(s) for "${matchResult.title}"`);
return streams;
} catch (err) {
console.error(`[OneTouchTV] Error: ${err.message}`);
return [];
}
}
module.exports = { getOnetouchtvStreams };
+101 -220
View File
@@ -1,12 +1,27 @@
const { config } = require('../utils/config'); const { config } = require('../utils/config');
// Lazy load showbox to avoid early side-effects; temporarily disabled until stabilized const fs = require('fs');
let showbox = null; const path = require('path');
let k4hdhub = null; // 4khdhub provider lazy ref
let moviesmod = null; // moviesmod provider lazy ref // Lazy load cache for providers
let mp4hydra = null; // MP4Hydra provider lazy ref const providerCache = new Map();
let vidzee = null; // VidZee provider lazy ref
let vixsrc = null; // vixsrc provider lazy ref // Function name mappings for existing providers
let xprime = null; // xprime provider lazy ref const providerFunctionMap = {
'Showbox.js': 'getStreamsFromTmdbId',
'4khdhub.js': 'get4KHDHubStreams',
'vixsrc.js': 'getVixsrcStreams',
'videasy.js': 'getVideasyStreams',
'vidlink.js': 'getVidlinkStreams',
'dahmermovies.js': 'getDahmermoviesStreams',
'streamflix.js': 'getStreamflixStreams',
'vaplayer.js': 'getVaplayerStreams',
'castletv.js': 'getCastletvStreams',
'hdghartv.js': 'getHdghartvStreams',
'netmirror.js': 'getNetmirrorStreams',
'onetouchtv.js': 'getOnetouchtvStreams',
'zxcstreams.js': 'getZxcstreamsStreams',
};
// Stats for debug endpoint // Stats for debug endpoint
let lastCookieStats = { selected: null, index: null, total: 0, remainingMB: null, timestamp: null }; let lastCookieStats = { selected: null, index: null, total: 0, remainingMB: null, timestamp: null };
@@ -14,11 +29,50 @@ async function getEffectiveCookies() {
return Array.isArray(config.febboxCookies) ? config.febboxCookies : []; return Array.isArray(config.febboxCookies) ? config.febboxCookies : [];
} }
// Adapter for Showbox provider using existing exported getStreamsFromTmdbId // Get all provider files
async function fetchShowbox(ctx) { function getProviderFiles() {
if (!showbox) { const providersDir = path.join(__dirname);
try { showbox = require('./Showbox.js'); } catch (e) { console.error('Failed to load Showbox provider:', e.message); return []; } return fs.readdirSync(providersDir)
.filter(file => file.endsWith('.js') && file !== 'registry.js')
.map(file => ({
name: path.parse(file).name.toLowerCase(),
file: file,
functionName: providerFunctionMap[file]
}));
} }
// Load a provider module
function loadProvider(providerFile) {
if (!providerCache.has(providerFile)) {
try {
providerCache.set(providerFile, require(path.join(__dirname, providerFile)));
} catch (e) {
console.error(`Failed to load provider ${providerFile}:`, e.message);
return null;
}
}
return providerCache.get(providerFile);
}
// Create fetch function for a provider
function createFetchFunction(providerInfo) {
return async function(ctx) {
const module = loadProvider(providerInfo.file);
if (!module) return [];
const funcName = providerInfo.functionName;
if (!module[funcName]) {
console.error(`Provider ${providerInfo.name} does not export ${funcName}`);
return [];
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
let result;
if (providerInfo.name === 'showbox') {
// Special case for Showbox with TMDB key and cookies
const { getTmdbApiKey } = require('../utils/tmdbKey'); const { getTmdbApiKey } = require('../utils/tmdbKey');
const tmdbApiKey = getTmdbApiKey(); const tmdbApiKey = getTmdbApiKey();
if (!tmdbApiKey) { if (!tmdbApiKey) {
@@ -37,227 +91,54 @@ async function fetchShowbox(ctx) {
lastCookieStats = { selected: selected.slice(0, 16) + '...', index, total: cookies.length, remainingMB: null, timestamp: Date.now() }; lastCookieStats = { selected: selected.slice(0, 16) + '...', index, total: cookies.length, remainingMB: null, timestamp: Date.now() };
console.log(`[registry] Cookie random pick index=${index} total=${cookies.length}`); console.log(`[registry] Cookie random pick index=${index} total=${cookies.length}`);
} }
try { result = await module[funcName](mediaType, ctx.tmdbId, ctx.season || null, ctx.episode || null, null, selected);
const tmdbType = ctx.type === 'movie' ? 'movie' : 'tv';
console.log(`[registry] Fetching Showbox streams for ${tmdbType} tmdbId=${ctx.tmdbId} season=${ctx.season||''} episode=${ctx.episode||''}`);
const t0 = Date.now();
const streams = await showbox.getStreamsFromTmdbId(tmdbType, ctx.tmdbId, ctx.season || null, ctx.episode || null, null, selected);
const durationMs = Date.now() - t0;
console.log(`[registry] Provider showbox fetch duration ${durationMs}ms`);
if (global.currentRequestUserCookieRemainingMB != null) { if (global.currentRequestUserCookieRemainingMB != null) {
lastCookieStats.remainingMB = global.currentRequestUserCookieRemainingMB; lastCookieStats.remainingMB = global.currentRequestUserCookieRemainingMB;
} }
console.log(`[registry] Showbox returned ${Array.isArray(streams)?streams.length:0} streams`);
return Array.isArray(streams) ? streams : [];
} catch (e) {
console.error('[registry] Showbox fetch error:', e.message);
return [];
} finally {
global.currentRequestConfig = previousConfig || {}; global.currentRequestConfig = previousConfig || {};
} } else {
// Standard provider call
result = await module[funcName](ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
} }
const durationMs = Date.now() - t0;
console.log(`[registry] ${providerInfo.name} fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
// Add provider name if not present
return result.map(s => ({ ...s, provider: s.provider || providerInfo.name }));
} catch (e) {
console.error(`[registry] ${providerInfo.name} fetch error:`, e.message);
return [];
}
};
}
// Initialize providers - always load all; enabled state is checked dynamically from live config
const providerFiles = getProviderFiles();
const providers = []; const providers = [];
if (config.enableShowboxProvider) { for (const providerInfo of providerFiles) {
providers.push({ name: 'showbox', enabled: true, fetch: fetchShowbox }); providers.push({
} else { name: providerInfo.name,
console.log('[registry] showbox disabled via config'); fetch: createFetchFunction(providerInfo)
}
// 4KHDHub adapter
async function fetch4khdhub(ctx) {
if (!k4hdhub) {
try { k4hdhub = require('./4khdhub.js'); } catch (e) { console.error('Failed to load 4khdhub provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await k4hdhub.get4KHDHubStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] 4khdhub fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || '4KHDHub', provider: '4khdhub' }));
} catch (e) {
console.error('[registry] 4khdhub fetch error:', e.message);
return [];
}
}
if (config.enable4khdhubProvider) {
providers.push({ name: '4khdhub', enabled: true, fetch: fetch4khdhub });
console.log('[registry] 4khdhub provider enabled');
} else {
console.log('[registry] 4khdhub disabled via config');
}
// MoviesMod adapter
async function fetchMoviesMod(ctx) {
if (!moviesmod) {
try { moviesmod = require('./moviesmod.js'); } catch (e) { console.error('Failed to load moviesmod provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await moviesmod.getMoviesModStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] moviesmod fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || 'MoviesMod', provider: 'moviesmod' }));
} catch (e) {
console.error('[registry] moviesmod fetch error:', e.message);
return [];
}
}
if (config.enableMoviesmodProvider) {
providers.push({ name: 'moviesmod', enabled: true, fetch: fetchMoviesMod });
console.log('[registry] moviesmod provider enabled');
} else {
console.log('[registry] moviesmod disabled via config');
}
// MP4Hydra adapter
async function fetchMp4hydra(ctx) {
if (!mp4hydra) {
try { mp4hydra = require('./MP4Hydra.js'); } catch (e) { console.error('Failed to load MP4Hydra provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await mp4hydra.getMP4HydraStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] mp4hydra fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || 'MP4Hydra', provider: 'mp4hydra' }));
} catch (e) {
console.error('[registry] mp4hydra fetch error:', e.message);
return [];
}
}
if (config.enableMp4hydraProvider) {
providers.push({ name: 'mp4hydra', enabled: true, fetch: fetchMp4hydra });
console.log('[registry] mp4hydra provider enabled');
} else {
console.log('[registry] mp4hydra disabled via config');
}
// VidZee adapter
async function fetchVidZee(ctx) {
if (!vidzee) {
try { vidzee = require('./VidZee.js'); } catch (e) { console.error('Failed to load VidZee provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await vidzee.getVidZeeStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] vidzee fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || 'VidZee', provider: 'vidzee' }));
} catch (e) {
console.error('[registry] vidzee fetch error:', e.message);
return [];
}
}
if (config.enableVidzeeProvider) {
providers.push({ name: 'vidzee', enabled: true, fetch: fetchVidZee });
console.log('[registry] vidzee provider enabled');
} else {
console.log('[registry] vidzee disabled via config');
}
// vixsrc adapter
async function fetchVixsrc(ctx) {
if (!vixsrc) {
try { vixsrc = require('./vixsrc.js'); } catch (e) { console.error('Failed to load vixsrc provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await vixsrc.getVixsrcStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] vixsrc fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || 'Vixsrc', provider: 'vixsrc' }));
} catch (e) {
console.error('[registry] vixsrc fetch error:', e.message);
return [];
}
}
if (config.enableVixsrcProvider) {
providers.push({ name: 'vixsrc', enabled: true, fetch: fetchVixsrc });
console.log('[registry] vixsrc provider enabled');
} else {
console.log('[registry] vixsrc disabled via config');
}
// xprime adapter (requires TMDB metadata lookup for title/year)
async function fetchXprime(ctx) {
if (!xprime) {
try { xprime = require('./xprime.js'); } catch (e) { console.error('Failed to load xprime provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const { getTmdbApiKey } = require('../utils/tmdbKey');
const tmdbApiKey = getTmdbApiKey();
if (!tmdbApiKey) {
console.warn('[registry] xprime skipped: TMDB API key missing');
return [];
}
// Fetch minimal TMDB metadata for title + year
let title = null; let year = null;
try {
const axios = require('axios');
const metaUrl = `https://api.themoviedb.org/3/${mediaType}/${ctx.tmdbId}?api_key=${tmdbApiKey}`;
const { data: meta } = await axios.get(metaUrl, { timeout: 8000 });
title = mediaType === 'movie' ? (meta.title || meta.original_title) : (meta.name || meta.original_name);
const dateStr = mediaType === 'movie' ? meta.release_date : meta.first_air_date;
if (dateStr) year = dateStr.split('-')[0];
} catch (mErr) {
console.warn('[registry] xprime metadata fetch failed:', mErr.message);
return [];
}
if (!title || !year) {
console.log('[registry] xprime missing title/year after TMDB lookup');
return [];
}
const t0 = Date.now();
const streams = await xprime.getXprimeStreams(title, year, mediaType, ctx.season || null, ctx.episode || null, true, null);
const durationMs = Date.now() - t0;
console.log(`[registry] xprime fetch duration ${durationMs}ms (title='${title}' year=${year}) returned ${Array.isArray(streams)?streams.length:0}`);
if (!Array.isArray(streams) || !streams.length) return [];
// Normalize quality labels that are non-numeric (e.g., FHD/HD/SD)
return streams.map(s => {
let q = s.quality || 'Unknown';
if (!/(\d{3,4})p/.test(q.toLowerCase())) {
const qLower = q.toLowerCase();
if (qLower.includes('fhd') || qLower.includes('full')) q = '1080p';
else if (qLower === 'hd' || qLower.includes('720')) q = '720p';
else if (qLower.includes('sd') || qLower.includes('480')) q = '480p';
else if (qLower.includes('cam')) q = '360p';
}
return { ...s, quality: q, name: s.name || 'Xprime', provider: 'xprime' };
}); });
} catch (e) { console.log(`[registry] ${providerInfo.name} provider loaded`);
console.error('[registry] xprime fetch error:', e.message);
return [];
}
} }
if (config.enableXprimeProvider) { function isProviderEnabled(name) {
providers.push({ name: 'xprime', enabled: true, fetch: fetchXprime }); const flag = `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`;
console.log('[registry] xprime provider enabled'); return config[flag] !== false;
} else {
console.log('[registry] xprime disabled via config');
} }
function listProviders() { return providers.map(p => ({ name: p.name, enabled: p.enabled })); } function listProviders() { return providers.map(p => ({ name: p.name, enabled: isProviderEnabled(p.name) })); }
function getProvider(name) { return providers.find(p => p.name === name.toLowerCase()) || null; } function getProvider(name) {
const p = providers.find(p => p.name === name.toLowerCase());
if (!p) return null;
return { ...p, enabled: isProviderEnabled(p.name) };
}
function getCookieStats() { return lastCookieStats; } function getCookieStats() { return lastCookieStats; }
+133
View File
@@ -0,0 +1,133 @@
const axios = require('axios');
const API_BASE = 'https://api.streamflix.app';
const FIREBASE_BASE = 'https://chilflix-410be-default-rtdb.asia-southeast1.firebasedatabase.app';
const DATA_TTL = 30 * 60 * 1000;
const CONFIG_TTL = 5 * 60 * 1000;
const EPISODES_TTL = 60 * 60 * 1000;
let dataCache = null;
let configCache = null;
const episodesCache = new Map();
const REQUEST_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': 'application/json, */*',
'Accept-Language': 'en-US,en;q=0.9'
};
async function getData() {
if (dataCache && Date.now() - dataCache.ts < DATA_TTL) return dataCache.items;
console.log('[StreamFlix] fetching data.json');
const res = await axios.get(`${API_BASE}/data.json`, { headers: REQUEST_HEADERS, timeout: 20000 });
const items = (res.data && res.data.data) || [];
dataCache = { items, ts: Date.now() };
console.log(`[StreamFlix] data.json cached (${items.length} items)`);
return items;
}
async function getConfig() {
if (configCache && Date.now() - configCache.ts < CONFIG_TTL) return configCache.config;
const res = await axios.get(`${API_BASE}/config/config-streamflixapp.json`, { headers: REQUEST_HEADERS, timeout: 8000 });
configCache = { config: res.data, ts: Date.now() };
return res.data;
}
async function getEpisodes(movieKey, season) {
const cacheKey = `${movieKey}:${season}`;
const cached = episodesCache.get(cacheKey);
if (cached && Date.now() - cached.ts < EPISODES_TTL) return cached.episodes;
const url = `${FIREBASE_BASE}/Data/${movieKey}/seasons/${season}/episodes.json`;
const res = await axios.get(url, { headers: REQUEST_HEADERS, timeout: 10000 });
const raw = res.data || {};
const episodes = {};
for (const [k, v] of Object.entries(raw)) {
episodes[parseInt(k, 10)] = v;
}
episodesCache.set(cacheKey, { episodes, ts: Date.now() });
return episodes;
}
function downloadBases(config) {
const seen = new Set();
const out = [];
for (const url of config.download || []) {
if (url && !seen.has(url)) {
seen.add(url);
out.push(url);
}
}
return out;
}
function subtitleHint(filename) {
const f = (filename || '').toLowerCase();
if (f.includes('esub') || f.includes('.srt') || f.includes('.ass') || f.includes('sub')) {
return ' [Embedded Subs]';
}
return '';
}
async function getStreamflixStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[StreamFlix] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
try {
const [items, config] = await Promise.all([getData(), getConfig()]);
const match = items.find((item) => item.tmdb === String(tmdbId));
if (!match) {
console.log(`[StreamFlix] No match found for TMDB ID ${tmdbId}`);
return [];
}
const bases = downloadBases(config);
if (bases.length === 0) {
console.log(`[StreamFlix] No download CDN bases in config`);
return [];
}
if (mediaType === 'movie') {
if (!match.movielink) return [];
const subs = subtitleHint(match.movielink);
return bases.map((base, i) => ({
name: 'StreamFlix',
title: `StreamFlix${i > 0 ? ` Mirror ${i}` : ''}${subs} | ${match.moviename}`,
url: `${base}${match.movielink}`,
quality: 'Auto',
provider: 'StreamFlix',
headers: { 'User-Agent': REQUEST_HEADERS['User-Agent'] }
}));
}
if (seasonNum === null || episodeNum === null) return [];
try {
const episodes = await getEpisodes(match.moviekey, seasonNum);
const ep = episodes[episodeNum - 1] || episodes[episodeNum];
if (ep && ep.link) {
const subs = subtitleHint(ep.link);
return bases.map((base, i) => ({
name: 'StreamFlix',
title: `StreamFlix${i > 0 ? ` Mirror ${i}` : ''}${subs} | ${match.moviename} S${seasonNum}E${episodeNum}${ep.name ? ` • ${ep.name}` : ''}`,
url: `${base}${ep.link}`,
quality: 'Auto',
provider: 'StreamFlix',
headers: { 'User-Agent': REQUEST_HEADERS['User-Agent'] }
}));
}
} catch (err) {
console.log(`[StreamFlix] Firebase fetch failed: ${err.message}`);
}
return [];
} catch (err) {
console.error(`[StreamFlix] Error: ${err.message}`);
return [];
}
}
module.exports = { getStreamflixStreams };
File diff suppressed because it is too large Load Diff
+87
View File
@@ -0,0 +1,87 @@
const { resolveImdbId } = require('../utils/tmdb');
const STREAM_API = 'https://streamdata.vaplayer.ru/api.php';
const PLAYER_ORIGIN = 'https://nextgencloudfabric.com';
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
async function fetchStreamUrls(imdb, type, season, episode) {
const params = new URLSearchParams({ imdb, type });
if (type === 'tv' && season != null && episode != null) {
params.set('season', String(season));
params.set('episode', String(episode));
}
const url = `${STREAM_API}?${params.toString()}`;
const referer = type === 'tv'
? `${PLAYER_ORIGIN}/embed/tv/${imdb}/${season}/${episode}`
: `${PLAYER_ORIGIN}/embed/movie/${imdb}`;
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 12000);
const resp = await fetch(url, {
signal: controller.signal,
headers: {
'User-Agent': USER_AGENT,
'Referer': referer,
'Origin': PLAYER_ORIGIN
}
});
clearTimeout(timer);
if (!resp.ok) {
throw new Error(`VaPlayer API HTTP ${resp.status}`);
}
const json = await resp.json();
if (json.status_code !== '200' && json.status_code !== 200) {
throw new Error(`VaPlayer API error: ${json.error || `status_code=${json.status_code}`}`);
}
return (json.data && json.data.stream_urls) || [];
} catch (err) {
console.warn(`[VaPlayer] fetch failed for ${imdb}: ${err.message}`);
return [];
}
}
function toStreams(streamUrls, labelSuffix = '') {
return streamUrls.map((url, i) => ({
name: 'VaPlayer',
title: `VaPlayer${labelSuffix ? ` ${labelSuffix}` : ''} · HLS ${i + 1}`,
url,
quality: 'Auto',
provider: 'VaPlayer',
headers: {
'Referer': `${PLAYER_ORIGIN}/`,
'Origin': PLAYER_ORIGIN,
'User-Agent': USER_AGENT
}
}));
}
async function getVaplayerStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[VaPlayer] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
try {
const imdbId = await resolveImdbId(mediaType === 'tv' ? 'tv' : 'movie', tmdbId);
if (!imdbId || !imdbId.startsWith('tt')) {
console.log(`[VaPlayer] No IMDB ID resolved for TMDB ${tmdbId}`);
return [];
}
const urls = mediaType === 'tv' && seasonNum !== null && episodeNum !== null
? await fetchStreamUrls(imdbId, 'tv', seasonNum, episodeNum)
: await fetchStreamUrls(imdbId, 'movie');
const streams = toStreams(urls, mediaType === 'tv' ? `S${seasonNum}E${episodeNum}` : '');
console.log(`[VaPlayer] Got ${streams.length} stream(s) for ${imdbId}`);
return streams;
} catch (err) {
console.error(`[VaPlayer] Error: ${err.message}`);
return [];
}
}
module.exports = { getVaplayerStreams };
+216
View File
@@ -0,0 +1,216 @@
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/'
};
const VIDEASY_API = 'https://api.speedracelight.com';
// Each server maps a name to its API route. moviesOnly:true skips TV requests.
const SERVERS = {
'CDN': { url: `${VIDEASY_API}/cdn/sources-with-title` },
'LaMovie': { url: `${VIDEASY_API}/lamovie/sources-with-title` },
'Meine': { url: `${VIDEASY_API}/meine/sources-with-title`, moviesOnly: true }
};
// --- Seed-based response decryption (mirrors player.videasy.net) ---
const MAGIC = [109, 118, 109, 49]; // "mvm1"
const HASH_TABLE = [1116352408, 1899447441, 3049323471, 3921009573, 961987163, 1508970993, 2453635748, 2870763221, 3624381080, 310598401, 607225278, 1426881987, 1925078388, 2162078206, 2614888103, 3248222580];
function u32x(x) { return x >>> 0; }
function mul32(a, b) { return Math.imul(a, b) >>> 0; }
function rotl32(x, n) {
x >>>= 0;
n &= 31;
return n === 0 ? x : ((x << n) | (x >>> (32 - n))) >>> 0;
}
function hash32(x) {
x = u32x(x);
x ^= x >>> 16;
x = mul32(x, 2246822507);
x ^= x >>> 13;
x = mul32(x, 3266489909);
x ^= x >>> 16;
return u32x(x);
}
function fnv1a(str) {
let h = 2166136261;
for (let i = 0; i < str.length; i++) h = mul32(h ^ str.charCodeAt(i), 16777619);
return hash32(h);
}
function initStream(seed, secondKey) {
const S = new Array(61);
let a = u32x(hash32(fnv1a(seed) ^ hash32(u32x((secondKey >>> 0) ^ 2654435769))));
for (let i = 0; i < 8; i++) {
if ((i * (i + 1) & 1) === 0) {
const idx = a % 61;
a = rotl32(a + u32x(2654435769), 7 + (7 & i));
S[idx] = u32x(a ^ hash32(a));
a = hash32(u32x(a + idx));
} else {
S[i] = HASH_TABLE[15 & i];
}
}
return { S, acc: u32x(hash32(2779096485 ^ a)) };
}
function nextByte(st, ctr) {
const r = st.S;
const o = st.acc;
const n = o % 61;
const inSet = 0 - Number(n in r);
const d = r[n] >>> 0;
const x = u32x(d ^ mul32(2654435769, ctr + 1));
const y = u32x((o ^ x) | (o & x & inSet));
const no = hash32(u32x(rotl32(u32x(y + o), 31 & n) ^ rotl32(o, 31 & Math.imul(n, 7))) + 2654435769);
r[n] = no >>> 0;
st.acc = no;
return no >>> 0;
}
function keystream(seed, secondKey, len) {
const st = initStream(seed, secondKey);
const out = new Uint8Array(len);
let ctr = 0;
for (let i = 0; i < len;) {
const b = nextByte(st, ctr++);
out[i++] = 255 & b;
if (i < len) out[i++] = (b >>> 8) & 255;
if (i < len) out[i++] = (b >>> 16) & 255;
if (i < len) out[i++] = (b >>> 24) & 255;
}
return out;
}
function decryptPayload(payload, seed, secondKey) {
const b64 = payload.replace(/-/g, '+').replace(/_/g, '/').padEnd(4 * Math.ceil(payload.length / 4), '=');
const data = Buffer.from(b64, 'base64');
const ks = keystream(seed, secondKey, data.length);
for (let i = 0; i < data.length; i++) data[i] ^= ks[i];
for (let i = 0; i < MAGIC.length; i++) {
if (data[i] !== MAGIC[i]) throw new Error('Invalid encrypted payload');
}
return data.subarray(MAGIC.length).toString('utf8');
}
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: Fetch a decryption seed (cached server-side per mediaId, short TTL)
let seed;
try {
const seedRes = await axios.get(`${VIDEASY_API}/seed?mediaId=${tmdbId}`, {
headers: VIDEASY_HEADERS,
timeout: 8000
});
seed = seedRes.data && seedRes.data.seed;
} catch (err) {
console.error(`[Videasy] Seed fetch failed: ${err.message}`);
return [];
}
if (!seed) {
console.error('[Videasy] No seed returned.');
return [];
}
const queryServers = async (activeSeed) => {
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 === 'tv' ? 'TV Series' : 'Movie'}&year=${details.year}`
+ `&tmdbId=${tmdbId}&imdbId=${details.imdbId || ''}`;
if (mediaType === 'tv') apiUrl += `&seasonId=${seasonNum}&episodeId=${episodeNum}`;
apiUrl += `&enc=2&seed=${activeSeed}`;
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 payload locally with the seed cipher
const plain = decryptPayload(encryptedText, activeSeed, String(tmdbId));
const resData = JSON.parse(plain);
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
}
}));
};
// Step 4: Query each server in parallel; retry once with a fresh seed if nothing matched
await queryServers(seed);
if (allStreams.length === 0) {
try {
const seedRes = await axios.get(`${VIDEASY_API}/seed?mediaId=${tmdbId}`, {
headers: VIDEASY_HEADERS,
timeout: 8000
});
const freshSeed = seedRes.data && seedRes.data.seed;
if (freshSeed && freshSeed !== seed) await queryServers(freshSeed);
} catch {
// ignore retry failure
}
}
console.log(`[Videasy] Total streams: ${allStreams.length}`);
return allStreams;
}
module.exports = { getVideasyStreams };
+74
View File
@@ -0,0 +1,74 @@
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'
};
function qualityToNumber(quality) {
if (quality === '4k') return 2160;
if (quality === 'Auto' || quality === 'unknown') return 0;
const parsed = parseInt(quality, 10);
return isNaN(parsed) ? 0 : parsed;
}
function qualityLabel(quality) {
if (quality === '4k') return '4K';
if (quality === 'Auto' || quality === 'unknown') return 'Auto';
return `${quality}p`;
}
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 file-quality streams 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 streamData = apiRes.data && apiRes.data.stream;
if (!streamData || !streamData.qualities) {
console.log('[Vidlink] No quality streams in response.');
return [];
}
const streams = Object.entries(streamData.qualities)
.filter(([, entry]) => entry && entry.url)
.sort((a, b) => qualityToNumber(b[0]) - qualityToNumber(a[0]))
.map(([qualityKey, entry]) => ({
name: 'Vidlink',
title: `Vidlink - ${qualityLabel(qualityKey)}`,
url: entry.url,
quality: qualityKey,
provider: 'Vidlink',
headers: { 'Referer': 'https://vidlink.pro' }
}));
if (streams.length === 0) {
console.log('[Vidlink] No usable stream URLs in response.');
return [];
}
console.log(`[Vidlink] Got ${streams.length} stream(s).`);
return streams;
} catch (err) {
console.error(`[Vidlink] Error: ${err.message}`);
return [];
}
}
module.exports = { getVidlinkStreams };
-422
View File
@@ -1,422 +0,0 @@
#!/usr/bin/env node
const cheerio = require('cheerio');
// --- Constants ---
const VIDSRC_PROXY_URL = process.env.VIDSRC_PROXY_URL;
let BASEDOM = "https://cloudnestra.com"; // This can be updated by serversLoad
const SOURCE_URL = "https://vidsrc.xyz/embed";
// --- Helper: Conditional Proxied Fetch ---
// This function wraps the native fetch. If VIDSRC_PROXY_URL is set in the environment,
// it routes requests through the proxy. Otherwise, it makes a direct request.
async function fetchWrapper(url, options) {
if (VIDSRC_PROXY_URL) {
const proxiedUrl = `${VIDSRC_PROXY_URL}${encodeURIComponent(url)}`;
console.log(`[VidSrc Proxy] Fetching: ${url} via proxy`);
// Note: The proxy will handle the actual fetching, so we send the request to the proxy URL.
// We pass the original headers in the options, the proxy should forward them.
return fetch(proxiedUrl, options);
}
// If no proxy is set, fetch directly.
console.log(`[VidSrc Direct] Fetching: ${url}`);
return fetch(url, options);
}
// --- Helper Functions (copied and adapted from src/extractor.ts) ---
async function serversLoad(html) {
const $ = cheerio.load(html);
const servers = [];
const title = $("title").text() ?? "";
const baseFrameSrc = $("iframe").attr("src") ?? "";
if (baseFrameSrc) {
try {
const fullUrl = baseFrameSrc.startsWith("//") ? "https:" + baseFrameSrc : baseFrameSrc;
BASEDOM = new URL(fullUrl).origin;
}
catch (e) {
console.warn(`(Attempt 1) Failed to parse base URL from iframe src: ${baseFrameSrc} using new URL(), error: ${e.message}`);
// Attempt 2: Regex fallback for origin
const originMatch = (baseFrameSrc.startsWith("//") ? "https:" + baseFrameSrc : baseFrameSrc).match(/^(https?:\/\/[^/]+)/);
if (originMatch && originMatch[1]) {
BASEDOM = originMatch[1];
console.log(`(Attempt 2) Successfully extracted origin using regex: ${BASEDOM}`);
} else {
console.error(`(Attempt 2) Failed to extract origin using regex from: ${baseFrameSrc}. Using default: ${BASEDOM}`);
// Keep the default BASEDOM = "https://cloudnestra.com" if all fails
}
}
}
$(".serversList .server").each((index, element) => {
const server = $(element);
servers.push({
name: server.text().trim(),
dataHash: server.attr("data-hash") ?? null,
});
});
return {
servers: servers,
title: title,
};
}
async function parseMasterM3U8(m3u8Content, masterM3U8Url) {
const lines = m3u8Content.split('\n').map(line => line.trim());
const streams = [];
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith("#EXT-X-STREAM-INF:")) {
const infoLine = lines[i];
let quality = "unknown";
const resolutionMatch = infoLine.match(/RESOLUTION=(\d+x\d+)/);
if (resolutionMatch && resolutionMatch[1]) {
quality = resolutionMatch[1];
}
else {
const bandwidthMatch = infoLine.match(/BANDWIDTH=(\d+)/);
if (bandwidthMatch && bandwidthMatch[1]) {
quality = `${Math.round(parseInt(bandwidthMatch[1]) / 1000)}kbps`;
}
}
if (i + 1 < lines.length && lines[i + 1] && !lines[i + 1].startsWith("#")) {
const streamUrlPart = lines[i + 1];
try {
const fullStreamUrl = new URL(streamUrlPart, masterM3U8Url).href;
streams.push({ quality: quality, url: fullStreamUrl });
}
catch (e) {
console.error(`Error constructing URL for stream part: ${streamUrlPart} with base: ${masterM3U8Url}`, e);
streams.push({ quality: quality, url: streamUrlPart }); // Store partial URL as a fallback
}
i++;
}
}
}
// Sort streams by quality (highest first)
streams.sort((a, b) => {
// Extract resolution height from quality (e.g., "1280x720" -> 720)
const getHeight = (quality) => {
const match = quality.match(/(\d+)x(\d+)/);
return match ? parseInt(match[2], 10) : 0;
};
const heightA = getHeight(a.quality);
const heightB = getHeight(b.quality);
// Higher resolution comes first
return heightB - heightA;
});
return streams;
}
async function PRORCPhandler(prorcp) {
try {
const prorcpUrl = `${BASEDOM}/prorcp/${prorcp}`;
const prorcpFetch = await fetchWrapper(prorcpUrl, {
headers: {
"accept": "*/*", "accept-language": "en-US,en;q=0.9", "priority": "u=1",
"sec-ch-ua": "\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"",
"sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "script", "sec-fetch-mode": "no-cors", "sec-fetch-site": "same-origin",
'Sec-Fetch-Dest': 'iframe', "Referer": `${BASEDOM}/`, "Referrer-Policy": "origin",
},
timeout: 10000
});
if (!prorcpFetch.ok) {
console.error(`Failed to fetch prorcp: ${prorcpUrl}, status: ${prorcpFetch.status}`);
return null;
}
const prorcpResponse = await prorcpFetch.text();
const regex = /file:\s*'([^']*)'/gm;
const match = regex.exec(prorcpResponse);
if (match && match[1]) {
const masterM3U8Url = match[1];
const m3u8FileFetch = await fetchWrapper(masterM3U8Url, {
headers: { "Referer": prorcpUrl, "Accept": "*/*" },
timeout: 10000
});
if (!m3u8FileFetch.ok) {
console.error(`Failed to fetch master M3U8: ${masterM3U8Url}, status: ${m3u8FileFetch.status}`);
return null;
}
const m3u8Content = await m3u8FileFetch.text();
return parseMasterM3U8(m3u8Content, masterM3U8Url);
}
console.warn("No master M3U8 URL found in prorcp response for:", prorcpUrl);
return null;
}
catch (error) {
console.error(`Error in PRORCPhandler for ${BASEDOM}/prorcp/${prorcp}:`, error);
return null;
}
}
async function SRCRCPhandler(srcrcpPath, refererForSrcrcp) {
try {
const srcrcpUrl = BASEDOM + srcrcpPath;
console.log(`[VidSrc - SRCRCP] Fetching: ${srcrcpUrl} (Referer: ${refererForSrcrcp})`);
const response = await fetchWrapper(srcrcpUrl, {
headers: {
"accept": "*/*",
"accept-language": "en-US,en;q=0.9",
"sec-ch-ua": "\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "iframe",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "same-origin", // This might need to be 'cross-site' if BASEDOM's origin differs from srcrcp link's origin
"Referer": refererForSrcrcp,
"Referrer-Policy": "origin",
},
timeout: 10000
});
if (!response.ok) {
console.error(`[VidSrc - SRCRCP] Failed to fetch ${srcrcpUrl}, status: ${response.status}`);
return null;
}
const responseText = await response.text();
console.log(`[VidSrc - SRCRCP] Response from ${srcrcpUrl} (first 500 chars): ${responseText.substring(0, 500)}`);
// Attempt 1: Check for "file: '...'" like in PRORCP
const fileRegex = /file:\s*'([^']*)'/gm;
const fileMatch = fileRegex.exec(responseText);
if (fileMatch && fileMatch[1]) {
const masterM3U8Url = fileMatch[1];
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (via fileMatch): ${masterM3U8Url}`);
const m3u8FileFetch = await fetchWrapper(masterM3U8Url, {
headers: { "Referer": srcrcpUrl, "Accept": "*/*" },
timeout: 10000
});
if (!m3u8FileFetch.ok) {
console.error(`[VidSrc - SRCRCP] Failed to fetch master M3U8: ${masterM3U8Url}, status: ${m3u8FileFetch.status}`);
return null;
}
const m3u8Content = await m3u8FileFetch.text();
return parseMasterM3U8(m3u8Content, masterM3U8Url);
}
// Attempt 2: Check if the responseText itself is an M3U8 playlist
if (responseText.trim().startsWith("#EXTM3U")) {
console.log(`[VidSrc - SRCRCP] Response from ${srcrcpUrl} appears to be an M3U8 playlist directly.`);
return parseMasterM3U8(responseText, srcrcpUrl);
}
// Attempt 3: Look for sources = [...] or sources: [...] in script tags or in JSON-like structures
const $ = cheerio.load(responseText);
let sourcesFound = null;
$('script').each((i, script) => {
const scriptContent = $(script).html();
if (scriptContent) {
// Regex for various ways sources might be defined
const sourcesRegexes = [
/sources\s*[:=]\s*(\[[^\]]*\{(?:\s*|.*?)file\s*:\s*['"]([^'"]+)['"](?:\s*|.*?)\}[^\]]*\])/si, // extracts the URL from sources: [{file: "URL"}]
/playerInstance\.setup\s*\(\s*\{\s*sources\s*:\s*(\[[^\]]*\{(?:\s*|.*?)file\s*:\s*['"]([^'"]+)['"](?:\s*|.*?)\}[^\]]*\])/si, // for playerInstance.setup({sources: [{file: "URL"}]})
/file\s*:\s*['"]([^'"]+\.m3u8[^'"]*)['"]/i, // Direct M3U8 link in a var or object e.g. file: "URL.m3u8"
/src\s*:\s*['"]([^'"]+\.m3u8[^'"]*)['"]/i, // Direct M3U8 link e.g. src: "URL.m3u8"
/loadSource\(['"]([^'"]+\.m3u8[^'"]*)['"]\)/i, // For .loadSource("URL.m3u8")
/new\s+Player\([^)]*\{\s*src\s*:\s*['"]([^'"]+)['"]\s*\}\s*\)/i // For new Player({src: "URL"})
];
for (const regex of sourcesRegexes) {
const sourcesMatch = scriptContent.match(regex);
// For regexes that capture a JSON array-like structure in group 1 and then the URL in group 2 (first two regexes)
if (regex.source.includes('file') && regex.source.includes('sources')) { // Heuristic to identify complex sources regex capturing group 2
if (sourcesMatch && sourcesMatch[2]) { // URL is in group 2
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (script complex): ${sourcesMatch[2]}`);
sourcesFound = [{ quality: 'default', url: sourcesMatch[2] }];
return false; // break cheerio loop
}
}
// For simpler regexes where URL is in group 1
else if (sourcesMatch && sourcesMatch[1]) {
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (script simple): ${sourcesMatch[1]}`);
sourcesFound = [{ quality: 'default', url: sourcesMatch[1] }];
return false; // break cheerio loop
}
}
// Fallback: Look for any absolute .m3u8 URL within the script tag
if (!sourcesFound) {
const m3u8GenericMatch = scriptContent.match(/['"](https?:\/\/[^'"\s]+\.m3u8[^'"\s]*)['"]/i);
if (m3u8GenericMatch && m3u8GenericMatch[1]) {
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (script generic fallback): ${m3u8GenericMatch[1]}`);
sourcesFound = [{ quality: 'default', url: m3u8GenericMatch[1] }];
return false; // break cheerio loop
}
}
}
});
if (sourcesFound && sourcesFound.length > 0) {
// Process the first valid M3U8 URL found, or return direct links
const m3u8Source = sourcesFound.find(s => s.url && s.url.includes('.m3u8'));
if (m3u8Source) {
console.log(`[VidSrc - SRCRCP] First M3U8 source from script: ${m3u8Source.url}`);
// Ensure URL is absolute
const absoluteM3u8Url = m3u8Source.url.startsWith('http') ? m3u8Source.url : new URL(m3u8Source.url, srcrcpUrl).href;
const m3u8FileFetch = await fetchWrapper(absoluteM3u8Url, {
headers: { "Referer": srcrcpUrl, "Accept": "*/*" },
timeout: 10000
});
if (!m3u8FileFetch.ok) {
console.error(`[VidSrc - SRCRCP] Failed to fetch M3U8 from script source: ${absoluteM3u8Url}, status: ${m3u8FileFetch.status}`);
return null;
}
const m3u8Content = await m3u8FileFetch.text();
return parseMasterM3U8(m3u8Content, absoluteM3u8Url);
} else {
// Assuming direct links if no .m3u8 found in the sources array
console.log(`[VidSrc - SRCRCP] Assuming direct links from script sources:`, sourcesFound);
return sourcesFound.map(s => ({
quality: s.quality || s.label || 'auto',
url: s.url.startsWith('http') ? s.url : new URL(s.url, srcrcpUrl).href
}));
}
}
console.warn(`[VidSrc - SRCRCP] No stream extraction method succeeded for ${srcrcpUrl}`);
return null;
} catch (error) {
console.error(`[VidSrc - SRCRCP] Error in SRCRCPhandler for ${srcrcpPath}:`, error);
return null;
}
}
async function rcpGrabber(html) {
const regex = /src:\s*'([^']*)'/;
const match = html.match(regex);
if (!match || !match[1])
return null;
return { metadata: { image: "" }, data: match[1] };
}
function getObject(id) {
const arr = id.split(':');
return { id: arr[0], season: arr[1], episode: arr[2] };
}
function getUrl(id, type) {
if (type === "movie") {
return `${SOURCE_URL}/movie/${id}`;
}
else {
const obj = getObject(id);
return `${SOURCE_URL}/tv/${obj.id}/${obj.season}-${obj.episode}`;
}
}
async function getStreamContent(id, type) {
const url = getUrl(id, type);
const embedRes = await fetchWrapper(url, { headers: { "Referer": SOURCE_URL } });
if (!embedRes.ok) {
console.error(`Failed to fetch embed page ${url}: ${embedRes.status}`);
return [];
}
const embedResp = await embedRes.text();
const { servers, title } = await serversLoad(embedResp);
const apiResponse = [];
// MODIFIED: Process servers in parallel
const serverPromises = servers.map(async (server) => {
if (!server.dataHash) return null; // Skip servers without dataHash
try {
const rcpUrl = `${BASEDOM}/rcp/${server.dataHash}`;
const rcpRes = await fetchWrapper(rcpUrl, {
headers: { 'Sec-Fetch-Dest': 'iframe', "Referer": url }
});
if (!rcpRes.ok) {
console.warn(`RCP fetch failed for server ${server.name}: ${rcpRes.status}`);
return null; // Failed to fetch RCP
}
const rcpHtml = await rcpRes.text();
const rcpData = await rcpGrabber(rcpHtml);
if (!rcpData || !rcpData.data) {
console.warn(`Skipping server ${server.name} due to missing rcp data.`);
return null; // Missing RCP data
}
let streamDetails = null;
if (rcpData.data.startsWith("/prorcp/")) {
streamDetails = await PRORCPhandler(rcpData.data.replace("/prorcp/", ""));
} else if (rcpData.data.startsWith("/srcrcp/")) {
if (server.name === "Superembed" || server.name === "2Embed") {
console.warn(`[VidSrc] Skipping SRCRCP for known problematic server: ${server.name}`);
return null;
}
streamDetails = await SRCRCPhandler(rcpData.data, rcpUrl); // Pass rcpUrl as referer
} else {
console.warn(`Unhandled rcp data type for server ${server.name}: ${rcpData.data.substring(0, 50)}`);
return null; // Unhandled type
}
if (streamDetails && streamDetails.length > 0) {
return {
name: title,
image: rcpData.metadata.image,
mediaId: id,
streams: streamDetails,
referer: BASEDOM,
};
} else {
console.warn(`No stream details from handler for server ${server.name} (${rcpData.data})`);
return null; // No stream details found by the handler
}
} catch (e) {
console.error(`Error processing server ${server.name} (${server.dataHash}):`, e);
return null; // Error during server processing
}
});
const results = await Promise.all(serverPromises);
// Filter out null results (failed servers or no streams) and add valid results to apiResponse
results.forEach(result => {
if (result) {
apiResponse.push(result);
}
});
// END MODIFICATION
return apiResponse;
}
// --- Main execution logic (conditional) ---
async function main() {
const args = process.argv.slice(2);
if (args.length < 2) {
console.error("Usage: node vidsrcextractor.js <id> <type>");
console.error("Example (movie): node vidsrcextractor.js tt0111161 movie");
console.error("Example (series): node vidsrcextractor.js tt0944947:1:1 series"); // Game of Thrones S1E1
return 1; // error code
}
const id = args[0];
const type = args[1];
if (type !== "movie" && type !== "series") {
console.error("Invalid type. Must be 'movie' or 'series'.");
return 1;
}
if (type === "series" && id.split(':').length < 3) {
console.error("Invalid series ID format. Expected 'ttID:season:episode' (e.g., tt0944947:1:1).");
return 1;
}
console.log(`Fetching streams for ID: ${id}, Type: ${type}`);
try {
const results = await getStreamContent(id, type);
if (results && results.length > 0) {
console.log("Extracted Data:");
console.log(JSON.stringify(results, null, 2));
} else {
console.log("No streams found or an error occurred.");
}
return 0;
} catch (error) {
console.error("Error during extraction process:", error);
return 1;
}
}
// Export the function for use in other modules
module.exports = { getStreamContent };
// Run main only if the script is executed directly
if (require.main === module) {
main()
.then(code => {
if (code !== 0) {
console.error(`[vidsrcextractor] Finished with non-zero code ${code}`);
}
})
.catch(error => {
console.error("Unhandled error in main execution:", error);
});
}
+143 -178
View File
@@ -1,215 +1,180 @@
/** const axios = require('axios');
* Vixsrc streaming provider integration (standalone)
* React Native compatible variant - no external addon framework
*/
// Constants
const TMDB_API_KEY = "68e094699525b18a70bab2f86b1fa706";
const BASE_URL = 'https://vixsrc.to'; const BASE_URL = 'https://vixsrc.to';
const VIXSRC_HEADERS = {
// Helper function to make HTTP requests with default headers 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150 Safari/537.36',
function makeRequest(url, options = {}) { 'Accept': 'application/json, text/javascript, */*; q=0.01',
const defaultHeaders = { 'Accept-Language': 'en-US,en;q=0.9',
'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', 'Referer': BASE_URL,
'Accept': 'application/json,*/*', 'Origin': BASE_URL
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
...options.headers
}; };
return fetch(url, { // Step 1: GET /api/movie/{id} or /api/tv/{id}/{s}/{e} → { src: "/embed/..." }
method: options.method || 'GET', async function fetchApi(url) {
headers: defaultHeaders, try {
...options const response = await axios.get(url, { headers: VIXSRC_HEADERS, timeout: 10000 });
}) if (response.status !== 200 || !response.data) return null;
.then(response => { return response.data;
if (!response.ok) { } catch {
throw new Error(`HTTP ${response.status}: ${response.statusText}`); return null;
} }
return response; }
})
.catch(error => { // Step 2: GET BASE_URL + sublink.src → HTML embed page
console.error(`[Vixsrc] Request failed for ${url}: ${error.message}`); async function fetchEmbedPage(suburl) {
throw error; try {
const response = await axios.get(BASE_URL + suburl, {
headers: { ...VIXSRC_HEADERS, Accept: 'text/html,application/xhtml+xml,*/*' },
timeout: 10000,
responseType: 'text'
}); });
} if (response.status !== 200) return null;
return response.data;
// Helper function to get TMDB info } catch {
function getTmdbInfo(tmdbId, mediaType) { return null;
const url = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`;
return makeRequest(url)
.then(response => response.json())
.then(data => ({ data }));
}
// 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) {
let vixsrcUrl;
let subtitleApiUrl;
if (contentType === 'movie') {
vixsrcUrl = `${BASE_URL}/movie/${contentId}`;
subtitleApiUrl = `https://sub.wyzie.ru/search?id=${contentId}`;
} else {
vixsrcUrl = `${BASE_URL}/tv/${contentId}/${seasonNum}/${episodeNum}`;
subtitleApiUrl = `https://sub.wyzie.ru/search?id=${contentId}&season=${seasonNum}&episode=${episodeNum}`;
}
console.log(`[Vixsrc] Fetching: ${vixsrcUrl}`);
return makeRequest(vixsrcUrl, {
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
}
})
.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 // Step 3: Extract token, expires, playlist URL from embed HTML
if (!masterPlaylistUrl) { function extractTokenData(html) {
const m3u8Match = html.match(/(https?:\/\/[^'"\s]+\.m3u8[^'"\s]*)/); const token = html.match(/token["']\s*:\s*["']([^"']+)/)?.[1];
if (m3u8Match) { const expires = html.match(/expires["']\s*:\s*["']([^"']+)/)?.[1];
masterPlaylistUrl = m3u8Match[1]; const playlist = html.match(/url\s*:\s*["']([^"']+)/)?.[1];
console.log('[Vixsrc] Found direct .m3u8 URL:', masterPlaylistUrl);
}
}
// Method 3: Look for stream URLs in script tags if (!token || !expires || !playlist) return null;
if (!masterPlaylistUrl) {
const scriptMatches = html.match(/<script[^>]*>(.*?)<\/script>/gs);
if (scriptMatches) {
for (const script of scriptMatches) {
const streamMatch = script.match(/['"]?(https?:\/\/[^'"\s]+(?:\.m3u8|playlist)[^'"\s]*)/);
if (streamMatch) {
masterPlaylistUrl = streamMatch[1];
console.log('[Vixsrc] Found stream in script:', masterPlaylistUrl);
break;
}
}
}
}
if (!masterPlaylistUrl) { // Reject expired tokens (with 60s grace period)
console.log('[Vixsrc] No master playlist URL found'); if (parseInt(expires, 10) * 1000 - 60_000 < Date.now()) {
console.log('[Vixsrc] Token is expired');
return null; return null;
} }
return { masterPlaylistUrl, subtitleApiUrl }; return { token, expires, playlist };
}
// 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;
}
} }
// Helper function to get subtitles // Step 6: Parse HLS manifest for quality variants, audio tracks, subtitles
function getSubtitles(subtitleApiUrl) { function parsePlaylist(content, masterUrl, pageApiUrl) {
return makeRequest(subtitleApiUrl) const sources = [];
.then(response => response.json()) const subtitles = [];
.then(subtitleData => { const audioTracks = [];
// Find English subtitle track (same logic as original)
let subtitleTrack = subtitleData.find(track =>
track.display.includes('English') && (track.encoding === 'ASCII' || track.encoding === 'UTF-8')
);
if (!subtitleTrack) { const lines = content.split('\n');
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1252');
// 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 });
} }
if (!subtitleTrack) { // Subtitles
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1250'); 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' });
} }
if (!subtitleTrack) { // Quality variants — find the highest resolution
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP850'); 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;
} }
const subtitles = subtitleTrack ? subtitleTrack.url : ''; if (bestResolution === 0) return { sources: [], subtitles: [] };
console.log(subtitles ? `[Vixsrc] Found subtitles: ${subtitles}` : '[Vixsrc] No English subtitles found');
return subtitles; sources.push({
}) name: `Vixsrc - ${bestResolution}p`,
.catch(error => { title: `Vixsrc - ${bestResolution}p`,
console.log('[Vixsrc] Subtitle fetch failed:', error.message); url: masterUrl,
return ''; quality: `${bestResolution}p`,
provider: 'Vixsrc',
headers: {
'Referer': pageApiUrl,
'User-Agent': VIXSRC_HEADERS['User-Agent']
}
}); });
return { sources, subtitles };
} }
// Main function to get streams async function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[Vixsrc] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`); console.log(`[Vixsrc] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
return getTmdbInfo(tmdbId, mediaType) // Build API URL
.then(() => extractStreamFromPage(null, mediaType, tmdbId, seasonNum, episodeNum)) let apiUrl;
.then(streamData => { if (mediaType === 'movie') {
if (!streamData) { apiUrl = `${BASE_URL}/api/movie/${tmdbId}`;
console.log('[Vixsrc] No stream data found'); } else {
apiUrl = `${BASE_URL}/api/tv/${tmdbId}/${seasonNum}/${episodeNum}`;
}
console.log(`[Vixsrc] Step 1 - Calling API: ${apiUrl}`);
const apiData = await fetchApi(apiUrl);
if (!apiData || !apiData.src) {
console.log('[Vixsrc] No src returned from API');
return [];
}
console.log(`[Vixsrc] Step 2 - Fetching embed page: ${BASE_URL}${apiData.src}`);
const html = await fetchEmbedPage(apiData.src);
if (!html) {
console.log('[Vixsrc] Failed to fetch embed page');
return [];
}
console.log(`[Vixsrc] Embed HTML length: ${html.length} characters`);
const tokenData = extractTokenData(html);
if (!tokenData) {
console.log('[Vixsrc] Could not extract token/expires/playlist from embed HTML');
return []; return [];
} }
const { masterPlaylistUrl, subtitleApiUrl } = streamData; const masterUrl = buildMasterUrl(tokenData);
console.log(`[Vixsrc] Step 3 - Master URL: ${masterUrl}`);
// Return single master playlist with Auto quality const playlistContent = await fetchPlaylist(masterUrl, apiUrl);
console.log('[Vixsrc] Returning master playlist with Auto quality...'); if (!playlistContent) {
console.log('[Vixsrc] Failed to fetch HLS playlist');
// Get subtitles
return getSubtitles(subtitleApiUrl)
.then(subtitles => {
// Return single stream with master playlist
const tmdbStreams = [{
name: "Vixsrc",
title: "Auto Quality Stream",
url: masterPlaylistUrl,
quality: '1080p',
type: 'direct',
headers: {
'Referer': BASE_URL,
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
}
}];
console.log('[Vixsrc] Successfully processed 1 stream with Auto quality');
return tmdbStreams;
});
})
.catch(error => {
console.error(`[Vixsrc] Error in getVixsrcStreams: ${error.message}`);
return []; return [];
}); }
const { sources, subtitles } = parsePlaylist(playlistContent, masterUrl, apiUrl);
if (sources.length === 0) {
console.log('[Vixsrc] No streams found in HLS playlist');
return [];
}
console.log(`[Vixsrc] Successfully extracted ${sources.length} stream(s). Subtitles: ${subtitles.length}`);
return sources;
} }
module.exports = { getVixsrcStreams }; module.exports = { getVixsrcStreams };
-166
View File
@@ -1,166 +0,0 @@
// xprime.js (simplified & cleaned)
require('dotenv').config();
const fs = require('fs').promises;
const path = require('path');
const crypto = require('crypto');
const XPRIME_PROXY_URL = process.env.XPRIME_PROXY_URL || null;
const RETRY_DELAY_MS_XPRIME = 1000;
const MAX_RETRIES_XPRIME = 3;
// Cache directory (Vercel ephemeral vs local)
const CACHE_DIR = process.env.VERCEL ? path.join('/tmp', '.cache') : path.join(__dirname, '.cache');
async function ensureCacheDir(dirPath) {
try { await fs.mkdir(dirPath, { recursive: true }); } catch (_) {
// swallow errors creating cache dir (ephemeral env or permission)
}
}
async function getFromCache(cacheKey, subDir = '') {
if (process.env.DISABLE_CACHE === 'true') return null;
try {
const fullDir = path.join(CACHE_DIR, subDir);
const filePath = path.join(fullDir, cacheKey);
const data = await fs.readFile(filePath, 'utf8');
return data;
} catch (_) { return null; }
}
async function saveToCache(cacheKey, data, subDir = '') {
if (process.env.DISABLE_CACHE === 'true') return;
try {
const fullDir = path.join(CACHE_DIR, subDir);
await fs.mkdir(fullDir, { recursive: true });
const filePath = path.join(fullDir, cacheKey);
await fs.writeFile(filePath, data, 'utf8');
} catch (_) {
// ignore cache write errors (non-fatal)
}
}
const BROWSER_HEADERS_XPRIME = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
'Accept': '*/*',
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Sec-Ch-Ua': '"Chromium";v="136", "Google Chrome";v="136", "Not.A/Brand";v="99"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
'Connection': 'keep-alive'
};
async function fetchWithRetry(url, options, maxRetries = MAX_RETRIES_XPRIME) {
const { default: fetch } = await import('node-fetch');
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res;
} catch (err) {
lastError = err;
if (attempt < maxRetries) {
await new Promise(r => setTimeout(r, RETRY_DELAY_MS_XPRIME * Math.pow(2, attempt - 1)));
}
}
}
throw lastError;
}
async function fetchStreamSize(url) {
const subDir = 'xprime_stream_sizes';
const hash = crypto.createHash('md5').update(url).digest('hex');
const key = `${hash}.txt`;
const cached = await getFromCache(key, subDir);
if (cached) return cached;
if (url.toLowerCase().includes('.m3u8')) {
await saveToCache(key, 'Playlist (size N/A)', subDir);
return 'Playlist (size N/A)';
}
try {
const { default: fetch } = await import('node-fetch');
const controller = new AbortController();
const t = setTimeout(() => controller.abort(), 5000);
let size = 'Unknown size';
try {
const head = await fetch(url, { method: 'HEAD', signal: controller.signal });
const cl = head.headers.get('content-length');
if (cl) {
const bytes = parseInt(cl, 10);
if (!isNaN(bytes)) {
if (bytes < 1024) size = `${bytes} B`;
else if (bytes < 1024 * 1024) size = `${(bytes / 1024).toFixed(2)} KB`;
else if (bytes < 1024 * 1024 * 1024) size = `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
else size = `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
}
} finally { clearTimeout(t); }
await saveToCache(key, size, subDir);
return size;
} catch (_) {
await saveToCache(key, 'Unknown size', subDir);
return 'Unknown size';
}
}
async function getXprimeStreams(title, year, type, seasonNum, episodeNum, useProxy = true) {
if (!title || !year) return [];
const encoded = encodeURIComponent(title);
let apiUrl;
if (type === 'movie') {
apiUrl = `https://backend.xprime.tv/primebox?name=${encoded}&year=${year}&fallback_year=${year}`;
} else if (type === 'tv') {
if (seasonNum == null || episodeNum == null) return [];
apiUrl = `https://backend.xprime.tv/primebox?name=${encoded}&year=${year}&fallback_year=${year}&season=${seasonNum}&episode=${episodeNum}`;
} else return [];
console.log(`[Xprime.tv] Fetch attempt '${title}' (${year}) type=${type} useProxy=${useProxy}`);
let resJson;
try {
if (useProxy && XPRIME_PROXY_URL) {
const base = XPRIME_PROXY_URL.replace(/\/$/, '');
const proxied = `${base}/?destination=${encodeURIComponent(apiUrl)}`;
const r = await fetchWithRetry(proxied, { headers: BROWSER_HEADERS_XPRIME });
resJson = await r.json();
} else {
const r = await fetchWithRetry(apiUrl, { headers: BROWSER_HEADERS_XPRIME });
resJson = await r.json();
}
} catch (e) {
console.error('[Xprime.tv] Fetch failed:', e.message);
return [];
}
const streams = [];
const processItem = (item) => {
if (!item || item.error || !item.streams) return;
Object.entries(item.streams).forEach(([quality, url]) => {
if (!url || typeof url !== 'string') return;
streams.push({
url,
quality: quality || 'Unknown',
title: `${title} - ${type === 'tv' ? `S${String(seasonNum).padStart(2,'0')}E${String(episodeNum).padStart(2,'0')} ` : ''}${quality}`,
provider: 'Xprime.tv',
codecs: [],
size: 'Unknown size'
});
});
};
if (Array.isArray(resJson)) resJson.forEach(processItem); else processItem(resJson);
if (streams.length) {
console.time('[Xprime.tv] Fetch stream sizes');
await Promise.all(streams.map(async s => { s.size = await fetchStreamSize(s.url); }));
console.timeEnd('[Xprime.tv] Fetch stream sizes');
}
console.log(`[Xprime.tv] Returning ${streams.length} streams.`);
return streams;
}
ensureCacheDir(CACHE_DIR).catch(()=>{});
module.exports = { getXprimeStreams };
+309
View File
@@ -0,0 +1,309 @@
const { createHash } = require('crypto');
const { getDetails, resolveImdbId } = require('../utils/tmdb');
const PORTALS = ['https://zxcstream.xyz', 'https://zxcprime.xyz'];
const INITIAL_BASE = 'https://r1.zxcstream.xyz';
const SALT = '3435443433';
const SERVERS = ['icarus', 'berkas', 'orion', 'athena'];
const BASE_TTL = 10 * 60 * 1000;
const PROBE_SUBDOMAINS = ['r1', 'r2', 'r3', 'r4', 'r5', 'r6', 'v4', 'cdn', 'api', 'stream'];
const F = {
id: 'rgrwsdsdfgwrwrwwr',
fToken: 'xfgdfgdsffgrwgrwyjhkjt',
ts: 'rdghhdghhfssft',
token: 'ZDDVHJFGHYRHG',
title: 'TUKTHFSSFGDGHJS',
year: '53653TRFG647GF',
season: 'adkljfhdahfladhfjahfjlahfhfljkadfdf',
episode: '546745ygy46ytfgty',
imdbId: '564745ygtuy5yi75yuy'
};
const COMMON_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0 Safari/537.36',
'Accept': 'application/json, text/plain, */*'
};
let _base = INITIAL_BASE;
let _baseValidatedAt = 0;
let discoveryPromise = null;
function sha512Hex(data) {
return createHash('sha512').update(data).digest('hex');
}
async function verifyBase(base) {
const rt = Date.now();
const xt = sha512Hex(`${rt}:${SALT}:550`).slice(0, 64);
const r = await fetch(`${base}/backend/token`, {
method: 'POST',
headers: {
...COMMON_HEADERS,
Origin: base,
'Content-Type': 'application/json',
Referer: `${base}/player/movie/550`
},
body: JSON.stringify({ [F.id]: '550', [F.fToken]: xt, [F.ts]: rt }),
signal: AbortSignal.timeout(6000)
});
if (r.ok) {
const d = await r.json();
if (d[F.token]) return base;
}
throw new Error(`verify failed: ${r.status}`);
}
async function tryPortal(portal) {
const r = await fetch(portal, {
headers: { 'User-Agent': COMMON_HEADERS['User-Agent'] },
redirect: 'follow',
signal: AbortSignal.timeout(10000)
});
const redirectedBase = new URL(r.url).origin;
if (redirectedBase === new URL(portal).origin) {
throw new Error(`portal ${portal} did not redirect`);
}
return await verifyBase(redirectedBase);
}
async function probeSubdomain(sub) {
return verifyBase(`https://${sub}.zxcstream.xyz`);
}
async function discoverBase() {
const portalResults = await Promise.allSettled(PORTALS.map((portal) => tryPortal(portal)));
for (const r of portalResults) {
if (r.status === 'fulfilled') {
console.log(`[ZXCStreams] discovered base via portal redirect: ${r.value}`);
return r.value;
}
}
console.warn('[ZXCStreams] both portals failed, falling back to subdomain probing');
const settled = await Promise.allSettled(PROBE_SUBDOMAINS.map((sub) => probeSubdomain(sub)));
for (const r of settled) {
if (r.status === 'fulfilled') {
console.log(`[ZXCStreams] discovered base via subdomain probe: ${r.value}`);
return r.value;
}
}
console.warn('[ZXCStreams] all discovery methods failed, keeping last known base:', _base);
return _base;
}
async function getBase() {
if (Date.now() - _baseValidatedAt <= BASE_TTL) {
return _base;
}
if (!discoveryPromise) {
discoveryPromise = discoverBase()
.then((base) => {
_base = base;
_baseValidatedAt = Date.now();
return base;
})
.finally(() => {
discoveryPromise = null;
});
}
return discoveryPromise;
}
function invalidateBase() {
_baseValidatedAt = 0;
}
function generateFrontendToken(tmdbId) {
const rt = Date.now();
const xt = sha512Hex(`${rt}:${SALT}:${tmdbId}`).slice(0, 64);
return { xt, rt };
}
async function requestServerToken(base, tmdbId, referer) {
const { xt, rt } = generateFrontendToken(tmdbId);
const body = JSON.stringify({
[F.id]: tmdbId,
[F.fToken]: xt,
[F.ts]: rt
});
const res = await fetch(`${base}/backend/token`, {
method: 'POST',
headers: {
...COMMON_HEADERS,
Origin: base,
'Content-Type': 'application/json',
Referer: referer
},
body
});
if (!res.ok) throw new Error(`token failed ${res.status}`);
const data = await res.json();
return { serverToken: data[F.token], serverTs: data[F.ts], xt };
}
async function fetchServer(server, meta, type, season, episode) {
let base = await getBase();
const buildReferer = (b) =>
`${b}/player/${type}/${meta.tmdbId}${season != null ? `/${season}/${episode}` : ''}`;
let referer = buildReferer(base);
let tokenData;
try {
tokenData = await requestServerToken(base, meta.tmdbId, referer);
} catch (err) {
console.warn(`[ZXCStreams] token request failed on ${base}, re-discovering...`, err.message);
invalidateBase();
base = await getBase();
referer = buildReferer(base);
tokenData = await requestServerToken(base, meta.tmdbId, referer);
}
const { serverToken, serverTs, xt } = tokenData;
const params = {
[F.id]: meta.tmdbId,
b: type,
[F.ts]: String(serverTs),
[F.token]: serverToken,
[F.fToken]: xt,
[F.title]: meta.title,
[F.year]: meta.year,
date: meta.releaseDate,
[F.imdbId]: meta.imdbId
};
if (season != null && episode != null) {
params[F.season] = String(season);
params[F.episode] = String(episode);
}
const qs = new URLSearchParams(params).toString();
const res = await fetch(`${base}/backend_/servers/${server}?${qs}`, {
headers: { ...COMMON_HEADERS, Origin: base, Referer: referer }
});
if (!res.ok) return [];
const data = await res.json();
if (!data.success || !Array.isArray(data.links)) return [];
const requestHeaders = {
Referer: referer,
Origin: base,
'User-Agent': COMMON_HEADERS['User-Agent']
};
return data.links
.filter((l) => l.link)
.map((l) => ({
server,
type: l.type || (l.link.includes('.m3u8') ? 'hls' : 'mp4'),
resolution: l.resolution ?? (l.source && l.source !== 'default' ? l.source : undefined) ?? '?',
size: l.size,
url: l.link,
requestHeaders
}));
}
async function getAllStreams(type, meta, season, episode) {
const results = await Promise.allSettled(
SERVERS.map((s) => fetchServer(s, meta, type, season, episode))
);
const streams = [];
for (const r of results) {
if (r.status === 'fulfilled') streams.push(...r.value);
}
return streams;
}
function resolutionLabel(server, res) {
if (typeof res === 'number' && res <= 4) {
return ['360p', '480p', '720p', '1080p', '4K'][res] ?? `q${res}`;
}
return typeof res === 'number' ? `${res}p` : String(res);
}
function formatSize(bytes) {
if (!bytes) return '';
const n = Number(bytes);
if (!Number.isFinite(n)) return '';
if (n > 1e9) return `${(n / 1e9).toFixed(2)} GB`;
if (n > 1e6) return `${(n / 1e6).toFixed(0)} MB`;
return `${(n / 1e3).toFixed(0)} KB`;
}
async function getZxcstreamsStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[ZXCStreams] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
try {
const type = mediaType === 'tv' ? 'tv' : 'movie';
const tmdbType = mediaType === 'tv' ? 'tv' : 'movie';
const details = await getDetails(tmdbType, tmdbId);
const title = (details && (details.title || details.name)) || '';
if (!title) {
console.log(`[ZXCStreams] No TMDB title resolved for ${tmdbId}`);
return [];
}
const releaseDate = (details && (details.release_date || details.first_air_date || '').slice(0, 10)) || '';
const year = (details && (details.release_date || details.first_air_date || '').slice(0, 4)) || '';
const imdbId = await resolveImdbId(tmdbType, tmdbId);
if (!imdbId || !imdbId.startsWith('tt')) {
console.log(`[ZXCStreams] No IMDB ID resolved for TMDB ${tmdbId}`);
return [];
}
const meta = {
tmdbId: String(tmdbId),
title,
year,
releaseDate,
imdbId
};
const links = await getAllStreams(type, meta, seasonNum, episodeNum);
if (!links.length) {
console.log(`[ZXCStreams] No streams found for "${title}"`);
return [];
}
const scored = links.map((l) => {
const r = typeof l.resolution === 'number' ? l.resolution : 0;
const height = r > 4 ? r : ([240, 480, 720, 1080, 2160][r] ?? 0);
return { l, score: (l.type === 'mp4' ? 10000 : 0) + height };
});
scored.sort((a, b) => b.score - a.score);
const streams = [];
for (const { l } of scored) {
const label = resolutionLabel(l.server, l.resolution);
const size = formatSize(l.size);
const kind = l.type === 'hls' ? 'HLS' : 'MP4';
const serverName =
l.server === 'icarus' ? 'Icarus' :
l.server === 'orion' ? 'Orion' :
l.server === 'athena' ? 'Athena' : 'Berkas';
const titleLine = [`${serverName} • ${label} • ${kind}`, size].filter(Boolean).join('\n');
streams.push({
name: `ZXCStreams ${serverName} ${label}`,
title: titleLine,
url: l.url,
quality: label,
provider: 'ZXCStreams',
headers: {
'Referer': l.requestHeaders.Referer,
'Origin': l.requestHeaders.Origin,
'User-Agent': l.requestHeaders['User-Agent']
}
});
}
console.log(`[ZXCStreams] Got ${streams.length} stream(s) for "${title}"`);
return streams;
} catch (err) {
console.error(`[ZXCStreams] Error: ${err.message}`);
return [];
}
}
module.exports = { getZxcstreamsStreams };
+453
View File
@@ -0,0 +1,453 @@
// Lightweight proxy/m3u8/segment/subtitle rewriting layer. thanks to https://github.com/cinepro-org/ for the original code.
// Adapted from external project (user supplied) to fit CommonJS style and existing config system.
// Provides conditional route registration via enableProxy flag.
const cors = require('cors');
const fetch = require('node-fetch');
const CACHE_MAX_SIZE = 2000;
const CACHE_EXPIRY_MS = 2 * 60 * 60 * 1000; // 2 hours
const segmentCache = new Map();
// Track first open-ended (bytes=0-) range per target to clamp only once per TTL window
const openRangeClampMap = new Map();
const OPEN_RANGE_CLAMP_TTL_MS = 5 * 60 * 1000; // 5 minutes
// Progressive open-ended growth tracking: keeps expanding bytes=0- served window.
const progressiveOpenMap = new Map(); // url -> { lastEnd }
// Tail prefetch cache: stores last N KB for quick tail range responses
const tailPrefetchMap = new Map(); // url -> { data: Buffer, start: number, end: number, size: number, ts: number }
const TAIL_PREFETCH_TTL_MS = 10 * 60 * 1000; // 10 minutes
function isCacheDisabled() {
return process.env.DISABLE_CACHE === 'true';
}
function cleanupCache() {
const now = Date.now();
for (const [url, entry] of segmentCache.entries()) {
if (now - entry.timestamp > CACHE_EXPIRY_MS) segmentCache.delete(url);
}
if (segmentCache.size > CACHE_MAX_SIZE) {
const entries = Array.from(segmentCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp);
const toRemove = entries.slice(0, segmentCache.size - CACHE_MAX_SIZE);
toRemove.forEach(([u]) => segmentCache.delete(u));
}
return segmentCache.size;
}
setInterval(cleanupCache, 30 * 60 * 1000).unref();
function cleanupClampMap() {
const now = Date.now();
for (const [url, ts] of openRangeClampMap.entries()) {
if (now - ts > OPEN_RANGE_CLAMP_TTL_MS) openRangeClampMap.delete(url);
}
}
setInterval(cleanupClampMap, 10 * 60 * 1000).unref();
function cleanupTailPrefetch() {
const now = Date.now();
for (const [url, entry] of tailPrefetchMap.entries()) {
if (now - entry.ts > TAIL_PREFETCH_TTL_MS) tailPrefetchMap.delete(url);
}
}
setInterval(cleanupTailPrefetch, 15 * 60 * 1000).unref();
function getCachedSegment(url) {
if (isCacheDisabled()) return null;
const e = segmentCache.get(url);
if (!e) return null;
if (Date.now() - e.timestamp > CACHE_EXPIRY_MS) { segmentCache.delete(url); return null; }
return e;
}
async function prefetchSegment(url, headers) {
if (isCacheDisabled() || segmentCache.size >= CACHE_MAX_SIZE) return;
const existing = segmentCache.get(url);
if (existing && Date.now() - existing.timestamp <= CACHE_EXPIRY_MS) return;
try {
const resp = await fetch(url, { headers: { 'User-Agent': DEFAULT_UA, ...headers } });
if (!resp.ok) return;
const data = new Uint8Array(await resp.arrayBuffer());
const responseHeaders = {};
resp.headers.forEach((v, k) => responseHeaders[k] = v);
segmentCache.set(url, { data, headers: responseHeaders, timestamp: Date.now() });
} catch (_e) { /* ignore */ }
}
const DEFAULT_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
function inferContentType(upstreamCT, targetUrl) {
const bad = !upstreamCT || /application\/octet-stream/i.test(upstreamCT) || /application\/(x-)?zip/i.test(upstreamCT);
if (!bad) return upstreamCT;
if (/\.mkv(\?|$)/i.test(targetUrl)) return 'video/x-matroska';
if (/\.mp4(\?|$)/i.test(targetUrl)) return 'video/mp4';
if (/\.m3u8(\?|$)/i.test(targetUrl)) return 'application/vnd.apple.mpegurl';
if (/\.ts(\?|$)/i.test(targetUrl)) return 'video/mp2t';
return 'application/octet-stream';
}
function extractOriginalUrl(proxyUrl) {
try {
const url = new URL(proxyUrl);
if (url.pathname.includes('/proxy/')) {
const m = url.pathname.match(/\/proxy\/(.+)$/);
if (m) {
let decoded = decodeURIComponent(m[1]);
while (decoded.includes('%2F')) {
try { decoded = decodeURIComponent(decoded); } catch { break; }
}
if (decoded.startsWith('http://') || decoded.startsWith('https://')) return decoded;
// If it's just a path (like Vidlink /proxy/wiwii/...), keep the full original URL
return proxyUrl;
}
}
if (url.searchParams.has('url')) return decodeURIComponent(url.searchParams.get('url'));
const patterns = [
/\/api\/[^/]+\/proxy\?url=(.+)$/,
/\/proxy\?.*url=([^&]+)/,
/\/stream\/proxy\/(.+)$/,
/\/p\/(.+)$/
];
for (const p of patterns) {
const m = proxyUrl.match(p); if (m) return decodeURIComponent(m[1]);
}
return proxyUrl;
} catch { return proxyUrl; }
}
function rewriteM3u8(content, targetUrl, baseProxyUrl, headers) {
const lines = content.split('\n');
const out = []; const segmentUrls = [];
for (const line of lines) {
if (line.startsWith('#')) {
if (line.startsWith('#EXT-X-KEY:')) {
const regex = /https?:\/\/[^""\s]+/g; const keyUrl = regex.exec(line)?.[0];
if (keyUrl) {
const proxyUrl = `${baseProxyUrl}/ts-proxy?url=${encodeURIComponent(keyUrl)}&headers=${encodeURIComponent(JSON.stringify(headers))}`;
out.push(line.replace(keyUrl, proxyUrl));
if (!isCacheDisabled()) prefetchSegment(keyUrl, headers);
} else out.push(line);
} else if (line.startsWith('#EXT-X-MEDIA:') || line.startsWith('#EXT-X-I-FRAME-STREAM-INF:')) {
const uriMatch = line.match(/URI="([^"]+)"/);
if (uriMatch) {
try {
const mediaUrl = new URL(uriMatch[1], targetUrl).href;
const proxyUrl = `${baseProxyUrl}/m3u8-proxy?url=${encodeURIComponent(mediaUrl)}&headers=${encodeURIComponent(JSON.stringify(headers))}`;
out.push(line.replace(uriMatch[1], proxyUrl));
} catch { out.push(line); }
} else out.push(line);
} else out.push(line);
} else if (line.trim()) {
try {
const abs = new URL(line, targetUrl).href;
if (/\.m3u8(\?|$)/i.test(abs)) {
out.push(`${baseProxyUrl}/m3u8-proxy?url=${encodeURIComponent(abs)}&headers=${encodeURIComponent(JSON.stringify(headers))}`);
} else if (/\.ts(\?|$)/i.test(abs)) {
segmentUrls.push(abs);
out.push(`${baseProxyUrl}/ts-proxy?url=${encodeURIComponent(abs)}&headers=${encodeURIComponent(JSON.stringify(headers))}`);
} else out.push(line);
} catch { out.push(line); }
} else out.push(line);
}
if (segmentUrls.length && !isCacheDisabled()) {
Promise.all(segmentUrls.map(u => prefetchSegment(u, headers))).catch(() => undefined);
}
return out.join('\n');
}
function createProxyRoutes(app) {
// m3u8 playlist proxy
app.get('/m3u8-proxy', cors(), async (req, res) => {
const targetUrl = req.query.url; if (!targetUrl) return res.status(400).json({ error: 'URL parameter required' });
let headers = {};
try { headers = JSON.parse(req.query.headers || '{}'); } catch {
// Ignore URL parsing errors
}
try {
const response = await fetch(targetUrl, { headers: { 'User-Agent': DEFAULT_UA, ...headers } });
if (!response.ok) return res.status(response.status).json({ error: `M3U8 fetch failed: ${response.status}` });
const text = await response.text();
const protocol = req.headers['x-forwarded-proto'] || req.protocol || 'http';
const baseProxyUrl = `${protocol}://${req.get('host')}`;
const rewritten = rewriteM3u8(text, targetUrl, baseProxyUrl, headers);
res.setHeader('Content-Type', 'application/vnd.apple.mpegurl');
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Access-Control-Allow-Origin', '*');
res.send(rewritten);
} catch (e) { res.status(500).json({ error: e.message }); }
});
// ts / key segment proxy
app.get('/ts-proxy', cors(), async (req, res) => {
const targetUrl = req.query.url; if (!targetUrl) return res.status(400).json({ error: 'URL parameter required' });
const debug = req.query.debug === '1';
const noSynth = req.query.noSynth === '1';
const force200 = req.query.force200 === '1';
const clampOpen = req.query.clampOpen !== '0';
const progressiveOpen = req.query.progressiveOpen !== '0';
const tailPrefetchEnabled = req.query.tailPrefetch !== '0';
let tailPrefetchKB = parseInt(req.query.tailPrefetchKB || '256', 10);
if (isNaN(tailPrefetchKB) || tailPrefetchKB < 64) tailPrefetchKB = 256;
if (tailPrefetchKB > 2048) tailPrefetchKB = 2048; // max 2MB tail window
let openChunkKB = parseInt(req.query.openChunkKB || '4096', 10);
if (isNaN(openChunkKB) || openChunkKB < 64) openChunkKB = 4096;
if (openChunkKB > 16384) openChunkKB = 16384; // cap at 16MB
let initChunkKB = parseInt(req.query.initChunkKB || '512', 10);
if (isNaN(initChunkKB) || initChunkKB < 64) initChunkKB = 512;
if (initChunkKB > 2048) initChunkKB = 2048; // hard cap 2MB
let headers = {}; try { headers = JSON.parse(req.query.headers || '{}'); } catch {
// Ignore URL parsing errors
}
// Pass through Range header for progressive playback / seeking
const range = req.headers['range'];
let appliedClamp = false;
let effectiveRange = range;
if (range) headers.Range = range;
// Clamp first open-ended bytes=0- request to a bounded span (once per TTL) to avoid gigantic initial 206
if (range && /^bytes=0-\s*$/i.test(range) && !force200) {
const now = Date.now();
// Progressive growth supersedes single-shot clamp if enabled
if (progressiveOpen) {
const prog = progressiveOpenMap.get(targetUrl) || { lastEnd: -1 };
const prevEnd = prog.lastEnd;
let nextEnd;
if (prog.lastEnd < 0) {
nextEnd = openChunkKB * 1024 - 1;
} else {
const increment = openChunkKB * 1024;
nextEnd = prog.lastEnd + increment;
const maxCap = 256 * 1024 * 1024 - 1; // 256MB
if (nextEnd > maxCap) nextEnd = maxCap;
}
const newRange = `bytes=0-${nextEnd}`;
headers.Range = newRange;
effectiveRange = newRange;
prog.lastEnd = nextEnd;
progressiveOpenMap.set(targetUrl, prog);
appliedClamp = true; // reuse flag for logging
if (debug) console.log('[ts-proxy] progressiveOpen expand', { prevEnd, nextEnd, increment: openChunkKB * 1024 });
} else if (clampOpen) {
const lastTs = openRangeClampMap.get(targetUrl);
if (!lastTs || (now - lastTs) > OPEN_RANGE_CLAMP_TTL_MS) {
const clampBytes = openChunkKB * 1024;
const newRange = `bytes=0-${clampBytes - 1}`;
headers.Range = newRange;
effectiveRange = newRange;
openRangeClampMap.set(targetUrl, now);
appliedClamp = true;
}
}
}
if (debug) console.log('[ts-proxy] incoming', { url: targetUrl, range, effectiveRange, appliedClamp, clampOpen, progressiveOpen, openChunkKB, noSynth, initChunkKB, force200, tailPrefetchEnabled, tailPrefetchKB });
if (!isCacheDisabled()) {
const cached = getCachedSegment(targetUrl);
if (cached) {
const ct = inferContentType(cached.headers['content-type'], targetUrl);
res.setHeader('Content-Type', ct);
res.setHeader('Cache-Control', 'public, max-age=3600');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Accept-Ranges', 'bytes');
if (debug) console.log('[ts-proxy] cache hit');
return res.send(Buffer.from(cached.data));
}
}
try {
// If we have a tail prefetch entry and the client requests a tail range fully inside it, serve from memory early.
if (effectiveRange && /^bytes=\d+-$/i.test(effectiveRange)) {
const tailEntry = tailPrefetchMap.get(targetUrl);
if (tailEntry) {
const m = effectiveRange.match(/bytes=(\d+)-/i);
if (m) {
const startReq = parseInt(m[1], 10);
if (!isNaN(startReq) && startReq >= tailEntry.start && startReq <= tailEntry.end) {
const offset = startReq - tailEntry.start;
const slice = tailEntry.data.subarray(offset);
const endByte = tailEntry.end;
res.status(206);
const ct = inferContentType(null, targetUrl);
res.setHeader('Content-Type', ct);
res.setHeader('Accept-Ranges', 'bytes');
res.setHeader('Content-Length', slice.length.toString());
res.setHeader('Content-Range', `bytes ${startReq}-${endByte}/${tailEntry.size}`);
res.setHeader('Cache-Control', 'public, max-age=3600');
res.setHeader('Access-Control-Allow-Origin', '*');
if (debug) console.log('[ts-proxy] tail serve hit', { startReq, cachedStart: tailEntry.start, cachedEnd: tailEntry.end, size: tailEntry.size });
return res.end(slice);
}
}
}
}
// HEAD preflight (only when no explicit Range requested) to get size for synthesized partial response
let contentLength = null;
let upstreamAcceptRanges = null;
if (!effectiveRange) {
try {
const headResp = await fetch(targetUrl, { method: 'HEAD', headers: { 'User-Agent': DEFAULT_UA, ...headers } });
if (headResp.ok) {
contentLength = headResp.headers.get('content-length');
upstreamAcceptRanges = headResp.headers.get('accept-ranges');
if (debug) console.log('[ts-proxy] HEAD ok', { contentLength, upstreamAcceptRanges });
} else if (debug) {
console.log('[ts-proxy] HEAD status', headResp.status);
}
} catch (_e) { if (debug) console.log('[ts-proxy] HEAD failed'); }
}
// Fallback probe: if still no size, attempt a 0-0 range request to derive total size from Content-Range
if (!effectiveRange && !contentLength) {
try {
const probeRange = 'bytes=0-0';
if (debug) console.log('[ts-proxy] probe range');
const probeResp = await fetch(targetUrl, { headers: { 'User-Agent': DEFAULT_UA, ...headers, 'Range': probeRange } });
if (probeResp.status === 206) {
const cr = probeResp.headers.get('content-range');
if (cr) {
const m = cr.match(/\/(\d+)$/); if (m) contentLength = m[1];
}
try { await probeResp.arrayBuffer(); } catch (readErr) {
if (debug) console.log('[ts-proxy] probe body read failed', readErr.message);
}
if (debug) console.log('[ts-proxy] probe success', { contentLength });
} else if (debug) {
console.log('[ts-proxy] probe status', probeResp.status);
}
} catch (_e) { if (debug) console.log('[ts-proxy] probe failed'); }
}
if (debug && contentLength) console.log('[ts-proxy] size determined', contentLength);
// Initiate tail prefetch asynchronously (once) when size known and feature enabled
if (tailPrefetchEnabled && contentLength && !tailPrefetchMap.has(targetUrl)) {
const total = parseInt(contentLength, 10);
if (!isNaN(total) && total > 0) {
const tailBytes = Math.min(tailPrefetchKB * 1024, total);
const start = Math.max(0, total - tailBytes);
const tailRange = `bytes=${start}-`;
(async () => {
try {
if (debug) console.log('[ts-proxy] tail prefetch start', { tailRange });
const tr = await fetch(targetUrl, { headers: { 'User-Agent': DEFAULT_UA, ...headers, Range: tailRange } });
if (tr.status === 206) {
const buf = Buffer.from(await tr.arrayBuffer());
const cr = tr.headers.get('content-range');
if (cr) {
const mm = cr.match(/bytes\s+(\d+)-(\d+)\/(\d+)/i);
if (mm) {
const s = parseInt(mm[1], 10); const e = parseInt(mm[2], 10); const sz = parseInt(mm[3], 10);
if (!isNaN(s) && !isNaN(e) && !isNaN(sz)) {
tailPrefetchMap.set(targetUrl, { data: buf, start: s, end: e, size: sz, ts: Date.now() });
if (debug) console.log('[ts-proxy] tail prefetch success', { start: s, end: e, bytes: buf.length });
}
}
}
} else if (debug) {
console.log('[ts-proxy] tail prefetch status', tr.status);
}
} catch (e) { if (debug) console.log('[ts-proxy] tail prefetch error', e.message); }
})();
}
}
// If no range requested but we now know size (HEAD or probe), synthesize an initial small range fetch to accelerate playback.
if (!force200 && !effectiveRange && contentLength && !noSynth && req.query.progressiveOpen === '0') {
const total = parseInt(contentLength, 10);
const desired = initChunkKB * 1024;
const chunkSize = Math.min(desired, Math.max(0, total - 1));
const syntheticRange = `bytes=0-${chunkSize}`;
const resp = await fetch(targetUrl, { headers: { 'User-Agent': DEFAULT_UA, ...headers, 'Range': syntheticRange } });
if (resp.status === 206) {
if (debug) console.log('[ts-proxy] synthetic 206', syntheticRange);
const upstreamCT = inferContentType(resp.headers.get('content-type'), targetUrl);
res.status(206);
res.setHeader('Content-Type', upstreamCT);
res.setHeader('Accept-Ranges', upstreamAcceptRanges || 'bytes');
const contentRange = resp.headers.get('content-range');
if (contentRange) res.setHeader('Content-Range', contentRange);
const cl = resp.headers.get('content-length'); if (cl) res.setHeader('Content-Length', cl);
res.setHeader('Cache-Control', 'public, max-age=3600');
res.setHeader('Access-Control-Allow-Origin', '*');
// Pipe first chunk then leave connection open? Simpler: end after chunk; player will request next range.
return resp.body.pipe(res);
}
// If server ignored range (e.g., returned 200), fall through to normal logic below.
}
const upstreamOptions = { headers: { 'User-Agent': DEFAULT_UA, ...headers } };
// If client supplied a suffix range like bytes=0- we pass it; force200 strips Range
if (force200 && headers.Range) delete headers.Range;
const resp = await fetch(targetUrl, upstreamOptions);
if (!resp.ok && resp.status !== 206) {
if (debug) console.log('[ts-proxy] upstream error', resp.status);
return res.status(resp.status).json({ error: `TS fetch failed: ${resp.status}` });
}
// Forward partial content status when range used
if (effectiveRange && resp.status === 206 && !force200) { res.status(206); if (debug) console.log('[ts-proxy] forwarding 206', { appliedClamp }); }
if (force200 && resp.status === 206) {
// Some origins still return 206 even without Range; normalize to 200 for players expecting full stream
if (debug) console.log('[ts-proxy] normalizing 206 -> 200 due to force200');
}
// Copy key streaming headers
const upstreamCT = inferContentType(resp.headers.get('content-type'), targetUrl);
res.setHeader('Content-Type', upstreamCT);
let cl = resp.headers.get('content-length');
const crHeader = resp.headers.get('content-range');
if (!cl && crHeader) {
// Try to derive length from range span
const m = crHeader.match(/bytes\s+(\d+)-(\d+)\/(\d+|\*)/i);
if (m) {
const start = parseInt(m[1], 10); const end = parseInt(m[2], 10);
if (!isNaN(start) && !isNaN(end)) cl = (end - start + 1).toString();
}
}
if (cl) res.setHeader('Content-Length', cl);
const acceptRanges = resp.headers.get('accept-ranges');
if (acceptRanges) res.setHeader('Accept-Ranges', acceptRanges); else res.setHeader('Accept-Ranges', 'bytes');
const contentRange = resp.headers.get('content-range'); if (contentRange && !force200) res.setHeader('Content-Range', contentRange);
res.setHeader('Cache-Control', 'public, max-age=3600');
res.setHeader('Access-Control-Allow-Origin', '*');
if (debug) console.log('[ts-proxy] streaming body', { status: resp.status, ct: upstreamCT, cl, contentRange: force200 ? undefined : contentRange });
resp.body.pipe(res);
} catch (e) { if (debug) console.log('[ts-proxy] exception', e.message); res.status(500).json({ error: e.message }); }
});
// subtitle proxy
app.get('/sub-proxy', cors(), async (req, res) => {
const targetUrl = req.query.url; if (!targetUrl) return res.status(400).json({ error: 'url parameter required' });
let headers = {}; try { headers = JSON.parse(req.query.headers || '{}'); } catch {
// Ignore URL parsing errors
}
try {
const resp = await fetch(targetUrl, { headers: { 'User-Agent': DEFAULT_UA, ...headers } });
if (!resp.ok) return res.status(resp.status).json({ error: `subtitle fetch failed: ${resp.status}` });
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Cache-Control', 'public, max-age=3600');
res.setHeader('Content-Type', resp.headers.get('content-type') || 'text/vtt');
resp.body.pipe(res);
} catch (e) { res.status(500).json({ error: e.message }); }
});
}
function processStreamsForProxy(streams, serverUrl) {
if (!Array.isArray(streams)) return streams;
return streams.map(s => {
if (!s || !s.url || typeof s.url !== 'string') return s;
const original = extractOriginalUrl(s.url);
const headers = s.headers || {};
const hParam = Object.keys(headers).length ? `&headers=${encodeURIComponent(JSON.stringify(headers))}` : '';
let host = '';
try {
host = new URL(original).hostname.toLowerCase();
} catch {
// Ignore errors
}
// Force specific hosts through ts-proxy (direct file style) even without extension
if (host.includes('pixeldrain.') || host === 'video-downloads.googleusercontent.com') {
return { ...s, url: `${serverUrl}/ts-proxy?url=${encodeURIComponent(original)}${hParam}` };
}
if (/\.(mp4|mkv)(\?|$)/i.test(original)) {
return { ...s, url: `${serverUrl}/ts-proxy?url=${encodeURIComponent(original)}${hParam}` };
}
return { ...s, url: `${serverUrl}/m3u8-proxy?url=${encodeURIComponent(original)}${hParam}` };
});
}
module.exports = { createProxyRoutes, processStreamsForProxy };
+51 -12
View File
@@ -12,6 +12,8 @@
<div class="brand">TMDB Embed API</div> <div class="brand">TMDB Embed API</div>
<nav class="nav"> <nav class="nav">
<button class="nav-item" id="logoutBtn" data-action="logout">Logout</button> <button class="nav-item" id="logoutBtn" data-action="logout">Logout</button>
<button class="nav-item" id="restartBtn" type="button">Restart Server</button>
<div class="nav-sep" role="separator" aria-hidden="true"></div>
<button class="nav-item" data-target="panel-status">Server Status</button> <button class="nav-item" data-target="panel-status">Server Status</button>
<button class="nav-item" data-target="panel-core">Core</button> <button class="nav-item" data-target="panel-core">Core</button>
<button class="nav-item" data-target="panel-quality">Quality / Filters</button> <button class="nav-item" data-target="panel-quality">Quality / Filters</button>
@@ -27,15 +29,15 @@
</footer> </footer>
<footer class="side-footer">Credits to :<a class="hint" style="margin-top: .5rem; color: var(--primary-fg);" href="https://github.com/tapframe/NuvioStreamsAddon" target="_blank">NuvioStreamsAddon for Stremio</a></footer> <footer class="side-footer">Credits to :<a class="hint" style="margin-top: .5rem; color: var(--primary-fg);" href="https://github.com/tapframe/NuvioStreamsAddon" target="_blank">NuvioStreamsAddon for Stremio</a></footer>
<div class="bmc-widget-container"> <div class="bmc-widget-container" style="text-align: center;">
<script type="text/javascript" src="https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js" data-name="bmc-button" data-slug="K3QSoR2" data-color="#5F7FFF" data-emoji="☕" data-font="Bree" data-text="Buy me a coffee" data-outline-color="#000000" data-font-color="#ffffff" data-coffee-color="#FFDD00" ></script> <iframe src="https://github.com/sponsors/Inside4ndroid/button" title="Sponsor Inside4ndroid" height="32" width="114" style="border:0;border-radius:6px;margin-top:.5rem;"></iframe>
</div> </div>
<footer class="side-footer">Made with ❤️ by Inside4ndroid</footer> <footer class="side-footer">Made with ❤️ by Inside4ndroid</footer>
</aside> </aside>
<main class="main"> <main class="main">
<div id="bootWarn" class="alert warning" style="display:none;">No FEBBOX_COOKIES configured. Showbox / PStream streams may be unavailable.</div> <div id="bootWarn" class="alert warning" style="display:none;">No FEBBOX_COOKIES configured. Showbox / PStream streams may be unavailable.</div>
<div id="cacheDisabledBanner" class="alert info" style="display:none;">Cache is <strong>DISABLED</strong>; expect slower responses & higher source load.</div> <div id="cacheDisabledBanner" class="alert info" style="display:none;">Cache is <strong>DISABLED</strong>; expect slower responses & higher source load.</div>
<div id="tmdbMissingBanner" class="alert warning" style="display:none;">No TMDB API Keys configured. Metadata-dependent providers (Showbox, 4khdhub, MoviesMod, MP4Hydra) may degrade.</div> <div id="tmdbMissingBanner" class="alert warning" style="display:none;">No TMDB API Keys configured. Metadata-dependent providers (Showbox, 4khdhub) may degrade.</div>
<form id="cfgForm" onsubmit="return false;" class="panels"> <form id="cfgForm" onsubmit="return false;" class="panels">
<section id="panel-status" class="panel"> <section id="panel-status" class="panel">
<h1>Server Status<button style="margin-left: 1rem;" id="refreshStatusBtn" type="button">Refresh Status</button></h1> <h1>Server Status<button style="margin-left: 1rem;" id="refreshStatusBtn" type="button">Refresh Status</button></h1>
@@ -46,6 +48,13 @@
</div> </div>
<div class="status-block"> <div class="status-block">
<h2>Providers</h2> <h2>Providers</h2>
<div class="field" style="margin-bottom:.75rem;">
<label>Provider check TMDB ID<p class="hint">Title used by "Run Provider Functional Checks" (e.g. 278 = Shawshank Redemption).</p></label>
<div style="display:flex; align-items:center; gap:.5rem;">
<input type="text" id="providerCheckTmdbId" placeholder="278" style="width:7rem;" />
<button id="saveProviderCheckTmdbBtn" type="button">Save</button>
</div>
</div>
<button style="margin-bottom: 1rem;" id="runProviderChecksBtn" type="button">Run Provider Functional Checks</button> <button style="margin-bottom: 1rem;" id="runProviderChecksBtn" type="button">Run Provider Functional Checks</button>
<span style="margin-left: .5rem; margin-bottom: 1rem;" id="providerCheckSummary" class="hint"></span> <span style="margin-left: .5rem; margin-bottom: 1rem;" id="providerCheckSummary" class="hint"></span>
<div id="statusProvidersEnhanced" class="providers-table" aria-live="polite"></div> <div id="statusProvidersEnhanced" class="providers-table" aria-live="polite"></div>
@@ -97,8 +106,9 @@
</label> </label>
<textarea name="minQualitiesRaw" class="mini quality-json" placeholder='{"default":"900p"}' disabled></textarea> <textarea name="minQualitiesRaw" class="mini quality-json" placeholder='{"default":"900p"}' disabled></textarea>
</div> </div>
<div class="field codec-group"><label>Exclude Codecs<p class="hint">Preset radios serialize to a simple string. Custom & advanced modes use the JSON fields.</p> <div class="field codec-group"><label>Exclude Codecs<p class="hint">Preset radios serialize to JSON. Advanced lets you edit the JSON directly.</p>
<div class="codec-presets" id="codecPresets"> <div class="codec-presets" id="codecPresets">
<label class="c-item"><input type="radio" name="codecPreset" value="none"> <span>None</span></label>
<label class="c-item"><input type="radio" name="codecPreset" value="all"> <span>All</span></label> <label class="c-item"><input type="radio" name="codecPreset" value="all"> <span>All</span></label>
<label class="c-item"><input type="radio" name="codecPreset" value="dv"> <span>Dolby Vision (DV)</span></label> <label class="c-item"><input type="radio" name="codecPreset" value="dv"> <span>Dolby Vision (DV)</span></label>
<label class="c-item"><input type="radio" name="codecPreset" value="hdr"> <span>HDR / HDR10(+)</span></label> <label class="c-item"><input type="radio" name="codecPreset" value="hdr"> <span>HDR / HDR10(+)</span></label>
@@ -172,20 +182,14 @@
<p class="hint">These settings allow you to customize the behavior of the api and will effect timings.</p> <p class="hint">These settings allow you to customize the behavior of the api and will effect timings.</p>
<div class="field adv-flag-grid" id="advFlags" aria-label="Core Feature Flags"> <div class="field adv-flag-grid" id="advFlags" aria-label="Core Feature Flags">
<label class="adv-flag-item"><input type="checkbox" name="adv_disableCache" value="1" /> <span>Disable Cache</span></label> <label class="adv-flag-item"><input type="checkbox" name="adv_disableCache" value="1" /> <span>Disable Cache</span></label>
<label class="adv-flag-item"><input type="checkbox" name="adv_showboxRotatingProxy" value="1" /> <span>Showbox Rotating Proxy</span></label>
<label class="adv-flag-item"><input type="checkbox" name="adv_disableUrlValidation" value="1" /> <span>Disable URL Validation</span></label> <label class="adv-flag-item"><input type="checkbox" name="adv_disableUrlValidation" value="1" /> <span>Disable URL Validation</span></label>
<label class="adv-flag-item"><input type="checkbox" name="adv_disable4khdhubValidation" value="1" /> <span>Disable 4KHDHub Validation</span></label> <label class="adv-flag-item"><input type="checkbox" name="adv_disable4khdhubValidation" value="1" /> <span>Disable 4KHDHub Validation</span></label>
<label class="adv-flag-item"><input type="checkbox" name="adv_enableProxy" value="1" /> <span>Enable Stream Proxy</span></label>
</div> </div>
<h1>Proxy / Paths</h1> <h1>Paths</h1>
<p class="hint">Provider enable/disable & feature flags apply immediately. Click toggles to enable (highlighted) or disable (dim).</p> <p class="hint">Provider enable/disable & feature flags apply immediately. Click toggles to enable (highlighted) or disable (dim).</p>
<div class="adv-group"> <div class="adv-group">
<div class="field small"><label>Showbox Cache Dir <input type="text" name="adv_showboxCacheDir" placeholder="/data/showbox-cache"/></label></div> <div class="field small"><label>Showbox Cache Dir <input type="text" name="adv_showboxCacheDir" placeholder="/data/showbox-cache"/></label></div>
<div class="field small"><label>Showbox Proxy (Primary) <input type="text" name="adv_showboxProxyPrimary" placeholder="https://proxy.example/"/></label></div>
<div class="field small"><label>Showbox Proxy (Alternate) <input type="text" name="adv_showboxProxyAlternate" placeholder="https://proxy2.example/"/></label></div>
<div class="field small"><label>Xprime Proxy URL <input type="text" name="adv_xprimeProxyUrl" placeholder="https://..."/></label></div>
<div class="field small"><label>VidZee Proxy URL <input type="text" name="adv_vidzeeProxyUrl" placeholder="https://..."/></label></div>
<div class="field small"><label>VidSrc Proxy URL <input type="text" name="adv_vidsrcProxyUrl" placeholder="https://..."/></label></div>
<div class="field small"><label>MoviesMod Proxy URL <input type="text" name="adv_moviesmodProxyUrl" placeholder="https://..."/></label></div>
</div> </div>
</section> </section>
<section id="panel-live" class="panel"> <section id="panel-live" class="panel">
@@ -206,4 +210,39 @@
<span id="validationSummary" class="validation-summary"></span> <span id="validationSummary" class="validation-summary"></span>
</div> </div>
</main> </main>
<!-- Confirmation Modal -->
<div id="confirmReset" class="modal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="confirmResetTitle">
<div class="modal-backdrop"></div>
<div class="modal-window">
<h3 id="confirmResetTitle">Reset All Settings?</h3>
<p class="modal-text">This will remove all overrides, delete all TMDB API keys and FebBox cookies, and restore provider and feature flags to their defaults. This action cannot be undone.</p>
<ul class="modal-list">
<li>Clear TMDB keys and FebBox cookies</li>
<li>Reset provider enable/default selections</li>
<li>Reapply default quality and validation settings</li>
</ul>
<label class="modal-opt"><input type="checkbox" id="confirmResetDontAsk" /> Don't ask again on this device</label>
<div class="modal-actions">
<button type="button" class="danger" data-action="confirm">Reset Everything</button>
<button type="button" data-action="cancel">Cancel</button>
</div>
</div>
</div>
<!-- Restart Confirmation Modal -->
<div id="confirmRestart" class="modal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="confirmRestartTitle">
<div class="modal-backdrop"></div>
<div class="modal-window">
<h3 id="confirmRestartTitle">Restart Server?</h3>
<p class="modal-text">This will stop the server process and attempt to restart it. Active requests will be interrupted. Make sure the app is managed by Docker, PM2, systemd, or another supervisor.</p>
<ul class="modal-list">
<li>Interrupts any in-flight API calls</li>
<li>Server will exit and come back up</li>
<li>UI will auto-reload when available</li>
</ul>
<div class="modal-actions">
<button type="button" class="danger" data-action="confirm">Restart Now</button>
<button type="button" data-action="cancel">Cancel</button>
</div>
</div>
</div>
</body></html> </body></html>
+150 -50
View File
@@ -124,13 +124,14 @@ function fillForm(data){
} }
const excRaw = override.excludeCodecsRaw !== undefined ? override.excludeCodecsRaw : (merged.excludeCodecsRaw || ''); const excRaw = override.excludeCodecsRaw !== undefined ? override.excludeCodecsRaw : (merged.excludeCodecsRaw || '');
let parsed = null; try { if(excRaw && /[\{\[]/.test(excRaw)) parsed = JSON.parse(excRaw); } catch {} let parsed = null; try { if(excRaw && /[\{\[]/.test(excRaw)) parsed = JSON.parse(excRaw); } catch {}
let mode = 'all'; let mode = 'none';
if(parsed){ if(parsed){
const dv = parsed.excludeDV === true; const dv = parsed.excludeDV === true;
const hdr = parsed.excludeHDR === true; const hdr = parsed.excludeHDR === true;
if(dv && !hdr) mode = 'dv'; if(!dv && !hdr) mode = 'none';
else if(dv && hdr) mode = 'all';
else if(dv && !hdr) mode = 'dv';
else if(hdr && !dv) mode = 'hdr'; else if(hdr && !dv) mode = 'hdr';
else if(dv && hdr) mode = 'advanced';
else mode = 'advanced'; else mode = 'advanced';
} else if(excRaw){ } else if(excRaw){
if(/excludeDV":true/.test(excRaw) && !/excludeHDR":true/.test(excRaw)) mode='dv'; if(/excludeDV":true/.test(excRaw) && !/excludeHDR":true/.test(excRaw)) mode='dv';
@@ -152,40 +153,27 @@ function fillForm(data){
const tmdbMissing = !((override.tmdbApiKeys && override.tmdbApiKeys.length) || (merged.tmdbApiKeys && merged.tmdbApiKeys.length) || override.tmdbApiKey || merged.tmdbApiKey); const tmdbMissing = !((override.tmdbApiKeys && override.tmdbApiKeys.length) || (merged.tmdbApiKeys && merged.tmdbApiKeys.length) || override.tmdbApiKey || merged.tmdbApiKey);
const tmdbProvidersEnabled = [ const tmdbProvidersEnabled = [
merged.enableShowboxProvider !== false, merged.enableShowboxProvider !== false,
merged.enable4khdhubProvider !== false, merged.enable4khdhubProvider !== false
merged.enableMoviesmodProvider !== false,
merged.enableMp4hydraProvider !== false
].some(Boolean); ].some(Boolean);
tmdbBanner.style.display = (tmdbMissing && tmdbProvidersEnabled)? 'block':'none'; tmdbBanner.style.display = (tmdbMissing && tmdbProvidersEnabled)? 'block':'none';
} }
const advBools = [ const advBools = [
['enableXprimeProvider','adv_enableXprime'],
['enable4khdhubProvider','adv_enable4khdhub'], ['enable4khdhubProvider','adv_enable4khdhub'],
['enableMoviesmodProvider','adv_enableMoviesmod'],
['enableMp4hydraProvider','adv_enableMp4hydra'],
['enableVidzeeProvider','adv_enableVidzee'],
['enableVixsrcProvider','adv_enableVixsrc'], ['enableVixsrcProvider','adv_enableVixsrc'],
['enableProxy','adv_enableProxy'],
['disableCache','adv_disableCache'], ['disableCache','adv_disableCache'],
['enablePStreamApi','adv_enablePStream'], ['enablePStreamApi','adv_enablePStream'],
['showboxUseRotatingProxy','adv_showboxRotatingProxy'],
['disableUrlValidation','adv_disableUrlValidation'], ['disableUrlValidation','adv_disableUrlValidation'],
['disable4khdhubUrlValidation','adv_disable4khdhubValidation'] ['disable4khdhubUrlValidation','adv_disable4khdhubValidation']
]; ];
advBools.forEach(([cfgKey, elName])=>{ const el = f.elements[elName]; if(!el) return; const src = (override[cfgKey]!==undefined? override[cfgKey] : merged[cfgKey]); if(el.type==='checkbox'){ el.checked = !!src; } else { el.value = src? 'true':''; } }); advBools.forEach(([cfgKey, elName])=>{ const el = f.elements[elName]; if(!el) return; const src = (override[cfgKey]!==undefined? override[cfgKey] : merged[cfgKey]); if(el.type==='checkbox'){ el.checked = !!src; } else { el.value = src? 'true':''; } });
const advTexts = [ const advTexts = [
['showboxCacheDir','adv_showboxCacheDir'], ['showboxCacheDir','adv_showboxCacheDir']
['showboxProxyPrimary','adv_showboxProxyPrimary'],
['showboxProxyAlternate','adv_showboxProxyAlternate'],
['xprimeProxyUrl','adv_xprimeProxyUrl'],
['vidzeeProxyUrl','adv_vidzeeProxyUrl'],
['vidsrcProxyUrl','adv_vidsrcProxyUrl'],
['moviesmodProxyUrl','adv_moviesmodProxyUrl']
]; ];
advTexts.forEach(([cfgKey, elName])=>{ const el = f.elements[elName]; if(!el) return; const val = override[cfgKey]!==undefined? override[cfgKey] : merged[cfgKey]; el.value = val || ''; }); advTexts.forEach(([cfgKey, elName])=>{ const el = f.elements[elName]; if(!el) return; const val = override[cfgKey]!==undefined? override[cfgKey] : merged[cfgKey]; el.value = val || ''; });
// Sync checkbox style advanced flags // Sync checkbox style advanced flags
const advFlagMap = { const advFlagMap = {
adv_disableCache: 'disableCache', adv_disableCache: 'disableCache',
adv_showboxRotatingProxy: 'showboxUseRotatingProxy',
adv_disableUrlValidation: 'disableUrlValidation', adv_disableUrlValidation: 'disableUrlValidation',
adv_disable4khdhubValidation: 'disable4khdhubUrlValidation' adv_disable4khdhubValidation: 'disable4khdhubUrlValidation'
}; };
@@ -234,7 +222,8 @@ async function save(){
} }
const cPreset = document.querySelector('input[name="codecPreset"]:checked'); const cPreset = document.querySelector('input[name="codecPreset"]:checked');
if(cPreset){ if(cPreset){
if(cPreset.value==='all') payload.excludeCodecsRaw = null; if(cPreset.value==='none') payload.excludeCodecsRaw = JSON.stringify({excludeDV:false,excludeHDR:false});
else if(cPreset.value==='all') payload.excludeCodecsRaw = JSON.stringify({excludeDV:true,excludeHDR:true});
else if(cPreset.value==='dv') payload.excludeCodecsRaw = JSON.stringify({excludeDV:true,excludeHDR:false}); else if(cPreset.value==='dv') payload.excludeCodecsRaw = JSON.stringify({excludeDV:true,excludeHDR:false});
else if(cPreset.value==='hdr') payload.excludeCodecsRaw = JSON.stringify({excludeDV:false,excludeHDR:true}); else if(cPreset.value==='hdr') payload.excludeCodecsRaw = JSON.stringify({excludeDV:false,excludeHDR:true});
else if(cPreset.value==='advanced') { else if(cPreset.value==='advanced') {
@@ -250,28 +239,16 @@ async function save(){
payload.febboxCookies = cookiesVal? cookiesVal.split(',').filter(Boolean) : []; payload.febboxCookies = cookiesVal? cookiesVal.split(',').filter(Boolean) : [];
} }
const boolMap = { const boolMap = {
adv_enableXprime:'enableXprimeProvider',
adv_enable4khdhub:'enable4khdhubProvider', adv_enable4khdhub:'enable4khdhubProvider',
adv_enableMoviesmod:'enableMoviesmodProvider',
adv_enableMp4hydra:'enableMp4hydraProvider',
adv_enableVidzee:'enableVidzeeProvider',
adv_enableVixsrc:'enableVixsrcProvider', adv_enableVixsrc:'enableVixsrcProvider',
adv_enableProxy:'enableProxy',
adv_disableCache:'disableCache', adv_disableCache:'disableCache',
adv_enablePStream:'enablePStreamApi', adv_enablePStream:'enablePStreamApi',
adv_showboxRotatingProxy:'showboxUseRotatingProxy',
adv_disableUrlValidation:'disableUrlValidation', adv_disableUrlValidation:'disableUrlValidation',
adv_disable4khdhubValidation:'disable4khdhubUrlValidation' adv_disable4khdhubValidation:'disable4khdhubUrlValidation'
}; };
Object.entries(boolMap).forEach(([formName,cfgKey])=>{ const el = f.elements[formName]; if(el) payload[cfgKey] = !!el.checked; }); Object.entries(boolMap).forEach(([formName,cfgKey])=>{ const el = f.elements[formName]; if(el) payload[cfgKey] = !!el.checked; });
const textMap = { const textMap = { adv_showboxCacheDir:'showboxCacheDir' };
adv_showboxCacheDir:'showboxCacheDir',
adv_showboxProxyPrimary:'showboxProxyPrimary',
adv_showboxProxyAlternate:'showboxProxyAlternate',
adv_xprimeProxyUrl:'xprimeProxyUrl',
adv_vidzeeProxyUrl:'vidzeeProxyUrl',
adv_vidsrcProxyUrl:'vidsrcProxyUrl',
adv_moviesmodProxyUrl:'moviesmodProxyUrl'
};
Object.entries(textMap).forEach(([formName,cfgKey])=>{ const el = f.elements[formName]; if(!el) return; const v = el.value.trim(); payload[cfgKey] = v? v : null; }); Object.entries(textMap).forEach(([formName,cfgKey])=>{ const el = f.elements[formName]; if(!el) return; const v = el.value.trim(); payload[cfgKey] = v? v : null; });
const tmdbHidden = f.elements['tmdbApiKeysHidden']; const tmdbHidden = f.elements['tmdbApiKeysHidden'];
if (tmdbHidden){ if (tmdbHidden){
@@ -288,18 +265,90 @@ async function save(){
} catch(e){ setStatus(e.message,true); } } catch(e){ setStatus(e.message,true); }
} }
function openConfirmReset(){
const modal = document.getElementById('confirmReset');
if(!modal) return Promise.resolve(false);
// Respect persisted preference
try {
const skip = localStorage.getItem('ns_confirmReset_skip') === '1';
if (skip) return Promise.resolve(true);
} catch {}
modal.classList.add('show');
return new Promise(resolve => {
const onClick = (e)=>{
const action = e.target?.dataset?.action;
if(action==='confirm'){
try {
const dontAsk = modal.querySelector('#confirmResetDontAsk')?.checked;
if (dontAsk) localStorage.setItem('ns_confirmReset_skip','1');
} catch {}
cleanup(); resolve(true);
}
if(action==='cancel' || e.target===modal.querySelector('.modal-backdrop')){ cleanup(); resolve(false); }
};
function onKey(e){ if(e.key==='Escape'){ cleanup(); resolve(false); } }
function cleanup(){ modal.classList.remove('show'); modal.removeEventListener('click', onClick); document.removeEventListener('keydown', onKey); }
modal.addEventListener('click', onClick);
document.addEventListener('keydown', onKey);
});
}
function openConfirmRestart(){
const modal = document.getElementById('confirmRestart');
if(!modal) return Promise.resolve(false);
modal.classList.add('show');
return new Promise(resolve => {
const onClick = (e)=>{
const action = e.target?.dataset?.action;
if(action==='confirm'){ cleanup(); resolve(true); }
if(action==='cancel' || e.target===modal.querySelector('.modal-backdrop')){ cleanup(); resolve(false); }
};
function onKey(e){ if(e.key==='Escape'){ cleanup(); resolve(false); } }
function cleanup(){ modal.classList.remove('show'); modal.removeEventListener('click', onClick); document.removeEventListener('keydown', onKey); }
modal.addEventListener('click', onClick);
document.addEventListener('keydown', onKey);
});
}
async function clearAll(){ async function clearAll(){
if(!confirm('Remove ALL overrides?')) return; const confirmed = await openConfirmReset();
const payload = { port:null, defaultRegion:null, defaultProviders:[], minQualitiesRaw:null, excludeCodecsRaw:null, febboxCookies:null, if(!confirmed) return;
enableXprimeProvider:null, enable4khdhubProvider:null, enableMoviesmodProvider:null, enableMp4hydraProvider:null, enableVidzeeProvider:null, enableVixsrcProvider:null, // To truly reset to app defaults, we must:
disableCache:null, enablePStreamApi:null, showboxUseRotatingProxy:null, disableUrlValidation:null, disable4khdhubUrlValidation:null, // - Explicitly clear arrays so env fallbacks don't persist
showboxCacheDir:null, showboxProxyPrimary:null, showboxProxyAlternate:null, xprimeProxyUrl:null, vidzeeProxyUrl:null, vidsrcProxyUrl:null, moviesmodProxyUrl:null }; // - Set boolean flags to their default values (rather than null)
// - Null legacy single TMDB key
const payload = {
port: null,
defaultRegion: null,
defaultProviders: [],
minQualitiesRaw: 'all',
excludeCodecsRaw: JSON.stringify({excludeDV:false,excludeHDR:false}),
// Explicit empty arrays ensure override wins over any prior env mirror
febboxCookies: [],
tmdbApiKeys: [],
tmdbApiKey: null,
// Provider enable flags default to true
enableShowboxProvider: true,
enable4khdhubProvider: true,
enableVixsrcProvider: true,
enableProxy: false,
// Other defaults
disableCache: false,
enablePStreamApi: true,
disableUrlValidation: false,
disable4khdhubUrlValidation: false,
showboxCacheDir: null,
providerCheckTmdbId: null
};
try { try {
const r = await fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}); const r = await fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
const js = await r.json(); const js = await r.json();
if(!js.success) throw new Error(js.error||'Clear failed'); if(!js.success) throw new Error(js.error||'Clear failed');
setStatus('Overrides cleared'); setStatus('All settings reset to defaults');
fillForm(js); fillForm(js);
// Re-render provider matrix immediately so toggles reflect defaults without page refresh
try { renderProviderMatrix(js.merged); } catch {}
validate();
} catch(e){ setStatus(e.message,true); } } catch(e){ setStatus(e.message,true); }
} }
@@ -363,6 +412,8 @@ async function loadServerStatus(){
const j = await r.json(); const j = await r.json();
if(!j.success) throw new Error('Status error'); if(!j.success) throw new Error('Status error');
metricsEl.textContent = JSON.stringify(j.metrics, null, 2); metricsEl.textContent = JSON.stringify(j.metrics, null, 2);
const checkTmdbEl = document.getElementById('providerCheckTmdbId');
if(checkTmdbEl) checkTmdbEl.value = j.providerCheckTmdbId || '278';
if(endpointsEl) endpointsEl.textContent = (j.endpoints||[]).join('\n'); if(endpointsEl) endpointsEl.textContent = (j.endpoints||[]).join('\n');
if(provEnhanced){ if(provEnhanced){
provEnhanced.innerHTML = ''; provEnhanced.innerHTML = '';
@@ -390,7 +441,8 @@ async function loadServerStatus(){
} }
} }
async function runProviderFunctionalChecks(){ async function runProviderFunctionalChecks(){
const targetTmdb = '293660'; // TMDB ID provided const checkTmdbEl = document.getElementById('providerCheckTmdbId');
const targetTmdb = (checkTmdbEl && checkTmdbEl.value.trim()) || '278'; // TMDB ID (editable on status page)
const provEnhanced = document.getElementById('statusProvidersEnhanced'); const provEnhanced = document.getElementById('statusProvidersEnhanced');
const summary = document.getElementById('providerCheckSummary'); const summary = document.getElementById('providerCheckSummary');
if(!provEnhanced) return; if(!provEnhanced) return;
@@ -430,6 +482,18 @@ async function runProviderFunctionalChecks(){
} }
summary.textContent = `Checks complete: ${passed}/${rows.length} passed`; summary.textContent = `Checks complete: ${passed}/${rows.length} passed`;
} }
async function saveProviderCheckTmdbId(){
const input = document.getElementById('providerCheckTmdbId');
const val = (input ? input.value : '').trim();
if(!val){ setStatus('Enter a TMDB ID to save', true); return; }
if(!/^\d+$/.test(val)){ setStatus('TMDB ID must be numeric', true); return; }
try {
const r = await fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ providerCheckTmdbId: val })});
const js = await r.json();
if(!js.success) throw new Error(js.error||'Save failed');
setStatus('Provider check TMDB ID saved: '+val);
} catch(e){ setStatus(e.message, true); }
}
document.addEventListener('DOMContentLoaded', ()=> { document.addEventListener('DOMContentLoaded', ()=> {
initTheme(); initTheme();
attachCoreListeners(); attachCoreListeners();
@@ -443,6 +507,30 @@ document.addEventListener('DOMContentLoaded', ()=> {
location.replace('/?loggedOut=1'); location.replace('/?loggedOut=1');
}); });
} }
const restartBtn = document.getElementById('restartBtn');
if(restartBtn){
restartBtn.addEventListener('click', async ()=>{
const ok = await openConfirmRestart();
if(!ok) return;
setStatus('Restarting server…');
try {
const r = await fetch('/api/restart',{ method:'POST' });
if(!r.ok){ const t = await r.text().catch(()=>r.statusText); throw new Error(t||('HTTP '+r.status)); }
// Server will shut down soon; start polling until it comes back
await new Promise(resolve => setTimeout(resolve, 1000));
const start = Date.now();
const timeoutMs = 60_000;
(async function poll(){
try {
const pr = await fetch('/api/health',{ cache:'no-store' });
if(pr.ok){ location.reload(); return; }
} catch {}
if(Date.now()-start > timeoutMs){ setStatus('Restart timed out', true); return; }
setTimeout(poll, 1000);
})();
} catch(e){ setStatus('Restart failed: '+e.message, true); }
});
}
reload(); reload();
const h = location.hash.slice(1); const h = location.hash.slice(1);
navTo(h||'panel-core'); navTo(h||'panel-core');
@@ -461,6 +549,8 @@ document.addEventListener('DOMContentLoaded', ()=> {
if(rsb) rsb.addEventListener('click', loadServerStatus); if(rsb) rsb.addEventListener('click', loadServerStatus);
const rpc = document.getElementById('runProviderChecksBtn'); const rpc = document.getElementById('runProviderChecksBtn');
if(rpc) rpc.addEventListener('click', runProviderFunctionalChecks); if(rpc) rpc.addEventListener('click', runProviderFunctionalChecks);
const spc = document.getElementById('saveProviderCheckTmdbBtn');
if(spc) spc.addEventListener('click', saveProviderCheckTmdbId);
}); });
function toggleSidebar(force){ function toggleSidebar(force){
const sb = document.getElementById('sidebar'); const sb = document.getElementById('sidebar');
@@ -513,15 +603,25 @@ async function renderProviderMatrix(merged){
if(!merged){ try { const data = await fetchConfig(); merged = data.merged||{}; } catch { return; } } if(!merged){ try { const data = await fetchConfig(); merged = data.merged||{}; } catch { return; } }
const container = document.getElementById('providerMatrix'); if(!container) return; const container = document.getElementById('providerMatrix'); if(!container) return;
const defaults = (merged.defaultProviders||[]).map(s=>s.toLowerCase()); const defaults = (merged.defaultProviders||[]).map(s=>s.toLowerCase());
const providers = [
['showbox','enableShowboxProvider'], // Fetch available providers from API
['4khdhub','enable4khdhubProvider'], let availableProviders = [];
['moviesmod','enableMoviesmodProvider'], try {
['mp4hydra','enableMp4hydraProvider'], const providersResponse = await fetch('/api/providers');
['vidzee','enableVidzeeProvider'], if (providersResponse.ok) {
['vixsrc','enableVixsrcProvider'], const providersData = await providersResponse.json();
['xprime','enableXprimeProvider'] availableProviders = providersData.providers || [];
]; }
} catch (e) {
console.warn('Failed to fetch providers list:', e.message);
}
// Create provider list with enable flags
const providers = availableProviders.map(p => {
const flag = `enable${p.name.charAt(0).toUpperCase() + p.name.slice(1)}Provider`;
return [p.name, flag];
});
container.innerHTML = providers.map(([name,flag])=>{ container.innerHTML = providers.map(([name,flag])=>{
const enabled = merged[flag] !== false; const enabled = merged[flag] !== false;
const isDefault = defaults.includes(name); const isDefault = defaults.includes(name);
+15
View File
@@ -129,6 +129,9 @@ body {
.nav-item.theme-toggle { margin-top: 12px; } .nav-item.theme-toggle { margin-top: 12px; }
.nav-item#logoutBtn { border:1px solid var(--danger); color: var(--danger); font-weight:700; } .nav-item#logoutBtn { border:1px solid var(--danger); color: var(--danger); font-weight:700; }
.nav-item#logoutBtn:hover { background: var(--danger); color:#fff; } .nav-item#logoutBtn:hover { background: var(--danger); color:#fff; }
.nav-item#restartBtn { border:1px solid var(--danger); color: var(--danger); font-weight:700; }
.nav-item#restartBtn:hover { background: var(--danger); color:#fff; }
.nav-sep { height: 1px; background: var(--border); margin: 6px 0 8px; opacity: .6; margin-bottom: 1rem; margin-top: 1rem;}
.grow { flex-grow: 1; } .grow { flex-grow: 1; }
.side-footer { font-size: .65rem; opacity: .6; line-height: 1.4; text-align: center;} .side-footer { font-size: .65rem; opacity: .6; line-height: 1.4; text-align: center;}
.main { .main {
@@ -364,3 +367,15 @@ input.invalid, textarea.invalid { border-color: var(--danger)!important; animati
.providers-table .prov-row { border:1px solid var(--border); } .providers-table .prov-row { border:1px solid var(--border); }
.prov-row strong { font-size:.7rem; letter-spacing:.5px; } .prov-row strong { font-size:.7rem; letter-spacing:.5px; }
} }
/* Modal */
.modal { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; z-index: 100; }
.modal.show { display: flex; }
.modal .modal-backdrop { position: absolute; inset:0; background: rgba(0,0,0,.45); backdrop-filter: blur(2px); }
.modal .modal-window { position: relative; z-index: 1; width: min(560px, 94vw); background: var(--bg-alt); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); padding: 18px 20px; }
.modal .modal-window h3 { margin: 0 0 8px; font-size: 1.05rem; letter-spacing: .5px; }
.modal .modal-text { margin: 0 0 10px; font-size: .85rem; color: var(--text-dim); }
.modal .modal-list { margin: 0 0 14px 18px; padding: 0; color: var(--text-dim); font-size: .8rem; }
.modal .modal-opt { display:flex; align-items:center; gap:8px; margin: 0 0 12px; font-size:.8rem; color: var(--text-dim); }
.modal .modal-opt input { accent-color: var(--primary); }
.modal .modal-actions { display: flex; gap: 10px; justify-content: flex-end; }
+1
View File
@@ -0,0 +1 @@
1787175846578
+1 -1
View File
@@ -3,7 +3,7 @@
"users": [ "users": [
{ {
"username": "admin", "username": "admin",
"hash": "7fffd5f131a733035eaf443cd99905c0:f1cfa876f9b84fede40cd618a4352221f7ab36e4f8bead432ba5d866b06cb9fcc2903ce623d8286bb541f8d28490fc692d0bf3fadda08002265374915cdb27b5" "hash": "3c2efa431b6de0bee19847236e2c296b:d88ccc2ee950eda254a03d073bc55bbc8d0efc263c93c4e20307154dc90fadbc3141dcc9556a150b39b52088bb8ad06b120bc8832d7d8dc35918e710803d207c"
} }
] ]
} }
+66
View File
@@ -0,0 +1,66 @@
// Episode count per season from Cinemeta.
// Ported from Infinite-streams (src/lib/cinemeta.ts getEpisodesPerSeason).
const CINEMETA_BASE = 'https://v3-cinemeta.strem.io';
const CACHE_TTL_MS = 6 * 60 * 60 * 1000;
const FETCH_TIMEOUT_MS = 12000;
const episodeCountCache = new Map();
async function getEpisodesPerSeason(imdbId) {
const cacheKey = `eps:${imdbId}`;
const cached = episodeCountCache.get(cacheKey);
if (cached && Date.now() - cached.ts < CACHE_TTL_MS) {
return cached.data;
}
try {
const url = `${CINEMETA_BASE}/meta/series/${imdbId}.json`;
const res = await fetch(url, {
headers: { 'User-Agent': 'Stremio Addon' },
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
});
if (!res.ok) {
episodeCountCache.set(cacheKey, { data: [], ts: Date.now() });
return [];
}
const json = await res.json();
const videos = (json.meta && json.meta.videos) || [];
if (videos.length === 0) {
episodeCountCache.set(cacheKey, { data: [], ts: Date.now() });
return [];
}
const seasonMaxEp = new Map();
for (const v of videos) {
if (!v.season || v.season <= 0) continue;
const ep = v.number || v.episode || 0;
if (ep > 0) {
const prev = seasonMaxEp.get(v.season) || 0;
seasonMaxEp.set(v.season, Math.max(prev, ep));
}
}
if (seasonMaxEp.size === 0) {
episodeCountCache.set(cacheKey, { data: [], ts: Date.now() });
return [];
}
const maxSeason = Math.max(...seasonMaxEp.keys());
const result = [];
for (let s = 1; s <= maxSeason; s++) {
result.push(seasonMaxEp.get(s) || 0);
}
episodeCountCache.set(cacheKey, { data: result, ts: Date.now() });
return result;
} catch (err) {
console.error(`[Cinemeta] getEpisodesPerSeason error: ${err.message}`);
episodeCountCache.set(cacheKey, { data: [], ts: Date.now() });
return [];
}
}
module.exports = { getEpisodesPerSeason };
+64 -45
View File
@@ -52,6 +52,19 @@ function writeOverrideFile(obj) {
} }
} }
function getProviderNames() {
try {
const providersDir = path.join(process.cwd(), 'providers');
if (!fs.existsSync(providersDir)) return [];
return fs.readdirSync(providersDir)
.filter(file => file.endsWith('.js') && file !== 'registry.js')
.map(file => path.parse(file).name.toLowerCase());
} catch (e) {
console.warn('[config] Failed to scan providers directory:', e.message);
return [];
}
}
function normalizeConfig(base) { function normalizeConfig(base) {
const cfg = { ...base }; const cfg = { ...base };
// Marker: if user explicitly provided an empty array for tmdbApiKeys in override file we should not resurrect legacy key // Marker: if user explicitly provided an empty array for tmdbApiKeys in override file we should not resurrect legacy key
@@ -78,25 +91,31 @@ function normalizeConfig(base) {
} }
// Ensure dedupe + trim // Ensure dedupe + trim
cfg.tmdbApiKeys = Array.from(new Set(cfg.tmdbApiKeys.map(k=>String(k).trim()).filter(Boolean))); cfg.tmdbApiKeys = Array.from(new Set(cfg.tmdbApiKeys.map(k=>String(k).trim()).filter(Boolean)));
// Booleans normalization // Do not expose legacy single-key field in merged config output
const boolKeys = [ delete cfg.tmdbApiKey;
'enableShowboxProvider','enableXprimeProvider','enable4khdhubProvider','enableMoviesmodProvider','enableMp4hydraProvider','enableVidzeeProvider','enableVixsrcProvider',
'disableCache','enablePStreamApi','showboxUseRotatingProxy','disableUrlValidation','disable4khdhubUrlValidation' // Dynamic provider enable flags
]; const providerNames = getProviderNames();
const boolKeys = providerNames.map(name => `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`);
boolKeys.push('disableCache', 'enablePStreamApi', 'disableUrlValidation', 'disable4khdhubUrlValidation', 'enableProxy');
boolKeys.forEach(k=>{ if (cfg[k] === 'true') cfg[k] = true; else if (cfg[k] === 'false') cfg[k] = false; }); boolKeys.forEach(k=>{ if (cfg[k] === 'true') cfg[k] = true; else if (cfg[k] === 'false') cfg[k] = false; });
// Default values if undefined
if (cfg.enableShowboxProvider === undefined) cfg.enableShowboxProvider = true; // Set defaults for provider enable flags
if (cfg.enableXprimeProvider === undefined) cfg.enableXprimeProvider = true; providerNames.forEach(name => {
if (cfg.enable4khdhubProvider === undefined) cfg.enable4khdhubProvider = true; const flag = `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`;
if (cfg.enableMoviesmodProvider === undefined) cfg.enableMoviesmodProvider = true; if (cfg[flag] === undefined) cfg[flag] = true; // Default to enabled
if (cfg.enableMp4hydraProvider === undefined) cfg.enableMp4hydraProvider = true; });
if (cfg.enableVidzeeProvider === undefined) cfg.enableVidzeeProvider = true;
if (cfg.enableVixsrcProvider === undefined) cfg.enableVixsrcProvider = true; // Default values for other flags
if (cfg.disableCache === undefined) cfg.disableCache = false; if (cfg.disableCache === undefined) cfg.disableCache = false;
if (cfg.enablePStreamApi === undefined) cfg.enablePStreamApi = true; if (cfg.enablePStreamApi === undefined) cfg.enablePStreamApi = true;
if (cfg.showboxUseRotatingProxy === undefined) cfg.showboxUseRotatingProxy = false; if (cfg.enableProxy === undefined) cfg.enableProxy = false; // default off
// Proxy features removed; always use direct connections
if (cfg.disableUrlValidation === undefined) cfg.disableUrlValidation = false; if (cfg.disableUrlValidation === undefined) cfg.disableUrlValidation = false;
if (cfg.disable4khdhubUrlValidation === undefined) cfg.disable4khdhubUrlValidation = false; if (cfg.disable4khdhubUrlValidation === undefined) cfg.disable4khdhubUrlValidation = false;
// TMDB ID used by the dashboard's "Run Provider Functional Checks" feature
if (!cfg.providerCheckTmdbId) cfg.providerCheckTmdbId = '278';
return cfg; return cfg;
} }
@@ -112,29 +131,32 @@ function applyConfigToEnv(cfg){
if (cfg.febboxCookies && cfg.febboxCookies.length) process.env.FEBBOX_COOKIES = cfg.febboxCookies.join(','); if (cfg.febboxCookies && cfg.febboxCookies.length) process.env.FEBBOX_COOKIES = cfg.febboxCookies.join(',');
else delete process.env.FEBBOX_COOKIES; else delete process.env.FEBBOX_COOKIES;
if (cfg.defaultRegion) process.env.DEFAULT_REGION = cfg.defaultRegion; else delete process.env.DEFAULT_REGION; if (cfg.defaultRegion) process.env.DEFAULT_REGION = cfg.defaultRegion; else delete process.env.DEFAULT_REGION;
// Provider enable flags
process.env.ENABLE_SHOWBOX_PROVIDER = cfg.enableShowboxProvider ? 'true':'false'; // Dynamic provider enable flags
process.env.ENABLE_XPRIME_PROVIDER = cfg.enableXprimeProvider ? 'true':'false'; const providerNames = getProviderNames();
process.env.ENABLE_4KHDHUB_PROVIDER = cfg.enable4khdhubProvider ? 'true':'false'; providerNames.forEach(name => {
process.env.ENABLE_MOVIESMOD_PROVIDER = cfg.enableMoviesmodProvider ? 'true':'false'; const flag = `enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`;
process.env.ENABLE_MP4HYDRA_PROVIDER = cfg.enableMp4hydraProvider ? 'true':'false'; const envName = `ENABLE_${name.toUpperCase()}_PROVIDER`;
process.env.ENABLE_VIDZEE_PROVIDER = cfg.enableVidzeeProvider ? 'true':'false'; process.env[envName] = cfg[flag] ? 'true' : 'false';
process.env.ENABLE_VIXSRC_PROVIDER = cfg.enableVixsrcProvider ? 'true':'false'; });
// Caching / validation / PStream // Caching / validation / PStream
process.env.DISABLE_CACHE = cfg.disableCache ? 'true':'false'; process.env.DISABLE_CACHE = cfg.disableCache ? 'true':'false';
process.env.ENABLE_PSTREAM_API = cfg.enablePStreamApi ? 'true':'false'; process.env.ENABLE_PSTREAM_API = cfg.enablePStreamApi ? 'true':'false';
process.env.DISABLE_URL_VALIDATION = cfg.disableUrlValidation ? 'true':'false'; process.env.DISABLE_URL_VALIDATION = cfg.disableUrlValidation ? 'true':'false';
process.env.DISABLE_4KHDHUB_URL_VALIDATION = cfg.disable4khdhubUrlValidation ? 'true':'false'; process.env.DISABLE_4KHDHUB_URL_VALIDATION = cfg.disable4khdhubUrlValidation ? 'true':'false';
process.env.ENABLE_PROXY = cfg.enableProxy ? 'true':'false';
if (cfg.providerCheckTmdbId) process.env.PROVIDER_CHECK_TMDB_ID = String(cfg.providerCheckTmdbId);
else delete process.env.PROVIDER_CHECK_TMDB_ID;
// Showbox specific // Showbox specific
if (cfg.showboxCacheDir) process.env.SHOWBOX_CACHE_DIR = cfg.showboxCacheDir; else delete process.env.SHOWBOX_CACHE_DIR; if (cfg.showboxCacheDir) process.env.SHOWBOX_CACHE_DIR = cfg.showboxCacheDir; else delete process.env.SHOWBOX_CACHE_DIR;
process.env.SHOWBOX_USE_ROTATING_PROXY = cfg.showboxUseRotatingProxy ? 'true':'false'; // Proxy settings removed; ensure legacy envs are cleared
if (cfg.showboxProxyPrimary) process.env.SHOWBOX_PROXY_URL_VALUE = cfg.showboxProxyPrimary; else delete process.env.SHOWBOX_PROXY_URL_VALUE; delete process.env.SHOWBOX_USE_ROTATING_PROXY;
if (cfg.showboxProxyAlternate) process.env.SHOWBOX_PROXY_URL_ALTERNATE = cfg.showboxProxyAlternate; else delete process.env.SHOWBOX_PROXY_URL_ALTERNATE; delete process.env.SHOWBOX_PROXY_URL_VALUE;
// Other provider proxies delete process.env.SHOWBOX_PROXY_URL_ALTERNATE;
if (cfg.xprimeProxyUrl) process.env.XPRIME_PROXY_URL = cfg.xprimeProxyUrl; else delete process.env.XPRIME_PROXY_URL; delete process.env.VIDZEE_PROXY_URL;
if (cfg.vidzeeProxyUrl) process.env.VIDZEE_PROXY_URL = cfg.vidzeeProxyUrl; else delete process.env.VIDZEE_PROXY_URL; delete process.env.VIDSRC_PROXY_URL;
if (cfg.vidsrcProxyUrl) process.env.VIDSRC_PROXY_URL = cfg.vidsrcProxyUrl; else delete process.env.VIDSRC_PROXY_URL; delete process.env.MOVIESMOD_PROXY_URL;
if (cfg.moviesmodProxyUrl) process.env.MOVIESMOD_PROXY_URL = cfg.moviesmodProxyUrl; else delete process.env.MOVIESMOD_PROXY_URL;
if (cfg.defaultRegion) process.env.FEBBOX_REGION = cfg.defaultRegion; // alias if (cfg.defaultRegion) process.env.FEBBOX_REGION = cfg.defaultRegion; // alias
} }
@@ -150,26 +172,23 @@ function loadConfig() {
tmdbApiKeys: parseJsonMaybe(process.env.TMDB_API_KEYS) || null, tmdbApiKeys: parseJsonMaybe(process.env.TMDB_API_KEYS) || null,
febboxCookies: parseCookies(process.env.FEBBOX_COOKIES), febboxCookies: parseCookies(process.env.FEBBOX_COOKIES),
// Extended advanced config (may not exist in env) // Extended advanced config (may not exist in env)
enableShowboxProvider: process.env.ENABLE_SHOWBOX_PROVIDER,
enableXprimeProvider: process.env.ENABLE_XPRIME_PROVIDER,
enable4khdhubProvider: process.env.ENABLE_4KHDHUB_PROVIDER,
enableMoviesmodProvider: process.env.ENABLE_MOVIESMOD_PROVIDER,
enableMp4hydraProvider: process.env.ENABLE_MP4HYDRA_PROVIDER,
enableVidzeeProvider: process.env.ENABLE_VIDZEE_PROVIDER,
enableVixsrcProvider: process.env.ENABLE_VIXSRC_PROVIDER,
disableCache: process.env.DISABLE_CACHE, disableCache: process.env.DISABLE_CACHE,
enablePStreamApi: process.env.ENABLE_PSTREAM_API, enablePStreamApi: process.env.ENABLE_PSTREAM_API,
showboxUseRotatingProxy: process.env.SHOWBOX_USE_ROTATING_PROXY,
showboxCacheDir: process.env.SHOWBOX_CACHE_DIR || null, showboxCacheDir: process.env.SHOWBOX_CACHE_DIR || null,
showboxProxyPrimary: process.env.SHOWBOX_PROXY_URL_VALUE || null, // Proxy/env paths removed; always direct connections
showboxProxyAlternate: process.env.SHOWBOX_PROXY_URL_ALTERNATE || null,
xprimeProxyUrl: process.env.XPRIME_PROXY_URL || null,
vidzeeProxyUrl: process.env.VIDZEE_PROXY_URL || null,
vidsrcProxyUrl: process.env.VIDSRC_PROXY_URL || null,
moviesmodProxyUrl: process.env.MOVIESMOD_PROXY_URL || null,
disableUrlValidation: process.env.DISABLE_URL_VALIDATION, disableUrlValidation: process.env.DISABLE_URL_VALIDATION,
disable4khdhubUrlValidation: process.env.DISABLE_4KHDHUB_URL_VALIDATION disable4khdhubUrlValidation: process.env.DISABLE_4KHDHUB_URL_VALIDATION,
enableProxy: process.env.ENABLE_PROXY,
providerCheckTmdbId: process.env.PROVIDER_CHECK_TMDB_ID || null
}; };
// Dynamic provider enable flags from env
const providerNames = getProviderNames();
providerNames.forEach(name => {
const envName = `ENABLE_${name.toUpperCase()}_PROVIDER`;
envCfg[`enable${name.charAt(0).toUpperCase() + name.slice(1)}Provider`] = process.env[envName];
});
const override = readOverrideFile(); const override = readOverrideFile();
const merged = { ...envCfg, ...override }; const merged = { ...envCfg, ...override };
const normalized = normalizeConfig(merged); const normalized = normalizeConfig(merged);
+117
View File
@@ -0,0 +1,117 @@
class JsUnpacker {
static Unbase = class {
constructor(radix) {
this.ALPHABET_62 =
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
this.ALPHABET_95 =
' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~';
this.alphabet = null;
this.dictionary = {};
this.radix = radix;
if (radix > 36) {
if (radix < 62) {
this.alphabet = this.ALPHABET_62.substring(0, radix);
} else if (radix > 62 && radix < 95) {
this.alphabet = this.ALPHABET_95.substring(0, radix);
} else if (radix === 62) {
this.alphabet = this.ALPHABET_62;
} else if (radix === 95) {
this.alphabet = this.ALPHABET_95;
}
for (let i = 0; i < this.alphabet.length; i++) {
this.dictionary[this.alphabet[i]] = i;
}
}
}
unbase(str) {
let ret = 0;
if (this.alphabet === null) {
ret = parseInt(str, this.radix);
} else {
const tmp = str.split('').reverse().join('');
for (let i = 0; i < tmp.length; i++) {
ret += Math.pow(this.radix, i) * this.dictionary[tmp[i]];
}
}
return ret;
}
};
constructor(packedJS) {
this.packedJS = packedJS;
}
detect() {
const js = this.packedJS.replace(/\s/g, '');
const regex = /eval\(function\(p,a,c,k,e,(?:r|d)/;
return regex.test(js);
}
unpack() {
let js = this.packedJS;
try {
const regex =
/}\s*\('(.*)',\s*(.*?),\s*(\d+),\s*'(.*?)'\.split\('\|'\)/s;
const match = js.match(regex);
if (match && match.length === 5) {
let payload = match[1].replace(/\\'/g, "'");
const radixStr = match[2];
const countStr = match[3];
const symtab = match[4].split('|');
let radix = 36;
let count = 0;
try {
radix = parseInt(radixStr);
} catch {
// Ignore URL parsing errors
}
try {
count = parseInt(countStr);
} catch {
// Ignore URL parsing errors
}
if (symtab.length !== count) {
throw new Error('Unknown p.a.c.k.e.r. encoding');
}
const unbase = new this.constructor.Unbase(radix);
const wordRegex = /\b\w+\b/g;
let decoded = payload;
let replaceOffset = 0;
let wordMatch;
while ((wordMatch = wordRegex.exec(payload)) !== null) {
const word = wordMatch[0];
const x = unbase.unbase(word);
let value = null;
if (x < symtab.length) {
value = symtab[x];
}
if (value && value.length > 0) {
const start = wordMatch.index + replaceOffset;
const end = start + word.length;
decoded =
decoded.substring(0, start) +
value +
decoded.substring(end);
replaceOffset += value.length - word.length;
}
}
return decoded;
}
} catch (e) {
return null;
}
return null;
}
}
// CommonJS exports
module.exports = JsUnpacker;
+264
View File
@@ -0,0 +1,264 @@
// Universal search-result matching system.
// Ported from Infinite-streams (src/utils/match.ts) for title-search providers.
const QUALITY_TAGS =
/\b(4k|2160p|1080p|720p|480p|360p|hd|fhd|uhd|hdr|web[-\s]?dl|webrip|web|bluray|blu-ray|brrip|bdrip|dvdrip|hdrip|hdtv|hdcam|cam|dual audio|multi audio|dual[-\s]?audio|multi[-\s]?audio|x264|x265|hevc|h264|h265|10bit|esub|esubs|msubs|amzn|nf|hin|eng|hindi|english|dubbed|dub|subbed|sub|season\s*\d+|s\d{1,2}|complete|full)\b/gi;
const STOP_WORDS = new Set(['the', 'a', 'an', 'of', 'in', 'at', 'to', 'with', 'and', 'or', 'for', 'on']);
function baseNormalize(s) {
return s
.toLowerCase()
.replace(/([a-z])\./g, '$1')
.replace(/[^\w\s]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function normalizeStripQuality(s) {
return baseNormalize(String(s).replace(QUALITY_TAGS, ' '));
}
function tokenize(s) {
return baseNormalize(s).split(' ').filter(Boolean);
}
function significantTokens(s) {
return tokenize(s).filter((w) => w.length > 1 && !STOP_WORDS.has(w));
}
function bigrams(s) {
const clean = baseNormalize(s).replace(/\s+/g, '');
const out = [];
for (let i = 0; i < clean.length - 1; i++) out.push(clean.slice(i, i + 2));
return out;
}
function diceCoefficient(a, b) {
const ba = bigrams(a);
const bb = bigrams(b);
if (ba.length === 0 || bb.length === 0) return ba.length === bb.length ? 1 : 0;
const counts = new Map();
for (const g of ba) counts.set(g, (counts.get(g) || 0) + 1);
let matches = 0;
for (const g of bb) {
const c = counts.get(g) || 0;
if (c > 0) {
matches++;
counts.set(g, c - 1);
}
}
return (2 * matches) / (ba.length + bb.length);
}
function scoreTitleText(query, candidate) {
if (!query || !candidate) {
return { exact: 0, normalized: 0, fuzzy: 0, wholeWord: 0, startsWith: 0, score: 0 };
}
const exact = String(query).trim().toLowerCase() === String(candidate).trim().toLowerCase() ? 1 : 0;
const nq = normalizeStripQuality(query);
const nc = normalizeStripQuality(candidate);
let normalized = nq === nc ? 1 : 0;
if (!normalized && nq.replace(/\s+/g, '') === nc.replace(/\s+/g, '')) normalized = 0.92;
const tokenJaccardOf = () => {
const qa = new Set(significantTokens(nq));
const ca = new Set(significantTokens(nc));
if (qa.size === 0 || ca.size === 0) return 0;
let intersection = 0;
for (const t of qa) if (ca.has(t)) intersection++;
const union = qa.size + ca.size - intersection;
return union === 0 ? 0 : intersection / union;
};
const fuzzy = 0.5 * tokenJaccardOf() + 0.5 * diceCoefficient(nq, nc);
const qSig = significantTokens(nq);
const cSigSet = new Set(significantTokens(nc));
const wholeWord = qSig.length === 0 ? 0 : qSig.filter((w) => cSigSet.has(w)).length / qSig.length;
let startsWith = 0;
if (nc.startsWith(nq) || nq.startsWith(nc)) startsWith = 1;
else {
const qFirst = nq.split(' ')[0];
const cFirst = nc.split(' ')[0];
if (qFirst && cFirst && qFirst === cFirst) startsWith = 0.5;
}
const score = exact * 0.35 + normalized * 0.25 + fuzzy * 0.2 + wholeWord * 0.1 + startsWith * 0.1;
return { exact, normalized, fuzzy, wholeWord, startsWith, score: Math.min(1, score) };
}
function bestTitleScore(query, candidate) {
const attempts = [{ label: 'title', text: candidate.title }];
if (candidate.originalTitle) attempts.push({ label: 'originalTitle', text: candidate.originalTitle });
for (const alias of candidate.aliases || []) attempts.push({ label: `alias:${alias}`, text: alias });
const queryTitles = [query.title, query.originalTitle, ...(query.aliases || [])].filter((t) => !!t);
let best = null;
for (const q of queryTitles) {
for (const a of attempts) {
const breakdown = scoreTitleText(q, a.text);
if (!best || breakdown.score > best.score) {
best = { score: breakdown.score, breakdown, matchedOn: a.label };
}
}
}
return best || { score: 0, breakdown: scoreTitleText('', ''), matchedOn: 'title' };
}
function scoreYear(query, candidate) {
if (!query || !candidate) return 0.5;
const diff = Math.abs(query - candidate);
if (diff === 0) return 1;
if (diff === 1) return 0.6;
if (diff === 2) return 0.3;
return 0.05;
}
function scoreType(query, candidate) {
if (!query || !candidate) return 0.5;
return query === candidate ? 1 : 0;
}
function scoreSeasonEpisode(query, candidate) {
if (query.type !== 'series') return 1;
if (query.season == null || candidate.season == null) return 0.7;
if (query.season !== candidate.season) return 0.15;
if (query.episode == null || candidate.episode == null) return 1;
return query.episode === candidate.episode ? 1 : 0.6;
}
function round(n) {
return Math.round(n * 1000) / 1000;
}
function scoreCandidate(query, candidate) {
const text = bestTitleScore(query, candidate);
const year = scoreYear(query.year, candidate.year);
const type = scoreType(query.type, candidate.type);
const seasonEpisode = scoreSeasonEpisode(query, candidate);
const score = text.score * 0.6 + year * 0.1 + type * 0.2 + seasonEpisode * 0.1;
return {
score: Math.min(1, Math.max(0, score)),
matchedOn: text.matchedOn,
breakdown: {
textScore: round(text.score),
exact: round(text.breakdown.exact),
normalized: round(text.breakdown.normalized),
fuzzy: round(text.breakdown.fuzzy),
wholeWord: round(text.breakdown.wholeWord),
startsWith: round(text.breakdown.startsWith),
year: round(year),
type: round(type),
seasonEpisode: round(seasonEpisode),
},
};
}
const DEFAULT_THRESHOLD = 0.45;
function findBestMatch(query, candidates, options) {
const threshold = options.threshold || DEFAULT_THRESHOLD;
const ranked = candidates
.map((candidate) => {
const { score, breakdown, matchedOn } = scoreCandidate(query, candidate);
return { candidate, score, breakdown, matchedOn };
})
.sort((a, b) => b.score - a.score);
if (!options.quiet) {
console.log(`[Match:${options.provider}] scored candidates`, {
provider: options.provider,
query: options.query || query.title,
resultCount: candidates.length,
candidates: ranked.map((r) => ({
title: r.candidate.title,
year: r.candidate.year,
type: r.candidate.type,
score: round(r.score),
matchedOn: r.matchedOn,
})),
});
}
const top = ranked[0];
if (!top) {
console.log(`[Match:${options.provider}] no candidates returned by search`);
return { best: null, score: 0, breakdown: {}, matchedOn: '', reason: 'no candidates returned by search', ranked };
}
const passed = top.score >= threshold;
const reason = passed
? `best candidate "${top.candidate.title}" scored ${round(top.score)} (>= threshold ${threshold}) via ${top.matchedOn}`
: `best candidate "${top.candidate.title}" scored ${round(top.score)} which is below threshold ${threshold} — rejected as a likely mismatch`;
console.log(`[Match:${options.provider}] ${passed ? 'selected result' : 'rejected — no match above threshold'}`, {
provider: options.provider,
query: options.query || query.title,
resultCount: candidates.length,
selected: passed ? top.candidate.title : null,
score: round(top.score),
threshold,
breakdown: top.breakdown,
reason,
});
if (!passed) {
return { best: null, score: top.score, breakdown: top.breakdown, matchedOn: top.matchedOn, reason, ranked };
}
return { best: top.candidate, score: top.score, breakdown: top.breakdown, matchedOn: top.matchedOn, reason, ranked };
}
async function findBestMatchWithRetry(query, variantTitles, search, options) {
const threshold = options.threshold || DEFAULT_THRESHOLD;
const tried = new Set();
let bestSoFar = null;
for (const variant of variantTitles) {
const key = String(variant).trim().toLowerCase();
if (!key || tried.has(key)) continue;
tried.add(key);
let candidates;
try {
candidates = await search(variant);
} catch (err) {
console.warn(`[Match:${options.provider}] retry search failed:`, err.message);
continue;
}
if (!candidates || !candidates.length) continue;
const result = findBestMatch({ ...query, title: variant }, candidates, { ...options, query: variant });
if (!bestSoFar || result.score > bestSoFar.score) bestSoFar = result;
if (result.best && result.score >= threshold) {
console.log(`[Match:${options.provider}] retry succeeded with variant "${variant}"`);
return result;
}
}
if (bestSoFar && bestSoFar.score > 0) {
console.log(`[Match:${options.provider}] no variant cleared threshold — treating as not found (near-miss logged, not returned)`);
} else {
console.log(`[Match:${options.provider}] no results found across any title variant`);
}
return {
best: null,
score: bestSoFar ? bestSoFar.score : 0,
breakdown: bestSoFar ? bestSoFar.breakdown : {},
matchedOn: bestSoFar ? bestSoFar.matchedOn : '',
reason: bestSoFar
? `best candidate across all variants scored ${round(bestSoFar.score)}, below threshold ${threshold} — rejected`
: 'no results found across any title variant',
ranked: bestSoFar ? bestSoFar.ranked : [],
};
}
function buildRetryTitleVariants(opts) {
const variants = [opts.title, opts.originalTitle, ...(opts.aliases || [])].filter((t) => !!t && String(t).trim().length > 0);
return [...new Set(variants)];
}
module.exports = { findBestMatch, findBestMatchWithRetry, buildRetryTitleVariants, scoreCandidate, normalizeStripQuality };
+4 -3
View File
@@ -1,5 +1,5 @@
const fetch = (...args) => import('node-fetch').then(m => m.default(...args)); const fetch = (...args) => import('node-fetch').then(m => m.default(...args));
const { config } = require('./config'); const { getTmdbApiKey } = require('./tmdbKey');
const cache = { const cache = {
imdbByTmdb: new Map(), imdbByTmdb: new Map(),
@@ -9,9 +9,10 @@ const TTL_MS = 6 * 60 * 60 * 1000;
function isFresh(entry) { return entry && (Date.now() - entry.ts) < TTL_MS; } function isFresh(entry) { return entry && (Date.now() - entry.ts) < TTL_MS; }
async function tmdbFetchJson(url) { async function tmdbFetchJson(url) {
if (!config.tmdbApiKey) throw new Error('TMDB_API_KEY missing'); const apiKey = getTmdbApiKey();
if (!apiKey) throw new Error('TMDB_API_KEY missing');
const sep = url.includes('?') ? '&' : '?'; const sep = url.includes('?') ? '&' : '?';
const full = `${url}${sep}api_key=${config.tmdbApiKey}`; const full = `${url}${sep}api_key=${apiKey}`;
const res = await fetch(full, { timeout: 15000 }); const res = await fetch(full, { timeout: 15000 });
if (!res.ok) throw new Error(`TMDB request failed ${res.status}`); if (!res.ok) throw new Error(`TMDB request failed ${res.status}`);
return res.json(); return res.json();

Some files were not shown because too many files have changed in this diff Show More