---
name: movie-streaming-web-app
description: Build a Flask-based movie search + streaming web app using TMDB for metadata, Jackett/TorrServer for P2P torrent streaming, and embed sources for instant playback. Covers the full stack from TMDB search to playable video in the browser.
category: software-development
tags: [flask, tmdb, jackett, torrserver, streaming, web-ui, movie-search, python, p2p]
---

# Movie Streaming Web App

Build a Flask-based web app that searches movies and streams them via multiple sources.

## Architecture

```
Browser ── HTTP ── Flask App ──┬── TMDB API (search + metadata)
                               ├── Jackett (torrent search via Torznab)
                               ├── Embed Services (vidsrc.to, 2embed, etc.)
                               └── TorrServer (P2P torrent streaming)
```

## Core Components

### Metadata: TMDB API
- Default key: `7e4d48e68abb9a4f2c86a1cc143cd8b5`
- Search: `GET https://api.themoviedb.org/3/search/movie?api_key={KEY}&query={Q}`
- Detail: `GET https://api.themoviedb.org/3/movie/{ID}?api_key={KEY}&append_to_response=credits,release_dates,videos`
- External IDs (IMDb lookup): `GET https://api.themoviedb.org/3/movie/{ID}/external_ids?api_key={KEY}`
- Poster URL: `https://image.tmdb.org/t/p/w342{poster_path}`

### Torrent Search: Fallback Chain (Jackett → TPB API → Embeds)\\n- **Priority**: Jackett first (if configured with indexers) → TPB API (zero setup) → embed sources\\n- **Jackett**: converts any tracker into Torznab API. See `references/jackett-setup.md`\\n- **TPB API** (`apibay.org/q.php`): no API key, no Cloudflare, no setup. Returns JSON with info_hash directly. See `references/tpb-api.md`\\n- **Torrentio is dead** (June 2026). Do not attempt to use it.\\n- Open firewall: `sudo ufw allow 9117/tcp && sudo ufw allow 8090/tcp && sudo ufw allow 8800/tcp`

### Embed Sources (instant, no setup)
- `https://vidsrc.to/embed/movie/{imdb_id}`
- `https://www.2embed.cc/embed/{imdb_id}`
- `https://embed.su/embed/movie/{imdb_id}`
- `https://moviesapi.club/movie/{imdb_id}`
- `https://vidbinge.dev/embed/movie/{imdb_id}`

### P2P: TorrServer
- Binary: `https://github.com/YouROK/TorrServer/releases/latest/download/TorrServer-linux-amd64`
- Run: `./TorrServer-linux-amd64 --port 8090`
- **API base path**: The MatriX fork uses **bare `/torrents`**, NOT `/api/torrents`. Official docs say `/api/torrents` but the actual binary from GitHub releases uses the un-prefixed path. Check version via `curl http://host:8090/echo` — if it starts with "MatriX", use bare paths.
- Add magnet: `POST /torrents` `{"action":"add","link":"magnet:?xt=urn:btih:{HASH}","save_to_db":true}`
- List: `POST /torrents` `{"action":"list"}` — returns array of TorrentStatus objects
- Get single status: `POST /torrents` `{"action":"get","hash":"{HASH}"}` — returns `{stat, stat_string, file_stats, total_peers}`
- **Stream**: `GET /stream/{hash}/{id}` — path format, NOT query params. `id` is 1-based from `file_stats`. Same: `GET /play/{hash}/{id}` (with auto-preloading)
- Health: `GET /echo` (returns version string if alive)
- Common gotchas:
  - `/play?hash=X&file_id=0` (query params) returns 404 — use `/play/{hash}/{id}` path format
  - The MatriX fork uses `/torrents` NOT `/api/torrents` — using the wrong path returns 404/405
  - Stream returns 400/500 until torrent reaches `stat=2` (Working) with `file_stats` populated
  - Torrents with 0 seeders will hang at "Torrent getting info" forever
  - Default video file_id is 1 (not 0 — 0 is often a .txt/.nfo file)
- Flask proxy pattern: read from TorrServer, stream response directly to `<video>` tag (see `references/torrserver-setup.md` for full proxy code)

### Python deps
```
flask python-dotenv requests gunicorn
```
Install with `--break-system-packages` on dedicated servers (PEP 668).

## Key Patterns

### SPA Frontend Integration (SanuFlix / React + Vite)
The SanuFlix app (`github.com/Sanuu7/SanuFlix-opensource`) can be used as the frontend instead of Flask templates:
1. Clone repo, copy `.env.example` -> `.env` with `VITE_TMDB_API_KEY=***
2. Update `src/config/servers.ts` with real embed URLs (VidSrc, 2Embed, etc.)
3. Modify `src/pages/WatchMoviePage.tsx` to add P2P torrent section:
   - Fetch torrents from `/api/torrents?title=X`
   - Render them as clickable buttons with seed count + quality badge
   - On click, set `torrentStreamUrl` to `/stream/{hash}` which triggers the Flask proxy
4. Build: `npm run build` -> outputs to `dist/`
5. Serve from Flask: `send_from_directory('path/to/dist', path)` at `/` catch-all route
6. Keep Flask API endpoints under `/api/*` -- the SPA calls these directly

### Snowflake/Christmas Effects Removal
If the SanuFlix frontend has ChristmasDecoration/ChristmasSnow components:
- Remove `import { ChristmasSnow }` and its usage in `App.tsx`
- Remove unused `seasonalEnabled` from `useTheme()` if it was the only call
- Remove `@keyframes snowfall` and `.animate-snowfall` CSS from `index.css`
- Watch for extra `}` braces after removing CSS blocks

### Phone-First UI Design
- Single-column layout with max-width container
- Touch-friendly buttons: min 44px tap target, rounded corners
- Grid for source buttons: 2 cols on mobile, 3-4 on desktop
- Video player fills full width (16:9 aspect ratio)
- Polling interval of 1s (not 2s) for P2P readiness
- Loading overlay shows status text + peer count
- Auto-scroll player into view when source selected

### Video Player Choice: iframe vs <video> tag
Two strategies depending on source type:

**Embed sources** (VidSrc, 2Embed, etc.): Use `<iframe>` -- these are HTML pages, not video files.
```javascript
player.innerHTML = '<iframe src="URL" class="w-full h-full" frameborder="0" allowfullscreen></iframe>';
```

**P2P torrent streams** (TorrServer): Use `<video>` tag with MP4 source.
```javascript
player.innerHTML = '<video controls autoplay><source src="/stream/HASH" type="video/mp4"></video>';
```

**Video.js** can be used for P2P streams when more controls are needed (HLS, custom UI):
```html
<link href="https://vjs.zencdn.net/8.16.1/video-js.css" rel="stylesheet">
<script src="https://vjs.zencdn.net/8.16.1/video.min.js"></script>
```
But do NOT use Video.js for embed URLs -- they require iframe playback.

### Torrent Quality Detection
```python
def detect_quality(name):
    n = name.lower()
    if any(x in n for x in ['2160', '4k', 'uhd']): return '4K'
    if '1080' in n: return '1080p'
    if '720' in n: return '720p'
    return 'SD'
```
Show quality as a badge/badge next to the seed count in each torrent button.

### Dead Torrent Filtering
Skip torrents with 0 seeders -- they will never connect:
```python
seeds = int(d.get('seeders', 0))
if seeds == 0: continue
```
Sort remaining results by seeders descending so the most viable torrents appear first.

- **Bypass PEP 668**: `pip3 install flask --break-system-packages`
- **TorrServer Docker gotcha**: `yourok/torrserver:latest` extracts binary with wrong extension. Download binary directly.
- **CORS proxy**: Flask route reads from TorrServer and streams response directly to `<video>` tag (see `references/torrserver-setup.md`)
- **Phone-friendly**: single-column, 2-col source buttons, auto-play first source, 1s polling for P2P readiness
- **Inline playback**: all sources play in-page. Embed sources load in `<iframe>`, P2P sources in `<video>` tag. Use a single `playSource(url, type)` JS function that checks `type === 'embed'` for iframe or `type === 'p2p'` for video.
- **P2P readiness: DO NOT POLL FOR files>0** — TorrServer needs an active reader on /play/{hash}/{id} to start downloading. Set the stream URL immediately when user taps a source. Browser shows loading until data arrives. If you poll for `files > 0` before setting the URL, TorrServer never starts downloading (chicken-and-egg). Set `activeUrl=/stream/{hash}` immediately, trigger magnet add via separate `/api/add_magnet?hash=X` endpoint (NOT via the stream endpoint, which consumes the response). Show a temporary "starting torrent" overlay for ~8s, then switch to `<video>` tag.
- **Wrong file_id shows 0:00**: If video element shows 0:00, stream returned non-video file (subtitle/nfo/poster). Flask proxy should retry with fallback file_ids: `[detected_fid, '2', '1']`. Skip Content-Types starting with `text/`.
- **Unified source grid**: Show ALL sources (embed + P2P torrents) in one grid sorted by reliability.
- **Embed source ad labeling**: Mark 2Embed/AutoEmbed as reliable, VidSrc/EmbedSu as having ads.
- **Free-scraper server** at `~/Scraper Suite/free-scraper/` (port 3092) has 6 server-side embed-to-direct URL extractors (FileMoon, StreamTape, VOE, RabbitStream, VidsrcNet, VidsrcTo). Integrate via `GET /resolve?url=EMBED_URL`. Add results as `type:'direct'` in `/api/free_sources`. Proxy via `/proxy?url=X`.
- **P2P priority**: display torrent sources BEFORE embed sources so users see ad-free options first. TorrServer streaming has zero ads.
- **TPB API returns all results** — remove any limit to show everything. Show seed count in the button label (e.g. "TPB 710s") so users pick well-seeded torrents.
- **Hash extraction from multi-param URLs**: `new URLSearchParams(url.split('?')[1]).get('hash')` — not naive `split('=')[1]`
- **Default file_id=1**: Most torrents use file_id=1 for video. file_id=0 is often .txt/.nfo.
- **Torrent state machine**: stat=0→1 (getting info, files=0) → stat=2 (working, streamable). Stream returns 400/500 until stat=2.
- **P2P network test**: Use Big Buck Bunny magnet (`magnet:?xt=urn:btih:dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c`) to verify TorrServer connectivity before debugging code.
- **TorrServer API path gotcha**: MatriX fork uses bare `/torrents`, NOT `/api/torrents`. Check with `curl /echo` — if it says "MatriX", use bare paths.
- **Torrent status**: `POST /torrents {"action":"get","hash":"X"}`. See `references/torrserver-setup.md`.```bash\ncurl -s http://localhost:8800/ | grep -c 'Movie Player'\ncurl -s "http://localhost:8800/movies?search=The+Matrix" | grep -c 'Matrix'\ncurl -s "http://localhost:8800/movie/603" | grep -c 'VidSrc'\ncurl -s http://localhost:8800/health\n```
