mirror of
https://github.com/Inside4ndroid/TMDB-Embed-API.git
synced 2026-10-22 17:45:11 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ebb0d32d5 | ||
|
|
c8ef89398f | ||
|
|
9714ad51c8 | ||
|
|
17079e1646 | ||
|
|
25cc59659d | ||
|
|
ab18e06a94 | ||
|
|
25acff24a7 | ||
|
|
0e9f6d92e4 | ||
|
|
3433c093bd | ||
|
|
e489a1b613 | ||
|
|
484ea4bb3f |
@@ -1 +1,3 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
|
restart.trigger
|
||||||
|
.vscode/
|
||||||
+160
-1
@@ -1,5 +1,164 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [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).
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- README: implicit improvements carried forward (no separate section required) plus badge bump.
|
||||||
|
- CHANGELOG: recorded AES decoder details & tail prefetch instrumentation.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
## [1.0.4] - 2025-09-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
```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
|
||||||
|
- Initial stable release.
|
||||||
|
|
||||||
|
[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.3]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.2...v1.0.3
|
||||||
|
[1.0.4]: https://github.com/Inside4ndroid/TMDB-Embed-API/compare/v1.0.3...v1.0.4
|
||||||
|
[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
|
||||||
|
|
||||||
## [1.0.0] - 2025-09-16
|
## [1.0.0] - 2025-09-16
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -47,4 +206,4 @@ This 1.0.0 release consolidates modernization work: provider cleanup, configurat
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[1.0.0]: https://github.com/Inside4ndroid/TMDB-Embed-API/releases/1.0.0
|
[1.0.0]: https://github.com/Inside4ndroid/TMDB-Embed-API/releases/v1.0.0
|
||||||
|
|||||||
@@ -22,6 +22,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
|
||||||
@@ -39,6 +40,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
|
||||||
|
|||||||
@@ -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.0.8-informational?style=flat" />
|
||||||
<img src="https://img.shields.io/docker/pulls/inside4ndroid/tmdb-embed-api?label=Docker%20Pulls&style=flat" />
|
<img src="https://img.shields.io/docker/pulls/inside4ndroid/tmdb-embed-api?label=Docker%20Pulls&style=flat" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -34,14 +34,32 @@
|
|||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
- **Multi‑TMDB Key Rotation** – Supply multiple API keys; one is chosen randomly per request.
|
- **Multi‑TMDB Key Rotation** – Supply multiple API keys; one is chosen randomly per request.
|
||||||
- **Provider Aggregation** – Pluggable providers (Showbox, 4khdhub, MoviesMod, MP4Hydra, VidZee, Vixsrc, Xprime) with per‑provider enable toggles + default selection.
|
- **Provider Aggregation** – Pluggable providers (Showbox, 4khdhub, MoviesMod, MP4Hydra, VidZee, Vixsrc, UHDMovies) with per‑provider enable toggles + default selection.
|
||||||
|
- **🔥 Plugin System** – Drop new provider files in `providers/` and add its exported function to the registry map (`providers/registry.js` → `providerFunctionMap`).
|
||||||
- **Dynamic Filtering** – Minimum quality presets, custom JSON quality map, codec exclusion rules (presets + JSON).
|
- **Dynamic Filtering** – Minimum quality presets, custom JSON quality map, codec exclusion rules (presets + JSON).
|
||||||
- **Runtime Overrides UI** – Fully interactive web admin at `/` (login protected) writing to `utils/user-config.json`.
|
- **Runtime Overrides UI** – Fully interactive web admin at `/` (login protected) writing to `utils/user-config.json`.
|
||||||
- **Session Auth + Rate Limiting** – Login system with brute‑force lockouts, logout, password change.
|
- **Session Auth + Rate Limiting** – Login system with brute‑force lockouts, logout, password change.
|
||||||
- **Status & Health Panel** – Live metrics, provider status, endpoint list, functional provider checks.
|
- **Status & Health Panel** – Live metrics, provider status, endpoint list, functional provider checks.
|
||||||
- **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.
|
- **Extensible** – Simple drop-in provider plugin system.
|
||||||
|
- **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 +71,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/
|
||||||
@@ -79,6 +97,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
|
||||||
@@ -142,6 +165,11 @@ If both `TMDB_API_KEY` and `TMDB_API_KEYS` are provided, rotation uses the array
|
|||||||
```bash
|
```bash
|
||||||
docker compose pull # if using an external registry (future)
|
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
|
||||||
@@ -188,7 +216,7 @@ Saving in the UI writes only changed keys. Setting a field to empty removes the
|
|||||||
| 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 |
|
| FebBox / PStream | Manage FebBox cookies powering Showbox provider |
|
||||||
| Advanced | Provider toggles, cache & validation flags, proxy paths |
|
| Advanced | Provider toggles, cache & validation flags |
|
||||||
| 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 +225,61 @@ 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/PStream integration
|
||||||
|
- `4khdhub` - 4KHDHub streams
|
||||||
|
- `moviesmod` - MoviesMod streams
|
||||||
|
- `mp4hydra` - MP4Hydra streams
|
||||||
|
- `vidzee` - VidZee streams
|
||||||
|
- `vixsrc` - Vixsrc streams
|
||||||
|
- `uhdmovies` - UHD Movies streams
|
||||||
|
|
||||||
### Adding a New Provider (Outline)
|
|
||||||
1. Create `providers/myprovider.js` exporting an async fetch function (see existing files for patterns).
|
### Adding a New Provider
|
||||||
2. Lazy‑load it in `providers/registry.js` similar to others.
|
1. **Create** `providers/yourprovider.js` with your stream fetching logic
|
||||||
3. Add a config flag (`enableMyproviderProvider`) if you want toggle support.
|
2. **Export** a function like `getYourproviderStreams(tmdbId, mediaType, season, episode)`
|
||||||
4. Update the provider matrix arrays in `public/index.js`.
|
3. **Register** it in `providers/registry.js` → `providerFunctionMap`:
|
||||||
|
```js
|
||||||
|
// providers/registry.js
|
||||||
|
const providerFunctionMap = {
|
||||||
|
'Showbox.js': 'getStreamsFromTmdbId',
|
||||||
|
'4khdhub.js': 'get4KHDHubStreams',
|
||||||
|
'moviesmod.js': 'getMoviesModStreams',
|
||||||
|
'MP4Hydra.js': 'getMP4HydraStreams',
|
||||||
|
'VidZee.js': 'getVidZeeStreams',
|
||||||
|
'vixsrc.js': 'getVixsrcStreams',
|
||||||
|
'uhdmovies.js': 'getUHDMoviesStreams',
|
||||||
|
'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 [{
|
||||||
|
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!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -233,23 +300,34 @@ Aggregate endpoint auto-resolves IMDb when needed and merges provider output bef
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧪 Stream Object Shape (Typical)
|
## 🧪 Stream Object Schema (Unified)
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"title": "Example Title",
|
"title": "Fight Club - 1080p [MP4Hydra #2]",
|
||||||
"url": "https://...",
|
"url": "https://stream.url/video.mp4",
|
||||||
"quality": "1080p",
|
"quality": "1080p",
|
||||||
"provider": "showbox",
|
"provider": "mp4hydra",
|
||||||
"size": null,
|
"headers": { "User-Agent": "Mozilla/5.0" }
|
||||||
"languages": ["en"],
|
|
||||||
"subtitles": [],
|
|
||||||
"codecs": ["H264"],
|
|
||||||
"headers": { "Referer": "..." }
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Filtering passes through `applyFilters` to enforce min quality + codec exclusions.
|
Filtering passes through `applyFilters` to enforce min quality + codec exclusions.
|
||||||
|
|
||||||
|
> 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 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| disableCache | false | Disables internal caches (Showbox + proxy segment cache) |
|
||||||
|
| enablePStreamApi | true | Enables Showbox PStream-specific handling |
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
+53
-4
@@ -3,14 +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();
|
||||||
|
|
||||||
|
// 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 }
|
||||||
const MAX_ATTEMPTS_WINDOW = 5; // attempts allowed
|
const MAX_ATTEMPTS_WINDOW = 5; // attempts allowed
|
||||||
@@ -66,8 +75,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 +210,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() });
|
||||||
@@ -315,6 +349,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 +381,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 +394,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');
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Generated
+13
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "tmdb-embed-api",
|
"name": "tmdb-embed-api",
|
||||||
"version": "1.0.0",
|
"version": "1.0.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "tmdb-embed-api",
|
"name": "tmdb-embed-api",
|
||||||
"version": "1.0.0",
|
"version": "1.0.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"aromanize": "^0.1.5",
|
"aromanize": "^0.1.5",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
@@ -674,6 +674,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios-cookiejar-support/node_modules/undici": {
|
||||||
|
"version": "7.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz",
|
||||||
|
"integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/b4a": {
|
"node_modules/b4a": {
|
||||||
"version": "1.6.7",
|
"version": "1.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
|
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
|
||||||
|
|||||||
+3
-4
@@ -1,12 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "tmdb-embed-api",
|
"name": "tmdb-embed-api",
|
||||||
"version": "1.0.0",
|
"version": "1.0.8",
|
||||||
"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 **/tmdb-embed-showbox-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": {
|
||||||
|
|||||||
@@ -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
|
|
||||||
+133
-34
@@ -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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -397,8 +409,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;
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
@@ -749,12 +763,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 +789,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 +824,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 +840,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 +871,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 +887,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 {
|
||||||
@@ -880,7 +926,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 +942,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 +962,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,7 +978,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: {}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (text.includes('10Gbps')) {
|
} else if (text.includes('10Gbps')) {
|
||||||
@@ -966,7 +1020,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,7 +1036,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: {}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1016,7 +1074,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 +1090,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 +1336,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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1388,7 +1454,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 +1470,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 +1533,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 +1549,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 {
|
||||||
@@ -1740,16 +1814,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;
|
||||||
|
|
||||||
|
|||||||
+11
-29
@@ -126,14 +126,11 @@ async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNu
|
|||||||
title: `${details.title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
|
title: `${details.title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
|
||||||
url: processedEpisode.videoUrl,
|
url: processedEpisode.videoUrl,
|
||||||
quality: processedEpisode.quality,
|
quality: processedEpisode.quality,
|
||||||
provider: "MP4Hydra",
|
provider: "mp4hydra",
|
||||||
size: "Unknown size",
|
|
||||||
behaviorHints: {
|
|
||||||
notWebReady: true,
|
|
||||||
headers: {
|
headers: {
|
||||||
'Referer': 'https://mp4hydra.org/'
|
'Referer': 'https://mp4hydra.org/'
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
// Add subtitle tracks if available
|
||||||
@@ -172,14 +169,11 @@ async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNu
|
|||||||
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
||||||
url: processedItem.videoUrl,
|
url: processedItem.videoUrl,
|
||||||
quality: processedItem.quality,
|
quality: processedItem.quality,
|
||||||
provider: "MP4Hydra",
|
provider: "mp4hydra",
|
||||||
size: "Unknown size",
|
|
||||||
behaviorHints: {
|
|
||||||
notWebReady: true,
|
|
||||||
headers: {
|
headers: {
|
||||||
'Referer': 'https://mp4hydra.org/'
|
'Referer': 'https://mp4hydra.org/'
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
// Add subtitle tracks if available
|
||||||
@@ -297,14 +291,11 @@ async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) {
|
|||||||
title: `${details.original_title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
|
title: `${details.original_title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
|
||||||
url: processedEpisode.videoUrl,
|
url: processedEpisode.videoUrl,
|
||||||
quality: processedEpisode.quality,
|
quality: processedEpisode.quality,
|
||||||
provider: "MP4Hydra",
|
provider: "mp4hydra",
|
||||||
size: "Unknown size",
|
|
||||||
behaviorHints: {
|
|
||||||
notWebReady: true,
|
|
||||||
headers: {
|
headers: {
|
||||||
'Referer': 'https://mp4hydra.org/'
|
'Referer': 'https://mp4hydra.org/'
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
// Add subtitle tracks if available
|
||||||
@@ -340,17 +331,10 @@ async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) {
|
|||||||
|
|
||||||
// Convert to standard stream format
|
// Convert to standard stream format
|
||||||
streams.push({
|
streams.push({
|
||||||
|
name: `${details.original_title}`,
|
||||||
title: `${details.original_title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
title: `${details.original_title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
||||||
url: processedItem.videoUrl,
|
url: processedItem.videoUrl,
|
||||||
quality: processedItem.quality,
|
quality: processedItem.quality
|
||||||
provider: "MP4Hydra",
|
|
||||||
size: "Unknown size",
|
|
||||||
behaviorHints: {
|
|
||||||
notWebReady: true,
|
|
||||||
headers: {
|
|
||||||
'Referer': 'https://mp4hydra.org/'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
// Add subtitle tracks if available
|
||||||
@@ -435,17 +419,15 @@ async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) {
|
|||||||
|
|
||||||
// Convert to standard stream format
|
// Convert to standard stream format
|
||||||
streams.push({
|
streams.push({
|
||||||
|
name: `${details.title}`,
|
||||||
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
||||||
url: processedItem.videoUrl,
|
url: processedItem.videoUrl,
|
||||||
quality: processedItem.quality,
|
quality: processedItem.quality,
|
||||||
provider: "MP4Hydra",
|
provider: "mp4hydra",
|
||||||
size: "Unknown size",
|
|
||||||
behaviorHints: {
|
|
||||||
notWebReady: true,
|
|
||||||
headers: {
|
headers: {
|
||||||
'Referer': 'https://mp4hydra.org/'
|
'Referer': 'https://mp4hydra.org/'
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtitle tracks if available
|
// Add subtitle tracks if available
|
||||||
|
|||||||
+33
-78
@@ -169,8 +169,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) => {
|
||||||
@@ -1355,8 +1354,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 +1372,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 +1390,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 +1423,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 +1980,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 +2009,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 +2206,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 +2228,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 +2514,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 +2680,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 +2820,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 +2871,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 +2897,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 +2918,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'
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-17
@@ -1,5 +1,43 @@
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
|
||||||
|
// Confirmed AES-CBC decoder for VidZee encrypted link format.
|
||||||
|
// Format (after initial atob in site script): ivBase64:cipherBase64
|
||||||
|
// ivBase64 -> CryptoJS Base64 parsed to WordArray (IV)
|
||||||
|
// Key: UTF-8 bytes of "qrincywincyspider" padded with nulls to 32 bytes (AES-256-CBC)
|
||||||
|
// Cipher: base64 ciphertext, PKCS7 padding, mode CBC.
|
||||||
|
let cryptoJs; // lazy load to avoid cost if not needed
|
||||||
|
function decodeVidZeeToken(token, debug) {
|
||||||
|
try {
|
||||||
|
if (typeof token !== 'string') return null;
|
||||||
|
if (/^https?:\/\//i.test(token)) return null; // already a URL
|
||||||
|
// Expect pattern base64:base64 (iv:cipher)
|
||||||
|
const raw = Buffer.from(token, 'base64').toString('utf8');
|
||||||
|
if (!raw.includes(':')) return null;
|
||||||
|
const [ivB64, cipherB64] = raw.split(':');
|
||||||
|
if (!ivB64 || !cipherB64) return null;
|
||||||
|
if (!cryptoJs) cryptoJs = require('crypto-js');
|
||||||
|
const iv = cryptoJs.enc.Base64.parse(ivB64.trim());
|
||||||
|
const keyStr = 'qrincywincyspider';
|
||||||
|
// Pad key with nulls to 32 bytes
|
||||||
|
const keyUtf8 = cryptoJs.enc.Utf8.parse(keyStr.padEnd(32, '\0'));
|
||||||
|
const decrypted = cryptoJs.AES.decrypt(cipherB64.trim(), keyUtf8, {
|
||||||
|
iv,
|
||||||
|
mode: cryptoJs.mode.CBC,
|
||||||
|
padding: cryptoJs.pad.Pkcs7
|
||||||
|
}).toString(cryptoJs.enc.Utf8);
|
||||||
|
if (!decrypted) return null;
|
||||||
|
if (!/^https?:\/\//i.test(decrypted)) {
|
||||||
|
if (debug) console.log('[VidZee] decrypted but not a URL', decrypted.slice(0,80));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (debug) console.log('[VidZee] AES decoded token', { tokenSnippet: token.slice(0, 24)+'...', url: decrypted.slice(0,120) });
|
||||||
|
return decrypted.trim();
|
||||||
|
} catch (e) {
|
||||||
|
if (debug) console.log('[VidZee] AES decode error', e.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Removed unused parseArgs helper
|
// Removed unused parseArgs helper
|
||||||
|
|
||||||
const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
|
const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
|
||||||
@@ -35,16 +73,11 @@ const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
|
|||||||
|
|
||||||
let finalApiUrl;
|
let finalApiUrl;
|
||||||
const headers = {
|
const headers = {
|
||||||
'Referer': `https://player.vidzee.wtf/embed/movie/${tmdbId}` // Default for proxy method
|
'Referer': `https://player.vidzee.wtf/embed/movie/${tmdbId}`
|
||||||
};
|
};
|
||||||
const timeout = 7000; // Reduced timeout
|
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;
|
finalApiUrl = targetApiUrl;
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[VidZee] Fetching from server ${sr}: ${targetApiUrl}`);
|
console.log(`[VidZee] Fetching from server ${sr}: ${targetApiUrl}`);
|
||||||
|
|
||||||
@@ -78,29 +111,28 @@ const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const streams = apiSources.map(sourceItem => {
|
const streams = apiSources.map(sourceItem => {
|
||||||
// Prefer sourceItem.name as label, fallback to sourceItem.type, then 'VidZee Stream'
|
|
||||||
const label = sourceItem.name || sourceItem.type || 'VidZee';
|
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;
|
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())) {
|
if (!/(\d{3,4})p/.test(quality.toLowerCase())) {
|
||||||
quality = '720p';
|
quality = '720p';
|
||||||
}
|
}
|
||||||
const language = sourceItem.language || sourceItem.lang;
|
const language = sourceItem.language || sourceItem.lang;
|
||||||
|
let rawLink = sourceItem.link;
|
||||||
|
const debug = process.env.VIDZEE_DEBUG === '1';
|
||||||
|
const decoded = decodeVidZeeToken(rawLink, debug);
|
||||||
|
if (decoded && /^https?:\/\//i.test(decoded)) {
|
||||||
|
if (debug) console.log('[VidZee] decoded link', { beforeSample: rawLink.slice(0,40)+'...', after: decoded.slice(0,80) });
|
||||||
|
rawLink = decoded;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
title: `VidZee S${sr} - ${quality}`,
|
name: `VidZee Server${sr} - ${quality} - ${language}`,
|
||||||
url: sourceItem.link, // Use sourceItem.link for the URL
|
title: `VidZee Server${sr} - ${quality} - ${language}`,
|
||||||
|
url: rawLink,
|
||||||
quality: quality,
|
quality: quality,
|
||||||
language: language,
|
|
||||||
provider: "VidZee",
|
provider: "VidZee",
|
||||||
size: "Unknown size",
|
|
||||||
behaviorHints: {
|
|
||||||
notWebReady: true,
|
|
||||||
headers: {
|
headers: {
|
||||||
'Referer': 'https://core.vidzee.wtf/'
|
'Referer': 'https://core.vidzee.wtf/'
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}).filter(stream => stream.url);
|
}).filter(stream => stream.url);
|
||||||
|
|
||||||
|
|||||||
+9
-88
@@ -25,13 +25,6 @@ function escapeRegExp(string) {
|
|||||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Proxy Configuration ---
|
|
||||||
const MOVIESMOD_PROXY_URL = process.env.MOVIESMOD_PROXY_URL;
|
|
||||||
if (MOVIESMOD_PROXY_URL) {
|
|
||||||
console.log(`[MoviesMod] Proxy support enabled: ${MOVIESMOD_PROXY_URL}`);
|
|
||||||
} else {
|
|
||||||
console.log('[MoviesMod] No proxy configured, using direct connections');
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Domain Fetching ---
|
// --- Domain Fetching ---
|
||||||
let moviesModDomain = 'https://moviesmod.chat'; // Fallback domain
|
let moviesModDomain = 'https://moviesmod.chat'; // Fallback domain
|
||||||
@@ -92,22 +85,14 @@ const saveToCache = async (key, data) => {
|
|||||||
// Initialize cache directory on startup
|
// Initialize cache directory on startup
|
||||||
ensureCacheDir();
|
ensureCacheDir();
|
||||||
|
|
||||||
// Proxy wrapper function
|
// Request helper
|
||||||
const makeRequest = async (url, options = {}) => {
|
const makeRequest = async (url, options = {}) => {
|
||||||
if (MOVIESMOD_PROXY_URL) {
|
|
||||||
// Route through proxy
|
|
||||||
const proxiedUrl = `${MOVIESMOD_PROXY_URL}${encodeURIComponent(url)}`;
|
|
||||||
console.log(`[MoviesMod] Making proxied request to: ${url}`);
|
|
||||||
return axios.get(proxiedUrl, options);
|
|
||||||
} else {
|
|
||||||
// Direct request
|
|
||||||
console.log(`[MoviesMod] Making direct request to: ${url}`);
|
console.log(`[MoviesMod] Making direct request to: ${url}`);
|
||||||
return axios.get(url, options);
|
return axios.get(url, options);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to create a proxied session for SID resolution
|
// Helper function to create a session for SID resolution
|
||||||
const createProxiedSession = async (jar) => {
|
const createSession = async (jar) => {
|
||||||
const { wrapper } = await getAxiosCookieJarSupport();
|
const { wrapper } = await getAxiosCookieJarSupport();
|
||||||
|
|
||||||
const sessionConfig = {
|
const sessionConfig = {
|
||||||
@@ -134,46 +119,6 @@ const createProxiedSession = async (jar) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// If proxy is enabled, wrap the session methods to use proxy
|
|
||||||
if (MOVIESMOD_PROXY_URL) {
|
|
||||||
console.log(`[MoviesMod] Creating SID session with proxy: ${MOVIESMOD_PROXY_URL}`);
|
|
||||||
const originalGet = session.get.bind(session);
|
|
||||||
const originalPost = session.post.bind(session);
|
|
||||||
|
|
||||||
session.get = async (url, options = {}) => {
|
|
||||||
const proxiedUrl = `${MOVIESMOD_PROXY_URL}${encodeURIComponent(url)}`;
|
|
||||||
console.log(`[MoviesMod] Making proxied SID GET request to: ${url}`);
|
|
||||||
|
|
||||||
// Extract cookies from jar and add to headers
|
|
||||||
const cookieString = await getCookiesForUrl(url);
|
|
||||||
if (cookieString) {
|
|
||||||
console.log(`[MoviesMod] Adding cookies to proxied request: ${cookieString}`);
|
|
||||||
options.headers = {
|
|
||||||
...options.headers,
|
|
||||||
'Cookie': cookieString
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalGet(proxiedUrl, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
session.post = async (url, data, options = {}) => {
|
|
||||||
const proxiedUrl = `${MOVIESMOD_PROXY_URL}${encodeURIComponent(url)}`;
|
|
||||||
console.log(`[MoviesMod] Making proxied SID POST request to: ${url}`);
|
|
||||||
|
|
||||||
// Extract cookies from jar and add to headers
|
|
||||||
const cookieString = await getCookiesForUrl(url);
|
|
||||||
if (cookieString) {
|
|
||||||
console.log(`[MoviesMod] Adding cookies to proxied request: ${cookieString}`);
|
|
||||||
options.headers = {
|
|
||||||
...options.headers,
|
|
||||||
'Cookie': cookieString
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalPost(proxiedUrl, data, options);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
};
|
};
|
||||||
@@ -430,8 +375,8 @@ async function resolveTechUnblockedLink(sidUrl) {
|
|||||||
const { origin } = new URL(sidUrl);
|
const { origin } = new URL(sidUrl);
|
||||||
const jar = new CookieJar();
|
const jar = new CookieJar();
|
||||||
|
|
||||||
// Create session with proxy support
|
// Create session
|
||||||
const session = await createProxiedSession(jar);
|
const session = await createSession(jar);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Step 0: Get the _wp_http value
|
// Step 0: Get the _wp_http value
|
||||||
@@ -679,24 +624,12 @@ async function resolveVideoSeedLink(videoSeedUrl) {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('keys', keys);
|
formData.append('keys', keys);
|
||||||
|
|
||||||
let apiResponse;
|
const apiResponse = await axios.post(apiUrl, formData, {
|
||||||
if (MOVIESMOD_PROXY_URL) {
|
|
||||||
const proxiedApiUrl = `${MOVIESMOD_PROXY_URL}${encodeURIComponent(apiUrl)}`;
|
|
||||||
console.log(`[MoviesMod] Making proxied POST request to VideoSeed API`);
|
|
||||||
apiResponse = await axios.post(proxiedApiUrl, formData, {
|
|
||||||
headers: {
|
headers: {
|
||||||
...formData.getHeaders(),
|
...formData.getHeaders(),
|
||||||
'x-token': new URL(videoSeedUrl).hostname
|
'x-token': new URL(videoSeedUrl).hostname
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
apiResponse = await axios.post(apiUrl, formData, {
|
|
||||||
headers: {
|
|
||||||
...formData.getHeaders(),
|
|
||||||
'x-token': new URL(videoSeedUrl).hostname
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (apiResponse.data && apiResponse.data.url) {
|
if (apiResponse.data && apiResponse.data.url) {
|
||||||
return apiResponse.data.url;
|
return apiResponse.data.url;
|
||||||
@@ -724,27 +657,13 @@ async function validateVideoUrl(url, timeout = 10000) {
|
|||||||
try {
|
try {
|
||||||
console.log(`[MoviesMod] Validating URL: ${url.substring(0, 100)}...`);
|
console.log(`[MoviesMod] Validating URL: ${url.substring(0, 100)}...`);
|
||||||
|
|
||||||
// Use proxy for URL validation if enabled
|
const response = await axios.head(url, {
|
||||||
let response;
|
|
||||||
if (MOVIESMOD_PROXY_URL) {
|
|
||||||
const proxiedUrl = `${MOVIESMOD_PROXY_URL}${encodeURIComponent(url)}`;
|
|
||||||
console.log(`[MoviesMod] Making proxied HEAD request for validation to: ${url}`);
|
|
||||||
response = await axios.head(proxiedUrl, {
|
|
||||||
timeout,
|
timeout,
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
'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',
|
||||||
'Range': 'bytes=0-1' // Just request first byte to test
|
'Range': 'bytes=0-1' // Just request first byte to test
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
response = await axios.head(url, {
|
|
||||||
timeout,
|
|
||||||
headers: {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
|
||||||
'Range': 'bytes=0-1' // Just request first byte to test
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if status is OK (200-299) or partial content (206)
|
// Check if status is OK (200-299) or partial content (206)
|
||||||
if (response.status >= 200 && response.status < 400) {
|
if (response.status >= 200 && response.status < 400) {
|
||||||
@@ -1187,6 +1106,8 @@ async function getMoviesModStreams(tmdbId, mediaType, seasonNum = null, episodeN
|
|||||||
title: `${cleanFileName}\n${sizeInfo || ''}${techDetailsString}`,
|
title: `${cleanFileName}\n${sizeInfo || ''}${techDetailsString}`,
|
||||||
url: selectedResult.url,
|
url: selectedResult.url,
|
||||||
quality: actualQuality,
|
quality: actualQuality,
|
||||||
|
provider: 'moviesmod',
|
||||||
|
headers: {}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[MoviesMod] Unsupported URL type for final processing: ${targetLink?.url}`);
|
console.warn(`[MoviesMod] Unsupported URL type for final processing: ${targetLink?.url}`);
|
||||||
|
|||||||
+92
-218
@@ -1,12 +1,21 @@
|
|||||||
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',
|
||||||
|
'moviesmod.js': 'getMoviesModStreams',
|
||||||
|
'MP4Hydra.js': 'getMP4HydraStreams',
|
||||||
|
'VidZee.js': 'getVidZeeStreams',
|
||||||
|
'vixsrc.js': 'getVixsrcStreams',
|
||||||
|
'uhdmovies.js': 'getUHDMoviesStreams',
|
||||||
|
};
|
||||||
|
|
||||||
// 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 +23,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' && file !== 'vidsrcextractor.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,223 +85,49 @@ 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
|
||||||
|
const providerFiles = getProviderFiles();
|
||||||
const providers = [];
|
const providers = [];
|
||||||
|
|
||||||
if (config.enableShowboxProvider) {
|
for (const providerInfo of providerFiles) {
|
||||||
providers.push({ name: 'showbox', enabled: true, fetch: fetchShowbox });
|
const enableFlag = `enable${providerInfo.name.charAt(0).toUpperCase() + providerInfo.name.slice(1)}Provider`;
|
||||||
} else {
|
const enabled = config[enableFlag] !== false;
|
||||||
console.log('[registry] showbox disabled via config');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
// 4KHDHub adapter
|
providers.push({
|
||||||
async function fetch4khdhub(ctx) {
|
name: providerInfo.name,
|
||||||
if (!k4hdhub) {
|
enabled: true,
|
||||||
try { k4hdhub = require('./4khdhub.js'); } catch (e) { console.error('Failed to load 4khdhub provider:', e.message); return []; }
|
fetch: createFetchFunction(providerInfo)
|
||||||
}
|
|
||||||
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 enabled`);
|
||||||
console.error('[registry] xprime fetch error:', e.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.enableXprimeProvider) {
|
|
||||||
providers.push({ name: 'xprime', enabled: true, fetch: fetchXprime });
|
|
||||||
console.log('[registry] xprime provider enabled');
|
|
||||||
} else {
|
} else {
|
||||||
console.log('[registry] xprime disabled via config');
|
console.log(`[registry] ${providerInfo.name} 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: p.enabled })); }
|
||||||
|
|||||||
+11
-102
@@ -15,13 +15,7 @@ const getAxiosCookieJarSupport = async () => {
|
|||||||
return axiosCookieJarSupport;
|
return axiosCookieJarSupport;
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Proxy Configuration ---
|
// All requests are direct
|
||||||
const UHDMOVIES_PROXY_URL = process.env.UHDMOVIES_PROXY_URL;
|
|
||||||
if (UHDMOVIES_PROXY_URL) {
|
|
||||||
console.log(`[UHDMovies] Proxy support enabled: ${UHDMOVIES_PROXY_URL}`);
|
|
||||||
} else {
|
|
||||||
console.log('[UHDMovies] No proxy configured, using direct connections');
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Domain Fetching ---
|
// --- Domain Fetching ---
|
||||||
let uhdMoviesDomain = 'https://uhdmovies.email'; // Fallback domain
|
let uhdMoviesDomain = 'https://uhdmovies.email'; // Fallback domain
|
||||||
@@ -91,7 +85,6 @@ const saveToCache = async (key, data) => {
|
|||||||
ensureCacheDir();
|
ensureCacheDir();
|
||||||
|
|
||||||
// Configure axios with headers to mimic a browser
|
// Configure axios with headers to mimic a browser
|
||||||
// Configure axios instance with optional proxy support
|
|
||||||
const createAxiosInstance = () => {
|
const createAxiosInstance = () => {
|
||||||
const config = {
|
const config = {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -105,32 +98,15 @@ const createAxiosInstance = () => {
|
|||||||
timeout: 30000
|
timeout: 30000
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add proxy configuration if UHDMOVIES_PROXY_URL is set
|
|
||||||
if (UHDMOVIES_PROXY_URL) {
|
|
||||||
console.log(`[UHDMovies] Using proxy: ${UHDMOVIES_PROXY_URL}`);
|
|
||||||
// For proxy URLs that expect the destination URL as a parameter
|
|
||||||
config.transformRequest = [(data, headers) => {
|
|
||||||
return data;
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
return axios.create(config);
|
return axios.create(config);
|
||||||
};
|
};
|
||||||
|
|
||||||
const axiosInstance = createAxiosInstance();
|
const axiosInstance = createAxiosInstance();
|
||||||
|
|
||||||
// Proxy wrapper function
|
// Request helper
|
||||||
const makeRequest = async (url, options = {}) => {
|
const makeRequest = async (url, options = {}) => {
|
||||||
if (UHDMOVIES_PROXY_URL) {
|
|
||||||
// Route through proxy
|
|
||||||
const proxiedUrl = `${UHDMOVIES_PROXY_URL}${encodeURIComponent(url)}`;
|
|
||||||
console.log(`[UHDMovies] Making proxied request to: ${url}`);
|
|
||||||
return axiosInstance.get(proxiedUrl, options);
|
|
||||||
} else {
|
|
||||||
// Direct request
|
|
||||||
console.log(`[UHDMovies] Making direct request to: ${url}`);
|
console.log(`[UHDMovies] Making direct request to: ${url}`);
|
||||||
return axiosInstance.get(url, options);
|
return axiosInstance.get(url, options);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Removed unused in-memory cache structure (uhdMoviesCache)
|
// Removed unused in-memory cache structure (uhdMoviesCache)
|
||||||
@@ -738,24 +714,12 @@ async function tryInstantDownload($) {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('keys', keys);
|
formData.append('keys', keys);
|
||||||
|
|
||||||
let apiResponse;
|
const apiResponse = await axiosInstance.post(apiUrl, formData, {
|
||||||
if (UHDMOVIES_PROXY_URL) {
|
|
||||||
const proxiedApiUrl = `${UHDMOVIES_PROXY_URL}${encodeURIComponent(apiUrl)}`;
|
|
||||||
console.log(`[UHDMovies] Making proxied POST request for Instant Download API to: ${apiUrl}`);
|
|
||||||
apiResponse = await axiosInstance.post(proxiedApiUrl, formData, {
|
|
||||||
headers: {
|
headers: {
|
||||||
...formData.getHeaders(),
|
...formData.getHeaders(),
|
||||||
'x-token': new URL(instantDownloadLink).hostname
|
'x-token': new URL(instantDownloadLink).hostname
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
apiResponse = await axiosInstance.post(apiUrl, formData, {
|
|
||||||
headers: {
|
|
||||||
...formData.getHeaders(),
|
|
||||||
'x-token': new URL(instantDownloadLink).hostname
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (apiResponse.data && apiResponse.data.url) {
|
if (apiResponse.data && apiResponse.data.url) {
|
||||||
let finalUrl = apiResponse.data.url;
|
let finalUrl = apiResponse.data.url;
|
||||||
@@ -859,25 +823,12 @@ async function validateVideoUrl(url, timeout = 10000) {
|
|||||||
try {
|
try {
|
||||||
console.log(`[UHDMovies] Validating URL: ${url.substring(0, 100)}...`);
|
console.log(`[UHDMovies] Validating URL: ${url.substring(0, 100)}...`);
|
||||||
|
|
||||||
// Use proxy for URL validation if enabled
|
const response = await axiosInstance.head(url, {
|
||||||
let response;
|
|
||||||
if (UHDMOVIES_PROXY_URL) {
|
|
||||||
const proxiedUrl = `${UHDMOVIES_PROXY_URL}${encodeURIComponent(url)}`;
|
|
||||||
console.log(`[UHDMovies] Making proxied HEAD request for validation to: ${url}`);
|
|
||||||
response = await axiosInstance.head(proxiedUrl, {
|
|
||||||
timeout,
|
timeout,
|
||||||
headers: {
|
headers: {
|
||||||
'Range': 'bytes=0-1' // Just request first byte to test
|
'Range': 'bytes=0-1'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
response = await axiosInstance.head(url, {
|
|
||||||
timeout,
|
|
||||||
headers: {
|
|
||||||
'Range': 'bytes=0-1' // Just request first byte to test
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if status is OK (200-299) or partial content (206)
|
// Check if status is OK (200-299) or partial content (206)
|
||||||
if (response.status >= 200 && response.status < 400) {
|
if (response.status >= 200 && response.status < 400) {
|
||||||
@@ -1076,8 +1027,8 @@ const getCookiesForUrl = async (jar, url) => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to create a proxied session for SID resolution
|
// Helper function to create a session for SID resolution
|
||||||
const createProxiedSession = async (jar) => {
|
const createSession = async (jar) => {
|
||||||
const { wrapper } = await getAxiosCookieJarSupport();
|
const { wrapper } = await getAxiosCookieJarSupport();
|
||||||
|
|
||||||
const sessionConfig = {
|
const sessionConfig = {
|
||||||
@@ -1093,46 +1044,7 @@ const createProxiedSession = async (jar) => {
|
|||||||
|
|
||||||
const session = wrapper(axios.create(sessionConfig));
|
const session = wrapper(axios.create(sessionConfig));
|
||||||
|
|
||||||
// If proxy is enabled, wrap the session methods to use proxy
|
|
||||||
if (UHDMOVIES_PROXY_URL) {
|
|
||||||
console.log(`[UHDMovies] Creating SID session with proxy: ${UHDMOVIES_PROXY_URL}`);
|
|
||||||
const originalGet = session.get.bind(session);
|
|
||||||
const originalPost = session.post.bind(session);
|
|
||||||
|
|
||||||
session.get = async (url, options = {}) => {
|
|
||||||
const proxiedUrl = `${UHDMOVIES_PROXY_URL}${encodeURIComponent(url)}`;
|
|
||||||
console.log(`[UHDMovies] Making proxied SID GET request to: ${url}`);
|
|
||||||
|
|
||||||
// Extract cookies from jar and add to headers
|
|
||||||
const cookieString = await getCookiesForUrl(jar, url);
|
|
||||||
if (cookieString) {
|
|
||||||
console.log(`[UHDMovies] Adding cookies to proxied request: ${cookieString}`);
|
|
||||||
options.headers = {
|
|
||||||
...options.headers,
|
|
||||||
'Cookie': cookieString
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalGet(proxiedUrl, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
session.post = async (url, data, options = {}) => {
|
|
||||||
const proxiedUrl = `${UHDMOVIES_PROXY_URL}${encodeURIComponent(url)}`;
|
|
||||||
console.log(`[UHDMovies] Making proxied SID POST request to: ${url}`);
|
|
||||||
|
|
||||||
// Extract cookies from jar and add to headers
|
|
||||||
const cookieString = await getCookiesForUrl(jar, url);
|
|
||||||
if (cookieString) {
|
|
||||||
console.log(`[UHDMovies] Adding cookies to proxied request: ${cookieString}`);
|
|
||||||
options.headers = {
|
|
||||||
...options.headers,
|
|
||||||
'Cookie': cookieString
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalPost(proxiedUrl, data, options);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
};
|
};
|
||||||
@@ -1143,8 +1055,8 @@ async function resolveSidToDriveleech(sidUrl) {
|
|||||||
const { origin } = new URL(sidUrl);
|
const { origin } = new URL(sidUrl);
|
||||||
const jar = new CookieJar();
|
const jar = new CookieJar();
|
||||||
|
|
||||||
// Create session with proxy support
|
// Create session
|
||||||
const session = await createProxiedSession(jar);
|
const session = await createSession(jar);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Step 0: Get the _wp_http value
|
// Step 0: Get the _wp_http value
|
||||||
@@ -1468,11 +1380,8 @@ async function getUHDMoviesStreams(tmdbId, mediaType = 'movie', season = null, e
|
|||||||
title: `${cleanFileName}\n${sizeInfo}`,
|
title: `${cleanFileName}\n${sizeInfo}`,
|
||||||
url: finalUrl,
|
url: finalUrl,
|
||||||
quality: linkInfo.quality,
|
quality: linkInfo.quality,
|
||||||
size: sizeInfo,
|
provider: 'uhdmovies',
|
||||||
fileName: fileName,
|
headers: {}
|
||||||
fullTitle: rawQuality,
|
|
||||||
codecs: codecs,
|
|
||||||
behaviorHints: { bingeGroup: `uhdmovies-${linkInfo.quality}` }
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,10 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
const cheerio = require('cheerio');
|
const cheerio = require('cheerio');
|
||||||
// --- Constants ---
|
// --- Constants ---
|
||||||
const VIDSRC_PROXY_URL = process.env.VIDSRC_PROXY_URL;
|
|
||||||
let BASEDOM = "https://cloudnestra.com"; // This can be updated by serversLoad
|
let BASEDOM = "https://cloudnestra.com"; // This can be updated by serversLoad
|
||||||
const SOURCE_URL = "https://vidsrc.xyz/embed";
|
const SOURCE_URL = "https://vidsrc.xyz/embed";
|
||||||
// --- Helper: Conditional Proxied Fetch ---
|
// --- Helper: Direct 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) {
|
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}`);
|
console.log(`[VidSrc Direct] Fetching: ${url}`);
|
||||||
return fetch(url, options);
|
return fetch(url, options);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-5
@@ -6,7 +6,8 @@
|
|||||||
// Constants
|
// Constants
|
||||||
const TMDB_API_KEY = "68e094699525b18a70bab2f86b1fa706";
|
const TMDB_API_KEY = "68e094699525b18a70bab2f86b1fa706";
|
||||||
const BASE_URL = 'https://vixsrc.to';
|
const BASE_URL = 'https://vixsrc.to';
|
||||||
|
let vixsrcUrl;
|
||||||
|
let subtitleApiUrl;
|
||||||
// Helper function to make HTTP requests with default headers
|
// Helper function to make HTTP requests with default headers
|
||||||
function makeRequest(url, options = {}) {
|
function makeRequest(url, options = {}) {
|
||||||
const defaultHeaders = {
|
const defaultHeaders = {
|
||||||
@@ -51,8 +52,6 @@ function getTmdbInfo(tmdbId, mediaType) {
|
|||||||
|
|
||||||
// Helper function to extract stream URL from Vixsrc page
|
// Helper function to extract stream URL from Vixsrc page
|
||||||
function extractStreamFromPage(url, contentType, contentId, seasonNum, episodeNum) {
|
function extractStreamFromPage(url, contentType, contentId, seasonNum, episodeNum) {
|
||||||
let vixsrcUrl;
|
|
||||||
let subtitleApiUrl;
|
|
||||||
|
|
||||||
if (contentType === 'movie') {
|
if (contentType === 'movie') {
|
||||||
vixsrcUrl = `${BASE_URL}/movie/${contentId}`;
|
vixsrcUrl = `${BASE_URL}/movie/${contentId}`;
|
||||||
@@ -195,9 +194,9 @@ function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episode
|
|||||||
title: "Auto Quality Stream",
|
title: "Auto Quality Stream",
|
||||||
url: masterPlaylistUrl,
|
url: masterPlaylistUrl,
|
||||||
quality: '1080p',
|
quality: '1080p',
|
||||||
type: 'direct',
|
provider: 'Vidsrc',
|
||||||
headers: {
|
headers: {
|
||||||
'Referer': BASE_URL,
|
'Referer': vixsrcUrl,
|
||||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
@@ -0,0 +1,451 @@
|
|||||||
|
// 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; }
|
||||||
|
}
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 };
|
||||||
+41
-9
@@ -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>
|
||||||
@@ -97,8 +99,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 +175,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 +203,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>
|
||||||
|
|||||||
+133
-40
@@ -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';
|
||||||
@@ -159,33 +160,25 @@ function fillForm(data){
|
|||||||
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'],
|
['enableMoviesmodProvider','adv_enableMoviesmod'],
|
||||||
['enableMp4hydraProvider','adv_enableMp4hydra'],
|
['enableMp4hydraProvider','adv_enableMp4hydra'],
|
||||||
['enableVidzeeProvider','adv_enableVidzee'],
|
['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 +227,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 +244,19 @@ 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_enableMoviesmod:'enableMoviesmodProvider',
|
||||||
adv_enableMp4hydra:'enableMp4hydraProvider',
|
adv_enableMp4hydra:'enableMp4hydraProvider',
|
||||||
adv_enableVidzee:'enableVidzeeProvider',
|
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 +273,92 @@ 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,
|
||||||
|
enableMoviesmodProvider: true,
|
||||||
|
enableMp4hydraProvider: true,
|
||||||
|
enableVidzeeProvider: true,
|
||||||
|
enableVixsrcProvider: true,
|
||||||
|
enableProxy: false,
|
||||||
|
// Other defaults
|
||||||
|
disableCache: false,
|
||||||
|
enablePStreamApi: true,
|
||||||
|
disableUrlValidation: false,
|
||||||
|
disable4khdhubUrlValidation: false,
|
||||||
|
showboxCacheDir: 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); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,6 +502,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');
|
||||||
@@ -513,15 +596,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);
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
1758225404146
|
||||||
+57
-44
@@ -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' && file !== 'vidsrcextractor.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,23 +91,27 @@ 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;
|
||||||
return cfg;
|
return cfg;
|
||||||
@@ -112,29 +129,30 @@ 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';
|
||||||
// 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 +168,21 @@ 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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|||||||
@@ -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;
|
||||||
+14
-1
@@ -2,5 +2,18 @@
|
|||||||
"defaultProviders": [],
|
"defaultProviders": [],
|
||||||
"configVersion": 1,
|
"configVersion": 1,
|
||||||
"tmdbApiKeys": [],
|
"tmdbApiKeys": [],
|
||||||
"enableShowboxProvider": true
|
"enableShowboxProvider": true,
|
||||||
|
"febboxCookies": [],
|
||||||
|
"enable4khdhubProvider": true,
|
||||||
|
"enableMoviesmodProvider": true,
|
||||||
|
"enableMp4hydraProvider": true,
|
||||||
|
"enableVidzeeProvider": true,
|
||||||
|
"enableVixsrcProvider": true,
|
||||||
|
"enableProxy": false,
|
||||||
|
"disableCache": false,
|
||||||
|
"enablePStreamApi": true,
|
||||||
|
"disableUrlValidation": false,
|
||||||
|
"disable4khdhubUrlValidation": false,
|
||||||
|
"excludeCodecsRaw": "{\"excludeDV\":false,\"excludeHDR\":false}",
|
||||||
|
"minQualitiesRaw": "all"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user