Moviebox-API

LOGO

GitHub Repo Author License Stars

MovieBox API Pro

A pure REST API wrapper around moviebox.ph with an indexed catalog, provider failover, FTS5 search, and a self-hosted frontend.

No scraping. No HTML parsing. No headless browser. It talks the exact same private JSON API the moviebox web player talks — h5-api.aoneroom.com/wefeed-h5api-bff — and re-exposes it as a clean, self-hosted REST surface plus a browser UI.

Built for people who want to wire moviebox into their own apps, scripts, bots, dashboards, or media servers without reverse-engineering the network tab every time the site ships an update.

Version 2.7.0 — module split, provider failover, FTS5 search, trailer playback, continue-watching row.


What it does

What it isn’t


Quickstart

git clone https://github.com/faisaljs/Moviebox-API
cd Moviebox-API
pip install -r requirements.txt
python main.py

Server boots on http://localhost:8000. Two entry points:

Dev with autoreload:

RELOAD=1 python main.py

Custom port:

PORT=9000 python main.py

Docker:

docker compose up -d

The first boot kicks off the catalog walk in the background. It takes 2–3 minutes to populate ~1700 rows and enrich the type fields. /catalog/stats shows progress.

Single instance only. SQLite is not multi-writer safe across processes. Two python main.py against one catalog.db corrupts it. If you’re testing, Ctrl+C the running server before starting a new one.


Endpoints

Discovery & home

Method Path Purpose
GET /home Featured sections and banners
GET /home/all Flattened deduped feed
GET /sections List every home section
GET /browse/{section} Direct access to any home section

Catalog (indexed)

Method Path Purpose
GET /catalog Indexed catalog with sort/filter/search
GET /catalog/stats Counts, walk progress, keyword telemetry
POST /catalog/refresh Re-run the walk (?mode=enrich for enrichment only)
POST /catalog/add Manually add a subject by slug

Home-fallback catalogs

Method Path Purpose
GET /movies Movies from home sections
GET /tv-series TV series from home sections
GET /animation Animation from home sections

Search & metadata

Method Path Purpose
GET /search Full-text search against upstream
GET /search/suggest Typeahead (?enrich=true for slugs)
GET /detail/{slug} Full metadata for a subject

Stream & subtitles

Method Path Purpose
GET /api/stream/{subject_id} Direct video sources
GET /api/stream/{subject_id}/proxy Range-aware MP4 passthrough
GET /api/stream/{subject_id}/captions Subtitle list
GET /api/stream/{subject_id}/subtitles/{lang} SRT → VTT inline

Providers

Method Path Purpose
GET /providers Live provider health with circuit breaker state
POST /providers/probe Force-probe all providers now
POST /providers/reset Clear all circuit breakers
GET /api/hosts Backwards-compat host status

Diagnostics

Method Path Purpose
GET /health Liveness, cache state, catalog counts
GET /api/tabs Probe tab IDs (diagnostic)
GET /api/tabs/variants Try every known filter payload shape
GET /api/debug/upstream Raw upstream passthrough

GET /home

Returns the operating list from the moviebox home feed, grouped into sections. Banner sections and SUBJECTS_* rows are normalized into the same item shape.

curl http://localhost:8000/home | jq
{
  "status": "success",
  "host": "moviebox.ph",
  "sections": [
    {
      "section": "Banner",
      "count": 11,
      "items": [
        {
          "name": "VisionQuest",
          "poster_url": "https://.../cover.webp",
          "slug": "visionquest-hindi-0qVaGP1YHE4",
          "subject_id": "3908280950886646112",
          "badge": "Hindi"
        }
      ]
    }
  ]
}

GET /catalog

The real catalog. Reads from the SQLite index populated by the background walk.

Query parameters

Param Type Default Notes
type string all all, movie, tv, anime
sort string recent recent, title, rating
page int 1 1-indexed
per_page int 24 Max 100
q string — FTS5 title search (with LIKE fallback)
host string — Filter by source host
curl "http://localhost:8000/catalog?type=movie&sort=rating&per_page=5" | jq
curl "http://localhost:8000/catalog?q=dragon" | jq
curl "http://localhost:8000/catalog?q=love%20you" | jq   # multi-word AND

Response includes total, total_pages, and items[] with name, poster_url, slug, subject_id, rating, year, badge, subject_type, genre, types[].

FTS5 search behavior:

GET /catalog/stats

Everything about the index:

{
  "total": 1737,
  "movies": 163,
  "tv": 140,
  "anime": 25,
  "untyped": 1409,
  "untyped_pending_enrichment": 1497,
  "subject_types": { "1": 119, "2": 89, "6": 23 },
  "by_host": { "moviebox.ph": 1737 },
  "walk_state": { "running": false, "enriching": false },
  "keyword_stats": {
    "m": { "pages": 11, "items": 211, "hit_cap": false, "upstream_total": 211 }
  }
}

POST /catalog/refresh

curl -X POST http://localhost:8000/catalog/refresh                          # full walk
curl -X POST "http://localhost:8000/catalog/refresh?mode=enrich"            # enrichment only
curl -X POST "http://localhost:8000/catalog/refresh?force=true"             # walk all hosts, no dedupe

Returns immediately; runs in background. Watch /catalog/stats.

POST /catalog/add?slug=

Manually add a subject the walk missed. Useful for anything upstream’s search ranking doesn’t surface (Matrix was the case study — search for m returns 211 results but Matrix isn’t among them).

curl -X POST "http://localhost:8000/catalog/add?slug=matrix-W1E5680Y2S5" | jq

Fetches the detail response, upserts, and enriches immediately.

GET /api/stream/{subject_id}

curl "http://localhost:8000/api/stream/6207982430134357800?detail_path=breaking-bad-ej6Bp0MCAo7&se=1&ep=1" | jq

Returns has_resource, sources[] (resolution, format, codec, url, size, duration), hls[], dash[], free_episodes, limited.

GET /api/stream/{subject_id}/proxy

Range-aware MP4 passthrough. Drop into media servers (Jellyfin, Plex, Infuse, VLC) that can’t set custom headers.

# full file
curl -O "http://localhost:8000/api/stream/6207982430134357800/proxy?detail_path=breaking-bad-ej6Bp0MCAo7&res=1080p"

# ranged seek
curl -H "Range: bytes=0-1048575" "http://localhost:8000/api/stream/.../proxy?..." -o chunk.bin

Forwards Range upstream, returns 206 with Content-Range on partial. Full-file requests stream through a 64KB chunked iterator.

GET /api/stream/{subject_id}/subtitles/{lang}

Fetches the SRT, strips index lines, converts timestamps, prepends WEBVTT. Drop-in for a <track> element.

curl "http://localhost:8000/api/stream/6207982430134357800/subtitles/en?detail_path=breaking-bad-ej6Bp0MCAo7" | head -20
WEBVTT

00:00:17.833 --> 00:00:18.916
[Vision] <i>Who am I?</i>

SRT formatting tags (<i>, <b>, {\an8}) survive the conversion.

GET /providers

Live provider state:

{
  "default_host": "moviebox.ph",
  "provider_count": 7,
  "healthy": 7,
  "degraded": 0,
  "providers": [
    {
      "host": "moviebox.ph",
      "alive": true,
      "circuit_open": false,
      "circuit_open_for": 0,
      "consecutive_failures": 0,
      "total_success": 25,
      "total_failure": 0,
      "avg_latency_ms": 374,
      "last_success_ago": 0,
      "last_failure_ago": null,
      "last_error": null
    }
  ],
  "circuit_breaker": { "threshold": 3, "window_seconds": 60 }
}

GET /health

{
  "status": "ok",
  "version": "2.7.0",
  "default_host": "moviebox.ph",
  "providers": { "moviebox.ph": { "alive": true, "latency_ms": 374, "fails": 0, "circuit_open": false } },
  "tokens_cached": 1,
  "domain_cached": "https://mzfi.me",
  "domain_expires_in": 287,
  "catalog_indexed": 1737,
  "walk_running": false,
  "enriching": false
}

Frontend at /app

Single-file vanilla JS, no build step, no dependencies. Served by the same FastAPI process.

Views:

Persistence (localStorage, per-browser):

Trailer playback — when subject.trailer.videoAddress.url exists, a ▶ Trailer button appears next to Bookmark on the detail page. Opens a minimal overlay with the promo MP4. No history tracking.

Player keyboard shortcuts:

Key Action
Space / k Play / pause
← / → Seek ±5s
↑ / ↓ Volume ±10%
f Fullscreen
m Mute
Esc Close player / exit fullscreen

Screenshots

Home Catalog Player

More at screenshots/.


Architecture

Module layout

config.py            env vars + headers + constants
state.py             mutable globals (client, db, tokens, caches, walk_state)
providers.py         provider health, circuit breaker, probe loop
upstream.py          token cache, bff_request (failover), player, stream resolution
storage.py           sqlite schema, FTS5, read, upsert, classify
helpers.py           normalize, srt_to_vtt, pick_mp4_stream
catalog_worker.py    walk, enrichment, maintenance loop
dashboard.py         DASHBOARD_HTML string
routes.py            all API endpoints
main.py              FastAPI app + lifespan + middleware + / and /app
static/app.html      frontend

No circular imports. Import order: config → state → providers → upstream → catalog_worker → routes → main. storage and helpers are leaf modules.

The rule throughout: import state / import providers — never from state import client. Mutable references stay live via module attribute access.

Guest tokens per provider

Each mirror issues its own guest JWT via the x-user response header. Tokens are cached per host. On 401, the token is cleared, re-acquired, and the request retried once on the same host.

Player domain

Stream URLs live on a different host than the BFF. Fetched from /media-player/get-domain, cached for 300s. On stream failure, the cache is invalidated and the domain re-discovered once before surfacing an error.

Provider failover

Every BFF request tries the primary first, then falls over to the next healthy provider on network failure, 5xx, 429 (honoring Retry-After), or non-JSON. Per-host circuit breaker opens after 3 consecutive failures for 60s. A background probe loop cycles one host per PROVIDER_PROBE_INTERVAL seconds, keeping latency and health fresh.

All 7 known mirrors share the same aoneroom backend — the failover is pure resilience, not multiple catalogs.

Catalog walk

Two-tier source for the index:

  1. Home ingest — every subject appearing in /home under a category section gets tagged with is_movie / is_tv / is_anime based on substring matching against the section name
  2. Keyword walk — 36 keywords (a-z, 0-9), paginate until hasMore: false or CATALOG_MAX_PAGES_PER_KEYWORD, upsert every subject

After walking, a background enricher hits /detail/{slug} for every subject whose type is unknown and reads subjectType (1=movie, 2=series, 6=music video) plus genre (contains “anime” → is_anime). This is what gets movies from 44 to 400+ and TV from 60 to 300+.

The walk re-runs every 6 hours. Enrichment runs once after each walk. POST /catalog/refresh forces both manually.

Home feed is geo-targeted. Mumbai sees Cinema, Bollywood, Top Anime. Singapore sees Popular Movie, K-Drama, Anime[English Dubbed]. The classifier uses substring matching (movie, series, anime, etc.) so both work. Genre-named sections (Comedy, Epic Fantasy, BL Story) fall through to enrichment.

subjects_fts is an FTS5 virtual table using external content mode — it mirrors the subjects table via triggers (subjects_fts_ai/ad/au). On first boot, if the row counts drift, init_schema rebuilds the FTS index. From then on, triggers keep it in sync on every insert/update/delete.

Query path: /catalog?q= calls storage.fts_search() which returns matching subject_ids or None. None (FTS unavailable or short query) → fall back to title LIKE ?. Empty list (FTS ran, no matches) → WHERE 1 = 0. Non-empty list → subject_id IN (...).

SQLite integrity check

On boot, right after init_schema, the lifespan runs PRAGMA integrity_check. If the DB is corrupt, the process prints the fix command and exits(1) before uvicorn binds the port. This is the safety net for the case where you accidentally run two python main.py against the same catalog.db — better to fail loudly than silently on every write while reads succeed.

SRT → VTT

/subtitles/{lang} fetches the SRT, strips numeric index lines, converts , timestamp separators to ., prepends WEBVTT, collapses whitespace. Italics and other SRT tags survive.


Project layout

.
├── main.py
├── config.py
├── state.py
├── providers.py
├── upstream.py
├── storage.py
├── helpers.py
├── catalog_worker.py
├── dashboard.py
├── routes.py
├── static/
│   └── app.html
├── requirements.txt
├── verify.py
├── Dockerfile
├── docker-compose.yml
├── README.md
├── CONTRIBUTING.md
├── .env.example
└── .gitignore

Verification

verify.py runs a live integration pass against a running server. 27 checks, chains real requests — pulls a slug from /home, feeds /detail, /api/stream, /captions, /proxy, /subtitles. Also exercises /providers, /api/hosts, catalog sorts, filters, FTS search.

# server must be running
python verify.py

# or against a remote host
python verify.py https://your-host

Exits non-zero on any failure. Drops straight into CI or a pre-commit hook.


Environment

See .env.example for the full list with defaults. Key variables:

Variable Default Notes
PORT 8000 Listen port
RELOAD 0 1 enables autoreload (dev)
REQUEST_TIMEOUT 25 Upstream timeout, seconds
CATALOG_DB catalog.db SQLite path
CATALOG_REFRESH_INTERVAL 21600 Walk re-run interval (6h)
CATALOG_MAX_PAGES_PER_KEYWORD 50 Bump to 200 for deeper keyword coverage
ENRICH_RATE_LIMIT 0.2 Seconds between /detail calls during enrichment
MOVIEBOX_HOST moviebox.ph Primary provider
MOVIEBOX_HOSTS 7 mirrors Comma-separated
PROVIDER_PROBE_INTERVAL 60 Seconds between probe ticks
CIRCUIT_BREAKER_THRESHOLD 3 Failures before host opens
CIRCUIT_BREAKER_SECONDS 60 How long host stays open

No config file. set -a && source .env && set +a && python main.py or docker compose up.


Deployment notes


Troubleshooting

database disk image is malformed in logs

Two processes running against one catalog.db. Stop the server, lsof catalog.db to find any orphan PIDs, kill them, then rm -f catalog.db catalog.db-wal catalog.db-shm and restart clean. The integrity check on boot will now catch this before the port is bound.

FATAL: catalog.db is corrupt on boot

Same as above — the integrity check fired. Run the fix command it printed and restart.

Catalog shows total: 0 or stays at 44

The walk hasn’t finished or enrichment is still running. POST /catalog/refresh and watch /catalog/stats. walk_state.running should flip to true within a second.

?type=movie returns fewer results than expected

Enrichment hasn’t finished. Only home-ingested subjects have type tags until enrichment runs. Watch untyped_pending_enrichment in /catalog/stats — when it hits 0, everything has a type.

/movies and /catalog?type=movie show different counts

They’re different tools. /movies is a home-section fallback (44 items from Cinema/Bollywood/etc.). /catalog?type=movie is the indexed DB (400+ items). Use /catalog for real browsing.

Stream proxy returns 502

Upstream CDN might be blocking the referer. Check /api/stream/{sid} first — if sources are present, the URLs are live and the proxy should work. If empty, the episode may be gated or mid-upload.

Search misses a known title

Upstream /subject/search is relevance-ranked, not exhaustive. Matrix isn’t in the top 200 for m, so no walk keyword catches it. Fix: POST /catalog/add?slug=<known-slug> — the detail endpoint always works.

Frontend /app 404

static/app.html missing. Confirm the file exists in the repo. Dockerfile copies static/ explicitly.


Changelog

v2.7.0 — module split (10 files), provider failover with per-host circuit breakers, multi-host detection, /providers + /providers/probe + /providers/reset, FTS5 catalog search, trailer playback on detail pages, continue-watching row on home, SQLite integrity check on boot.

v2.6.0 — multi-host probe, ?host= on every BFF endpoint, source_host column in catalog.

v2.4.1 — type enrichment from /detail, keyword_stats telemetry, POST /catalog/add, ?mode=enrich refresh.

v2.4.0 — indexed catalog (/catalog, /catalog/stats, /catalog/refresh), SQLite storage, background walk + enrichment.

v2.3.3 — re-added / dashboard route; /api/tabs/variants confirms /subject/filter requires auth as guest.

v2.3.2 — catalog home-fallback, /sections, /browse/{section}, fuzzy section matching.

v2.3.1 — /search/suggest honesty (text-only upstream), total_sentinel flag, ?tab= override, /api/tabs.

v2.3.0 — shared httpx.AsyncClient, async-locked token init, retry with backoff, /home/all, /api/stream/{sid}/proxy, /api/stream/{sid}/subtitles/{lang}, Docker setup.


Author

Faisal (@faisaljs)

If you use this project, a star on GitHub is appreciated.

License

MIT — see LICENSE for the full text.


Not affiliated with moviebox.ph. Provided as-is. You are responsible for how you use it.