Full overhaul of the repo please see changelog

This commit is contained in:
Inside4ndroid
2025-09-16 18:54:12 +01:00
parent 2119398460
commit e0ad35d2b1
115 changed files with 26066 additions and 6426 deletions
+18
View File
@@ -0,0 +1,18 @@
node_modules
npm-debug.log
.DS_Store
.vscode
screenshots
streams_cache
providers/.cache
utils/user-config.json
.env
.git
.gitignore
*.log
*.md
!README.md
tests
coverage
.github
docker-compose.yml
-2
View File
@@ -1,2 +0,0 @@
TMDB_API_KEY="YOUR_TMDB_API_KEY_HERE"
PORT=3000
+3
View File
@@ -0,0 +1,3 @@
node_modules/
public/
*.log
+23
View File
@@ -0,0 +1,23 @@
module.exports = {
root: true,
env: {
node: true,
es2022: true,
},
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
ignorePatterns: ["public/*"],
extends: [
'eslint:recommended'
],
rules: {
'no-unused-vars': ['warn', { args: 'none', ignoreRestSiblings: true }],
'no-undef': 'error',
'no-console': 'off',
'prefer-const': 'warn',
'eqeqeq': ['warn', 'smart'],
'no-var': 'warn'
}
};
+74
View File
@@ -0,0 +1,74 @@
name: Docker Publish
on:
push:
branches: [ main, master ]
tags: [ 'v*' ]
workflow_dispatch:
env:
IMAGE_NAME: tmdb-embed-api
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract version
id: vars
run: |
PKG_VERSION=$(node -p "require('./package.json').version")
echo "pkg_version=$PKG_VERSION" >> $GITHUB_OUTPUT
if [[ $GITHUB_REF == refs/tags/v* ]]; then
TAG=${GITHUB_REF#refs/tags/}
echo "ref_tag=$TAG" >> $GITHUB_OUTPUT
fi
- name: Build & Push (branch)
if: startsWith(github.ref, 'refs/heads/')
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.pkg_version }}
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.version=${{ steps.vars.outputs.pkg_version }}
- name: Build & Push (tag)
if: startsWith(github.ref, 'refs/tags/v')
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.pkg_version }}
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.ref_tag }}
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.version=${{ steps.vars.outputs.pkg_version }}
org.opencontainers.image.ref.name=${{ steps.vars.outputs.ref_tag }}
+50
View File
@@ -0,0 +1,50 @@
# Changelog
## [1.0.0] - 2025-09-16
### Added
- Comprehensive `README.md` (features, endpoints, admin UI overview, screenshots gallery, Docker usage, troubleshooting).
- `LICENSE` (MIT) file.
- Multi‑TMDB key rotation support (array of keys; random selection per request).
- Config override system writing to `utils/user-config.json` with live merged view.
- Session-based authentication (login, logout, session check, password change) with brute-force mitigation.
- Rate limiting + exponential lockouts for failed login attempts.
- Provider status & metrics endpoints: `/api/health`, `/api/metrics`, `/api/status`, `/api/providers`.
- Stream aggregation endpoints (aggregate + provider-specific) with filtering pipeline.
- Diagnostics instrumentation (intercept `process.exit`, `beforeExit`, unhandled rejection / exception logging, periodic heartbeat interval).
- Docker assets: multi-stage `Dockerfile`, `.dockerignore`, `docker-compose.yml` with persistent volume for overrides.
- GitHub Actions workflow (`.github/workflows/docker-publish.yml`) for automatic multi-arch (amd64+arm64) build & push on branch and tag (`v*`).
- OCI metadata labels and build argument (`VERSION`) in Docker image.
- Version + (placeholder) Docker pulls badges in README header.
- VidSrc extractor refactor: removed direct `process.exit` calls; `main()` now returns status code (safer when required as a module).
### Changed
- Config normalization now clears legacy single `tmdbApiKey` when `tmdbApiKeys` override is explicitly emptied.
- Dockerfile slimmed: narrowed COPY set, added labels, build arg, retained only necessary runtime artifacts.
- `.dockerignore` expanded to reduce build context (`.git`, logs, markdown except README, tests, CI configs, caches, compose file, etc.).
### Removed
- Deprecated `uhdmovies` provider: code file, registry references, UI toggles, documentation mentions.
### Security
- Hardened auth flow: session cookies (HttpOnly), no-store headers for admin pages, escalating lockouts against brute force.
### CI / Automation
- Added multi-arch Docker publish workflow using Buildx & QEMU.
### Documentation
- Added Docker usage section (local build, compose, multi-key usage, env vars table, healthcheck notes).
- Added screenshots gallery of admin UI.
- Updated docs to reflect provider removal and new configuration semantics.
- Added this `CHANGELOG.md`.
### Developer Experience
- Heartbeat diagnostic interval to aid investigation of unexpected exits.
- Intercepted premature `process.exit` calls to avoid silent shutdowns during debugging.
## Historical Context
This 1.0.0 release consolidates modernization work: provider cleanup, configuration clarity, deployment ergonomics (Docker + CI), security hardening, and observability.
---
[1.0.0]: https://github.com/Inside4ndroid/TMDB-Embed-API/releases/1.0.0
+54
View File
@@ -0,0 +1,54 @@
# ---------- Build Stage ----------
FROM node:20-alpine AS build
ARG VERSION=dev
WORKDIR /app
# Install only production dependencies first (leveraging cache)
COPY package.json package-lock.json ./
# Using npm install instead of npm ci because lock file appears out-of-sync
# If you later regenerate lock (npm install locally) you can revert to npm ci for reproducibility
RUN npm install --omit=dev
# Copy only required source (avoid sending screenshots, node_modules already installed)
COPY apiServer.js ./
COPY providers ./providers
COPY public ./public
COPY utils ./utils
COPY README.md ./
# ---------- Runtime Stage ----------
FROM node:20-alpine AS runtime
ARG VERSION=dev
WORKDIR /app
ENV NODE_ENV=production \
API_PORT=8787 \
APP_VERSION=${VERSION}
# Create non-root user
RUN addgroup -S app && adduser -S app -G app
# Copy node_modules from build and necessary source
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/apiServer.js ./
COPY --from=build /app/public ./public
COPY --from=build /app/providers ./providers
COPY --from=build /app/utils ./utils
COPY --from=build /app/package.json ./
COPY --from=build /app/README.md ./
# Expose port (documentational; runtime can override)
EXPOSE 8787
USER app
# Labels / metadata
LABEL org.opencontainers.image.title="TMDB Embed API" \
org.opencontainers.image.description="Streaming metadata + source aggregation API with multi-key TMDB rotation" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.source="https://github.com/Inside4ndroid/TMDB-Embed-API" \
org.opencontainers.image.licenses="MIT"
# Healthcheck (simple)
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s CMD wget -qO- http://localhost:${API_PORT:-8787}/api/health || exit 1
CMD ["node","apiServer.js"]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Inside4ndroid
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+301 -54
View File
@@ -1,82 +1,329 @@
# 🍿🎥✮⋆˙ TMDB Embed API! 🍿🎥✮⋆˙ # 🍿 TMDB Embed API
Hello! I'm Inside4ndroid, a passionate Software Developer. > Modern, configurable streaming metadata + source aggregation API with a secure admin panel and multi-key TMDB rotation.
- 🐛 Bugs will be found and squashed as soon as possible please report any isues. <p align="center">
- 🌱 Currently in a state that works and is recieving updates. <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/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/docker/pulls/inside4ndroid/tmdb-embed-api?label=Docker%20Pulls&style=flat" />
</p>
--- ---
## ℹ️ Current Embed Providers ℹ️ ## 📸 Screenshots
<p align="center">
<img src="screenshots/Screenshot 2025-09-16 174931.png" width="46%" />
<img src="screenshots/Screenshot 2025-09-16 175002.png" width="46%" />
</p>
<p align="center">
<img src="screenshots/Screenshot 2025-09-16 175008.png" width="46%" />
<img src="screenshots/Screenshot 2025-09-16 175013.png" width="46%" />
</p>
<p align="center">
<img src="screenshots/Screenshot 2025-09-16 175020.png" width="46%" />
<img src="screenshots/Screenshot 2025-09-16 175026.png" width="46%" />
</p>
<p align="center">
<img src="screenshots/Screenshot 2025-09-16 175033.png" width="46%" />
<img src="screenshots/Screenshot 2025-09-16 175040.png" width="46%" />
</p>
1. embedsu ---
2. 2embed
3. autoembed
4. vidsrcsu
## 🤝 Sponsorship / Donations 🤝 ## ✨ Features
- **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.
- **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`.
- **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.
- **Config Propagation** – Overrides mirrored to `process.env` for legacy compatibility (no `.env` required after first save).
- **Back‑Navigation Safe** – Cache-control + visibility/session revalidation.
- **Extensible** – Simple provider registry pattern.
Your sponsorship is vital in helping me achieve this mission. With your support, I can: ---
Dedicate more time to developing and improving my projects ## 📦 Quick Start
Cover costs for essential tools, services and premium hosting to run public projects ```bash
Provide detailed documentation and support for users # 1. Install dependencies
Every contribution, no matter the size, makes a significant impact. npm install
[Sponsor Me!](https://github.com/sponsors/Inside4ndroid) # 2. (Optional) Copy example env if you want an initial TMDB key
cp .env.example .env # then edit TMDB_API_KEY=
Thank you for considering supporting my work! # 3. Start API (production style)
node apiServer.js
### ✨ Usage ✨ # Or with automatic restarts (if you add a dev script)
# npm run dev:api
This is an api for id's provided at [TMDB](https://www.themoviedb.org/). # 4. Open the Admin UI (login page) in browser
http://localhost:8787/
you must use the ID from the tmdb url like this : # 5. Health check
curl http://localhost:8787/api/health
```
🎬 MOVIES:
single provider - localhost:PORT/movie/{PROVIDER}/{TMDBID}
all providers - localhost:PORT/movie/{TMDBID}
📺 TV SHOWS:
single provider - localhost:PORT/tv/{PROVIDER}/{TMDBID}?s={SEASON}&e={EPISODE}
all providers - localhost:PORT/tv/{TMDBID}?s={SEASON}&e={EPISODE}
``` ```
## 📋 Providers 📋 ---
The providers are currently : ## 🐳 Docker Usage
``` ### Pull & Run (Fastest)
embedsu If you just want to run it (no building):
2embed ```bash
autoembed docker pull inside4ndroid/tmdb-embed-api:latest
vidsrcsu docker run --name tmdb-embed-api -p 8787:8787 \
-e TMDB_API_KEY=YOUR_TMDB_KEY \
inside4ndroid/tmdb-embed-api:latest
``` ```
## 🚀 Deployment 🚀 Persist overrides (Windows PowerShell example) by mounting a local file:
```powershell
New-Item -ItemType File -Path .\utils\user-config.json -Force | Out-Null
docker run --name tmdb-embed-api -p 8787:8787 `
-e TMDB_API_KEY=YOUR_TMDB_KEY `
-v ${PWD}/utils/user-config.json:/app/utils/user-config.json `
inside4ndroid/tmdb-embed-api:latest
```
You can self host by doing the following : ### Build Locally
```bash
docker build -t tmdb-embed-api .
docker run --name tmdb-embed -p 8787:8787 \
-e TMDB_API_KEY=YOUR_TMDB_KEY \
-v "$(pwd)/utils/user-config.json:/app/utils/user-config.json" \
tmdb-embed-api
```
1. `git clone https://github.com/Inside4ndroid/TMDB-Embed-API.git` After first login + save, the UI writes overrides into the mounted `user-config.json` so they persist across container restarts.
2. `cd TMDB-Embed-API`
3. `rename .env_example to .env`
4. `edit .env and insert your preferred port and your tmdb api key`
5. `npm install`
6. `npm run start`
## Contribute ### docker-compose
An example `docker-compose.yml` is included. Start with:
```bash
docker compose up -d --build
```
Environment variables can be supplied via a `.env` file in the same directory (Compose automatically loads it). Example `.env`:
```
TMDB_API_KEY=first_key
FEBBOX_COOKIES=cookie1,cookie2
```
If you know how to code then please dont hesitate to conribute to this project by forking and making pull requests with your new providers or edits etc. To stop & remove:
```bash
docker compose down
```
## <span style="color:red">⚠️ WARNING ⚠️</span> ### Switching to Multiple TMDB Keys
Either set `TMDB_API_KEYS` to a JSON array string:
```bash
docker run -p 8787:8787 \
-e TMDB_API_KEYS='["KEY1","KEY2","KEY3"]' \
tmdb-embed-api
```
or add / remove keys inside the Admin UI (Keys panel) and save.
Using this without a proxy may lead to your server ip being banned there are 3 ways to get around this : ### Key Environment Variables
| Variable | Purpose | Notes |
|----------|---------|-------|
| `API_PORT` | Port the server listens on | Defaults to 8787 |
| `TMDB_API_KEY` | Single TMDB key (legacy) | Use if you only have one key |
| `TMDB_API_KEYS` | JSON array of keys | Overrides single key when present |
| `FEBBOX_COOKIES` | Comma separated FebBox cookies | Enables Showbox provider immediately |
| `PASSWORD_HASH` | Pre-seed admin password hash | Optional (UI can set) |
| `ADMIN_USERNAME` | Override default username | Default: `admin` |
1. Use a proxy : [Find One Here](https://github.com/search?q=m3u8+proxy&type=repositories). If both `TMDB_API_KEY` and `TMDB_API_KEYS` are provided, rotation uses the array. Clearing the array in the UI also clears the legacy key.
2. Get a server with rotating ip addresses or dynamic ip adresses and use the ips at random.
3. use a cloudflare proxy worker.
Some providers need cloudflare resolving and i am working on a method for this. but using correct headers etc in your network request when playing the media urls should suffice for now. ---
### Updating the Image
```bash
docker compose pull # if using an external registry (future)
docker compose up -d --build
```
### Healthcheck
Container health relies on `GET /api/health`. If you disable or modify that route, adjust the Dockerfile / compose healthcheck accordingly.
---
## �🔐 Authentication
The root (`/`) serves the login page. After successful login a session cookie (`session`) is issued (HttpOnly; 12h lifetime). All admin pages (e.g. `config.html`) require an active session.
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/auth/login` | POST | Authenticate (JSON: `{ username, password }`) |
| `/auth/logout` | POST | Destroy session |
| `/auth/session` | GET | Check session status |
| `/auth/change-password` | POST | Update password (requires session) |
Repeated failed logins trigger exponential lockouts (Retry-After header emitted).
---
## 🛠 Configuration Model
All runtime state collapses into a *merged* object displayed in the UI (Live Config panel). Source order:
1. Initial environment variables / optional `.env`
2. JSON overrides: `utils/user-config.json`
Saving in the UI writes only changed keys. Setting a field to empty removes the override (reverting to env/default). Removing all TMDB keys (and saving) clears `tmdbApiKeys` and the legacy `tmdbApiKey`.
**Override File:** `utils/user-config.json`
```json
{
"defaultProviders": ["showbox","4khdhub"],
"tmdbApiKeys": ["KEY_A","KEY_B"],
"enableShowboxProvider": true
}
```
---
## 🎛 Admin UI Sections
| Panel | Summary |
|-------|---------|
| Core | Port, default providers, default region |
| Quality / Filters | Min quality presets & codec exclusion JSON |
| Keys | Add/remove TMDB API keys (rotated randomly) |
| FebBox / PStream | Manage FebBox cookies powering Showbox provider |
| Advanced | Provider toggles, cache & validation flags, proxy paths |
| Server Status | Live metrics, provider functional checks |
| Live Config | View merged + override JSON snapshots |
Session is revalidated on visibility and back/forward navigation to prevent stale access.
---
## 🔌 Providers
Current built‑in providers:
- `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.
### Adding a New Provider (Outline)
1. Create `providers/myprovider.js` exporting an async fetch function (see existing files for patterns).
2. Lazy‑load it in `providers/registry.js` similar to others.
3. Add a config flag (`enableMyproviderProvider`) if you want toggle support.
4. Update the provider matrix arrays in `public/index.js`.
---
## 📡 Key Endpoints
| Endpoint | Description |
|----------|-------------|
| `GET /api/health` | Basic heartbeat |
| `GET /api/metrics` | Runtime counters & summary |
| `GET /api/status` | Metrics + providers + endpoints list |
| `GET /api/providers` | Enabled providers summary |
| `GET /api/providers/:name` | Single provider status |
| `GET /api/streams/:type/:tmdbId` | Aggregate streams (`type` = movie|series) |
| `GET /api/streams/:provider/:type/:tmdbId` | Provider-specific streams |
| `GET /api/config` | `{ merged, override }` |
| `POST /api/config` | Apply override patch |
Aggregate endpoint auto-resolves IMDb when needed and merges provider output before filtering.
---
## 🧪 Stream Object Shape (Typical)
```json
{
"title": "Example Title",
"url": "https://...",
"quality": "1080p",
"provider": "showbox",
"size": null,
"languages": ["en"],
"subtitles": [],
"codecs": ["H264"],
"headers": { "Referer": "..." }
}
```
Filtering passes through `applyFilters` to enforce min quality + codec exclusions.
---
## 🧩 Quality & Codec Filtering
- Presets: `all`, `480p`, `720p`, `1080p`, `1440p`, `2160p`.
- Custom quality JSON example:
```json
{ "default": "900p", "showbox": "1080p" }
```
- Codec exclusion JSON example:
```json
{ "excludeDV": true, "excludeHDR": false }
```
---
## 🔐 Security Notes
- Admin UI requires login; session cookie is HttpOnly.
- Cache-control headers disable storing sensitive pages.
- Login is rate limited with escalating lockouts.
- Password change endpoint enforces minimum length.
---
## 🚀 Deployment Tips
| Aspect | Recommendation |
|--------|---------------|
| Node Version | 18+ LTS |
| Reverse Proxy | Terminate TLS (e.g., Nginx) and forward to API port |
| Persistent Config | Mount / persist `utils/user-config.json` |
| Logs | Pipe stdout to centralized logger |
| Scaling | Use a single instance unless providers are CPU bound |
For ephemeral platforms (e.g., Vercel) note that some providers use temporary directories; avoid enabling disk-heavy cache directories.
---
## 💡 Troubleshooting
| Symptom | Cause / Fix |
|---------|------------|
| No streams from Showbox | Missing FebBox cookies |
| TMDB quota issues | Add more keys under Keys panel |
| Provider missing in matrix | Ensure its enable flag exists & UI updated |
| Empty merged config after restart | `user-config.json` deleted or unreadable |
| Streams low quality | Adjust min quality preset or custom JSON |
---
## 🤝 Contributing
PRs welcome. Keep changes focused and avoid unrelated formatting churn. For new providers include:
- A short rationale
- Retry / timeout safeguards
- Respect for existing filtering structure
---
## ❤️ Sponsorship
If this project helps you, consider sponsoring to support continued development & maintenance:
➡️ **GitHub Sponsors:** https://github.com/sponsors/Inside4ndroid
Every contribution accelerates feature delivery & sustainability.
---
## 📜 License
MIT. See `LICENSE`.
---
## 🙏 Acknowledgements
Inspired by community scraping/stream aggregation efforts. Credits also to the original NuvioStreamsAddon work for earlier concepts.
---
> *Happy streaming & hacking!* ✨
---
+365
View File
@@ -0,0 +1,365 @@
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const os = require('os');
const { config, saveConfigPatch, OVERRIDE_PATH } = require('./utils/config');
const { authenticate, issueSession, requireAuth, getSession, updatePassword, AUTH_FILE } = require('./utils/auth');
const path = require('path');
const { listProviders, getProvider, getCookieStats } = require('./providers/registry');
const { resolveImdbId } = require('./utils/tmdb');
const { applyFilters } = require('./utils/streamFilters');
const app = express();
// --- Simple In-Memory Rate Limiting for /auth/login ---
const loginAttempts = new Map(); // key: ip, value: { count, first, last, lockedUntil }
const MAX_ATTEMPTS_WINDOW = 5; // attempts allowed
const WINDOW_MS = 10 * 60 * 1000; // 10 minutes window
const BASE_LOCK_MS = 5 * 60 * 1000; // 5 minutes base lock
function getClientIp(req){
return (req.headers['x-forwarded-for'] || req.connection.remoteAddress || '').split(',')[0].trim();
}
function recordLoginFailure(ip){
const now = Date.now();
let entry = loginAttempts.get(ip);
if (!entry) {
entry = { count:1, first: now, last: now, lockedUntil:0 };
loginAttempts.set(ip, entry);
return entry;
}
// Reset window if outside timeframe and not locked
if (now - entry.first > WINDOW_MS && now > entry.lockedUntil) {
entry.count = 1;
entry.first = now;
} else {
entry.count++;
}
entry.last = now;
if (entry.count > MAX_ATTEMPTS_WINDOW) {
// Exponential backoff lock: base * 2^(count - limit)
const over = entry.count - MAX_ATTEMPTS_WINDOW;
const lockMs = BASE_LOCK_MS * Math.min(8, Math.pow(2, over-1));
entry.lockedUntil = now + lockMs;
}
return entry;
}
function canAttempt(ip){
const entry = loginAttempts.get(ip);
if (!entry) return { allowed:true };
const now = Date.now();
if (entry.lockedUntil && now < entry.lockedUntil) {
return { allowed:false, retryAfter: Math.ceil((entry.lockedUntil - now)/1000) };
}
if (now - entry.first > WINDOW_MS) {
// Window passed; reset
loginAttempts.delete(ip);
return { allowed:true };
}
return { allowed:true };
}
function recordLoginSuccess(ip){
// On success clear state to avoid lingering count
loginAttempts.delete(ip);
}
// Guard against premature process.exit from imported legacy modules
process.exit = function(code){
console.warn('[diagnostic] Intercepted process.exit with code', code, new Error('exit trace').stack);
// keep process alive for debugging
};
setImmediate(()=>console.log('[diagnostic] post-start setImmediate fired'));
app.use(cors());
app.use(express.json());
// --- Auth Routes (login before static serving) ---
app.post('/auth/login', (req,res) => {
const { username, password } = req.body || {};
const ip = getClientIp(req);
const attemptState = canAttempt(ip);
if (!attemptState.allowed) {
res.setHeader('Retry-After', String(attemptState.retryAfter));
return res.status(429).json({ success:false, error:'TOO_MANY_ATTEMPTS', retryAfter: attemptState.retryAfter });
}
if (!username || !password) return res.status(400).json({ success:false, error:'MISSING_CREDENTIALS' });
if (!authenticate(username, password)) {
const entry = recordLoginFailure(ip);
if (entry.lockedUntil && Date.now() < entry.lockedUntil) {
const retryAfter = Math.ceil((entry.lockedUntil - Date.now())/1000);
res.setHeader('Retry-After', String(retryAfter));
return res.status(429).json({ success:false, error:'LOCKED', retryAfter });
}
return res.status(401).json({ success:false, error:'INVALID_CREDENTIALS', remaining: Math.max(0, MAX_ATTEMPTS_WINDOW - entry.count) });
}
recordLoginSuccess(ip);
const token = issueSession(username);
res.setHeader('Set-Cookie', `session=${token}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${12*60*60}`);
res.json({ success:true, username });
});
app.post('/auth/logout', (req,res) => {
res.setHeader('Set-Cookie', 'session=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0');
res.json({ success:true });
});
app.get('/auth/session', (req,res) => {
const sess = getSession(req);
if (!sess) return res.json({ authenticated:false });
res.json({ authenticated:true, username: sess.u });
});
app.post('/auth/change-password', requireAuth, (req,res) => {
const { oldPassword, newPassword } = req.body || {};
if (!oldPassword || !newPassword) return res.status(400).json({ success:false, error:'MISSING_FIELDS' });
const sess = req.session;
if (!authenticate(sess.u, oldPassword)) return res.status(401).json({ success:false, error:'INVALID_OLD_PASSWORD' });
if (newPassword.length < 8) return res.status(400).json({ success:false, error:'PASSWORD_TOO_SHORT' });
if (!updatePassword(sess.u, newPassword)) return res.status(500).json({ success:false, error:'UPDATE_FAILED' });
res.json({ success:true, message:'PASSWORD_UPDATED' });
});
// Protect config panel (HTML) explicitly before static middleware
app.get('/config.html', (req,res,next) => {
const sess = getSession(req);
if (!sess) return res.redirect(302, '/');
res.setHeader('Cache-Control','no-store, must-revalidate');
res.setHeader('Pragma','no-cache');
res.setHeader('Expires','0');
res.sendFile(path.join(process.cwd(),'public','config.html'));
});
// Explicit root handler for login page to ensure no-store
app.get('/', (req,res) => {
res.setHeader('Cache-Control','no-store, must-revalidate');
res.setHeader('Pragma','no-cache');
res.setHeader('Expires','0');
res.sendFile(path.join(process.cwd(),'public','index.html'));
});
// Diagnostics for unexpected exits
process.on('beforeExit', (code) => {
console.log('[diagnostic] beforeExit code=', code);
});
process.on('exit', (code) => {
console.log('[diagnostic] exit code=', code);
});
process.on('uncaughtException', (err) => {
console.error('[diagnostic] uncaughtException', err);
});
process.on('unhandledRejection', (reason) => {
console.error('[diagnostic] unhandledRejection', reason);
});
// Periodic heartbeat to confirm event loop activity (can be removed later)
let hbCount = 0;
setInterval(()=>{
hbCount++;
if (hbCount % 6 === 0) { // every 60s if interval is 10s
console.log('[diagnostic] heartbeat 60s elapsed, process alive');
}
}, 10_000).unref();
// --- Metrics (in-memory) ---
const metrics = {
startTime: Date.now(),
requestsTotal: 0,
streamRequests: 0,
providerCalls: {},
lastRequestAt: null,
lastError: null,
streamsReturned: 0,
tmdbToImdbLookups: 0
};
app.use((req,res,next)=>{ metrics.requestsTotal++; metrics.lastRequestAt = Date.now(); next(); });
// Serve static UI (login page at /)
app.use(express.static(path.join(process.cwd(),'public')));
// Config API
app.get('/api/config', (req,res) => {
const fs = require('fs');
let override = {};
try { if (fs.existsSync(OVERRIDE_PATH)) override = JSON.parse(fs.readFileSync(OVERRIDE_PATH,'utf8')); } catch (e) {
// ignore JSON parse or fs errors reading override; return base config
}
res.json({ success:true, merged: config, override, overridePath: OVERRIDE_PATH });
});
app.post('/api/config', (req,res) => {
const patch = req.body || {};
if (patch.port) {
const p = Number(patch.port); if (!Number.isFinite(p) || p<=0 || p>65535) return res.status(400).json({ success:false, error:'INVALID_PORT'});
patch.port = p;
}
if (patch.defaultProviders && !Array.isArray(patch.defaultProviders)) return res.status(400).json({ success:false, error:'DEFAULT_PROVIDERS_NOT_ARRAY'});
const ok = saveConfigPatch(patch);
res.json({ success: ok, merged: config });
});
// --- Basic informational endpoints ---
app.get('/api/health', (req, res) => {
res.json({ ok: true, service: 'tmdb-embed-api', time: new Date().toISOString() });
});
// Metrics endpoint
app.get('/api/metrics', (req,res) => {
res.json({
uptimeSeconds: Math.round((Date.now()-metrics.startTime)/1000),
requestsTotal: metrics.requestsTotal,
streamRequests: metrics.streamRequests,
providerCalls: metrics.providerCalls,
streamsReturned: metrics.streamsReturned,
tmdbToImdbLookups: metrics.tmdbToImdbLookups,
lastRequestAt: metrics.lastRequestAt,
memoryMB: Math.round(process.memoryUsage().rss/1024/1024),
loadAvg: os.loadavg ? os.loadavg() : [],
nodeVersion: process.version,
configDefaults: {
region: config.defaultRegion,
providers: config.defaultProviders,
minQualities: config.minQualities ? Object.keys(config.minQualities).length : 0,
excludeCodecs: config.excludeCodecs ? Object.keys(config.excludeCodecs).filter(k=>config.excludeCodecs[k]).length : 0,
febboxCookies: config.febboxCookies.length
}
});
});
// Consolidated status (metrics + providers + endpoints)
app.get('/api/status', (req,res) => {
const endpoints = [
'GET /api/health',
'GET /api/metrics',
'GET /api/status',
'GET /api/providers',
'GET /api/providers/:name',
'GET /api/streams/:type/:tmdbId',
'GET /api/streams/:provider/:type/:tmdbId',
'POST /api/config',
'GET /api/config'
];
// Determine cookie requirement heuristically (currently Showbox / PStream)
const cookieRequiredProviders = new Set(['showbox']);
const providers = listProviders().map(p => {
const cookieRequired = cookieRequiredProviders.has(p.name);
const cookieOk = !cookieRequired || (config.febboxCookies && config.febboxCookies.length > 0);
return { name: p.name, enabled: p.enabled, cookieRequired, cookieOk };
});
res.json({ success:true, metrics: {
uptimeSeconds: Math.round((Date.now()-metrics.startTime)/1000),
requestsTotal: metrics.requestsTotal,
streamRequests: metrics.streamRequests,
providerCalls: metrics.providerCalls,
streamsReturned: metrics.streamsReturned,
tmdbToImdbLookups: metrics.tmdbToImdbLookups,
lastRequestAt: metrics.lastRequestAt,
memoryMB: Math.round(process.memoryUsage().rss/1024/1024)
}, endpoints, providers });
});
// Providers list
app.get('/api/providers', (req,res) => {
res.json({ success: true, providers: listProviders() });
});
// Debug environment/config endpoint (do not expose publicly in production)
app.get('/api/debug/env', (req,res) => {
const cookieStats = getCookieStats ? getCookieStats() : null;
res.json({
port: config.port,
defaultProviders: config.defaultProviders,
febboxCookieCount: config.febboxCookies.length,
showboxCacheDir: process.env.SHOWBOX_CACHE_DIR || '(os tmp)',
nodeVersion: process.version,
cookieStats
});
});
// Single provider info
app.get('/api/providers/:name', (req,res) => {
const p = getProvider(req.params.name);
if (!p) return res.status(404).json({ success:false, error:'PROVIDER_NOT_FOUND' });
res.json({ success:true, provider:{ name: p.name, enabled: p.enabled } });
});
// Aggregate streams across all enabled providers
app.get('/api/streams/:type/:tmdbId', async (req,res) => {
const { type, tmdbId } = req.params;
if (!['movie','series'].includes(type)) return res.status(400).json({ success:false, error:'INVALID_TYPE' });
const season = req.query.season ? Number(req.query.season) : null;
const episode = req.query.episode ? Number(req.query.episode) : null;
try {
metrics.streamRequests++;
const tmdbType = type === 'movie' ? 'movie' : 'tv';
const imdbId = await resolveImdbId(tmdbType, tmdbId); if (imdbId) metrics.tmdbToImdbLookups++;
const selectedProviders = (config.defaultProviders.length ? config.defaultProviders : listProviders().map(p=>p.name));
const providerTimings = {};
const results = await Promise.all(selectedProviders.map(async name => {
const prov = getProvider(name);
if (!prov || !prov.enabled) return [];
metrics.providerCalls[name] = (metrics.providerCalls[name]||0)+1;
try {
console.log(`[api] invoking provider ${name} for tmdbId=${tmdbId}`);
const t0 = Date.now();
const r = await prov.fetch({ tmdbId, type, season, episode, imdbId, filters:{ } });
providerTimings[name] = Date.now()-t0;
console.log(`[api] provider ${name} returned ${Array.isArray(r)?r.length:0} streams`);
return r;
} catch (e) {
console.error(`[api] provider ${name} failed:`, e.message);
providerTimings[name] = null;
return [];
}
}));
let streams = results.flat();
streams = applyFilters(streams, 'aggregate', config.minQualities, config.excludeCodecs);
metrics.streamsReturned += streams.length;
res.json({ success:true, tmdbId, imdbId, count: streams.length, providerTimings, streams });
} catch (e) {
metrics.lastError = e.message;
res.status(500).json({ success:false, error:'INTERNAL_ERROR', message:e.message });
}
});
// Provider-specific streams
app.get('/api/streams/:provider/:type/:tmdbId', async (req,res) => {
const { provider, type, tmdbId } = req.params;
if (!['movie','series'].includes(type)) return res.status(400).json({ success:false, error:'INVALID_TYPE' });
const season = req.query.season ? Number(req.query.season) : null;
const episode = req.query.episode ? Number(req.query.episode) : null;
const prov = getProvider(provider);
if (!prov) return res.status(404).json({ success:false, error:'PROVIDER_NOT_FOUND' });
if (!prov.enabled) return res.status(503).json({ success:false, error:'PROVIDER_DISABLED' });
try {
metrics.streamRequests++;
metrics.providerCalls[prov.name] = (metrics.providerCalls[prov.name]||0)+1;
const tmdbType = type === 'movie' ? 'movie' : 'tv';
const imdbId = await resolveImdbId(tmdbType, tmdbId); if (imdbId) metrics.tmdbToImdbLookups++;
const t0 = Date.now();
let streams = await prov.fetch({ tmdbId, type, season, episode, imdbId, filters:{} });
const providerTimings = { [prov.name]: Date.now()-t0 };
streams = applyFilters(streams, prov.name, config.minQualities, config.excludeCodecs);
metrics.streamsReturned += streams.length;
res.json({ success:true, provider: prov.name, tmdbId, imdbId, count: streams.length, providerTimings, streams });
} catch (e) {
metrics.lastError = e.message;
res.status(500).json({ success:false, error:'INTERNAL_ERROR', message:e.message });
}
});
const PORT = config.port;
const server = app.listen(PORT, () => {
console.log(`TMDB Embed REST API listening on http://localhost:${PORT}`);
console.log('Endpoints:');
console.log(' GET /api/health');
console.log(' GET /api/metrics');
console.log(' GET /api/providers');
console.log(' GET /api/streams/:type/:id');
console.log(' POST /api/streams/:type/:id');
if (!config.febboxCookies || config.febboxCookies.length === 0) {
console.warn('[startup][warning] No FEBBOX_COOKIES configured. Showbox / PStream related streams may be unavailable. Set FEBBOX_COOKIES in your environment to enable these sources.');
}
});
server.on('error', (err)=>{ console.error('[diagnostic] server error', err); });
+27
View File
@@ -0,0 +1,27 @@
version: '3.9'
services:
tmdb-embed-api:
build: .
container_name: tmdb-embed-api
environment:
# Provide at least one TMDB key (comma separated or override via UI later)
- TMDB_API_KEY=${TMDB_API_KEY:-}
# Alternative: supply JSON array
# - TMDB_API_KEYS=["key1","key2"]
- API_PORT=8787
# FebBox cookies (comma separated) if you want Showbox immediately functional
- FEBBOX_COOKIES=${FEBBOX_COOKIES:-}
ports:
- "8787:8787"
volumes:
# Persist runtime overrides (created by the admin UI)
- user_config_data:/app/utils/user-config.json
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8787/api/health || exit 1"]
interval: 30s
timeout: 5s
start_period: 20s
retries: 3
volumes:
user_config_data:
-184
View File
@@ -1,184 +0,0 @@
import express from "express";
import { getMovie, getTv } from './src/api.js';
import { getMovieFromTmdb, getTvFromTmdb } from './src/workers/tmdb.js';
import cors from "cors";
const PORT = process.env.PORT;
const app = express();
app.use(cors());
app.get('/', (req, res) => {
res.status(200).json({
INTRO: "Welcome to the TMDB Embed API",
PROVIDERS: "2embed | embedsu | autoembed | vidsrcsu",
ROUTES: {
movie: "/movie/{PROVIDER}/{TMDBID}",
show: "/tv/{PROVIDER}/{TMDBID}?s={SEASON}&e={EPISODE}",
all_movie_providers: "/movie/{TMDBID}",
all_show_providers: "/tv/{TMDBID}?s={SEASON}&e={EPISODE}"
},
INFORMATION: "This project is for educational purposes only. We do not host any kind of content. We provide only the links to already available content on the internet. We do not host, upload any videos, films or media files. We are not responsible for the accuracy, compliance, copyright, legality, decency, or any other aspect of the content of other linked sites. If you have any legal issues please contact the appropriate media file owners or host sites.",
SOURCE: "https://github.com/Inside4ndroid/TMDB-Embed-API"
});
});
app.get('/movie/:tmdbId', async (req, res) => {
if (isNaN(parseInt(req.params.tmdbId))) {
res.status(405).json({
error: "Invalid movie id",
hint: "Check the documentation again to see how to use this endpoint"
});
return;
}
const media = await getMovieFromTmdb(req.params.tmdbId);
if (media instanceof Error) {
res.status(405).json({ error: media.message });
return;
}
let output = await getMovie(media);
if (output === null || output instanceof Error) {
res.status(404).json({
error: "No sources fond for this movie.",
hint: "If you know where to find this movie and know programming feel free to join us on GitHub: https://github.com/Inside4ndroid/TMDB-Embed-API to add it."
});
} else {
res.status(200).json(output);
}
});
app.get('/movie/:provider/:tmdbId', async (req, res) => {
const allowedProviders = ['2embed', 'autoembed', 'embedsu', 'vidsrcsu'];
if (!allowedProviders.includes(req.params.provider)) {
res.status(400).json({
error: "Invalid provider",
hint: `Provider must be one of: ${allowedProviders.join(', ')}`
});
return;
}
if (isNaN(parseInt(req.params.tmdbId))) {
res.status(400).json({
error: "Invalid movie id",
hint: "Check the documentation again to see how to use this endpoint"
});
return;
}
const media = await getMovieFromTmdb(req.params.tmdbId);
if (media instanceof Error) {
res.status(405).json({ error: media.message });
return;
}
let output = await getMovie(media, req.params.provider);
if (output === null || output instanceof Error) {
res.status(404).json({
error: "No sources fond for this movie.",
hint: "If you know where to find this movie and know programming feel free to join us on GitHub: https://github.com/Inside4ndroid/TMDB-Embed-API to add it."
});
} else {
res.status(200).json(output);
}
});
app.get('/tv/:tmdbId', async (req, res) => {
if (!req.params.tmdbId || isNaN(parseInt(req.params.tmdbId)) || !req.query.s || isNaN(parseInt(req.query.s)) || !req.query.e || isNaN(parseInt(req.query.e))) {
res.status(405).json({
error: "Invalid show id, season, or episode number",
hint: "Check the documentation again to see how to use this endpoint"
});
return;
}
const media = await getTvFromTmdb(req.params.tmdbId, req.query.s, req.query.e);
if (media instanceof Error) {
res.status(405).json({ error: media.message });
return;
}
let output = await getTv(media, req.query.s, req.query.e);
if (output === null || output instanceof Error) {
res.status(404).json({
error: "No sources found for this show.",
hint: "If you know where to find this show and know programming feel free to join us on GitHub: https://github.com/Inside4ndroid/TMDB-Embed-API to add it."
});
} else {
res.status(200).json(output);
}
});
app.get('/tv/:provider?/:tmdbId', async (req, res) => {
const allowedProviders = ['2embed', 'autoembed', 'embedsu', 'vidsrcsu'];
if (!allowedProviders.includes(req.params.provider)) {
res.status(400).json({
error: "Invalid provider",
hint: `Provider must be one of: ${allowedProviders.join(', ')}`
});
return;
}
if (isNaN(parseInt(req.params.tmdbId))) {
res.status(400).json({
error: "Invalid show id",
hint: "Check the documentation again to see how to use this endpoint",
});
return;
}
if (!req.query.s || isNaN(parseInt(req.query.s)) || !req.query.e || isNaN(parseInt(req.query.e))) {
return res.status(400).json({
error: "Invalid season, or episode number",
hint: "Check the documentation again to see how to use this endpoint"
});
}
const media = await getTvFromTmdb(req.params.tmdbId, req.query.s, req.query.e);
if (media instanceof Error) {
return res.status(405).json({ error: media.message });
}
let output = await getTv(media, req.query.s, req.query.e, req.params.provider);
if (output === null || output instanceof Error) {
return res.status(404).json({
error: "No sources found for this show.",
hint: "If you know where to find this show and know programming feel free to join us on GitHub: https://github.com/Inside4ndroid/TMDB-Embed-API to add it."
});
}
return res.status(200).json(output);
});
app.get('/movie/', (req, res) => {
res.status(405).json({
error: "Invalid movie id",
hint: "Check the documentation again to see how to use this endpoint"
});
});
app.get('/tv/', (req, res) => {
res.status(405).json({
error: "Invalid show id",
hint: "Check the documentation again to see how to use this endpoint"
});
});
app.get('*', (req, res) => {
res.status(404).json({ error: 'Not found', hint: 'Go to /' });
});
export const server = app.listen(PORT, () => {
console.log(`Server is running on port http://localhost:${PORT}`);
});
export default app;
+2491 -4433
View File
File diff suppressed because it is too large Load Diff
+31 -10
View File
@@ -1,20 +1,41 @@
{ {
"name": "tmdb-embed-api", "name": "tmdb-embed-api",
"version": "1.0.0", "version": "1.0.0",
"description": "An api to get embed sources using tmdbid.", "description": "Standalone streaming embed source aggregation API (TMDB ID in, streams out)",
"license": "ISC", "main": "apiServer.js",
"author": "Inside4ndroid Studios",
"type": "module",
"main": "index.js",
"scripts": { "scripts": {
"start": "node --watch index.js" "start": "node apiServer.js",
"start:api": "node --watch apiServer.js",
"dev:api": "nodemon --ignore providers/.cache --ignore **/tmdb-embed-showbox-cache apiServer.js",
"lint": "eslint . --ext .js"
}, },
"dependencies": { "dependencies": {
"aromanize": "^0.1.5",
"axios": "^1.9.0",
"axios-cookiejar-support": "^6.0.2",
"body-parser": "^2.2.0",
"cheerio": "^1.0.0", "cheerio": "^1.0.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.7", "crypto-js": "^4.2.0",
"express": "^4.18.2", "dotenv": "^16.5.0",
"fetch": "^1.1.0", "express": "^4.21.2",
"node-fetch": "^3.3.2" "form-data": "^4.0.3",
"hls-parser": "^0.13.5",
"jsdom": "^26.1.0",
"node-fetch": "^2.7.0",
"p-limit": "^6.2.0",
"puppeteer": "^24.11.0",
"string-similarity": "^4.0.4",
"tough-cookie": "^5.1.2",
"ttl-cache": "^1.0.2",
"vm2": "^3.9.19",
"yargs": "^17.7.2"
},
"engines": {
"node": ">=14.0.0"
},
"devDependencies": {
"nodemon": "^3.1.10",
"eslint": "^8.57.0"
} }
} }
@@ -0,0 +1,14 @@
{
"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"
]
}
@@ -0,0 +1,14 @@
{
"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"
]
}
@@ -0,0 +1,20 @@
{
"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"
]
}
@@ -0,0 +1 @@
[]
@@ -0,0 +1,3 @@
{
"data": null
}
@@ -0,0 +1,64 @@
{
"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=="
}
]
}
]
}
@@ -0,0 +1,224 @@
{
"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=="
}
]
}
]
}
@@ -0,0 +1,64 @@
{
"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=="
}
]
}
]
}
@@ -0,0 +1,4 @@
{
"showboxUrl": "NO_URL_FOUND",
"timestamp": 1757925353144
}
@@ -0,0 +1,4 @@
{
"showboxUrl": "NO_URL_FOUND",
"timestamp": 1757926777078
}
@@ -0,0 +1,4 @@
{
"showboxUrl": "NO_URL_FOUND",
"timestamp": 1757926668402
}
@@ -0,0 +1,4 @@
{
"showboxUrl": "NO_URL_FOUND",
"timestamp": 1757950186614
}
@@ -0,0 +1,4 @@
{
"result": "NO_MATCH_FOUND",
"timestamp": 1757950186613
}
@@ -0,0 +1,4 @@
{
"result": "NO_MATCH_FOUND",
"timestamp": 1757926668401
}
@@ -0,0 +1,4 @@
{
"result": "NO_MATCH_FOUND",
"timestamp": 1757926777077
}
@@ -0,0 +1,4 @@
{
"result": "NO_MATCH_FOUND",
"timestamp": 1757925353143
}
@@ -0,0 +1,10 @@
{
"id": 1263256,
"titles": [
{
"iso_3166_1": "VN",
"title": "Chàng Happy Gilmore 2",
"type": ""
}
]
}
@@ -0,0 +1,8 @@
{
"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
@@ -0,0 +1,58 @@
{
"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
}
@@ -0,0 +1,305 @@
{
"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": ""
}
]
}
@@ -0,0 +1,8 @@
{
"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
@@ -0,0 +1,66 @@
{
"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
}
@@ -0,0 +1,110 @@
{
"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": ""
}
]
}
@@ -0,0 +1,8 @@
{
"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
@@ -0,0 +1,57 @@
{
"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
}
@@ -0,0 +1,25 @@
{
"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": ""
}
]
}
@@ -0,0 +1,8 @@
{
"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
@@ -0,0 +1,84 @@
{
"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
}
@@ -0,0 +1,5 @@
{
"tmdbId": "278",
"tmdbType": "movie",
"title": "The Shawshank Redemption"
}
@@ -0,0 +1,5 @@
{
"tmdbId": "1893",
"tmdbType": "movie",
"title": "Star Wars: Episode I - The Phantom Menace"
}
@@ -0,0 +1,5 @@
{
"tmdbId": "1263256",
"tmdbType": "movie",
"title": "Happy Gilmore 2"
}
@@ -0,0 +1,74 @@
{
"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/"
}
]
}
@@ -0,0 +1,25 @@
{
"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=="
}
]
}
@@ -0,0 +1,123 @@
{
"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/"
}
]
}
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
@@ -0,0 +1 @@
Unknown size
+1764
View File
File diff suppressed because it is too large Load Diff
+506
View File
@@ -0,0 +1,506 @@
const axios = require('axios');
const FormData = require('form-data');
// Helper function to generate slug from title
function generateSlug(title) {
return title.toLowerCase()
.replace(/[^\w\s-]/g, '') // Remove special characters
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-'); // Replace multiple hyphens with a single hyphen
}
// Process episode data from MP4Hydra response
function processEpisode(episode, baseServer, serverName, serverNumber) {
const videoUrl = `${baseServer}${episode.src}`;
const subtitles = episode.subs ? episode.subs.map(sub => ({
label: sub.label,
url: `${baseServer}${sub.src}`
})) : [];
return {
title: episode.show_title || episode.title,
episode: episode.title,
type: episode.type,
quality: episode.quality || episode.label,
videoUrl: videoUrl,
server: serverName,
serverNumber: serverNumber,
subtitles: subtitles
};
}
// Main function to get streams from MP4Hydra
async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNum = null) {
try {
console.log(`[MP4Hydra] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}, Season: ${seasonNum}, Episode: ${episodeNum}`);
// Get TMDB details
const details = await getTMDBDetails(tmdbId, mediaType);
if (!details) {
console.log(`[MP4Hydra] Could not fetch details for TMDB ID: ${tmdbId}`);
return [];
}
console.log(`[MP4Hydra] Found title: ${details.title} (${details.year})`);
// Generate slug in the format "movie-name-year"
let slug = details.slug;
if (mediaType === 'movie' && details.year) {
slug = `${details.slug}-${details.year}`;
}
console.log(`[MP4Hydra] Using slug: ${slug}`);
// Create form data for multipart/form-data request
const formData = new FormData();
formData.append('v', '8');
formData.append('z', JSON.stringify([{
s: slug,
t: mediaType,
se: seasonNum,
ep: episodeNum
}]));
// Make request to MP4Hydra API
const response = await axios({
method: 'post',
url: 'https://mp4hydra.org/info2?v=8',
data: formData,
headers: {
...formData.getHeaders(),
'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36',
'Accept': '*/*',
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
'Origin': 'https://mp4hydra.org',
'Referer': `https://mp4hydra.org/${mediaType}/${slug}`,
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin'
},
timeout: 10000
});
// Process response
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
const playlist = response.data.playlist;
const servers = response.data.servers;
console.log(`[MP4Hydra] Found ${playlist.length} videos`);
console.log(`[MP4Hydra] Available servers: ${Object.keys(servers).join(', ')}`);
// For TV shows, find the specific episode
if (mediaType === 'tv' && seasonNum && episodeNum) {
const paddedSeason = seasonNum.toString().padStart(2, '0');
const paddedEpisode = episodeNum.toString().padStart(2, '0');
const seasonEpisode = `S${paddedSeason}E${paddedEpisode}`;
const targetEpisode = playlist.find(item =>
item.title && item.title.toUpperCase() === seasonEpisode.toUpperCase()
);
if (!targetEpisode) {
console.log(`[MP4Hydra] Could not find ${seasonEpisode}`);
return [];
}
console.log(`[MP4Hydra] Found episode: ${targetEpisode.show_title || targetEpisode.title}`);
// Process streams from main servers
const streams = [];
const serverConfig = [
{ name: 'Beta', number: '#1' },
{ name: 'Beta#3', number: '#2' }
];
serverConfig.forEach(server => {
const serverName = server.name;
const serverNumber = server.number;
if (servers[serverName]) {
const baseServer = servers[serverName];
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
const processedEpisode = processEpisode(targetEpisode, baseServer, serverName, serverNumber);
// Convert to standard stream format
streams.push({
title: `${details.title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
url: processedEpisode.videoUrl,
quality: processedEpisode.quality,
provider: "MP4Hydra",
size: "Unknown size",
behaviorHints: {
notWebReady: true,
headers: {
'Referer': 'https://mp4hydra.org/'
}
}
});
// Add subtitle tracks if available
if (processedEpisode.subtitles && processedEpisode.subtitles.length > 0) {
streams[streams.length - 1].subtitles = processedEpisode.subtitles.map(sub => ({
url: sub.url,
lang: sub.label
}));
}
}
});
return streams;
}
// For movies, process all videos
const streams = [];
const serverConfig = [
{ name: 'Beta', number: '#1' },
{ name: 'Beta#3', number: '#2' }
];
serverConfig.forEach(server => {
const serverName = server.name;
const serverNumber = server.number;
if (servers[serverName]) {
const baseServer = servers[serverName];
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
playlist.forEach(item => {
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
// Convert to standard stream format
streams.push({
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
url: processedItem.videoUrl,
quality: processedItem.quality,
provider: "MP4Hydra",
size: "Unknown size",
behaviorHints: {
notWebReady: true,
headers: {
'Referer': 'https://mp4hydra.org/'
}
}
});
// Add subtitle tracks if available
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
url: sub.url,
lang: sub.label
}));
}
});
}
});
return streams;
} else {
// Try with original title if the first attempt failed
if (details.title !== details.original_title) {
return await tryAlternativeTitle(details, mediaType, seasonNum, episodeNum);
}
// Try without year for movies
if (mediaType === 'movie' && details.year) {
return await tryWithoutYear(details, mediaType, seasonNum, episodeNum);
}
console.log('[MP4Hydra] No streaming links found');
return [];
}
} catch (error) {
console.error(`[MP4Hydra] Error fetching streams:`, error.message);
return [];
}
}
// Helper function to try with original title
async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) {
try {
console.log(`[MP4Hydra] Retrying with original title: ${details.original_title}`);
const originalSlug = generateSlug(details.original_title);
let originalFullSlug = originalSlug;
if (mediaType === 'movie' && details.year) {
originalFullSlug = `${originalSlug}-${details.year}`;
}
// Create new form data with original title
const formData = new FormData();
formData.append('v', '8');
formData.append('z', JSON.stringify([{
s: originalFullSlug,
t: mediaType,
se: seasonNum,
ep: episodeNum
}]));
const response = await axios({
method: 'post',
url: 'https://mp4hydra.org/info2?v=8',
data: formData,
headers: {
...formData.getHeaders(),
'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36',
'Accept': '*/*',
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
'Origin': 'https://mp4hydra.org',
'Referer': `https://mp4hydra.org/${mediaType}/${originalFullSlug}`,
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin'
},
timeout: 10000
});
// Process response
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
const playlist = response.data.playlist;
const servers = response.data.servers;
console.log(`[MP4Hydra] Found ${playlist.length} videos with original title`);
// For TV shows, find the specific episode
if (mediaType === 'tv' && seasonNum && episodeNum) {
const paddedSeason = seasonNum.toString().padStart(2, '0');
const paddedEpisode = episodeNum.toString().padStart(2, '0');
const seasonEpisode = `S${paddedSeason}E${paddedEpisode}`;
const targetEpisode = playlist.find(item =>
item.title && item.title.toUpperCase() === seasonEpisode.toUpperCase()
);
if (!targetEpisode) {
console.log(`[MP4Hydra] Could not find ${seasonEpisode} with original title`);
return [];
}
// Process streams from main servers
const streams = [];
const serverConfig = [
{ name: 'Beta', number: '#1' },
{ name: 'Beta#3', number: '#2' }
];
serverConfig.forEach(server => {
const serverName = server.name;
const serverNumber = server.number;
if (servers[serverName]) {
const baseServer = servers[serverName];
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
const processedEpisode = processEpisode(targetEpisode, baseServer, serverName, serverNumber);
// Convert to standard stream format
streams.push({
title: `${details.original_title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
url: processedEpisode.videoUrl,
quality: processedEpisode.quality,
provider: "MP4Hydra",
size: "Unknown size",
behaviorHints: {
notWebReady: true,
headers: {
'Referer': 'https://mp4hydra.org/'
}
}
});
// Add subtitle tracks if available
if (processedEpisode.subtitles && processedEpisode.subtitles.length > 0) {
streams[streams.length - 1].subtitles = processedEpisode.subtitles.map(sub => ({
url: sub.url,
lang: sub.label
}));
}
}
});
return streams;
}
// For movies, process all videos
const streams = [];
const serverConfig = [
{ name: 'Beta', number: '#1' },
{ name: 'Beta#3', number: '#2' }
];
serverConfig.forEach(server => {
const serverName = server.name;
const serverNumber = server.number;
if (servers[serverName]) {
const baseServer = servers[serverName];
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
playlist.forEach(item => {
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
// Convert to standard stream format
streams.push({
title: `${details.original_title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
url: processedItem.videoUrl,
quality: processedItem.quality,
provider: "MP4Hydra",
size: "Unknown size",
behaviorHints: {
notWebReady: true,
headers: {
'Referer': 'https://mp4hydra.org/'
}
}
});
// Add subtitle tracks if available
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
url: sub.url,
lang: sub.label
}));
}
});
}
});
return streams;
}
return [];
} catch (error) {
console.error(`[MP4Hydra] Error with alternative title:`, error.message);
return [];
}
}
// Helper function to try without year
async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) {
try {
console.log(`[MP4Hydra] Retrying with title only (without year): ${details.title}`);
const titleOnlySlug = details.slug;
// Create new form data with title only
const formData = new FormData();
formData.append('v', '8');
formData.append('z', JSON.stringify([{
s: titleOnlySlug,
t: mediaType,
se: seasonNum,
ep: episodeNum
}]));
const response = await axios({
method: 'post',
url: 'https://mp4hydra.org/info2?v=8',
data: formData,
headers: {
...formData.getHeaders(),
'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36',
'Accept': '*/*',
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
'Origin': 'https://mp4hydra.org',
'Referer': `https://mp4hydra.org/${mediaType}/${titleOnlySlug}`,
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin'
},
timeout: 10000
});
// Process response
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
const playlist = response.data.playlist;
const servers = response.data.servers;
console.log(`[MP4Hydra] Found ${playlist.length} videos with title only`);
// For movies, process all videos
const streams = [];
const serverConfig = [
{ name: 'Beta', number: '#1' },
{ name: 'Beta#3', number: '#2' }
];
serverConfig.forEach(server => {
const serverName = server.name;
const serverNumber = server.number;
if (servers[serverName]) {
const baseServer = servers[serverName];
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
playlist.forEach(item => {
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
// Convert to standard stream format
streams.push({
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
url: processedItem.videoUrl,
quality: processedItem.quality,
provider: "MP4Hydra",
size: "Unknown size",
behaviorHints: {
notWebReady: true,
headers: {
'Referer': 'https://mp4hydra.org/'
}
}
});
// Add subtitle tracks if available
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
url: sub.url,
lang: sub.label
}));
}
});
}
});
return streams;
}
return [];
} catch (error) {
console.error(`[MP4Hydra] Error with title-only:`, error.message);
return [];
}
}
// Helper function to get TMDB details
async function getTMDBDetails(tmdbId, mediaType) {
const { getTmdbApiKey } = require('../utils/tmdbKey');
const TMDB_API_KEY = getTmdbApiKey();
if(!TMDB_API_KEY){
console.warn('[MP4Hydra] TMDB API key missing; skipping TMDB metadata fetch');
return null;
}
try {
console.log(`[MP4Hydra] Fetching ${mediaType} details for TMDB ID: ${tmdbId}`);
const response = await axios.get(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${TMDB_API_KEY}`);
if (mediaType === 'movie') {
return {
title: response.data.title,
original_title: response.data.original_title,
year: response.data.release_date ? response.data.release_date.split('-')[0] : null,
slug: generateSlug(response.data.title)
};
} else {
return {
title: response.data.name,
original_title: response.data.original_name,
year: response.data.first_air_date ? response.data.first_air_date.split('-')[0] : null,
slug: generateSlug(response.data.name)
};
}
} catch (error) {
console.error(`[MP4Hydra] Error fetching details from TMDB:`, error.message);
return null;
}
}
module.exports = { getMP4HydraStreams };
+3139
View File
File diff suppressed because it is too large Load Diff
+129
View File
@@ -0,0 +1,129 @@
const axios = require('axios');
// Removed unused parseArgs helper
const getVidZeeStreams = async (tmdbId, mediaType, seasonNum, episodeNum) => {
if (!tmdbId) {
console.error('[VidZee] Error: TMDB ID (tmdbId) is required.');
return [];
}
if (!mediaType || (mediaType !== 'movie' && mediaType !== 'tv')) {
console.error('[VidZee] Error: mediaType is required and must be either "movie" or "tv".');
return [];
}
if (mediaType === 'tv') {
if (!seasonNum) {
console.error('[VidZee] Error: Season (seasonNum) is required for TV shows.');
return [];
}
if (!episodeNum) {
console.error('[VidZee] Error: Episode (episodeNum) is required for TV shows.');
return [];
}
}
const servers = [1,2,3,4,5,6,7,8,9,10]; // VidZee server identifiers
const streamPromises = servers.map(async (sr) => {
let targetApiUrl = `https://player.vidzee.wtf/api/server?id=${tmdbId}&sr=${sr}`;
if (mediaType === 'tv') {
targetApiUrl += `&ss=${seasonNum}&ep=${episodeNum}`;
}
let finalApiUrl;
const headers = {
'Referer': `https://player.vidzee.wtf/embed/movie/${tmdbId}` // Default for proxy method
};
const timeout = 7000; // Reduced timeout
const proxyBaseUrl = process.env.VIDZEE_PROXY_URL || process.env.SHOWBOX_PROXY_URL_VALUE;
if (proxyBaseUrl) {
finalApiUrl = proxyBaseUrl + encodeURIComponent(targetApiUrl);
} else {
finalApiUrl = targetApiUrl;
}
console.log(`[VidZee] Fetching from server ${sr}: ${targetApiUrl}`);
try {
const response = await axios.get(finalApiUrl, {
headers: headers,
timeout: timeout
});
const responseData = response.data;
if (!responseData || typeof responseData !== 'object') {
console.error(`[VidZee S${sr}] Error: Invalid response data from API.`);
return [];
}
if (responseData.tracks) {
delete responseData.tracks;
}
let apiSources = [];
if (responseData.url && Array.isArray(responseData.url)) {
apiSources = responseData.url;
} else if (responseData.link && typeof responseData.link === 'string') {
apiSources = [responseData];
}
if (!apiSources || apiSources.length === 0) {
console.log(`[VidZee S${sr}] No stream sources found in API response.`);
return [];
}
const streams = apiSources.map(sourceItem => {
// Prefer sourceItem.name as label, fallback to sourceItem.type, then 'VidZee Stream'
const label = sourceItem.name || sourceItem.type || 'VidZee';
// Ensure quality has 'p' if it's a resolution, or keep it as is
let quality = String(label).match(/^\d+$/) ? `${label}p` : label;
// Normalize non-numeric or ambiguous quality labels to a baseline so they survive minQuality filter
if (!/(\d{3,4})p/.test(quality.toLowerCase())) {
quality = '720p';
}
const language = sourceItem.language || sourceItem.lang;
return {
title: `VidZee S${sr} - ${quality}`,
url: sourceItem.link, // Use sourceItem.link for the URL
quality: quality,
language: language,
provider: "VidZee",
size: "Unknown size",
behaviorHints: {
notWebReady: true,
headers: {
'Referer': 'https://core.vidzee.wtf/'
}
}
};
}).filter(stream => stream.url);
console.log(`[VidZee S${sr}] Successfully extracted ${streams.length} streams. Qualities: ${streams.map(s=>s.quality).join(', ')}`);
return streams;
} catch (error) {
if (error.response) {
console.error(`[VidZee S${sr}] Error fetching: ${error.response.status} ${error.response.statusText}`);
} else if (error.request) {
console.error(`[VidZee S${sr}] Error fetching: No response received.`);
} else {
console.error(`[VidZee S${sr}] Error fetching:`, error.message);
}
return [];
}
});
const allStreamsNested = await Promise.all(streamPromises);
const allStreams = allStreamsNested.flat();
console.log(`[VidZee] Found a total of ${allStreams.length} streams from servers ${servers.join(', ')}.`);
return allStreams;
};
module.exports = { getVidZeeStreams };
File diff suppressed because it is too large Load Diff
+264
View File
@@ -0,0 +1,264 @@
const { config } = require('../utils/config');
// Lazy load showbox to avoid early side-effects; temporarily disabled until stabilized
let showbox = null;
let k4hdhub = null; // 4khdhub provider lazy ref
let moviesmod = null; // moviesmod provider lazy ref
let mp4hydra = null; // MP4Hydra provider lazy ref
let vidzee = null; // VidZee provider lazy ref
let vixsrc = null; // vixsrc provider lazy ref
let xprime = null; // xprime provider lazy ref
// Stats for debug endpoint
let lastCookieStats = { selected: null, index: null, total: 0, remainingMB: null, timestamp: null };
async function getEffectiveCookies() {
return Array.isArray(config.febboxCookies) ? config.febboxCookies : [];
}
// Adapter for Showbox provider using existing exported getStreamsFromTmdbId
async function fetchShowbox(ctx) {
if (!showbox) {
try { showbox = require('./Showbox.js'); } catch (e) { console.error('Failed to load Showbox provider:', e.message); return []; }
}
const { getTmdbApiKey } = require('../utils/tmdbKey');
const tmdbApiKey = getTmdbApiKey();
if (!tmdbApiKey) {
console.warn('[registry] showbox skipped: TMDB API key missing');
return [];
}
const cookies = await getEffectiveCookies();
const previousConfig = global.currentRequestConfig;
global.currentRequestConfig = { ...(previousConfig || {}) };
let selected = null;
if (cookies.length > 0) {
const index = Math.floor(Math.random() * cookies.length);
selected = cookies[index];
global.currentRequestConfig.cookie = selected.startsWith('ui=') ? selected : `ui=${selected}`;
global.currentRequestConfig.cookies = cookies.map(c => c.startsWith('ui=') ? c : `ui=${c}`);
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}`);
}
try {
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) {
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 || {};
}
}
const providers = [];
if (config.enableShowboxProvider) {
providers.push({ name: 'showbox', enabled: true, fetch: fetchShowbox });
} else {
console.log('[registry] showbox disabled via config');
}
// 4KHDHub adapter
async function fetch4khdhub(ctx) {
if (!k4hdhub) {
try { k4hdhub = require('./4khdhub.js'); } catch (e) { console.error('Failed to load 4khdhub provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await k4hdhub.get4KHDHubStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] 4khdhub fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || '4KHDHub', provider: '4khdhub' }));
} catch (e) {
console.error('[registry] 4khdhub fetch error:', e.message);
return [];
}
}
if (config.enable4khdhubProvider) {
providers.push({ name: '4khdhub', enabled: true, fetch: fetch4khdhub });
console.log('[registry] 4khdhub provider enabled');
} else {
console.log('[registry] 4khdhub disabled via config');
}
// MoviesMod adapter
async function fetchMoviesMod(ctx) {
if (!moviesmod) {
try { moviesmod = require('./moviesmod.js'); } catch (e) { console.error('Failed to load moviesmod provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await moviesmod.getMoviesModStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] moviesmod fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || 'MoviesMod', provider: 'moviesmod' }));
} catch (e) {
console.error('[registry] moviesmod fetch error:', e.message);
return [];
}
}
if (config.enableMoviesmodProvider) {
providers.push({ name: 'moviesmod', enabled: true, fetch: fetchMoviesMod });
console.log('[registry] moviesmod provider enabled');
} else {
console.log('[registry] moviesmod disabled via config');
}
// MP4Hydra adapter
async function fetchMp4hydra(ctx) {
if (!mp4hydra) {
try { mp4hydra = require('./MP4Hydra.js'); } catch (e) { console.error('Failed to load MP4Hydra provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await mp4hydra.getMP4HydraStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] mp4hydra fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || 'MP4Hydra', provider: 'mp4hydra' }));
} catch (e) {
console.error('[registry] mp4hydra fetch error:', e.message);
return [];
}
}
if (config.enableMp4hydraProvider) {
providers.push({ name: 'mp4hydra', enabled: true, fetch: fetchMp4hydra });
console.log('[registry] mp4hydra provider enabled');
} else {
console.log('[registry] mp4hydra disabled via config');
}
// VidZee adapter
async function fetchVidZee(ctx) {
if (!vidzee) {
try { vidzee = require('./VidZee.js'); } catch (e) { console.error('Failed to load VidZee provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await vidzee.getVidZeeStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] vidzee fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || 'VidZee', provider: 'vidzee' }));
} catch (e) {
console.error('[registry] vidzee fetch error:', e.message);
return [];
}
}
if (config.enableVidzeeProvider) {
providers.push({ name: 'vidzee', enabled: true, fetch: fetchVidZee });
console.log('[registry] vidzee provider enabled');
} else {
console.log('[registry] vidzee disabled via config');
}
// vixsrc adapter
async function fetchVixsrc(ctx) {
if (!vixsrc) {
try { vixsrc = require('./vixsrc.js'); } catch (e) { console.error('Failed to load vixsrc provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const t0 = Date.now();
const result = await vixsrc.getVixsrcStreams(ctx.tmdbId, mediaType, ctx.season || null, ctx.episode || null);
const durationMs = Date.now() - t0;
console.log(`[registry] vixsrc fetch duration ${durationMs}ms`);
if (!Array.isArray(result)) return [];
return result.map(s => ({ ...s, name: s.name || 'Vixsrc', provider: 'vixsrc' }));
} catch (e) {
console.error('[registry] vixsrc fetch error:', e.message);
return [];
}
}
if (config.enableVixsrcProvider) {
providers.push({ name: 'vixsrc', enabled: true, fetch: fetchVixsrc });
console.log('[registry] vixsrc provider enabled');
} else {
console.log('[registry] vixsrc disabled via config');
}
// xprime adapter (requires TMDB metadata lookup for title/year)
async function fetchXprime(ctx) {
if (!xprime) {
try { xprime = require('./xprime.js'); } catch (e) { console.error('Failed to load xprime provider:', e.message); return []; }
}
try {
const mediaType = ctx.type === 'movie' ? 'movie' : 'tv';
const { getTmdbApiKey } = require('../utils/tmdbKey');
const tmdbApiKey = getTmdbApiKey();
if (!tmdbApiKey) {
console.warn('[registry] xprime skipped: TMDB API key missing');
return [];
}
// Fetch minimal TMDB metadata for title + year
let title = null; let year = null;
try {
const axios = require('axios');
const metaUrl = `https://api.themoviedb.org/3/${mediaType}/${ctx.tmdbId}?api_key=${tmdbApiKey}`;
const { data: meta } = await axios.get(metaUrl, { timeout: 8000 });
title = mediaType === 'movie' ? (meta.title || meta.original_title) : (meta.name || meta.original_name);
const dateStr = mediaType === 'movie' ? meta.release_date : meta.first_air_date;
if (dateStr) year = dateStr.split('-')[0];
} catch (mErr) {
console.warn('[registry] xprime metadata fetch failed:', mErr.message);
return [];
}
if (!title || !year) {
console.log('[registry] xprime missing title/year after TMDB lookup');
return [];
}
const t0 = Date.now();
const streams = await xprime.getXprimeStreams(title, year, mediaType, ctx.season || null, ctx.episode || null, true, null);
const durationMs = Date.now() - t0;
console.log(`[registry] xprime fetch duration ${durationMs}ms (title='${title}' year=${year}) returned ${Array.isArray(streams)?streams.length:0}`);
if (!Array.isArray(streams) || !streams.length) return [];
// Normalize quality labels that are non-numeric (e.g., FHD/HD/SD)
return streams.map(s => {
let q = s.quality || 'Unknown';
if (!/(\d{3,4})p/.test(q.toLowerCase())) {
const qLower = q.toLowerCase();
if (qLower.includes('fhd') || qLower.includes('full')) q = '1080p';
else if (qLower === 'hd' || qLower.includes('720')) q = '720p';
else if (qLower.includes('sd') || qLower.includes('480')) q = '480p';
else if (qLower.includes('cam')) q = '360p';
}
return { ...s, quality: q, name: s.name || 'Xprime', provider: 'xprime' };
});
} catch (e) {
console.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 {
console.log('[registry] xprime disabled via config');
}
function listProviders() { return providers.map(p => ({ name: p.name, enabled: p.enabled })); }
function getProvider(name) { return providers.find(p => p.name === name.toLowerCase()) || null; }
function getCookieStats() { return lastCookieStats; }
module.exports = { listProviders, getProvider, getCookieStats };
File diff suppressed because it is too large Load Diff
+422
View File
@@ -0,0 +1,422 @@
#!/usr/bin/env node
const cheerio = require('cheerio');
// --- Constants ---
const VIDSRC_PROXY_URL = process.env.VIDSRC_PROXY_URL;
let BASEDOM = "https://cloudnestra.com"; // This can be updated by serversLoad
const SOURCE_URL = "https://vidsrc.xyz/embed";
// --- Helper: Conditional Proxied Fetch ---
// This function wraps the native fetch. If VIDSRC_PROXY_URL is set in the environment,
// it routes requests through the proxy. Otherwise, it makes a direct request.
async function fetchWrapper(url, options) {
if (VIDSRC_PROXY_URL) {
const proxiedUrl = `${VIDSRC_PROXY_URL}${encodeURIComponent(url)}`;
console.log(`[VidSrc Proxy] Fetching: ${url} via proxy`);
// Note: The proxy will handle the actual fetching, so we send the request to the proxy URL.
// We pass the original headers in the options, the proxy should forward them.
return fetch(proxiedUrl, options);
}
// If no proxy is set, fetch directly.
console.log(`[VidSrc Direct] Fetching: ${url}`);
return fetch(url, options);
}
// --- Helper Functions (copied and adapted from src/extractor.ts) ---
async function serversLoad(html) {
const $ = cheerio.load(html);
const servers = [];
const title = $("title").text() ?? "";
const baseFrameSrc = $("iframe").attr("src") ?? "";
if (baseFrameSrc) {
try {
const fullUrl = baseFrameSrc.startsWith("//") ? "https:" + baseFrameSrc : baseFrameSrc;
BASEDOM = new URL(fullUrl).origin;
}
catch (e) {
console.warn(`(Attempt 1) Failed to parse base URL from iframe src: ${baseFrameSrc} using new URL(), error: ${e.message}`);
// Attempt 2: Regex fallback for origin
const originMatch = (baseFrameSrc.startsWith("//") ? "https:" + baseFrameSrc : baseFrameSrc).match(/^(https?:\/\/[^/]+)/);
if (originMatch && originMatch[1]) {
BASEDOM = originMatch[1];
console.log(`(Attempt 2) Successfully extracted origin using regex: ${BASEDOM}`);
} else {
console.error(`(Attempt 2) Failed to extract origin using regex from: ${baseFrameSrc}. Using default: ${BASEDOM}`);
// Keep the default BASEDOM = "https://cloudnestra.com" if all fails
}
}
}
$(".serversList .server").each((index, element) => {
const server = $(element);
servers.push({
name: server.text().trim(),
dataHash: server.attr("data-hash") ?? null,
});
});
return {
servers: servers,
title: title,
};
}
async function parseMasterM3U8(m3u8Content, masterM3U8Url) {
const lines = m3u8Content.split('\n').map(line => line.trim());
const streams = [];
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith("#EXT-X-STREAM-INF:")) {
const infoLine = lines[i];
let quality = "unknown";
const resolutionMatch = infoLine.match(/RESOLUTION=(\d+x\d+)/);
if (resolutionMatch && resolutionMatch[1]) {
quality = resolutionMatch[1];
}
else {
const bandwidthMatch = infoLine.match(/BANDWIDTH=(\d+)/);
if (bandwidthMatch && bandwidthMatch[1]) {
quality = `${Math.round(parseInt(bandwidthMatch[1]) / 1000)}kbps`;
}
}
if (i + 1 < lines.length && lines[i + 1] && !lines[i + 1].startsWith("#")) {
const streamUrlPart = lines[i + 1];
try {
const fullStreamUrl = new URL(streamUrlPart, masterM3U8Url).href;
streams.push({ quality: quality, url: fullStreamUrl });
}
catch (e) {
console.error(`Error constructing URL for stream part: ${streamUrlPart} with base: ${masterM3U8Url}`, e);
streams.push({ quality: quality, url: streamUrlPart }); // Store partial URL as a fallback
}
i++;
}
}
}
// Sort streams by quality (highest first)
streams.sort((a, b) => {
// Extract resolution height from quality (e.g., "1280x720" -> 720)
const getHeight = (quality) => {
const match = quality.match(/(\d+)x(\d+)/);
return match ? parseInt(match[2], 10) : 0;
};
const heightA = getHeight(a.quality);
const heightB = getHeight(b.quality);
// Higher resolution comes first
return heightB - heightA;
});
return streams;
}
async function PRORCPhandler(prorcp) {
try {
const prorcpUrl = `${BASEDOM}/prorcp/${prorcp}`;
const prorcpFetch = await fetchWrapper(prorcpUrl, {
headers: {
"accept": "*/*", "accept-language": "en-US,en;q=0.9", "priority": "u=1",
"sec-ch-ua": "\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"",
"sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "script", "sec-fetch-mode": "no-cors", "sec-fetch-site": "same-origin",
'Sec-Fetch-Dest': 'iframe', "Referer": `${BASEDOM}/`, "Referrer-Policy": "origin",
},
timeout: 10000
});
if (!prorcpFetch.ok) {
console.error(`Failed to fetch prorcp: ${prorcpUrl}, status: ${prorcpFetch.status}`);
return null;
}
const prorcpResponse = await prorcpFetch.text();
const regex = /file:\s*'([^']*)'/gm;
const match = regex.exec(prorcpResponse);
if (match && match[1]) {
const masterM3U8Url = match[1];
const m3u8FileFetch = await fetchWrapper(masterM3U8Url, {
headers: { "Referer": prorcpUrl, "Accept": "*/*" },
timeout: 10000
});
if (!m3u8FileFetch.ok) {
console.error(`Failed to fetch master M3U8: ${masterM3U8Url}, status: ${m3u8FileFetch.status}`);
return null;
}
const m3u8Content = await m3u8FileFetch.text();
return parseMasterM3U8(m3u8Content, masterM3U8Url);
}
console.warn("No master M3U8 URL found in prorcp response for:", prorcpUrl);
return null;
}
catch (error) {
console.error(`Error in PRORCPhandler for ${BASEDOM}/prorcp/${prorcp}:`, error);
return null;
}
}
async function SRCRCPhandler(srcrcpPath, refererForSrcrcp) {
try {
const srcrcpUrl = BASEDOM + srcrcpPath;
console.log(`[VidSrc - SRCRCP] Fetching: ${srcrcpUrl} (Referer: ${refererForSrcrcp})`);
const response = await fetchWrapper(srcrcpUrl, {
headers: {
"accept": "*/*",
"accept-language": "en-US,en;q=0.9",
"sec-ch-ua": "\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "iframe",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "same-origin", // This might need to be 'cross-site' if BASEDOM's origin differs from srcrcp link's origin
"Referer": refererForSrcrcp,
"Referrer-Policy": "origin",
},
timeout: 10000
});
if (!response.ok) {
console.error(`[VidSrc - SRCRCP] Failed to fetch ${srcrcpUrl}, status: ${response.status}`);
return null;
}
const responseText = await response.text();
console.log(`[VidSrc - SRCRCP] Response from ${srcrcpUrl} (first 500 chars): ${responseText.substring(0, 500)}`);
// Attempt 1: Check for "file: '...'" like in PRORCP
const fileRegex = /file:\s*'([^']*)'/gm;
const fileMatch = fileRegex.exec(responseText);
if (fileMatch && fileMatch[1]) {
const masterM3U8Url = fileMatch[1];
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (via fileMatch): ${masterM3U8Url}`);
const m3u8FileFetch = await fetchWrapper(masterM3U8Url, {
headers: { "Referer": srcrcpUrl, "Accept": "*/*" },
timeout: 10000
});
if (!m3u8FileFetch.ok) {
console.error(`[VidSrc - SRCRCP] Failed to fetch master M3U8: ${masterM3U8Url}, status: ${m3u8FileFetch.status}`);
return null;
}
const m3u8Content = await m3u8FileFetch.text();
return parseMasterM3U8(m3u8Content, masterM3U8Url);
}
// Attempt 2: Check if the responseText itself is an M3U8 playlist
if (responseText.trim().startsWith("#EXTM3U")) {
console.log(`[VidSrc - SRCRCP] Response from ${srcrcpUrl} appears to be an M3U8 playlist directly.`);
return parseMasterM3U8(responseText, srcrcpUrl);
}
// Attempt 3: Look for sources = [...] or sources: [...] in script tags or in JSON-like structures
const $ = cheerio.load(responseText);
let sourcesFound = null;
$('script').each((i, script) => {
const scriptContent = $(script).html();
if (scriptContent) {
// Regex for various ways sources might be defined
const sourcesRegexes = [
/sources\s*[:=]\s*(\[[^\]]*\{(?:\s*|.*?)file\s*:\s*['"]([^'"]+)['"](?:\s*|.*?)\}[^\]]*\])/si, // extracts the URL from sources: [{file: "URL"}]
/playerInstance\.setup\s*\(\s*\{\s*sources\s*:\s*(\[[^\]]*\{(?:\s*|.*?)file\s*:\s*['"]([^'"]+)['"](?:\s*|.*?)\}[^\]]*\])/si, // for playerInstance.setup({sources: [{file: "URL"}]})
/file\s*:\s*['"]([^'"]+\.m3u8[^'"]*)['"]/i, // Direct M3U8 link in a var or object e.g. file: "URL.m3u8"
/src\s*:\s*['"]([^'"]+\.m3u8[^'"]*)['"]/i, // Direct M3U8 link e.g. src: "URL.m3u8"
/loadSource\(['"]([^'"]+\.m3u8[^'"]*)['"]\)/i, // For .loadSource("URL.m3u8")
/new\s+Player\([^)]*\{\s*src\s*:\s*['"]([^'"]+)['"]\s*\}\s*\)/i // For new Player({src: "URL"})
];
for (const regex of sourcesRegexes) {
const sourcesMatch = scriptContent.match(regex);
// For regexes that capture a JSON array-like structure in group 1 and then the URL in group 2 (first two regexes)
if (regex.source.includes('file') && regex.source.includes('sources')) { // Heuristic to identify complex sources regex capturing group 2
if (sourcesMatch && sourcesMatch[2]) { // URL is in group 2
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (script complex): ${sourcesMatch[2]}`);
sourcesFound = [{ quality: 'default', url: sourcesMatch[2] }];
return false; // break cheerio loop
}
}
// For simpler regexes where URL is in group 1
else if (sourcesMatch && sourcesMatch[1]) {
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (script simple): ${sourcesMatch[1]}`);
sourcesFound = [{ quality: 'default', url: sourcesMatch[1] }];
return false; // break cheerio loop
}
}
// Fallback: Look for any absolute .m3u8 URL within the script tag
if (!sourcesFound) {
const m3u8GenericMatch = scriptContent.match(/['"](https?:\/\/[^'"\s]+\.m3u8[^'"\s]*)['"]/i);
if (m3u8GenericMatch && m3u8GenericMatch[1]) {
console.log(`[VidSrc - SRCRCP] Found M3U8 URL (script generic fallback): ${m3u8GenericMatch[1]}`);
sourcesFound = [{ quality: 'default', url: m3u8GenericMatch[1] }];
return false; // break cheerio loop
}
}
}
});
if (sourcesFound && sourcesFound.length > 0) {
// Process the first valid M3U8 URL found, or return direct links
const m3u8Source = sourcesFound.find(s => s.url && s.url.includes('.m3u8'));
if (m3u8Source) {
console.log(`[VidSrc - SRCRCP] First M3U8 source from script: ${m3u8Source.url}`);
// Ensure URL is absolute
const absoluteM3u8Url = m3u8Source.url.startsWith('http') ? m3u8Source.url : new URL(m3u8Source.url, srcrcpUrl).href;
const m3u8FileFetch = await fetchWrapper(absoluteM3u8Url, {
headers: { "Referer": srcrcpUrl, "Accept": "*/*" },
timeout: 10000
});
if (!m3u8FileFetch.ok) {
console.error(`[VidSrc - SRCRCP] Failed to fetch M3U8 from script source: ${absoluteM3u8Url}, status: ${m3u8FileFetch.status}`);
return null;
}
const m3u8Content = await m3u8FileFetch.text();
return parseMasterM3U8(m3u8Content, absoluteM3u8Url);
} else {
// Assuming direct links if no .m3u8 found in the sources array
console.log(`[VidSrc - SRCRCP] Assuming direct links from script sources:`, sourcesFound);
return sourcesFound.map(s => ({
quality: s.quality || s.label || 'auto',
url: s.url.startsWith('http') ? s.url : new URL(s.url, srcrcpUrl).href
}));
}
}
console.warn(`[VidSrc - SRCRCP] No stream extraction method succeeded for ${srcrcpUrl}`);
return null;
} catch (error) {
console.error(`[VidSrc - SRCRCP] Error in SRCRCPhandler for ${srcrcpPath}:`, error);
return null;
}
}
async function rcpGrabber(html) {
const regex = /src:\s*'([^']*)'/;
const match = html.match(regex);
if (!match || !match[1])
return null;
return { metadata: { image: "" }, data: match[1] };
}
function getObject(id) {
const arr = id.split(':');
return { id: arr[0], season: arr[1], episode: arr[2] };
}
function getUrl(id, type) {
if (type === "movie") {
return `${SOURCE_URL}/movie/${id}`;
}
else {
const obj = getObject(id);
return `${SOURCE_URL}/tv/${obj.id}/${obj.season}-${obj.episode}`;
}
}
async function getStreamContent(id, type) {
const url = getUrl(id, type);
const embedRes = await fetchWrapper(url, { headers: { "Referer": SOURCE_URL } });
if (!embedRes.ok) {
console.error(`Failed to fetch embed page ${url}: ${embedRes.status}`);
return [];
}
const embedResp = await embedRes.text();
const { servers, title } = await serversLoad(embedResp);
const apiResponse = [];
// MODIFIED: Process servers in parallel
const serverPromises = servers.map(async (server) => {
if (!server.dataHash) return null; // Skip servers without dataHash
try {
const rcpUrl = `${BASEDOM}/rcp/${server.dataHash}`;
const rcpRes = await fetchWrapper(rcpUrl, {
headers: { 'Sec-Fetch-Dest': 'iframe', "Referer": url }
});
if (!rcpRes.ok) {
console.warn(`RCP fetch failed for server ${server.name}: ${rcpRes.status}`);
return null; // Failed to fetch RCP
}
const rcpHtml = await rcpRes.text();
const rcpData = await rcpGrabber(rcpHtml);
if (!rcpData || !rcpData.data) {
console.warn(`Skipping server ${server.name} due to missing rcp data.`);
return null; // Missing RCP data
}
let streamDetails = null;
if (rcpData.data.startsWith("/prorcp/")) {
streamDetails = await PRORCPhandler(rcpData.data.replace("/prorcp/", ""));
} else if (rcpData.data.startsWith("/srcrcp/")) {
if (server.name === "Superembed" || server.name === "2Embed") {
console.warn(`[VidSrc] Skipping SRCRCP for known problematic server: ${server.name}`);
return null;
}
streamDetails = await SRCRCPhandler(rcpData.data, rcpUrl); // Pass rcpUrl as referer
} else {
console.warn(`Unhandled rcp data type for server ${server.name}: ${rcpData.data.substring(0, 50)}`);
return null; // Unhandled type
}
if (streamDetails && streamDetails.length > 0) {
return {
name: title,
image: rcpData.metadata.image,
mediaId: id,
streams: streamDetails,
referer: BASEDOM,
};
} else {
console.warn(`No stream details from handler for server ${server.name} (${rcpData.data})`);
return null; // No stream details found by the handler
}
} catch (e) {
console.error(`Error processing server ${server.name} (${server.dataHash}):`, e);
return null; // Error during server processing
}
});
const results = await Promise.all(serverPromises);
// Filter out null results (failed servers or no streams) and add valid results to apiResponse
results.forEach(result => {
if (result) {
apiResponse.push(result);
}
});
// END MODIFICATION
return apiResponse;
}
// --- Main execution logic (conditional) ---
async function main() {
const args = process.argv.slice(2);
if (args.length < 2) {
console.error("Usage: node vidsrcextractor.js <id> <type>");
console.error("Example (movie): node vidsrcextractor.js tt0111161 movie");
console.error("Example (series): node vidsrcextractor.js tt0944947:1:1 series"); // Game of Thrones S1E1
return 1; // error code
}
const id = args[0];
const type = args[1];
if (type !== "movie" && type !== "series") {
console.error("Invalid type. Must be 'movie' or 'series'.");
return 1;
}
if (type === "series" && id.split(':').length < 3) {
console.error("Invalid series ID format. Expected 'ttID:season:episode' (e.g., tt0944947:1:1).");
return 1;
}
console.log(`Fetching streams for ID: ${id}, Type: ${type}`);
try {
const results = await getStreamContent(id, type);
if (results && results.length > 0) {
console.log("Extracted Data:");
console.log(JSON.stringify(results, null, 2));
} else {
console.log("No streams found or an error occurred.");
}
return 0;
} catch (error) {
console.error("Error during extraction process:", error);
return 1;
}
}
// Export the function for use in other modules
module.exports = { getStreamContent };
// Run main only if the script is executed directly
if (require.main === module) {
main()
.then(code => {
if (code !== 0) {
console.error(`[vidsrcextractor] Finished with non-zero code ${code}`);
}
})
.catch(error => {
console.error("Unhandled error in main execution:", error);
});
}
+215
View File
@@ -0,0 +1,215 @@
/**
* Vixsrc streaming provider integration (standalone)
* React Native compatible variant - no external addon framework
*/
// Constants
const TMDB_API_KEY = "68e094699525b18a70bab2f86b1fa706";
const BASE_URL = 'https://vixsrc.to';
// Helper function to make HTTP requests with default headers
function makeRequest(url, options = {}) {
const defaultHeaders = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'application/json,*/*',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
...options.headers
};
return fetch(url, {
method: options.method || 'GET',
headers: defaultHeaders,
...options
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response;
})
.catch(error => {
console.error(`[Vixsrc] Request failed for ${url}: ${error.message}`);
throw error;
});
}
// Helper function to get TMDB info
function getTmdbInfo(tmdbId, mediaType) {
const url = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`;
return makeRequest(url)
.then(response => response.json())
.then(data => ({ data }));
}
// Helper function to parse M3U8 playlist
// Removed obsolete parseM3U8Playlist implementation
// Removed unused helpers getQualityFromResolution and resolveUrl
// Helper function to extract stream URL from Vixsrc page
function extractStreamFromPage(url, contentType, contentId, seasonNum, episodeNum) {
let vixsrcUrl;
let subtitleApiUrl;
if (contentType === 'movie') {
vixsrcUrl = `${BASE_URL}/movie/${contentId}`;
subtitleApiUrl = `https://sub.wyzie.ru/search?id=${contentId}`;
} else {
vixsrcUrl = `${BASE_URL}/tv/${contentId}/${seasonNum}/${episodeNum}`;
subtitleApiUrl = `https://sub.wyzie.ru/search?id=${contentId}&season=${seasonNum}&episode=${episodeNum}`;
}
console.log(`[Vixsrc] Fetching: ${vixsrcUrl}`);
return makeRequest(vixsrcUrl, {
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
}
})
.then(response => response.text())
.then(html => {
console.log(`[Vixsrc] HTML length: ${html.length} characters`);
let masterPlaylistUrl = null;
// Method 1: Look for window.masterPlaylist (primary method)
if (html.includes('window.masterPlaylist')) {
console.log('[Vixsrc] Found window.masterPlaylist');
const urlMatch = html.match(/url:\s*['"]([^'"]+)['"]/);
const tokenMatch = html.match(/['"]?token['"]?\s*:\s*['"]([^'"]+)['"]/);
const expiresMatch = html.match(/['"]?expires['"]?\s*:\s*['"]([^'"]+)['"]/);
if (urlMatch && tokenMatch && expiresMatch) {
const baseUrl = urlMatch[1];
const token = tokenMatch[1];
const expires = expiresMatch[1];
console.log('[Vixsrc] Extracted tokens:');
console.log(` - Base URL: ${baseUrl}`);
console.log(` - Token: ${token.substring(0, 20)}...`);
console.log(` - Expires: ${expires}`);
// Construct the master playlist URL
if (baseUrl.includes('?b=1')) {
masterPlaylistUrl = `${baseUrl}&token=${token}&expires=${expires}&h=1&lang=en`;
} else {
masterPlaylistUrl = `${baseUrl}?token=${token}&expires=${expires}&h=1&lang=en`;
}
console.log(`[Vixsrc] Constructed master playlist URL: ${masterPlaylistUrl}`);
}
}
// Method 2: Look for direct .m3u8 URLs
if (!masterPlaylistUrl) {
const m3u8Match = html.match(/(https?:\/\/[^'"\s]+\.m3u8[^'"\s]*)/);
if (m3u8Match) {
masterPlaylistUrl = m3u8Match[1];
console.log('[Vixsrc] Found direct .m3u8 URL:', masterPlaylistUrl);
}
}
// Method 3: Look for stream URLs in script tags
if (!masterPlaylistUrl) {
const scriptMatches = html.match(/<script[^>]*>(.*?)<\/script>/gs);
if (scriptMatches) {
for (const script of scriptMatches) {
const streamMatch = script.match(/['"]?(https?:\/\/[^'"\s]+(?:\.m3u8|playlist)[^'"\s]*)/);
if (streamMatch) {
masterPlaylistUrl = streamMatch[1];
console.log('[Vixsrc] Found stream in script:', masterPlaylistUrl);
break;
}
}
}
}
if (!masterPlaylistUrl) {
console.log('[Vixsrc] No master playlist URL found');
return null;
}
return { masterPlaylistUrl, subtitleApiUrl };
});
}
// Helper function to get subtitles
function getSubtitles(subtitleApiUrl) {
return makeRequest(subtitleApiUrl)
.then(response => response.json())
.then(subtitleData => {
// Find English subtitle track (same logic as original)
let subtitleTrack = subtitleData.find(track =>
track.display.includes('English') && (track.encoding === 'ASCII' || track.encoding === 'UTF-8')
);
if (!subtitleTrack) {
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1252');
}
if (!subtitleTrack) {
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1250');
}
if (!subtitleTrack) {
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP850');
}
const subtitles = subtitleTrack ? subtitleTrack.url : '';
console.log(subtitles ? `[Vixsrc] Found subtitles: ${subtitles}` : '[Vixsrc] No English subtitles found');
return subtitles;
})
.catch(error => {
console.log('[Vixsrc] Subtitle fetch failed:', error.message);
return '';
});
}
// Main function to get streams
function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
console.log(`[Vixsrc] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
return getTmdbInfo(tmdbId, mediaType)
.then(() => extractStreamFromPage(null, mediaType, tmdbId, seasonNum, episodeNum))
.then(streamData => {
if (!streamData) {
console.log('[Vixsrc] No stream data found');
return [];
}
const { masterPlaylistUrl, subtitleApiUrl } = streamData;
// Return single master playlist with Auto quality
console.log('[Vixsrc] Returning master playlist with Auto quality...');
// Get subtitles
return getSubtitles(subtitleApiUrl)
.then(subtitles => {
// Return single stream with master playlist
const tmdbStreams = [{
name: "Vixsrc",
title: "Auto Quality Stream",
url: masterPlaylistUrl,
quality: '1080p',
type: 'direct',
headers: {
'Referer': BASE_URL,
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
}
}];
console.log('[Vixsrc] Successfully processed 1 stream with Auto quality');
return tmdbStreams;
});
})
.catch(error => {
console.error(`[Vixsrc] Error in getVixsrcStreams: ${error.message}`);
return [];
});
}
module.exports = { getVixsrcStreams };
+166
View File
@@ -0,0 +1,166 @@
// 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 };
+209
View File
@@ -0,0 +1,209 @@
<!DOCTYPE html><html lang="en"><head>
<meta charset="utf-8" />
<title>TMDB Embed Admin</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="stylesheet" href="/style.css" />
<script defer src="/index.js"></script>
</head>
<body>
<button class="burger" aria-label="Toggle navigation">☰</button>
<div class="overlay"></div>
<aside class="sidebar" id="sidebar">
<div class="brand">TMDB Embed API</div>
<nav class="nav">
<button class="nav-item" id="logoutBtn" data-action="logout">Logout</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-quality">Quality / Filters</button>
<button class="nav-item" data-target="panel-keys">Keys</button>
<button class="nav-item" data-target="panel-febbox">FebBox / PStream</button>
<button class="nav-item" data-target="panel-advanced">Advanced</button>
<button class="nav-item" data-target="panel-live">Live Config</button>
<div class="grow"></div>
</nav>
<footer class="side-footer">
<a class="hint" style="margin-top: .5rem; color: var(--primary-fg);" href="https://github.com/Inside4ndroid" target="_blank">Github</a>
<a class="hint" style="margin-top: .5rem; color: var(--primary-fg);" href="https://x.com/Inside_4ndroid" target="_blank">X (Twitter)</a>
</footer>
<footer class="side-footer">Credits to :<a class="hint" style="margin-top: .5rem; color: var(--primary-fg);" href="https://github.com/tapframe/NuvioStreamsAddon" target="_blank">NuvioStreamsAddon for Stremio</a></footer>
<div class="bmc-widget-container">
<script type="text/javascript" src="https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js" data-name="bmc-button" data-slug="K3QSoR2" data-color="#5F7FFF" data-emoji="☕" data-font="Bree" data-text="Buy me a coffee" data-outline-color="#000000" data-font-color="#ffffff" data-coffee-color="#FFDD00" ></script>
</div>
<footer class="side-footer">Made with ❤️ by Inside4ndroid</footer>
</aside>
<main class="main">
<div id="bootWarn" class="alert warning" style="display:none;">No FEBBOX_COOKIES configured. Showbox / PStream streams may be unavailable.</div>
<div id="cacheDisabledBanner" class="alert info" style="display:none;">Cache is <strong>DISABLED</strong>; expect slower responses & higher source load.</div>
<div id="tmdbMissingBanner" class="alert warning" style="display:none;">No TMDB API Keys configured. Metadata-dependent providers (Showbox, 4khdhub, MoviesMod, MP4Hydra) may degrade.</div>
<form id="cfgForm" onsubmit="return false;" class="panels">
<section id="panel-status" class="panel">
<h1>Server Status<button style="margin-left: 1rem;" id="refreshStatusBtn" type="button">Refresh Status</button></h1>
<div class="status-grid">
<div class="status-block">
<h2>Metrics</h2>
<pre id="statusMetrics" class="status-pre">loading...</pre>
</div>
<div class="status-block">
<h2>Providers</h2>
<button style="margin-bottom: 1rem;" id="runProviderChecksBtn" type="button">Run Provider Functional Checks</button>
<span style="margin-left: .5rem; margin-bottom: 1rem;" id="providerCheckSummary" class="hint"></span>
<div id="statusProvidersEnhanced" class="providers-table" aria-live="polite"></div>
</div>
<div class="status-block wide">
<h2>Endpoints</h2>
<pre id="statusEndpoints" class="status-pre">loading...</pre>
</div>
</div>
</section>
<section id="panel-core" class="panel">
<h1>Core Settings</h1>
<div class="field"><label>API Port<p class="hint">Provider enable/disable & feature flags apply immediately. Click toggles to enable (highlighted) or disable (dim).</p><input type="text" name="port" placeholder="8787" /></label></div>
<div class="field"><label>Default Region<p class="hint">This is best left set to auto/none.</p>
<select name="defaultRegion">
<option value="">(auto / none)</option>
<option value="USA7">US East</option>
<option value="USA6">US West</option>
<option value="USA5">US Middle</option>
<option value="UK3">United Kingdom</option>
<option value="CA1">Canada</option>
<option value="FR1">France</option>
<option value="DE2">Germany</option>
<option value="HK1">Hong Kong</option>
<option value="IN1">India</option>
<option value="AU1">Australia</option>
<option value="SZ">China</option>
</select>
</label></div>
<div class="field"><label>Providers (Enable + Default)<p class="hint">Click power icon to enable/disable, star to mark as default. If no defaults selected all enabled providers aggregate.</p>
<div id="providerMatrix" class="provider-matrix" aria-live="polite"></div>
<input type="hidden" name="defaultProviders" />
</label>
</div>
</section>
<section id="panel-quality" class="panel">
<h1>Quality & Filtering</h1>
<div class="field quality-group"><label>Minimum Quality<p class="hint">Preset radios serialize to a simple string. Custom & advanced modes use the JSON fields.</p>
<div class="quality-presets" id="qualityPresets">
<label class="q-item"><input type="radio" name="qualityPreset" value="all"> <span>All</span></label>
<label class="q-item"><input type="radio" name="qualityPreset" value="480p"> <span>480p+</span></label>
<label class="q-item"><input type="radio" name="qualityPreset" value="720p"> <span>720p+</span></label>
<label class="q-item"><input type="radio" name="qualityPreset" value="1080p"> <span>1080p+</span></label>
<label class="q-item"><input type="radio" name="qualityPreset" value="1440p"> <span>1440p+</span></label>
<label class="q-item"><input type="radio" name="qualityPreset" value="2160p"> <span>4K+</span></label>
<label class="q-item adv"><input type="radio" name="qualityPreset" value="custom"> <span>Custom JSON</span></label>
</div>
</label>
<textarea name="minQualitiesRaw" class="mini quality-json" placeholder='{"default":"900p"}' disabled></textarea>
</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="codec-presets" id="codecPresets">
<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="hdr"> <span>HDR / HDR10(+)</span></label>
<label class="c-item adv"><input type="radio" name="codecPreset" value="advanced"> <span>Advanced JSON</span></label>
</div>
</label>
<textarea name="excludeCodecsRaw" class="mini json-adv" placeholder='{"excludeDV":true,"excludeHDR":false}' disabled></textarea>
</div>
</section>
<section id="panel-keys" class="panel">
<h1>API Keys</h1>
<div class="field"><label>TMDB API Keys<p class="hint">Add multiple TMDB API keys. They rotate randomly per request to spread usage.</p>
<div class="key-add-row">
<input id="tmdbKeyInput" type="text" placeholder="Enter TMDB key" autocomplete="off" />
<button id="addTmdbKeyBtn" type="button" class="small">Add Key</button>
</div>
<div id="tmdbKeyList" class="key-list" aria-live="polite"></div>
<input type="hidden" name="tmdbApiKeysHidden" />
</label></div>
</section>
<section id="panel-febbox" class="panel">
<h1>FebBox / PStream</h1>
<p class="hint">ShowBox fetches content from a cloud storage provider called FebBox. Each FebBox user gets 100GB/month of fast streaming before speeds are reduced.</p>
<a class="hint" style="margin-bottom: 1rem;" href="https://febbox.com">Febbox Website</a>
<div class="field"><label>FebBox Cookies<p class="hint">Add multiple FebBox ui tokens. They will rotate / be evaluated. Removal updates overrides after saving.</p>
<div class="cookie-add-row">
<input id="febboxCookieInput" type="text" placeholder="Enter ui token (without ui=)" autocomplete="off" />
<button id="addFebboxCookieBtn" type="button" class="small">Add Cookie</button>
</div>
<div id="febboxCookieList" class="cookie-list" aria-live="polite"></div>
<input type="hidden" name="febboxCookiesHidden" />
</label></div>
<div class="field">
<label><strong>How to find your cookie</strong></label>
</div>
<ol class="hint">
<li>
<strong>Open Febbox.com</strong><br>
Go to https://febbox.com in your web browser.
</li>
<li>
<strong>Log in with Google</strong><br>
Use a Google account to log in to the Febbox site.<br>
<em>Tip:</em> For safety, it’s recommended to use a fresh account.
</li>
<li>
<strong>Open Developer Tools</strong><br>
Press the <kbd>F12</kbd> key on your keyboard.<br>
Alternatively, right-click anywhere on the page and select
<code>Inspect</code> or <code>Inspect Element</code>.
</li>
<li>
<strong>Navigate to Cookies</strong><br>
In the Developer Tools panel, go to the <code>Application</code> tab.<br>
Expand the <code>Storage</code> section and click on <code>Cookies</code>.
</li>
<li>
<strong>Find the "ui" Cookie</strong><br>
In the list of cookies, look for the cookie with the name <code>ui</code>.
</li>
<li>
<strong>Copy the value</strong><br>
Copy the cookie value and paste it above then click add cookie.
</li>
</ol>
</section>
<section id="panel-advanced" class="panel">
<h1>Advanced & Providers</h1>
<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">
<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_disable4khdhubValidation" value="1" /> <span>Disable 4KHDHub Validation</span></label>
</div>
<h1>Proxy / Paths</h1>
<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="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>
</section>
<section id="panel-live" class="panel">
<h1>Live Configuration</h1>
<h2>Merged</h2>
<p class="hint">This section shows the currently active configuration, including all merged settings from various sources.</p>
<pre id="currentCfg">loading...</pre>
<h2>Overrides (user-config.json)</h2>
<p class="hint">These are the user-defined settings from this panel that override the default configuration.</p>
<pre id="overrideCfg">(none)</pre>
</section>
</form>
<div class="actions-bar">
<button id="saveBtn" type="button" class="primary" disabled>Save Overrides</button>
<button id="reloadBtn" type="button">Reload</button>
<button id="clearBtn" type="button" class="danger">Clear All</button>
<span id="status" class="status"></span>
<span id="validationSummary" class="validation-summary"></span>
</div>
</main>
</body></html>
+65
View File
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>TMDB Embed Login</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="stylesheet" href="/style.css" />
<style>
body { display:flex; align-items:center; justify-content:center; min-height:100vh; background:#0f141a; font-family: system-ui, sans-serif; }
.login-card { background:#1e1e24; padding:2rem 2.25rem; border-radius:12px; width:100%; max-width:380px; box-shadow:0 4px 24px -4px rgba(0,0,0,.6); }
.login-card h1 { margin:0 0 1rem; font-size:1.3rem; color: white; text-align: center;}
.pw-change { display:none; background:#1e1e24; padding:2rem 2.25rem; border-radius:12px; width:100%; max-width:380px; box-shadow:0 4px 24px -4px rgba(0,0,0,.6); }
.pw-change h2 { margin:0 0 1rem; font-size:1.3rem; color: white; text-align: center;}
.field { display:flex; flex-direction:column; gap:.35rem; margin-bottom:.9rem; }
.field label { font-size:.85rem; text-transform:uppercase; letter-spacing:.08em; color:#bbb; text-align: center;}
.field input { padding:.65rem .75rem !important; border:1px solid #333 !important; color:#bbb !important; border-radius:6px !important; background:#111 !important; font-size:.95rem !important; }
.actions { display:block; gap:.75rem; align-items:center; text-align: center;}
button.primary { background:#2563eb; color:#fff; padding:.65rem 1rem; border:none; border-radius:6px; cursor:pointer; font-weight:600; font-size:.95rem; }
button.primary:disabled { opacity:.55; cursor:default; }
.status { min-height:1.1rem; font-size:.8rem; color:#eee; }
.error { color:#f87171; }
.success { color:#4ade80; }
.meta { margin-top:1.25rem; font-size:.7rem; line-height:1.2; color:#666; text-align:center; }
.link-btn { background:none; border:none; color:#60a5fa; cursor:pointer; font-size:.75rem; padding:0; }
.expiry-text { margin-top:.5rem; margin-bottom: .5rem; font-weight: 500; color: yellow;}
</style>
<script defer src="/login.js"></script>
</head>
<body>
<div class="login-card" id="loginCard">
<div id="lockoutBanner" style="display:none; background:#7f1d1d; color:#fca5a5; padding:.6rem .75rem; border-radius:8px; font-size:.8rem; line-height:1.2; margin-bottom:1rem;">
<strong>Too Many Attempts.</strong> Try again in <span id="lockoutCountdown">00:00</span>.
</div>
<h1>TMDB Embed Admin</h1>
<form id="loginForm" autocomplete="off">
<div class="field">
<label for="username">Username</label>
<input id="username" name="username" type="text" required autofocus />
</div>
<div class="field">
<label for="password">Password</label>
<input id="password" name="password" type="password" required />
</div>
<div class="actions">
<button id="loginBtn" class="primary" type="submit">Login</button>
<div class="status" id="status" aria-live="polite"></div>
</div>
</form>
<div class="pw-change" id="pwChange">
<h2>Change Password</h2>
<form id="pwForm" autocomplete="off">
<div class="field"><label for="oldPw">Old Password</label><input id="oldPw" type="password" required /></div>
<div class="field"><label for="newPw">New Password</label><input id="newPw" type="password" minlength="8" required /></div>
<div class="actions"><button id="pwBtn" class="primary" type="submit">Update</button><div class="status" id="pwStatus" aria-live="polite"></div></div>
</form>
</div>
<div class="meta">
<button id="togglePw" class="link-btn" type="button">Change Password</button>
<div class="expiry-text">Sessions expire after 12h or on server restart.</div>
<p class="login-hint">Default user created if none exists: admin / change-me (change it!).</p>
</div>
</div>
</body>
</html>
+573
View File
@@ -0,0 +1,573 @@
// Admin panel logic (refactored into IIFE, no inline handlers)
(function(){
async function fetchConfig(){
const r = await fetch('/api/config');
if(!r.ok) throw new Error('Failed to fetch config');
return r.json();
}
async function fetchProviders(){
const r = await fetch('/api/providers');
if(!r.ok) throw new Error('Failed to fetch providers');
return r.json();
}
function setFebboxCookies(cookiesArray){
const listEl = document.getElementById('febboxCookieList');
const hidden = document.querySelector('input[name="febboxCookiesHidden"]');
if(!listEl || !hidden) return;
const arr = Array.isArray(cookiesArray)? cookiesArray.filter(c=>c && typeof c==='string') : [];
hidden.value = arr.join(',');
listEl.innerHTML = arr.length? arr.map((c,i)=>`<div class="cookie-item" data-idx="${i}"><code>${c}</code><button type="button" class="remove" title="Remove" data-action="rm" data-idx="${i}">✕</button></div>`).join('') : '<div class="empty">No cookies added</div>';
}
function addFebboxCookie(){
const input = document.getElementById('febboxCookieInput');
const val = (input.value||'').trim();
if(!val) return;
const hidden = document.querySelector('input[name="febboxCookiesHidden"]');
const existing = hidden.value? hidden.value.split(',').filter(Boolean):[];
if(existing.includes(val)) { input.value=''; return; }
existing.push(val);
setFebboxCookies(existing);
input.value='';
validate();
}
function removeFebboxCookie(idx){
const hidden = document.querySelector('input[name="febboxCookiesHidden"]');
const existing = hidden.value? hidden.value.split(',').filter(Boolean):[];
const next = existing.filter((_,i)=> i!=idx);
setFebboxCookies(next);
validate();
}
document.addEventListener('click', (e)=>{
const btn = e.target.closest('#addFebboxCookieBtn');
if(btn){ addFebboxCookie(); }
const rm = e.target.closest('.cookie-item button.remove');
if(rm){ const idx = rm.dataset.idx; removeFebboxCookie(idx); }
});
document.addEventListener('keydown',(e)=>{ if(e.key==='Enter' && e.target.id==='febboxCookieInput'){ e.preventDefault(); addFebboxCookie(); }});
// TMDB multi-key management
function setTmdbKeys(keys){
const listEl = document.getElementById('tmdbKeyList');
const hidden = document.querySelector('input[name="tmdbApiKeysHidden"]');
if(!listEl || !hidden) return;
const arr = Array.isArray(keys)? keys.filter(k=>k && typeof k==='string') : [];
hidden.value = arr.join(',');
listEl.innerHTML = arr.length? arr.map((k,i)=>`<div class="key-item" data-idx="${i}"><code>${k}</code><button type="button" class="remove" data-action="rm-tmdb" data-idx="${i}" title="Remove">✕</button></div>`).join('') : '<div class="empty">No TMDB keys</div>';
}
function addTmdbKey(){
const input = document.getElementById('tmdbKeyInput');
const val = (input.value||'').trim();
if(!val) return;
const hidden = document.querySelector('input[name="tmdbApiKeysHidden"]');
const existing = hidden.value? hidden.value.split(',').filter(Boolean):[];
if(existing.includes(val)) { input.value=''; return; }
existing.push(val);
setTmdbKeys(existing);
input.value='';
validate();
}
function removeTmdbKey(idx){
const hidden = document.querySelector('input[name="tmdbApiKeysHidden"]');
const existing = hidden.value? hidden.value.split(',').filter(Boolean):[];
setTmdbKeys(existing.filter((_,i)=> i!=idx));
validate();
}
document.addEventListener('click',(e)=>{
if(e.target.closest('#addTmdbKeyBtn')) addTmdbKey();
const rm = e.target.closest('.key-item button.remove');
if(rm){ removeTmdbKey(rm.dataset.idx); }
});
document.addEventListener('keydown',(e)=>{ if(e.key==='Enter' && e.target.id==='tmdbKeyInput'){ e.preventDefault(); addTmdbKey(); }});
function fillForm(data){
const merged = data.merged || {};
const override = data.override || {};
const f = document.getElementById('cfgForm');
const fields = ['port'];
fields.forEach(k=>{ if(!f.elements[k]) return; const ov = override[k]; f.elements[k].value = ov !== undefined ? ov : (merged[k]||''); });
if (f.elements['defaultRegion']) {
const val = override.defaultRegion !== undefined ? override.defaultRegion : (merged.defaultRegion || '');
f.elements['defaultRegion'].value = val || '';
}
f.elements['defaultProviders'].value = (override.defaultProviders || merged.defaultProviders || []).join(',');
(function(){
let cookiesVal = override.febboxCookies || merged.febboxCookies || [];
if(typeof cookiesVal === 'string') {
const trimmed = cookiesVal.trim();
if(trimmed.startsWith('[') && trimmed.endsWith(']')) { try { const arr = JSON.parse(trimmed); if(Array.isArray(arr)) cookiesVal = arr; } catch{} }
else if (trimmed.includes(',') || trimmed.includes('\n') || trimmed.includes(';')) {
cookiesVal = trimmed.split(/[\n;,]+/).map(s=>s.trim()).filter(Boolean);
} else if(trimmed) cookiesVal = [trimmed]; else cookiesVal = [];
}
if(!Array.isArray(cookiesVal)) cookiesVal = [];
setFebboxCookies(cookiesVal);
})();
(function(){
let keys = override.tmdbApiKeys || merged.tmdbApiKeys || [];
if(!Array.isArray(keys)) {
if (override.tmdbApiKey) keys = [override.tmdbApiKey];
else if (merged.tmdbApiKey) keys = [merged.tmdbApiKey];
else keys = [];
}
setTmdbKeys(keys);
})();
const mqRaw = override.minQualitiesRaw !== undefined ? override.minQualitiesRaw : (merged.minQualitiesRaw || '');
const presetValues = ['all','480p','720p','1080p','1440p','2160p'];
let matchedPreset = presetValues.includes((mqRaw||'').toLowerCase()) ? mqRaw.toLowerCase() : (mqRaw? 'custom':'all');
const presetInput = document.querySelector(`input[name="qualityPreset"][value="${matchedPreset}"]`) || document.querySelector('input[name="qualityPreset"][value="all"]');
if (presetInput) presetInput.checked = true;
const customField = document.querySelector('textarea[name="minQualitiesRaw"]');
if (customField) {
if (matchedPreset === 'custom') { customField.disabled = false; customField.value = mqRaw; }
else { customField.disabled = true; customField.value = ''; }
}
const excRaw = override.excludeCodecsRaw !== undefined ? override.excludeCodecsRaw : (merged.excludeCodecsRaw || '');
let parsed = null; try { if(excRaw && /[\{\[]/.test(excRaw)) parsed = JSON.parse(excRaw); } catch {}
let mode = 'all';
if(parsed){
const dv = parsed.excludeDV === true;
const hdr = parsed.excludeHDR === true;
if(dv && !hdr) mode = 'dv';
else if(hdr && !dv) mode = 'hdr';
else if(dv && hdr) mode = 'advanced';
else mode = 'advanced';
} else if(excRaw){
if(/excludeDV":true/.test(excRaw) && !/excludeHDR":true/.test(excRaw)) mode='dv';
else if(/excludeHDR":true/.test(excRaw) && !/excludeDV":true/.test(excRaw)) mode='hdr';
else mode='advanced';
}
const presetInputC = document.querySelector(`input[name="codecPreset"][value="${mode}"]`) || document.querySelector('input[name="codecPreset"][value="all"]');
if(presetInputC) presetInputC.checked = true;
const codecTa = document.querySelector('textarea[name="excludeCodecsRaw"]');
if(mode==='advanced') { codecTa.disabled=false; codecTa.value = excRaw || JSON.stringify({excludeDV:false,excludeHDR:false}); }
else { codecTa.disabled=true; codecTa.value=''; }
document.getElementById('currentCfg').textContent = JSON.stringify(merged, null, 2);
document.getElementById('overrideCfg').textContent = Object.keys(override).length? JSON.stringify(override,null,2): '(none)';
document.getElementById('bootWarn').style.display = (!merged.febboxCookies || merged.febboxCookies.length===0)? 'block':'none';
// Removed version & overrides pills from UI; logic deleted
const cacheBanner = document.getElementById('cacheDisabledBanner'); if(cacheBanner) cacheBanner.style.display = merged.disableCache? 'block':'none';
const tmdbBanner = document.getElementById('tmdbMissingBanner');
if(tmdbBanner){
const tmdbMissing = !((override.tmdbApiKeys && override.tmdbApiKeys.length) || (merged.tmdbApiKeys && merged.tmdbApiKeys.length) || override.tmdbApiKey || merged.tmdbApiKey);
const tmdbProvidersEnabled = [
merged.enableShowboxProvider !== false,
merged.enable4khdhubProvider !== false,
merged.enableMoviesmodProvider !== false,
merged.enableMp4hydraProvider !== false
].some(Boolean);
tmdbBanner.style.display = (tmdbMissing && tmdbProvidersEnabled)? 'block':'none';
}
const advBools = [
['enableXprimeProvider','adv_enableXprime'],
['enable4khdhubProvider','adv_enable4khdhub'],
['enableMoviesmodProvider','adv_enableMoviesmod'],
['enableMp4hydraProvider','adv_enableMp4hydra'],
['enableVidzeeProvider','adv_enableVidzee'],
['enableVixsrcProvider','adv_enableVixsrc'],
['disableCache','adv_disableCache'],
['enablePStreamApi','adv_enablePStream'],
['showboxUseRotatingProxy','adv_showboxRotatingProxy'],
['disableUrlValidation','adv_disableUrlValidation'],
['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':''; } });
const advTexts = [
['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 || ''; });
// Sync checkbox style advanced flags
const advFlagMap = {
adv_disableCache: 'disableCache',
adv_showboxRotatingProxy: 'showboxUseRotatingProxy',
adv_disableUrlValidation: 'disableUrlValidation',
adv_disable4khdhubValidation: 'disable4khdhubUrlValidation'
};
Object.entries(advFlagMap).forEach(([inputName, cfgKey]) => {
const el = f.elements[inputName];
if(!el) return;
const val = override[cfgKey] !== undefined ? override[cfgKey] : merged[cfgKey];
el.checked = !!val;
});
}
async function reload(){
try {
const data = await fetchConfig();
fillForm(data);
renderProviderMatrix(data.merged);
validate();
setStatus('');
} catch(e){ setStatus('Load failed: '+e.message,true); }
}
function setStatus(msg, isErr){
const el = document.getElementById('status');
el.textContent = msg;
el.style.color = isErr? '#dc2626':'inherit';
}
async function save(){
const f = document.getElementById('cfgForm');
const payload = {};
const dp = f.elements['defaultProviders'].value.trim();
if (dp) payload.defaultProviders = dp.split(/[\s,]+/).filter(Boolean);
['port'].forEach(k=>{
if(!f.elements[k]) return;
const v = f.elements[k].value.trim();
payload[k] = v? v : null;
});
const selPreset = document.querySelector('input[name="qualityPreset"]:checked');
if (selPreset) {
if (selPreset.value === 'custom') {
const cv = f.elements['minQualitiesRaw'].value.trim();
if (cv) payload.minQualitiesRaw = cv; else payload.minQualitiesRaw = null;
} else {
payload.minQualitiesRaw = selPreset.value === 'all' ? 'all' : selPreset.value;
}
}
const cPreset = document.querySelector('input[name="codecPreset"]:checked');
if(cPreset){
if(cPreset.value==='all') payload.excludeCodecsRaw = null;
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==='advanced') {
const raw = document.querySelector('textarea[name="excludeCodecsRaw"]').value.trim();
payload.excludeCodecsRaw = raw || null;
}
}
const drSel = f.elements['defaultRegion'];
if (drSel) payload.defaultRegion = drSel.value ? drSel.value : null;
const cookiesHidden = f.elements['febboxCookiesHidden'];
if(cookiesHidden){
const cookiesVal = cookiesHidden.value.trim();
payload.febboxCookies = cookiesVal? cookiesVal.split(',').filter(Boolean) : [];
}
const boolMap = {
adv_enableXprime:'enableXprimeProvider',
adv_enable4khdhub:'enable4khdhubProvider',
adv_enableMoviesmod:'enableMoviesmodProvider',
adv_enableMp4hydra:'enableMp4hydraProvider',
adv_enableVidzee:'enableVidzeeProvider',
adv_enableVixsrc:'enableVixsrcProvider',
adv_disableCache:'disableCache',
adv_enablePStream:'enablePStreamApi',
adv_showboxRotatingProxy:'showboxUseRotatingProxy',
adv_disableUrlValidation:'disableUrlValidation',
adv_disable4khdhubValidation:'disable4khdhubUrlValidation'
};
Object.entries(boolMap).forEach(([formName,cfgKey])=>{ const el = f.elements[formName]; if(el) payload[cfgKey] = !!el.checked; });
const textMap = {
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; });
const tmdbHidden = f.elements['tmdbApiKeysHidden'];
if (tmdbHidden){
const raw = tmdbHidden.value.trim();
payload.tmdbApiKeys = raw? raw.split(',').filter(Boolean) : [];
payload.tmdbApiKey = null;
}
try {
const r = await fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
const js = await r.json();
if(!js.success) throw new Error(js.error||'Save failed');
setStatus('Saved ✔');
fillForm(js);
} catch(e){ setStatus(e.message,true); }
}
async function clearAll(){
if(!confirm('Remove ALL overrides?')) return;
const payload = { port:null, defaultRegion:null, defaultProviders:[], minQualitiesRaw:null, excludeCodecsRaw:null, febboxCookies:null,
enableXprimeProvider:null, enable4khdhubProvider:null, enableMoviesmodProvider:null, enableMp4hydraProvider:null, enableVidzeeProvider:null, enableVixsrcProvider:null,
disableCache:null, enablePStreamApi:null, showboxUseRotatingProxy:null, disableUrlValidation:null, disable4khdhubUrlValidation:null,
showboxCacheDir:null, showboxProxyPrimary:null, showboxProxyAlternate:null, xprimeProxyUrl:null, vidzeeProxyUrl:null, vidsrcProxyUrl:null, moviesmodProxyUrl:null };
try {
const r = await fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
const js = await r.json();
if(!js.success) throw new Error(js.error||'Clear failed');
setStatus('Overrides cleared');
fillForm(js);
} catch(e){ setStatus(e.message,true); }
}
function setTheme(t){
document.documentElement.dataset.theme = t;
localStorage.setItem('ns_theme', t);
}
function initTheme(){
const saved = localStorage.getItem('ns_theme');
setTheme(saved || 'dark');
}
function toggleTheme(){
setTheme(document.documentElement.dataset.theme === 'dark' ? 'light':'dark');
}
function navTo(id){
document.querySelectorAll('.panel').forEach(p=>p.classList.toggle('active', p.id===id));
document.querySelectorAll('.nav-item').forEach(a=>a.classList.toggle('active', a.dataset.target===id));
if(history.replaceState) history.replaceState(null,'', '#'+id);
if(id==='panel-status') loadServerStatus();
}
function attachCoreListeners(){
const saveBtn = document.getElementById('saveBtn');
const reloadBtn = document.getElementById('reloadBtn');
const clearBtn = document.getElementById('clearBtn');
if(saveBtn) saveBtn.addEventListener('click', save);
if(reloadBtn) reloadBtn.addEventListener('click', reload);
if(clearBtn) clearBtn.addEventListener('click', clearAll);
}
function bindNavigation(){
document.querySelectorAll('.nav .nav-item[data-target]').forEach(btn=>{
btn.addEventListener('click', ()=> navTo(btn.dataset.target));
});
const themeBtn = document.querySelector('.nav .theme-toggle');
if(themeBtn) themeBtn.addEventListener('click', toggleTheme);
}
function bindShellControls(){
const burger = document.querySelector('.burger');
if(burger) burger.addEventListener('click', ()=> toggleSidebar());
const overlay = document.querySelector('.overlay');
if(overlay) overlay.addEventListener('click', ()=> toggleSidebar(false));
}
async function verifySession(){
try {
const r = await fetch('/auth/session',{cache:'no-store'});
const j = await r.json();
if(!j.authenticated){ location.replace('/?expired=1'); }
} catch { location.replace('/?expired=1'); }
}
async function loadServerStatus(){
const metricsEl = document.getElementById('statusMetrics');
const endpointsEl = document.getElementById('statusEndpoints');
const provEnhanced = document.getElementById('statusProvidersEnhanced');
if(!metricsEl) return;
metricsEl.textContent = 'Loading...';
if(endpointsEl) endpointsEl.textContent = 'Loading...';
if(provEnhanced) provEnhanced.innerHTML = '<div class="prov-row">Loading...</div>';
try {
const r = await fetch('/api/status');
if(!r.ok) throw new Error('Status fetch failed');
const j = await r.json();
if(!j.success) throw new Error('Status error');
metricsEl.textContent = JSON.stringify(j.metrics, null, 2);
if(endpointsEl) endpointsEl.textContent = (j.endpoints||[]).join('\n');
if(provEnhanced){
provEnhanced.innerHTML = '';
const header = document.createElement('div');
header.className='prov-row header';
header.innerHTML = '<span>Name</span><span>Enabled</span><span>Cookie Req</span><span>Cookie OK</span><span>Functional</span><span>Notes</span>';
provEnhanced.appendChild(header);
j.providers.forEach(p=>{
const row = document.createElement('div');
row.className = 'prov-row';
row.dataset.provider = p.name;
row.innerHTML = `<strong>${p.name}</strong>`+
`<span class="badge ${p.enabled? 'ok':'off'}">${p.enabled? 'on':'off'}</span>`+
`<span class="badge ${p.cookieRequired? 'warn':'off'}">${p.cookieRequired? 'yes':'no'}</span>`+
`<span class="badge ${p.cookieOk? 'ok':'fail'}">${p.cookieOk? 'ok':'missing'}</span>`+
`<span class="badge" data-role="functional" title="Run provider checks first">?</span>`+
`<span class="notes" data-role="notes"></span>`;
provEnhanced.appendChild(row);
});
}
} catch(e){
metricsEl.textContent = 'Failed: '+e.message;
if(endpointsEl) endpointsEl.textContent = 'n/a';
if(provEnhanced) provEnhanced.innerHTML = '<div class="prov-row">Status load failed</div>';
}
}
async function runProviderFunctionalChecks(){
const targetTmdb = '293660'; // TMDB ID provided
const provEnhanced = document.getElementById('statusProvidersEnhanced');
const summary = document.getElementById('providerCheckSummary');
if(!provEnhanced) return;
summary.textContent = 'Running checks...';
const rows = Array.from(provEnhanced.querySelectorAll('.prov-row:not(.header)'));
let completed=0, passed=0;
for(const row of rows){
const name = row.dataset.provider;
const funcBadge = row.querySelector('[data-role="functional"]');
const notesEl = row.querySelector('[data-role="notes"]');
if(funcBadge){ funcBadge.textContent='…'; funcBadge.className='badge'; }
if(notesEl) notesEl.textContent='';
// Skip disabled providers
if(row.querySelector('.badge.ok, .badge.off') && row.children[1].textContent==='off'){
if(funcBadge){ funcBadge.textContent='skip'; funcBadge.className='badge off'; }
completed++; continue;
}
try {
const r = await fetch(`/api/streams/${name}/movie/${targetTmdb}`);
if(!r.ok) throw new Error('HTTP '+r.status);
const j = await r.json();
const isOk = j && Array.isArray(j.streams) && j.streams.length>0;
if(isOk){
passed++;
if(funcBadge){ funcBadge.textContent='ok'; funcBadge.className='badge ok'; }
if(notesEl) notesEl.textContent = `${j.streams.length} streams`;
}else{
if(funcBadge){ funcBadge.textContent='none'; funcBadge.className='badge fail'; }
if(notesEl) notesEl.textContent = 'No streams array';
}
}catch(err){
if(funcBadge){ funcBadge.textContent='fail'; funcBadge.className='badge fail'; }
if(notesEl) notesEl.textContent = err.message;
}
completed++;
summary.textContent = `Progress: ${completed}/${rows.length} (passed: ${passed})`;
}
summary.textContent = `Checks complete: ${passed}/${rows.length} passed`;
}
document.addEventListener('DOMContentLoaded', ()=> {
initTheme();
attachCoreListeners();
bindNavigation();
bindShellControls();
const logoutBtn = document.getElementById('logoutBtn');
if(logoutBtn){
logoutBtn.addEventListener('click', async ()=>{
try { await fetch('/auth/logout',{method:'POST'}); } catch {}
// Use replace to avoid creating extra history entry
location.replace('/?loggedOut=1');
});
}
reload();
const h = location.hash.slice(1);
navTo(h||'panel-core');
// Revalidate session on back-forward cache navigation or visibility
window.addEventListener('pageshow', (e)=>{
if(e.persisted){ verifySession(); }
});
document.addEventListener('visibilitychange', ()=>{ if(document.visibilityState==='visible') verifySession(); });
// Advanced flag checkbox change -> validation
document.getElementById('advFlags')?.addEventListener('change', (e)=>{
if(e.target.matches('.adv-flag-item input[type="checkbox"]')) {
validate();
}
});
const rsb = document.getElementById('refreshStatusBtn');
if(rsb) rsb.addEventListener('click', loadServerStatus);
const rpc = document.getElementById('runProviderChecksBtn');
if(rpc) rpc.addEventListener('click', runProviderFunctionalChecks);
});
function toggleSidebar(force){
const sb = document.getElementById('sidebar');
const showing = force !== undefined ? force : !sb.classList.contains('open');
sb.classList.toggle('open', showing);
document.querySelector('.overlay').classList.toggle('show', showing);
}
function validate(){
const f = document.getElementById('cfgForm');
const errors = [];
const qp = document.querySelector('input[name="qualityPreset"]:checked');
if(qp && qp.value==='custom') {
const mq = f.elements['minQualitiesRaw'].value.trim();
if(mq && /[\{\[]/.test(mq)) { try { JSON.parse(mq); } catch(e){ errors.push('Min Qualities invalid JSON'); markInvalid(f.elements['minQualitiesRaw']); } }
}
const codecPreset = document.querySelector('input[name="codecPreset"]:checked');
if(codecPreset && codecPreset.value==='advanced') {
const ec = f.elements['excludeCodecsRaw'].value.trim();
if(ec && /[\{\[]/.test(ec)) { try { JSON.parse(ec); } catch(e){ errors.push('Exclude Codecs invalid JSON'); markInvalid(f.elements['excludeCodecsRaw']); } }
}
const p = f.elements['port'].value.trim();
if(p) { const pn = Number(p); if(!(pn>0 && pn<=65535)) { errors.push('Port must be 1-65535'); markInvalid(f.elements['port']); } }
const dp = f.elements['defaultProviders'].value.trim();
if(dp && !/^[a-z0-9_,\-\s]+$/.test(dp)) { errors.push('Default Providers contains invalid chars'); markInvalid(f.elements['defaultProviders']); }
const sum = document.getElementById('validationSummary');
if(errors.length){ sum.textContent = errors.join(' • '); sum.classList.add('has-errors'); }
else { sum.textContent=''; sum.classList.remove('has-errors'); }
document.getElementById('saveBtn').disabled = errors.length>0;
return errors.length===0;
}
function markInvalid(el){ el.classList.add('invalid'); setTimeout(()=>el.classList.remove('invalid'), 3000); }
document.addEventListener('input', (e)=>{
if(e.target.closest('#cfgForm')) validate();
});
document.addEventListener('change', (e)=>{
const t = e.target;
if(t.name === 'qualityPreset') {
const customField = document.querySelector('textarea[name="minQualitiesRaw"]');
if(t.value === 'custom') { customField.disabled = false; } else { customField.disabled = true; customField.value=''; }
validate();
}
if(t.name === 'codecPreset') {
const ta = document.querySelector('textarea[name="excludeCodecsRaw"]');
if(t.value==='advanced') { ta.disabled=false; if(!ta.value.trim()) ta.value = JSON.stringify({excludeDV:false,excludeHDR:false}); }
else { ta.disabled=true; ta.value=''; }
validate();
}
});
async function renderProviderMatrix(merged){
if(!merged){ try { const data = await fetchConfig(); merged = data.merged||{}; } catch { return; } }
const container = document.getElementById('providerMatrix'); if(!container) return;
const defaults = (merged.defaultProviders||[]).map(s=>s.toLowerCase());
const providers = [
['showbox','enableShowboxProvider'],
['4khdhub','enable4khdhubProvider'],
['moviesmod','enableMoviesmodProvider'],
['mp4hydra','enableMp4hydraProvider'],
['vidzee','enableVidzeeProvider'],
['vixsrc','enableVixsrcProvider'],
['xprime','enableXprimeProvider']
];
container.innerHTML = providers.map(([name,flag])=>{
const enabled = merged[flag] !== false;
const isDefault = defaults.includes(name);
return `<div class=\"pm-item\" data-prov=\"${name}\" data-flag=\"${flag}\" data-enabled=\"${enabled}\" data-default=\"${isDefault}\">`
+`<button type=\"button\" class=\"pm-enable\" title=\"Enable/Disable\" data-action=\"toggle-enable\">${enabled? '⏻':'✖'}</button>`
+`<span class=\"pm-name\">${name}</span>`
+`<button type=\"button\" class=\"pm-default\" title=\"Toggle Default\" data-action=\"toggle-default\">${isDefault? '★':'☆'}</button>`
+`</div>`;
}).join('');
const hidden = document.querySelector('input[name="defaultProviders"]'); if(hidden) hidden.value = defaults.join(',');
if(!container._bound){
container.addEventListener('click', async (e)=>{
const btn = e.target.closest('button[data-action]'); if(!btn) return;
const item = btn.closest('.pm-item'); if(!item) return;
const prov = item.dataset.prov; const flag = item.dataset.flag;
let data; try { data = await fetchConfig(); } catch(err){ setStatus('Provider fetch failed',true); return; }
const mergedNow = data.merged||{};
let defaultsNow = (mergedNow.defaultProviders||[]).slice();
const patch = {};
if(btn.dataset.action==='toggle-enable'){
const currentlyEnabled = item.dataset.enabled==='true';
patch[flag] = !currentlyEnabled;
if(currentlyEnabled){ defaultsNow = defaultsNow.filter(d=>d!==prov); patch.defaultProviders = defaultsNow; }
} else if(btn.dataset.action==='toggle-default'){
const isDefault = item.dataset.default==='true';
if(isDefault){ defaultsNow = defaultsNow.filter(d=>d!==prov); }
else { if(!defaultsNow.includes(prov)) defaultsNow.push(prov); patch[flag] = true; }
patch.defaultProviders = defaultsNow;
}
try {
await fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(patch)});
const updated = await fetchConfig();
fillForm(updated);
renderProviderMatrix(updated.merged);
reloadLiveOnly();
} catch(err){ setStatus('Update failed: '+err.message,true); }
});
container._bound = true;
}
}
function reloadLiveOnly(){
fetchConfig().then(c=>{
document.getElementById('currentCfg').textContent = JSON.stringify(c.merged||{},null,2);
document.getElementById('overrideCfg').textContent = (c.override && Object.keys(c.override).length)? JSON.stringify(c.override,null,2):'(none)';
}).catch(()=>{});
}
// No globals exported; all handlers bound programmatically
})();
+127
View File
@@ -0,0 +1,127 @@
/* eslint-env browser */
(function(){
const $ = sel => document.querySelector(sel);
async function checkSession(){
try {
const r = await fetch('/auth/session');
const j = await r.json();
if (j.authenticated) {
window.location.href = '/config.html';
}
} catch(e) { /* ignore */ }
}
async function login(username, password){
const status = $('#status');
status.textContent = 'Authenticating...';
status.className = 'status';
try {
const r = await fetch('/auth/login', { method:'POST', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify({ username, password }) });
const j = await r.json();
if (!j.success) {
if (j.error === 'LOCKED' || j.error === 'TOO_MANY_ATTEMPTS') {
showLockout(j.retryAfter);
status.textContent = 'Locked due to excessive attempts';
} else if (j.error === 'INVALID_CREDENTIALS') {
status.textContent = 'Invalid credentials';
if (typeof j.remaining === 'number') {
status.textContent += ` (${j.remaining} attempts left)`;
}
} else {
status.textContent = j.error || 'Login failed';
}
status.classList.add('error');
return false;
}
status.textContent = 'Success! Redirecting...';
status.classList.add('success');
setTimeout(()=>{ window.location.href = '/config.html'; }, 400);
return true;
} catch (e) {
status.textContent = 'Network error';
status.classList.add('error');
return false;
}
}
// Lockout handling
let lockoutTimer = null;
function showLockout(retryAfterSeconds){
if (!retryAfterSeconds || retryAfterSeconds <= 0) return;
const banner = $('#lockoutBanner');
const countdownEl = $('#lockoutCountdown');
banner.style.display = 'block';
const end = Date.now() + (retryAfterSeconds * 1000);
if (lockoutTimer) clearInterval(lockoutTimer);
lockoutTimer = setInterval(()=>{
const remain = end - Date.now();
if (remain <= 0){
clearInterval(lockoutTimer);
banner.style.display = 'none';
countdownEl.textContent = '00:00';
return;
}
const sec = Math.ceil(remain/1000);
const m = String(Math.floor(sec/60)).padStart(2,'0');
const s = String(sec%60).padStart(2,'0');
countdownEl.textContent = `${m}:${s}`;
}, 1000);
}
async function changePassword(oldPassword, newPassword){
const pwStatus = $('#pwStatus');
pwStatus.textContent = 'Updating...';
pwStatus.className = 'status';
try {
const r = await fetch('/auth/change-password', { method:'POST', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify({ oldPassword, newPassword }) });
const j = await r.json();
if (!j.success) {
pwStatus.textContent = j.error || 'Update failed';
pwStatus.classList.add('error');
return;
}
pwStatus.textContent = 'Password updated';
pwStatus.classList.add('success');
setTimeout(()=>{ $('#pwForm').reset(); }, 600);
} catch(e){
pwStatus.textContent = 'Network error';
pwStatus.classList.add('error');
}
}
document.addEventListener('DOMContentLoaded', () => {
// Flash messaging based on query params
const params = new URLSearchParams(location.search);
const status = $('#status');
if(params.has('loggedOut')) { status.textContent = 'Logged out successfully'; status.classList.add('success'); }
else if(params.has('expired')) { status.textContent = 'Session expired. Please login again.'; status.classList.add('error'); }
if(params.has('loggedOut') || params.has('expired')) {
// Remove params from URL without reloading
history.replaceState(null,'', location.pathname);
}
checkSession();
$('#loginForm').addEventListener('submit', e => {
e.preventDefault();
const u = $('#username').value.trim();
const p = $('#password').value;
if (!u || !p) return;
login(u,p);
});
$('#togglePw').addEventListener('click', () => {
const chg = $('#pwChange');
const visible = chg.style.display === 'block';
chg.style.display = visible ? 'none' : 'block';
});
$('#pwForm').addEventListener('submit', e => {
e.preventDefault();
const oldPw = $('#oldPw').value;
const newPw = $('#newPw').value;
if (newPw.length < 8) {
$('#pwStatus').textContent = 'Password too short';
$('#pwStatus').classList.add('error');
return;
}
changePassword(oldPw, newPw);
});
});
})();
+366
View File
@@ -0,0 +1,366 @@
/*TMDB Embed Admin Panel Styles */
:root {
--bg: #f7f9fc;
--bg-alt: #ffffff;
--bg-accent: #eef2f7;
--border: #d8e0ea;
--border-strong: #b6c2cf;
--text: #1f2933;
--text-dim: #5a6b7b;
--primary: #2563eb;
--primary-accent: #1d4ed8;
--primary-fg: #ffffff;
--danger: #dc2626;
--warn: #b45309;
--radius-sm: 4px;
--radius: 8px;
--shadow: 0 1px 2px rgba(0,0,0,.05), 0 4px 16px -4px rgba(0,0,0,.08);
--mono: "SFMono-Regular",Consolas,Menlo,monospace;
--transition: .18s cubic-bezier(.4,0,.2,1);
}
:root[data-theme='dark'] {
--bg: #0f141a;
--bg-alt: #161d25;
--bg-accent: #1e2630;
--border: #29323d;
--border-strong: #3a4654;
--text: #e5edf3;
--text-dim: #a0b1c0;
--primary: #3b82f6;
--primary-accent: #1d4ed8;
--primary-fg: #f0f6ff;
--danger: #f87171;
--warn: #f59e0b;
--shadow: 0 2px 4px rgba(0,0,0,.4), 0 8px 28px -6px rgba(0,0,0,.55);
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial, sans-serif;
background: var(--bg);
color: var(--text);
display: flex;
min-height: 100vh;
overflow: hidden; /* Prevent body scroll; main will scroll */
}
.bmc-btn {
min-width: 50px !important;
color: #ffffff !important;
background-color: #5F7FFF !important;
height: 40px !important;
border-radius: 12px !important;
font-size: 12px !important;
font-weight: Bold !important;
border: none !important;
padding: 0px 24px !important;
line-height: 27px !important;
text-decoration: none !important;
display: inline-flex !important;
align-items: center !important;
font-family: 'Bree Serif', cursive !important;
-webkit-box-sizing: border-box !important;
box-sizing: border-box !important;
}
.bmc-btn-container{
text-align: center;
}
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: var(--bg-alt); }
::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 20px; }
::-webkit-scrollbar-thumb:hover { background: var(--primary); }
.sidebar {
width: 250px;
background: linear-gradient(180deg, var(--bg-alt), var(--bg-accent));
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
padding: 16px 14px 10px;
gap: 12px;
position: fixed;
top: 0; left: 0; bottom: 0;
overflow-y: auto;
z-index: 45;
}
/* Mobile burger */
.burger {
position: fixed; top:10px; left:12px; z-index:60; background: var(--primary); color: var(--primary-fg);
border:1px solid var(--primary-accent); border-radius: var(--radius-sm); padding:8px 10px; font-size:1rem; cursor:pointer; display:none;
box-shadow: var(--shadow);
}
.overlay { position: fixed; inset:0; background:rgba(0,0,0,.45); backdrop-filter: blur(2px); z-index:40; display:none; }
.overlay.show { display:block; }
#sidebar { transition: transform var(--transition); }
@media (max-width: 880px){
.burger { display:block; }
#sidebar { position:fixed; inset:0 auto 0 0; transform: translateX(-100%); z-index:50; width:240px; }
#sidebar.open { transform: translateX(0); }
body { padding-left:0; }
.actions-bar { left:0; }
}
.brand {
font-weight: 700;
font-size: 1.05rem;
letter-spacing: .5px;
display: flex;
align-items: center;
gap: 8px;
}
.pill { background: var(--primary); color: var(--primary-fg); padding: 2px 8px; border-radius: 999px; font-size: .65rem; letter-spacing: .5px; text-transform: uppercase; }
.nav { display: flex; flex-direction: column; gap: 4px; }
.nav-item {
background: transparent;
border: 1px solid transparent;
color: var(--text-dim);
padding: 10px 12px;
text-align: left;
border-radius: var(--radius-sm);
font-size: .85rem;
font-weight: 600;
letter-spacing: .5px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: background var(--transition), color var(--transition), border-color var(--transition);
}
.nav-item:hover { background: var(--bg-accent); color: var(--text); }
.nav-item.active { background: var(--primary); color: var(--primary-fg); }
.nav-item.theme-toggle { margin-top: 12px; }
.nav-item#logoutBtn { border:1px solid var(--danger); color: var(--danger); font-weight:700; }
.nav-item#logoutBtn:hover { background: var(--danger); color:#fff; }
.grow { flex-grow: 1; }
.side-footer { font-size: .65rem; opacity: .6; line-height: 1.4; text-align: center;}
.main {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
min-width: 0;
max-width: 1400px;
margin-left: 250px; /* compensate for fixed sidebar */
height: 100vh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.panels { flex: 1; display: flex; flex-direction: column; padding: 28px 36px 140px; gap: 0; }
.panel { display: none; animation: fade .35s ease; max-width: 760px; }
.panel.active { display: block; }
@keyframes fade { from { opacity: 0; transform: translateY(6px);} to { opacity:1; transform:translateY(0);} }
.panel h1 { margin: 0 0 1.2rem; font-size: 1.4rem; }
.field { margin-bottom: 18px; }
.field label { font-size: .75rem; letter-spacing: .5px; text-transform: uppercase; font-weight: 700; display: flex; flex-direction: column; gap: 6px; color: var(--text-dim); }
.field input[type=text], .field textarea, .field select {
background: var(--bg-alt);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 10px 12px;
font-family: var(--mono);
color: var(--text);
font-size: .85rem;
outline: none;
transition: border-color var(--transition),
background var(--transition);
resize: vertical;
}
.field select { cursor: pointer; appearance: none; -webkit-appearance: none; line-height:1.2; padding-right: 36px; background-image: linear-gradient(45deg, transparent 50%, var(--text-dim) 50%), linear-gradient(135deg, var(--text-dim) 50%, transparent 50%); background-position: calc(100% - 20px) 16px, calc(100% - 14px) 16px; background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; }
.field input[type=text]:focus, .field input[type=password]:focus, .field textarea:focus, .field select:focus { border-color: var(--primary); outline: none; }
.field select:disabled { opacity:.55; cursor: not-allowed; }
textarea { min-height: 110px; }
.hint {
font-size: .65rem;
opacity: .75;
margin-top: -4px;
letter-spacing: .5px;
text-transform: none;
font-weight: 500;
display: flex;
flex-direction: column;
gap: 6px;
color: var(--text-dim);
}
.login-hint {
font-size: .65rem;
opacity: .75;
margin-top: -4px;
letter-spacing: .5px;
text-transform: none;
font-weight: 500;
display: flex;
flex-direction: column;
gap: 6px;
color: orange;
}
.provider-toggle-list { display:flex; flex-wrap:wrap; gap:8px; margin-top:8px; }
/* Uniform provider width: auto-fill responsive sizing */
.prov-item { background: var(--bg-alt); border:1px solid var(--border); padding:6px 10px; border-radius: var(--radius-sm); font-size:.65rem; letter-spacing:.5px; text-transform:uppercase; display:flex; align-items:center; gap:6px; cursor:pointer; user-select:none; box-shadow: var(--shadow); flex: 0 0 132px; width:132px; justify-content:center; }
.prov-item input { accent-color: var(--primary); cursor:pointer; }
.prov-item:hover { border-color: var(--primary); }
.disabled-hint { font-style:normal; color: var(--warn); }
/* Quality presets */
.quality-presets, .codec-toggles { display:flex; flex-wrap:wrap; gap:10px; margin-top:10px; }
.quality-presets label, .codec-toggles label.codec-item { text-align: center; background: var(--bg-alt); border:1px solid var(--border); padding:6px 12px; border-radius: var(--radius-sm); font-size:.65rem; letter-spacing:.5px; text-transform:uppercase; display:flex; align-items:center; gap:6px; cursor:pointer; user-select:none; box-shadow: var(--shadow); }
.quality-presets .q-item { flex:0 0 100px; justify-content:center; }
.quality-presets .q-item.adv { background: var(--bg-accent); }
.quality-json { display:block; width:100%; margin-top:10px; font-size:.65rem; font-family: var(--mono); min-height:90px; background: var(--bg-alt); border:1px solid var(--border); border-radius: var(--radius-sm); padding:10px 12px; }
.quality-json[disabled] { opacity:.4; }
.json-adv {
display: block;
width: 100%;
margin-top: 10px;
font-size: .65rem;
font-family: var(--mono);
min-height: 90px;
background: var(--bg-alt);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 10px 12px;
}
.codec-presets { display:flex; flex-wrap:wrap; gap:10px; margin-top:10px; }
.codec-presets .c-item { background: var(--bg-alt); border:1px solid var(--border); padding:6px 12px; border-radius: var(--radius-sm); font-size:.65rem; letter-spacing:.5px; text-transform:uppercase; display:flex; align-items:center; gap:6px; cursor:pointer; user-select:none; box-shadow: var(--shadow); flex:0 0 130px; justify-content:center; }
.codec-presets .c-item.adv { background: var(--bg-accent); }
.codec-presets input[type=radio] { accent-color: var(--primary); cursor:pointer; }
.codec-presets .c-item:hover { border-color: var(--primary); }
.quality-presets input[type=radio], .codec-toggles input[type=checkbox] { accent-color: var(--primary); cursor:pointer; }
.quality-presets label:hover, .codec-toggles label.codec-item:hover { border-color: var(--primary); }
.quality-presets label.custom-q { position:relative; }
.quality-presets .mini { width:170px; margin-left:4px; font-size:.65rem; padding:6px 8px; font-family: var(--mono); }
.codec-toggles .json-adv { flex: 1 1 100%; margin-top:4px; font-size:.65rem; font-family: var(--mono); min-height:90px; display:block; }
.codec-toggles .json-adv[disabled] { opacity:.4; }
.codec-toggles label.adv { background: var(--bg-accent); }
.alert { padding: 12px 16px; border-radius: var(--radius-sm); margin: 20px 36px 0; font-size: .75rem; font-weight: 600; letter-spacing: .5px; background: var(--bg-accent); border: 1px solid var(--border); }
.alert.warning { color: var(--warn); border-color: var(--warn); background: rgba(180,83,9,.08); }
[data-theme='dark'] .alert.warning { background: rgba(245,158,11,.12); }
.actions-bar {
position: sticky;
bottom: 0; left: 0; right: 0;
background: linear-gradient(90deg,var(--bg-alt),var(--bg-accent));
border-top: 1px solid var(--border);
display: flex;
gap: 14px;
align-items: center;
padding: 14px 36px;
backdrop-filter: blur(6px);
margin-left: 0;
}
button { font-family: inherit; }
button.primary { background: var(--primary); border: 1px solid var(--primary-accent); color: var(--primary-fg); }
button.primary:hover { background: var(--primary-accent); }
button.danger { background: var(--danger); border: 1px solid var(--danger); color: #fff; }
button.danger:hover { filter: brightness(.9); }
button { border-radius: var(--radius-sm); padding: 10px 18px; font-size: .75rem; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; border: 1px solid var(--border); background: var(--bg-alt); color: var(--text-dim); cursor: pointer; transition: background var(--transition), color var(--transition), border-color var(--transition), transform .15s ease; }
button:hover { background: var(--bg-accent); color: var(--text); }
.status { font-size: .7rem; letter-spacing: .5px; font-weight: 600; }
pre { background: var(--bg-alt); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 14px 16px; line-height: 1.35; font-size: .7rem; box-shadow: var(--shadow); overflow: auto; }
pre code { font-family: var(--mono); }
#currentCfg, #overrideCfg { max-height: 380px; }
::-moz-selection { background: var(--primary); color: var(--primary-fg); }
::selection { background: var(--primary); color: var(--primary-fg); }
@media (max-width: 880px) {
body { overflow: auto; }
.sidebar { position: fixed; inset:0 auto 0 0; width:210px; transform: translateX(-100%); transition: transform var(--transition); }
#sidebar.open { transform: translateX(0); }
.main { margin-left:0; height:auto; overflow-y:visible; }
.actions-bar { position: fixed; left:0; right:0; bottom:0; }
.panels { padding: 24px 20px 140px; }
}
/* Validation */
.validation-summary { font-size:.65rem; margin-left:12px; color: var(--danger); font-weight:600; letter-spacing:.5px; }
.adv-flag-grid { display:flex; flex-wrap:wrap; gap:14px; margin:14px 0 28px; }
.adv-flag-item { position:relative; background: var(--bg-alt); border:1px solid var(--border); padding:16px 18px 14px; border-radius: var(--radius-sm); font-size:.55rem; letter-spacing:.55px; text-transform:uppercase; font-weight:700; display:flex; flex-direction:column; align-items:center; gap:10px; cursor:pointer; user-select:none; box-shadow: var(--shadow); width:170px; min-height:92px; transition: background var(--transition), border-color var(--transition), color var(--transition), transform .15s ease; }
.adv-flag-item:hover { border-color: var(--primary); }
.adv-flag-item.on { background: var(--primary); border-color: var(--primary-accent); color: var(--primary-fg); }
[data-theme='dark'] .adv-flag-item.on { background: var(--primary); }
.adv-flag-item input { appearance:none; -webkit-appearance:none; accent-color: var(--primary); width:15px; height:15px; border:2px solid var(--primary); border-radius:6px; display:inline-flex; align-items:center; justify-content:center; position:relative; background:transparent; cursor:pointer; margin-top:2px; }
.adv-flag-item input:focus-visible { outline:2px solid var(--primary); outline-offset:3px; }
.adv-flag-item input:checked { background: var(--primary); }
.adv-flag-item input:checked:after { width:7px; height:12px; border:2px solid var(--primary-fg); border-top:none; border-left:none; transform: rotate(45deg); position:absolute; }
.adv-flag-item span { text-align:center; line-height:1.25; display:block; width:100%; }
.adv-flag-item.on input { border-color: var(--primary-fg); }
.adv-group .field.small input { background: var(--bg-alt); border:1px solid var(--border); }
.adv-group .field.small input:focus { border-color: var(--primary); outline:none; }
.validation-summary.has-errors:before { content:'⚠ '; }
input.invalid, textarea.invalid { border-color: var(--danger)!important; animation: pulse-invalid 1.2s ease; }
@keyframes pulse-invalid { 0%{ box-shadow:0 0 0 0 rgba(220,38,38,.6);} 70%{ box-shadow:0 0 0 6px rgba(220,38,38,0);} 100%{ box-shadow:0 0 0 0 rgba(220,38,38,0);} }
.adv-flag-item input:checked { border-color: var(--primary); }
/* Provider Matrix */
.provider-matrix { display:flex; flex-wrap:wrap; gap:10px; margin-top:10px; }
.pm-item { background: var(--bg-alt); border:1px solid var(--border); border-radius: var(--radius-sm); padding:6px 10px; display:flex; align-items:center; gap:8px; font-size:.65rem; letter-spacing:.5px; text-transform:uppercase; box-shadow: var(--shadow); }
.pm-item[data-enabled='false'] { opacity:.45; filter:saturate(.5); }
.pm-item .pm-enable, .pm-item .pm-default { background: var(--bg-accent); border:1px solid var(--border); padding:4px 8px; font-size:.7rem; cursor:pointer; border-radius: var(--radius-sm); line-height:1; display:flex; align-items:center; justify-content:center; }
.pm-item .pm-enable { color: var(--primary); }
.pm-item[data-enabled='false'] .pm-enable { color: var(--danger); }
.pm-item .pm-default { color: gold; }
.pm-item[data-default='false'] .pm-default { opacity:.5; color: var(--text-dim); }
.pm-item .pm-enable:hover, .pm-item .pm-default:hover { border-color: var(--primary); }
.pm-name { min-width:70px; }
/* FebBox Cookie Manager */
.cookie-add-row { display:flex; gap:10px; align-items:center; }
.cookie-add-row input { flex:1; }
.cookie-add-row button.small { padding:8px 14px; font-size:.65rem; }
.cookie-list { margin-top:10px; display:flex; flex-direction:column; gap:6px; background: var(--bg-alt); border:1px solid var(--border); padding:10px 12px; border-radius: var(--radius-sm); max-height:220px; overflow:auto; }
.cookie-item { display:flex; align-items:center; justify-content:space-between; gap:12px; font-size:.65rem; background: var(--bg-accent); border:1px solid var(--border); padding:6px 10px; border-radius: var(--radius-sm); font-family: var(--mono); }
.cookie-item code { font-family: var(--mono); word-break: break-all; flex:1; }
.cookie-item button.remove { background: var(--danger); border:1px solid var(--danger); color:#fff; padding:4px 8px; font-size:.6rem; cursor:pointer; border-radius: var(--radius-sm); }
.cookie-item button.remove:hover { filter:brightness(.9); }
.cookie-list .empty { opacity:.55; font-size:.65rem; font-style:italic; }
/* TMDB Keys Manager */
.key-add-row { display:flex; gap:10px; align-items:center; }
.key-add-row input { flex:1; }
.key-add-row button.small { padding:8px 14px; font-size:.65rem; }
.key-list { margin-top:10px; display:flex; flex-direction:column; gap:6px; background: var(--bg-alt); border:1px solid var(--border); padding:10px 12px; border-radius: var(--radius-sm); max-height:220px; overflow:auto; }
.key-item { display:flex; align-items:center; justify-content:space-between; gap:12px; font-size:.65rem; background: var(--bg-accent); border:1px solid var(--border); padding:6px 10px; border-radius: var(--radius-sm); font-family: var(--mono); }
.key-item code { font-family: var(--mono); word-break: break-all; flex:1; }
.key-item button.remove { background: var(--danger); border:1px solid var(--danger); color:#fff; padding:4px 8px; font-size:.6rem; cursor:pointer; border-radius: var(--radius-sm); }
.key-item button.remove:hover { filter:brightness(.9); }
.key-list .empty { opacity:.55; font-size:.65rem; font-style:italic; }
/* Server Status Enhancements */
.status-grid { display:grid; grid-template-columns: 1fr; gap:24px; margin-bottom:24px; }
.status-block { background: var(--bg-alt); border:1px solid var(--border); border-radius: var(--radius); padding:18px 20px 20px; box-shadow: var(--shadow); position:relative; }
.status-block h2 { margin:0 0 12px; font-size:1rem; letter-spacing:.5px; }
.status-block.wide { grid-column: 1 / -1; }
.status-pre { max-height:260px; }
.providers-table { display:flex; flex-direction:column; gap:6px; font-size:.65rem; }
.prov-row { display:grid; grid-template-columns: 110px 70px 90px 90px 110px 1fr; gap:6px; align-items:center; background: var(--bg-accent); border:1px solid var(--border); padding:6px 8px; border-radius: var(--radius-sm); }
.prov-row.header { background: var(--primary); color: var(--primary-fg); font-weight:700; text-transform:uppercase; letter-spacing:.5px; border-color: var(--primary-accent); }
.badge { display:inline-flex; align-items:center; justify-content:center; padding:2px 8px; border-radius:999px; font-size:.55rem; font-weight:600; letter-spacing:.5px; text-transform:uppercase; background: var(--bg-accent); border:1px solid var(--border); min-width:54px; }
.badge.ok { background:#065f46; border-color:#065f46; color:#ecfdf5; }
[data-theme='dark'] .badge.ok { background:#059669; border-color:#059669; }
.badge.fail { background:#7f1d1d; border-color:#7f1d1d; color:#fee2e2; }
[data-theme='dark'] .badge.fail { background:#dc2626; border-color:#dc2626; }
.badge.warn { background:#92400e; border-color:#92400e; color:#fef3c7; }
[data-theme='dark'] .badge.warn { background:#d97706; border-color:#d97706; }
.badge.off { background:#374151; border-color:#374151; color:#e5e7eb; }
[data-theme='dark'] .badge.off { background:#4b5563; border-color:#4b5563; }
.inline-actions { display:flex; gap:14px; align-items:center; }
.inline-actions button { margin:0; }
.functional-status { display:flex; flex-direction:column; gap:6px; margin-top:10px; }
.prov-row .spinner { width:14px; height:14px; border:2px solid rgba(255,255,255,.3); border-top-color:#fff; border-radius:50%; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* Responsive adjustments for Server Status */
/* Previously two-column; intentionally stacked for readability even on wide screens */
/* If a future toggle is desired, we can add a class to re-enable multi-column */
@media (max-width: 760px){
.prov-row { grid-template-columns: 90px 60px 70px 70px 90px 1fr; font-size:.55rem; }
.status-block { padding:14px 14px 16px; }
.status-block h2 { font-size:.9rem; }
.badge { min-width:40px; padding:2px 6px; font-size:.5rem; }
}
@media (max-width: 560px){
.providers-table { gap:10px; }
.prov-row.header { display:none; }
.prov-row { grid-template-columns: 1fr 1fr; grid-template-areas: 'name name' 'enabled cookieReq' 'cookieOk functional' 'notes notes'; position:relative; padding:10px 12px; }
.prov-row strong { grid-area: name; }
.prov-row > span:nth-child(2){ grid-area: enabled; }
.prov-row > span:nth-child(3){ grid-area: cookieReq; }
.prov-row > span:nth-child(4){ grid-area: cookieOk; }
.prov-row > span:nth-child(5){ grid-area: functional; }
.prov-row > span:nth-child(6){ grid-area: notes; justify-self:start; }
.prov-row { background: var(--bg-alt); }
.providers-table .prov-row { border:1px solid var(--border); }
.prov-row strong { font-size:.7rem; letter-spacing:.5px; }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

+67
View File
@@ -0,0 +1,67 @@
Param(
[string]$ImageName = "tmdb-embed-api",
[string]$Registry = "docker.io",
[string]$Repo = "yourdockeruser", # e.g. docker hub username or org
[string]$Tag = "latest",
[switch]$UseGitTag,
[switch]$Push,
[switch]$MultiArch,
[string]$Platforms = "linux/amd64,linux/arm64"
)
# Usage examples:
# .\scripts\publish-docker.ps1 -Repo youruser -Tag test -Push
# .\scripts\publish-docker.ps1 -Repo youruser -UseGitTag -Push
# .\scripts\publish-docker.ps1 -Repo youruser -UseGitTag -Push -MultiArch
$ErrorActionPreference = 'Stop'
function Write-Info($msg){ Write-Host "[INFO] $msg" -ForegroundColor Cyan }
function Write-Warn($msg){ Write-Host "[WARN] $msg" -ForegroundColor Yellow }
function Write-Err($msg){ Write-Host "[ERR] $msg" -ForegroundColor Red }
# Derive tag from git if requested
if($UseGitTag){
if(Test-Path .git){
$gitTag = git describe --tags --abbrev=0 2>$null
if(-not $gitTag){ Write-Err "No git tag found. Create one or omit -UseGitTag."; exit 1 }
$Tag = $gitTag
} else {
Write-Err "No .git directory. Cannot derive tag."; exit 1
}
}
# Normalize registry prefix
if($Registry -in @('docker.io','index.docker.io','')){
$FullImage = "$Repo/$ImageName:$Tag"
} else {
$FullImage = "$Registry/$Repo/$ImageName:$Tag"
}
Write-Info "Building image: $FullImage"
if($MultiArch){
# Ensure buildx exists
$bx = docker buildx version 2>$null
if($LASTEXITCODE -ne 0){
Write-Info "Creating buildx builder (first time)"
docker buildx create --use --name multiarch-builder | Out-Null
}
docker buildx use multiarch-builder
docker buildx inspect --bootstrap | Out-Null
Write-Info "Building multi-arch for: $Platforms"
docker buildx build --platform $Platforms --build-arg VERSION=$Tag -t $FullImage --push:$Push .
if(-not $Push){
Write-Warn "Multi-arch buildx without --push stores result in registry only when --push is set; local images may not exist."
}
} else {
docker build --build-arg VERSION=$Tag -t $FullImage .
if($Push){
Write-Info "Pushing $FullImage"
docker push $FullImage
} else {
Write-Info "Skipping push (add -Push to push)."
}
}
Write-Info "Done."
-102
View File
@@ -1,102 +0,0 @@
import { getTwoEmbed } from "./workers/sites/2Embed.js";
import { getAutoembed } from "./workers/sites/AutoEmbed.js";
import { getEmbedSu } from "./workers/sites/Embedsu.js";
import { getVidSrcSu } from "./workers/sites/VidsrcSU.js";
export async function getMovie(media, provider = null) {
const combinedResponse = [];
const tmdb_id = media.tmdbId;
if (provider) {
try {
let result;
switch (provider) {
case '2embed':
result = await getTwoEmbed(tmdb_id);
break;
case 'embedsu':
result = await getEmbedSu(tmdb_id);
break;
case 'autoembed':
result = await getAutoembed(tmdb_id);
break;
case 'vidsrcsu':
result = await getVidSrcSu(tmdb_id);
break;
default:
throw new Error(`Unknown provider: ${provider}`);
}
if (result) combinedResponse.push(result);
} catch (e) {
console.error(`Error fetching from ${provider}:`, e);
}
return combinedResponse;
}
const providers = [
{ name: '2embed', func: getTwoEmbed },
{ name: 'embedsu', func: getEmbedSu },
{ name: 'autoembed', func: getAutoembed },
{ name: 'vidsrcsu', func: getVidSrcSu }
];
for (const { name, func } of providers) {
try {
const result = await func(tmdb_id);
if (result) combinedResponse.push(result);
} catch (e) {
console.error(`Error fetching from ${name}:`, e);
}
}
return combinedResponse;
}
export async function getTv(media, season, episode, provider = null) {
const combinedResponse = [];
const tmdb_id = media.tmdbId;
if (provider) {
try {
let result;
switch (provider) {
case '2embed':
result = await getTwoEmbed(tmdb_id, season, episode);
break;
case 'embedsu':
result = await getEmbedSu(tmdb_id, season, episode);
break;
case 'autoembed':
result = await getAutoembed(tmdb_id, season, episode);
break;
case 'vidsrcsu':
result = await getVidSrcSu(tmdb_id, season, episode);
break;
default:
throw new Error(`Unknown provider: ${provider}`);
}
if (result) combinedResponse.push(result);
} catch (e) {
console.error(`Error fetching from ${provider}:`, e);
}
return combinedResponse;
}
const providers = [
{ name: '2embed', func: getTwoEmbed },
{ name: 'embedsu', func: getEmbedSu },
{ name: 'autoembed', func: getAutoembed },
{ name: 'vidsrcsu', func: getVidSrcSu }
];
for (const { name, func } of providers) {
try {
const result = await func(tmdb_id, season, episode);
if (result) combinedResponse.push(result);
} catch (e) {
console.error(`Error fetching from ${name}:`, e);
}
}
return combinedResponse;
}
-388
View File
@@ -1,388 +0,0 @@
import fetch from "node-fetch";
import JsUnpacker from '../utils/jsunpack.js';
import * as cheerio from 'cheerio';
const DOMAIN = "https://www.2embed.cc";
const PLAYER_URL = "https://uqloads.xyz";
let subtitles = [];
let sources = [];
const headers = {
'User-Agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
'Referer': DOMAIN,
'Origin': DOMAIN,
};
function parseQuality(qualityString) {
if (!qualityString) return 0;
const q = qualityString.toUpperCase();
if (q === '4K') return 4000;
if (q.includes('1080P')) return 1080;
if (q.includes('720P')) return 720;
const numMatch = q.match(/(\d+)/);
return numMatch ? parseInt(numMatch[1], 10) : 0;
}
function createErrorObject(errorMessage) {
return {
provider: "2Embed",
ERROR: [{
error: `ERROR`,
what_happened: errorMessage,
report_issue: 'https://github.com/Inside4ndroid/TMDB-Embed-API/issues'
}]
};
}
export async function getTwoEmbed(tmdb_id, s, e) {
subtitles = [];
sources = [];
const tmdbId = tmdb_id;
const initialReferer = s && e ? `${DOMAIN}/embedtv/${tmdbId}&s=${s}&e=${e}` : `${DOMAIN}/embed/${tmdbId}`;
const url = initialReferer;
try {
const response = await fetch(url, {
method: "POST",
headers: {
Referer: url,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": headers['User-Agent'],
},
body: "pls=pls"
});
if (!response.ok) {
return { sources: [createErrorObject(`HTTP error fetching initial URL! Status: ${response.status}`)] };
}
const data = await response.text();
let extractedValue = null;
let isSwishId = false;
let match = data.match(/swish\?id=(?<id>[\w\d]+)/);
if (match && match.groups && match.groups.id) {
extractedValue = match.groups.id;
isSwishId = true;
} else {
match = data.match(/'(.*?player4u.*?)'/);
if (match && match[1]) {
extractedValue = match[1];
} else {
return { sources: [createErrorObject("No relevant swish ID or player4u URL found in initial data.")] };
}
}
if (extractedValue) {
if (isSwishId) {
const streamUrl = await resolve(`${PLAYER_URL}/e/${extractedValue}`, DOMAIN);
if (!streamUrl) {
return { sources: [createErrorObject(`Could not resolve stream URL for swish ID: ${extractedValue}`)] };
}
const parsedSources = await parseM3U8(streamUrl, PLAYER_URL);
if (parsedSources.length > 0) {
sources.push({
provider: "2Embed/Swish",
files: parsedSources,
subtitles: subtitles,
headers: {
"Referer": PLAYER_URL,
"User-Agent": headers['User-Agent'],
"Origin": PLAYER_URL
}
});
} else {
return { sources: [createErrorObject("No valid sources found in M3U8 for swish ID")] };
}
} else {
const listPageResponse = await fetch(extractedValue, {
headers: {
Referer: url,
"User-Agent": headers['User-Agent']
}
});
if (!listPageResponse.ok) {
return { sources: [createErrorObject(`Failed to fetch player4u list page (${extractedValue}): ${listPageResponse.status}`)] };
}
const listPageHtml = await listPageResponse.text();
const $ = cheerio.load(listPageHtml);
let highestQuality = -1;
let bestPartialUrl = null;
$('li.slide-toggle a.playbtnx').each((index, element) => {
const linkText = $(element).text();
const onclickAttr = $(element).attr('onclick');
if (!linkText || !onclickAttr) return;
const qualityMatch = linkText.match(/\s*(\d+p|4K)\s*/i);
const qualityString = qualityMatch ? qualityMatch[1].toUpperCase() : null;
const urlMatch = onclickAttr.match(/go\('([^']+)'\)/);
const partialUrl = urlMatch ? urlMatch[1] : null;
if (!qualityString || !partialUrl) return;
const qualityValue = parseQuality(qualityString);
if (qualityValue > highestQuality) {
highestQuality = qualityValue;
bestPartialUrl = partialUrl;
}
});
if (bestPartialUrl) {
const idMatch = bestPartialUrl.match(/\?id=([\w\d]+)/);
if (idMatch && idMatch[1]) {
const player4uId = idMatch[1];
const resolveUrl = `${PLAYER_URL}/e/${player4uId}`;
const streamUrl = await resolve(resolveUrl, extractedValue);
if (!streamUrl) {
return { sources: [createErrorObject(`Could not resolve stream URL for player4u ID: ${player4uId}`)] };
}
const parsedSources = await parseM3U8(streamUrl, PLAYER_URL);
if (parsedSources.length > 0) {
sources.push({
provider: "2Embed/Player4u",
files: parsedSources,
subtitles: subtitles,
headers: {
"Referer": PLAYER_URL,
"User-Agent": headers['User-Agent'],
"Origin": PLAYER_URL
}
});
} else {
return { sources: [createErrorObject("No valid sources found in M3U8 for player4u ID")] };
}
} else {
return { sources: [createErrorObject("Could not extract player4u ID from best quality URL")] };
}
} else {
return { sources: [createErrorObject("No valid quality options found on player4u page")] };
}
}
}
return { sources: sources.length > 0 ? { sources } : [createErrorObject("No sources found after processing")] };
} catch (error) {
return { sources: [createErrorObject(`Unexpected error: ${error.message}`)] };
}
}
async function resolve(url, referer) {
try {
const response = await fetch(url, {
headers: {
Referer: referer,
"User-Agent": headers['User-Agent']
}
});
if (!response.ok) {
console.error(`Resolve failed for ${url}: Status ${response.status}`);
return null;
}
const data = await response.text();
const packedDataRegex = /eval\(function(.*?)split.*\)\)\)/s;
const packedDataMatch = data.match(packedDataRegex);
if (packedDataMatch) {
const packedJS = packedDataMatch[0];
const unpacker = new JsUnpacker(packedJS);
if (unpacker.detect()) {
const unpackedJS = unpacker.unpack();
if (!unpackedJS) {
console.error("JsUnpacker failed to unpack");
return null;
}
await parseSubs(unpackedJS);
const docheck = unpackedJS.includes("\"hls2\":\"https");
if (docheck) {
const fileRegex = /links=.*hls2\":\"(.*?)\"};/;
const matchUri = unpackedJS.match(fileRegex);
if (matchUri && matchUri[1]) {
return matchUri[1];
} else {
console.error("Could not find file URL in unpacked JS");
return null;
}
} else {
const fileRegex = /sources\s*:\s*\[\s*\{\s*file\s*:\s*"([^"]+)"/;
const matchUri = unpackedJS.match(fileRegex);
if (matchUri && matchUri[1]) {
return matchUri[1];
} else {
console.error("Could not find file URL in unpacked JS");
return null;
}
}
} else {
console.error("JsUnpacker could not detect packed data in resolve response");
return null;
}
} else {
console.error("No packed JS data found in resolve response for:", url);
if (url.includes('.m3u8')) return url;
return null;
}
} catch (error) {
console.error(`Error during resolve for ${url}:`, error);
return null;
}
}
async function parseM3U8(m3u8Url, referer) {
const parsedSources = [];
try {
const response = await fetch(m3u8Url, {
headers: {
"User-Agent": headers['User-Agent'],
"Referer": referer
}
});
if (!response.ok) {
console.error(`Failed to fetch M3U8 ${m3u8Url}: Status ${response.status}`);
return [];
}
const m3u8Content = await response.text();
const lines = m3u8Content.split('\n');
let currentQuality = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('#EXT-X-STREAM-INF:')) {
currentQuality = null;
const resolutionMatch = line.match(/RESOLUTION=\d+x(\d+)/);
if (resolutionMatch && resolutionMatch[1]) {
currentQuality = resolutionMatch[1] + "p";
} else {
const bwMatch = line.match(/BANDWIDTH=(\d+)/);
if (bwMatch) {
currentQuality = `bw${bwMatch[1]}`;
}
}
} else if (currentQuality && (line.startsWith('http') || (line.includes('.m3u8') && !line.startsWith('#')))) {
let resolvedUrl = line;
if (!line.startsWith('http')) {
const baseUrl = new URL(m3u8Url);
resolvedUrl = new URL(line, baseUrl.origin + baseUrl.pathname.substring(0, baseUrl.pathname.lastIndexOf('/') + 1)).href;
}
parsedSources.push({
file: resolvedUrl,
type: 'hls',
quality: currentQuality,
lang: 'en'
});
currentQuality = null;
}
}
parsedSources.sort((a, b) => parseQuality(b.quality) - parseQuality(a.quality));
return parsedSources;
} catch (error) {
console.error(`Error parsing M3U8 ${m3u8Url}:`, error);
return [];
}
}
async function parseSubs(scriptstring) {
if (typeof subtitles === 'undefined') {
console.error("Subtitles array is not accessible in parseSubs");
return;
}
try {
// First extract the links object from the script
const linksMatch = scriptstring.match(/var links\s*=\s*({[^;]*});/);
if (!linksMatch) {
console.error("Could not find links object in script");
return;
}
let linksStr = linksMatch[1]
.replace(/'/g, '"') // Replace single quotes with double quotes for JSON
.replace(/([{,]\s*)([a-zA-Z0-9_$]+)(\s*:)/g, '$1"$2"$3'); // Ensure proper JSON keys
const links = JSON.parse(linksStr);
const videoUrl = links.hls2; // Use hls2 as fallback if hls4 doesn't exist
// Now extract the JWPlayer setup configuration
const setupMatch = scriptstring.match(/jwplayer\(["']vplayer["']\)\.setup\((\{[\s\S]*?\})\);[\s\S]*?$/);
if (!setupMatch || !setupMatch[1]) {
console.error("Could not find JWPlayer setup configuration");
return;
}
let setupStr = setupMatch[1];
// Replace JavaScript expressions with actual values
setupStr = setupStr.replace(/links\.hls4\s*\|\|\s*links\.hls2/g, `"${videoUrl}"`);
// Clean the string for JSON parsing
setupStr = setupStr
.replace(/\\'/g, "'")
.replace(/([{,]\s*)([a-zA-Z0-9_$]+)(\s*:)/g, '$1"$2"$3')
.replace(/'/g, '"')
.replace(/,\s*([}\]])/g, '$1')
.replace(/"true"/g, 'true')
.replace(/"false"/g, 'false')
.replace(/"null"/g, 'null');
let setupConfig;
try {
setupConfig = JSON.parse(setupStr);
} catch (err) {
console.error("JSON parse error:", err, "in string:", setupStr);
return;
}
if (!setupConfig.tracks) {
console.log("No tracks found in setup configuration");
return;
}
const subtitleTracks = (setupConfig.tracks || []).filter(track =>
track && (track.kind === "captions" || track.kind === "subtitles") && track.file
).map(track => ({
url: track.file,
lang: track.label || track.kind
}));
subtitleTracks.forEach(newSub => {
if (!subtitles.some(existingSub => existingSub.url === newSub.url && existingSub.lang === newSub.lang)) {
subtitles.push(newSub);
}
});
} catch (error) {
console.error("Error during subtitle parsing:", error);
}
}

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