# Web Movie Player (Flask + TMDB + TPB + TorrServer)

A zero-debrid web movie streaming app: search via TMDB, find torrent hashes via The Pirate Bay API, stream via TorrServer, play in browser `<video>`.

Built June 2026. Lives at `~/addon-audit/movie-player/movie/`.

## Architecture

```
Browser ──→ Flask App (:8800)
               ├── TMDB API (search + metadata + artwork)
               ├── TPB API (apibay.org → info hashes, no setup)
               ├── Jackett (optional → Torznab indexers)
               ├── TorrServer daemon (:8090)
               │     └── HTTP stream URL → Browser <video>
               └── Embed iframes (VidSrc, 2Embed, etc. — instant fallback)
```

## Torrent Search Sources

### The Pirate Bay API (free, no account)

```
GET https://apibay.org/q.php?q={title}&cat=0
```

Returns JSON array directly:
```json
[
  {
    "name": "The Matrix (1999) 1080p BrRip x264 - YIFY",
    "info_hash": "D7A46713EAEE18C746B3B980A0A6E87A8E0D1C9B",
    "seeders": "710",
    "leechers": "89",
    "size": "1986420000"
  }
]
```

No API key, no registration. `info_hash` is the 40-char hex BTIH for TorrServer.

### Jackett (self-hosted, optional)

Torznab XML API:
```
GET {host}:{port}/api/v2.0/indexers/all/results/torznab/api?apikey={key}&t=movie&q={title}
```

**Install:** Download binary from GitHub, extract to /opt/Jackett/, run ./jackett (binds :9117).
**Config:** ~/.config/Jackett/
**API key:** Auto-generated, in ServerConfig.json.
**Add indexers:** Must use web UI at http://host:9117/UI/Dashboard — no programmatic API.
**Parse:** Torznab XML with `<torznab:attr name="infohash" value="...">` elements.

## TorrServer Pipeline

### Install
```bash
wget "https://github.com/YouROK/TorrServer/releases/latest/download/TorrServer-linux-amd64"
chmod +x TorrServer-linux-amd64
./TorrServer-linux-amd64 --port 8090 &
```

### API Endpoints (MatriX v141+)

| Endpoint | Method | Purpose |
|----------|--------|---------|
| /echo | GET | Health check (returns version string) |
| /torrents | POST | Add — `{"action":"add","link":"magnet:...","save_to_db":true}` |
| /torrents | POST | List — `{"action":"list"}` |
| /torrents | POST | Get — `{"action":"get","hash":...}` returns status + file_stats |
| /torrents | POST | Remove — `{"action":"rem","hash":...}` |
| /play/{hash}/{file_id} | GET | Stream with preloading (PATH format) |
| /stream/{hash}/{file_id} | GET | Stream without preloading |

**⚠️ Path format required:** `/play/{hash}/{id}` works. `/play?hash=X&file_id=Y` returns 404. `/api/torrents` returns 404. Use bare `/torrents`.

### Torrent States

- **Torrent added** (stat=0): Just created
- **Torrent getting info** (stat=1, peers=N): Connecting to peers, downloading metadata. file_stats empty. Takes 10-30s for well-seeded torrents.
- **Torrent working** (stat=2): Ready. file_stats populated. Stream URLs work.

### Auto-Detect Video File

Default file_id=0/1 often points to subtitle/.nfo files. Always find the largest video file:

```python
video_exts = ('.mp4', '.mkv', '.avi', '.mov', '.webm', '.m4v', '.mpeg', '.mpg')
video_files = [f for f in file_stats if any(f.get('path','').lower().endswith(e) for e in video_exts)]
if video_files:
    video_files.sort(key=lambda f: f.get('length', 0), reverse=True)
    file_id = str(video_files[0]['id'])
```

### Flask Proxy (CORS bypass)

Browser `<video>` needs Accept-Ranges: bytes + correct Content-Type. Flask proxies TorrServer:

```python
@app.route('/torrent/play')
def torrent_play():
    hash_val = request.args.get('hash', '')
    # auto-detect video file
    ts_url = f'http://127.0.0.1:8090/play/{hash_val}/{file_id}'
    ts_resp = urllib.request.urlopen(ts_url, timeout=30)
    def generate():
        while True:
            chunk = ts_resp.read(65536)
            if not chunk: break
            yield chunk
    return Response(generate(), 200, headers={
        'Content-Type': 'video/mp4',
        'Accept-Ranges': 'bytes',
        'Access-Control-Allow-Origin': '*',
    })
```

### JavaScript Polling

```javascript
var check = setInterval(function() {
    fetch(url, {method:'HEAD'}).then(r => {
        if(r.ok || r.status === 206) { clearInterval(check); /* create video element */ }
    }).catch(()=>{});
    fetch('/api/torrent_status?hash=' + hash).then(r => r.json()).then(d => {
        if(d.stat_string === 'Working' && d.files > 0) { /* ready */ }
    }).catch(()=>{});
}, 1000);
```

## Embed Sources (instant play)

| Source | URL Pattern | Reliability |
|--------|-------------|-------------|
| VidSrc | vidsrc.to/embed/movie/{imdb_id} | Always works |
| 2Embed | 2embed.cc/embed/{imdb_id} | Reliable |
| EmbedSu | embed.su/embed/movie/{imdb_id} | Reliable |
| MoviesAPI | moviesapi.club/movie/{imdb_id} | OK |
| VidBinge | vidbinge.dev/embed/movie/{imdb_id} | OK |

These show ads on third-party sites. Loaded in iframe.

## Setup

```bash
pip3 install flask requests --break-system-packages
sudo ufw allow 8800/tcp && sudo ufw allow 8090/tcp
# TorrServer + Flask app as above
```

## Caveats

- TorrServer peer discovery may fail depending on ISP/network. Embed sources always work as fallback.
- P2P takes 10-30s for well-seeded torrents. Dead torrents never connect.
- TPB API unofficial, may change.
- Jackett indexers must be added manually via web UI.
- Embed sources show ads.
- TV episodes not wired up — movies only.
