Skip to main content

Lightweight self-hosted movie browser and streaming server with optional Plex integration.

Project description

7844F597-CBA9-4EAD-80DE-19991552F906

Cat Theatre Movies Server 🐱

English | Deutsch | Français | 日本語 | 한국어 | Nederlands | ไทย | Tiếng Việt | 简体中文 | 繁體中文(香港) | 繁體中文(台灣)

Super lightweight, Privacy Mode 🔐, Cross-device, Smart Streaming

No App is required. Easy server installation. Mobile friendly user interface, connecting your NAS anywhere, with optional PLEX integration

Experimental MIT License Latest Release Python 3.9+

No heavy dependency, everything transparent. Lightweight self-hosted movie browser and streaming server built with Flask, Waitress, and ffmpeg, with optional Plex integration for compatibility-focused playback.


Screenshot 2026-03-22 at 9 39 12 PM


✨ Why Use It

Cat Theatre is intentionally lightweight:

  • 🩷 No Plex subscription 💰 is reuqired for Remote Access
  • ✅ Small Python dependency surface
  • ✅ No database requirement
  • ✅ File-system-first cataloging
  • ✅ Compatible with 🖥️ Desktop, 📱 Mobile, Tablet
  • ✅ Portable polling-based scan flow instead of OS-specific watcher dependence
  • 🔶 Optional Plex integration layered on top rather than required for core playback

✴️ Features

  • 🎬 Local / NAS media libraries spread across multiple folders
  • 🌄 Thumbnail / Poster and preview generation
  • 🔐 Private-folder with device-based unlock
  • 🔗 Reverse-proxy deployment under a path prefix such as http://192.168.1.100/movie/
  • 📽️ Mixed playback strategies: direct playback, built-in local transcoding for .mkv and .ts , or Plex-backed HLS proxying. Easy switch per media
  • 🌐 Browser image caching plus IndexedDB metadata caching

→ Setup with one-liner:

curl -fsSL https://raw.githubusercontent.com/daocha/plex-cat-theatre/main/install.sh | bash

🟢 Requirements

Python 3.9 or newer

Current Python packages:

  • Flask
  • waitress

System Binaries Required for metadata probing, previews, thumbnails, and local transcoding:

  • ffmpeg
  • ffprobe

Verify they are available:

which ffmpeg
which ffprobe

🚀 Quick Start

→ Option A: Setup with one-liner:

curl -fsSL https://raw.githubusercontent.com/daocha/plex-cat-theatre/main/install.sh | bash

→ Option B: Install from PyPI with pip

pip install plex-cat-theatre
plex-cat-theatre-init
plex-cat-theatre --config ~/movies_config.json

→ Option C: Preferred startup method

git clone https://github.com/daocha/plex-cat-theatre
cd plex-cat-theatre
./startup.sh

This bootstrap script can:

  • create movies_config.json from the sample config on first run
  • create a local .venv
  • install Python dependencies into that local virtual environment
  • create config-relative cache/thumbnails and logs folders when needed
  • check ffmpeg and ffprobe
  • optionally help you generate the private-mode passcode hash
  • start the server with your local config

You can still use the manual flow below:

  1. Copy the sample config:
cp movies_config.sample.json movies_config.json
  1. Edit movies_config.json for your environment.

🌐 Start the server:

# if you follow Option A or Option B, then run
plex-cat-theatre --config ~/movies_config.json

# if you follow Option C, then run
python3 movies_server.py --config movies_config.json

Open the UI:

http://localhost:9245

🔑 Change Passcode

# if you follow Option A or Option B, then run
plex-cat-theatre-passcode newpasscode

# if you follow Option C, then run
python3 passcode.py newpasscode
  • private folders are hidden unless the device is authorized
  • unlock state is tied to a device ID
  • approved devices are stored server-side
  • The script can rotate the private-mode passcode and clear approvals

🗂️ Project Structure

  • movies_server.py: Flask entrypoint and route wiring
  • movies_server_core.py: shared server helpers for auth, config, cookies, and mount-path handling
  • movies_catalog.py: catalog scanning, thumbnail generation, subtitle extraction, and local transcode helpers
  • movies_server_plex.py: Plex adapter, poster/subtitle mapping, and Plex HLS proxying
  • movies.js: frontend source
  • movies.min.js: minified frontend bundle
  • movies.css: gallery and player styles
  • passcode.py: helper for rotating the private-mode passcode

⚙️ Configuration

The sample config is intentionally sanitized and does not include:

  • real file-system paths
  • real Plex tokens
  • real hashed passcodes
  • device-specific values

📍 Important Fields

root media roots to scan (support multiple folders)
thumbs_dir directory for thumbnails and preview frames. Default ./cache/thumbnails
private_folder Folder prefixes treated as private. Example Personal. Anything under Personal folder will be locked until you unlock it from the UI.
private_passcode private-mode passcode hash (you should not directly update it with plain text). You you want to update it, refer to Change Passcode section.
mount_script [optional] Command used when playback hits a missing media folder due to folder accidential unmounted.
transcode Enable the catalog-side background transcode worker for source containers such as `.mkv` and `.ts`; this can generate separate transcoded sidecar media files alongside the source library, so it is usually best left `false`, especially when Plex integration is enabled. Default false/td>
auto_scan_on_start Rescan media on startup. Default false
on_demand_transcode Enable runtime player transcoding for source containers, using hardware encode when available and falling back to software encode when needed. Default true
on_demand_hls Enable built-in HLS playlists for source containers. Default true
enable_plex_server 📍 [optional] Enable Plex integration. Default false. Please make sure you have Plex Server installed and configured properly before enabling this.
This server supports native subtitles, but if you want to automatically fetch subtitles, it is better to use Plex to fetch these.
If you want better on-demand transcoding experience, it is strongly recommended to install Plex server to enable seamless media streaming.
Even without Plex server, this server can still work well but pls note the following:
→ For the media that can be directly played in your device, seeking feature is working perfectly.
→ For those your device can not directly play: i.e. h.265 with DTS audio (h.265 with acc or mp3 is not affected), .mkv, .ts or .wmv this server is still able to transcode on the fly but seeking feature might not be available.
plex.base_url Plex server base URL.
plex.token Plex token
debug_enabled Show the built-in debug overlay
direct_playback Object with enabled and audio_whitelist. With enabled=true, this allows you play the media with native player without transcoding (fast). Suggest to use default settings.

Minimal Local-Only Example

{
  "root": [
    "~/Movies"
  ],
  "thumbs_dir": "./cache/thumbs",
  "mount_script": "",
  "private_folder": [],
  "private_passcode": "",
  "on_demand_transcode": true,
  "on_demand_hls": true,
  "enable_plex_server": false,
  "auto_scan_on_start": true
}

Plex-Integrated Example

{
  "root": [
    "~/Movies"
  ],
  "thumbs_dir": "./cache/thumbs",
  "mount_script": "",
  "private_folder": [],
  "private_passcode": "",
  "on_demand_transcode": true,
  "on_demand_hls": true,
  "enable_plex_server": true,
  "plex": {
    "base_url": "http://127.0.0.1:32400",
    "token": "REPLACE_WITH_YOUR_PLEX_TOKEN"
  },
  "auto_scan_on_start": true
}

🅿️ Plex Scan Behavior

  • local poster thumbnail generation is skipped when Plex posters are available
  • existing cached local thumbnails can still be reused
  • preview-frame generation remains enabled
  • Plex integration stays optional and local-only mode still works

→ How To Get A Plex Token

Method 1: Existing Plex Web Session

  1. Open Plex Web and sign in.
  2. Open browser developer tools.
  3. Go to the Network tab.
  4. Refresh the page.
  5. Inspect a request sent to your Plex server.
  6. Find X-Plex-Token in the URL or headers.

Method 2: Browser Storage

Check:

  • Local Storage
  • Session Storage
  • request URLs and headers in DevTools

Method 3: Direct Local Request

If you already have an active Plex Web session on the same machine, inspect Plex requests in DevTools and look for:

X-Plex-Token=...

‼️ Security notes:

  • treat the Plex token like a password
  • do not commit it into git
  • keep it only in movies_config.json

🎥 Playback Modes

1. Native Direct Playback

Used for browser-safe files such as .mp4, .m4v, and .webm.

Behavior:

  • serves the local file directly from /video/<id>
  • supports HTTP range requests
  • avoids transcoding overhead when the browser can play the file natively

Best for:

  • MP4/H.264-style files
  • browsers that already support the file directly
  • files whose audio codecs match the direct-play whitelist

2. Built-In Local Transcoding Without Plex

This is the fallback path when Plex is not enabled, or when you intentionally want to stay fully local.

Current implementation:

  • .mkv and .ts can be exposed as local HLS at /hls/<id>/index.m3u8
  • the same files can also be streamed as fragmented MP4 from /video/<id>?fmp4=1
  • HLS segments are generated on demand with ffmpeg
  • hardware encode can be tried first and fall back to libx264
  • fMP4 output is generated with libx264 plus AAC

3. Plex-Backed Playback

When Plex integration is enabled:

  • the frontend can use plex_stream_url for compatibility-sensitive playback
  • Plex generates the upstream HLS playlist
  • this server rewrites the playlist and proxies nested playlist and segment requests
  • the browser still talks to this app, not directly to Plex

Best for:

  • MKV or TS content on devices with weaker codec or container support
  • cases where Plex subtitle selection or stream normalization is preferred

Playback Selection Policy

  • direct playback wins for browser-safe files whose audio codecs match direct_playback.audio_whitelist
  • Plex remains preferred for .mkv, .ts, HLS, fMP4, or unsupported audio codecs
  • iOS-native HLS fallback timing is longer so the Plex stream has time to warm up

Default Playback Logic

  • Direct is preferred for .mp4, .m4v, .webm, and .avi when the direct URL is a real file path and the audio codecs are whitelist-safe
  • if audio codec metadata is missing for one of those browser-safe extensions, the app still prefers Direct
  • Plex is preferred for .mkv, .ts, HLS/fMP4 direct URLs, and files whose known audio codecs fall outside the whitelist
  • if no Plex match exists, the app falls back to Direct

Authentication Model

The app uses different transport methods depending on request type:

  • API requests use the X-Device-Id header
  • HLS and Plex proxy requests use the X-Device-Id header
  • native direct media requests use the movies_device_id cookie fallback

This split exists because native <video src="..."> requests cannot attach arbitrary custom headers.


Reverse Proxy And Context Path Support

The app supports deployment under subpaths such as:

  • https://example.com/movie/
  • https://example.com/cinema/

Routing preserves the active mount prefix for:

  • direct media
  • local HLS
  • Plex HLS proxy requests
  • poster and subtitle assets

Remote Plex Access With Tailscale

If the custom UI is reachable remotely but Plex is only reachable on a private LAN (i.e. Free subcription), the movies server host must still be able to reach the Plex backend directly.

Same Host

"plex": {
  "base_url": "http://127.0.0.1:32400"
}

Plex On Another LAN Machine

Advertise the route from a Tailscale node that can reach Plex:

sudo tailscale up --advertise-routes=192.168.50.0/24

Then verify reachability from the movies server host:

curl http://192.168.50.10:32400/identity

📌 Notes:

  • the browser does not need direct network access to Plex
  • the movies server process must be able to reach plex.base_url
  • reverse-proxy or MagicDNS names for the UI do not make Plex reachable by themselves

💾 Caching Strategy

Image Caching

Thumbnails, preview frames, and Plex poster images are served with long-lived immutable cache headers.

Metadata Caching

Gallery metadata snapshots are cached in IndexedDB with bounded storage:

  • 1-day TTL
  • up to 8 snapshot records
  • up to about 18 MB estimated total size
  • older entries evicted when limits are exceeded

Each cached snapshot stores:

  • server catalogStatus
  • folder list cache
  • loaded videos
  • pagination counters such as serverTotal, serverOffset, and serverExhausted

Eviction is opportunistic rather than scheduled:

  • expired entries are removed on read or later pruning
  • pruning runs after fresh snapshots are saved
  • browser storage pressure or manual site-data clearing can also remove IndexedDB data

🔍 Scan Behavior

The catalog scan is designed to stay incremental in cost even though it still walks each configured root.

Current behavior:

  • unchanged files reuse cached mtime + size signatures
  • periodic scans no longer sort the full path list before processing
  • deleted files are removed from the in-memory catalog and persisted index
  • deleted files also trigger cleanup of generated thumbnail and preview artifacts
  • index saves reuse cached file signature data instead of statting every file again

What the scan still does:

  • walks configured media roots to detect added, changed, and deleted files
  • queues preview generation when preview images are missing

What it does not do:

  • it does not checksum large media files during periodic scans
  • it does not regenerate thumbnails or metadata for unchanged files unless cached artifacts are missing

→ Trigger Rescan

Normal incremental rescan:

curl -s http://localhost:9245/rescan | python3 -m json.tool

Forced full rescan:

curl -s "http://localhost:9245/rescan?full=1" | python3 -m json.tool

→ Rescan UI

The Rescan button opens an action dialog instead of immediately starting an incremental scan.

Available actions:

  • Rescan: incremental scan for new or changed files
  • Full Scan: clears saved scan state and forces full metadata revalidation
  • Refresh Database: clears browser IndexedDB snapshots and reloads fresh catalog data

⛓️‍💥 Missing Mount Recovery

This feature is designed for the case some NAS might be configured with auto sleep mode, hence SMB mount might be auto ejected from some Operation system.

If mount_script is configured and a media request hits a missing folder, the server will:

  1. detect that the parent folder does not exist
  2. invoke the configured mount script once
  3. re-check the target path
  4. return Media folder is not mounted with HTTP 404 only if the folder is still unavailable

The frontend treats playback 404s as terminal for that attempt and shows a retry message instead of repeatedly hammering the server.


📄 Generated Files

These files are runtime-generated and should not be committed:

  • movies_config.json
  • movies_state.json
  • movies_auth_state.json
  • movies_catalog_index.json
  • cache/

🛠️ Troubleshooting

→ Debug Overlay

Enable debug_enabled in movies_config.json to keep a permanent debug overlay in the lower-right corner.

The panel reports:

  • whether the server is favoring direct playback or Plex
  • the configured direct-play audio whitelist
  • the current playback candidate and video ID
  • recent scan progress metrics

Inspect active config values with:

curl -s http://localhost:9245/api/config | python3 -m json.tool

→ UI Changes Do Not Appear

  • The app currently loads movies.js directly from index.html, so frontend changes take effect without rebuilding movies.min.js.
  • refresh the page normally first
  • if the JS bundle changed, confirm index.html references the expected bundle version

→ Direct Private Playback Fails

  • unlock private mode again so the movies_device_id cookie is refreshed

→ Plex Playback Fails But Direct Playback Works

  • verify the movies server host can reach plex.base_url
  • verify Plex is enabled in config
  • verify the configured token is valid

→ Direct Playback Fails But Plex Works

  • the container or codec is likely not safe for native browser playback on that device
  • keep Plex enabled for those files, or force the compatibility path through local transcode or Plex

→ Local Transcoding Does Not Work

  • verify ffmpeg and ffprobe are installed
  • verify on_demand_transcode is enabled
  • verify the source file is one of the currently supported containers: .mkv or .ts

📦 Release Versioning

Package versions are derived from Git tags.

  • TestPyPI/testing: use a development version such as 2026.3.26.dev1
  • PyPI prerelease: use a release candidate such as 2026.3.26rc1
  • PyPI stable: use a stable version such as 2026.3.26
  • Git tags should be v2026.3.26.dev1, v2026.3.26rc1, and v2026.3.26

©️ License

This project is released under the MIT License. Add a LICENSE file containing the MIT text when publishing or redistributing it.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

plex_cat_theatre-2026.3.29.tar.gz (264.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

plex_cat_theatre-2026.3.29-py3-none-any.whl (140.7 kB view details)

Uploaded Python 3

File details

Details for the file plex_cat_theatre-2026.3.29.tar.gz.

File metadata

  • Download URL: plex_cat_theatre-2026.3.29.tar.gz
  • Upload date:
  • Size: 264.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for plex_cat_theatre-2026.3.29.tar.gz
Algorithm Hash digest
SHA256 fb35798eb90cbeedfaafd7ec00e4758a984aba5c99ad48d78ff217bd720023f6
MD5 aa3e2760b8e034ea2ac7eb07bea582d8
BLAKE2b-256 54584e9f05e9345d260012088afddd825401758f4342916db88f75f7bbe6a3f7

See more details on using hashes here.

Provenance

The following attestation bundles were made for plex_cat_theatre-2026.3.29.tar.gz:

Publisher: python-publish.yml on daocha/plex-cat-theatre

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file plex_cat_theatre-2026.3.29-py3-none-any.whl.

File metadata

File hashes

Hashes for plex_cat_theatre-2026.3.29-py3-none-any.whl
Algorithm Hash digest
SHA256 79bc5726f3f9fffa9f09ce4bb7276cae7d90ac2a357ecf59a5ccdc376e8d3f72
MD5 f02d648b6be521ddf1aed26f0fd06f41
BLAKE2b-256 f030a565ab67becc41c3b12058a98e96238062e4165daf8ad50589c4ebe72058

See more details on using hashes here.

Provenance

The following attestation bundles were made for plex_cat_theatre-2026.3.29-py3-none-any.whl:

Publisher: python-publish.yml on daocha/plex-cat-theatre

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page