First Commit
This commit is contained in:
1 parent
a9a0a0bf8c
commit
676a52199c
8 files changed
+177
-2
No files matched your search
@@ -0,0 +1,3 @@
|
||||
MP4_VIEWER_DIR=/path/to/your/mp4/folder
|
||||
FLASK_HOST=127.0.0.1
|
||||
FLASK_PORT=5000
|
||||
@@ -0,0 +1,21 @@
|
||||
# AGENTS.md — mp4-viewer
|
||||
|
||||
## Environment
|
||||
- Python 3.14 virtual environment at `venv/`
|
||||
- Activate: `source venv/bin/activate`
|
||||
|
||||
## Commands
|
||||
- Install deps: `source venv/bin/activate && pip install -r requirements.txt`
|
||||
- Run: `python app.py` (reads `.env` via python-dotenv; override with env vars)
|
||||
- Test listing: `curl http://127.0.0.1:5000/`
|
||||
- Test video: `curl -I http://127.0.0.1:5000/video/<filename>`
|
||||
|
||||
## Architecture
|
||||
- `app.py` — Flask entrypoint, routes, path validation
|
||||
- `templates/` — Jinja2 HTML templates (`index.html`, `play.html`)
|
||||
- `.env` — default config (gitignored); `.env.example` checked in
|
||||
- `MP4_VIEWER_DIR` — single source dir for MP4s; all served paths validated with `is_relative_to`
|
||||
|
||||
## Notes
|
||||
- `venv/` is gitignored via its own `.gitignore`; keep it out of the repo root
|
||||
- `.env` is gitignored; copy `.env.example` to `.env` and edit
|
||||
@@ -0,0 +1,51 @@
|
||||
# SPEC — mp4-viewer
|
||||
|
||||
## §G — Goal
|
||||
|
||||
Web app to browse & play MP4 files from a configurable directory in-browser.
|
||||
|
||||
## §C — Constraints
|
||||
|
||||
- Python 3.14, venv `venv/`
|
||||
- Flask + python-dotenv
|
||||
- Jinja2 templates in `templates/` directory
|
||||
- No JS build toolchain — keep it simple HTML + minimal inline JS
|
||||
- MP4 playback via native browser `<video>` element
|
||||
- No auth — local/single-user tool
|
||||
|
||||
## §I — Interfaces
|
||||
|
||||
- file: `.env` — config via python-dotenv (see `.env.example`)
|
||||
- env: `MP4_VIEWER_DIR` — path to directory to serve MP4s from (default `.`)
|
||||
- env: `FLASK_HOST` ? — bind host (default `127.0.0.1`)
|
||||
- env: `FLASK_PORT` ? — bind port (default `5000`)
|
||||
- web: `GET /` → HTML listing all `.mp4` files in watched dir
|
||||
- web: `GET /video/<path>` → serve `.mp4` file bytes
|
||||
- web: `GET /play/<path>` → HTML page with `<video>` player for one file
|
||||
- cmd: `python app.py` — start dev server
|
||||
|
||||
## §V — Invariants
|
||||
|
||||
- V1: `MP4_VIEWER_DIR` must exist & be a directory at startup → else abort
|
||||
- V2: only files ending `.mp4` (case-insensitive) shown in listing
|
||||
- V3: served video paths must resolve within `MP4_VIEWER_DIR` (no traversal)
|
||||
- V4: listing sorts files alphabetically
|
||||
- V5: non-`.mp4` files & subdirectories not listed
|
||||
- V6: HTML escaped in filenames to prevent XSS
|
||||
|
||||
## §T — Tasks
|
||||
|
||||
| id | status | task | cites |
|
||||
|----|--------|------|-------|
|
||||
| T1 | x | init Flask project: `requirements.txt`, activate venv, install Flask | C.venv |
|
||||
| T2 | x | implement env loading: `MP4_VIEWER_DIR`, `FLASK_HOST`, `FLASK_PORT` | I.env |
|
||||
| T3 | x | implement `GET /` route — list `.mp4` files, link to player | V2,V4,V5,V6 |
|
||||
| T4 | x | implement `GET /play/<path>` route — `<video>` page | |
|
||||
| T5 | x | implement `GET /video/<path>` route — serve raw file bytes | V3 |
|
||||
| T6 | x | startup checks: dir exists, readable (V1) | V1 |
|
||||
| T7 | x | run & verify: navigate, play an MP4 in browser | T1–T6 |
|
||||
|
||||
## §B — Bugs
|
||||
|
||||
| id | date | cause | fix |
|
||||
|----|------|-------|-----|
|
||||
@@ -0,0 +1,58 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from flask import Flask, abort, render_template, send_file
|
||||
|
||||
load_dotenv()
|
||||
|
||||
BASE_DIR = Path(os.environ.get("MP4_VIEWER_DIR", ".")).resolve()
|
||||
FLASK_HOST = os.environ.get("FLASK_HOST", "127.0.0.1")
|
||||
FLASK_PORT = int(os.environ.get("FLASK_PORT", "5000"))
|
||||
|
||||
if not BASE_DIR.exists() or not BASE_DIR.is_dir():
|
||||
print(f"Error: MP4_VIEWER_DIR does not exist or is not a directory: {BASE_DIR}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _validate_path(rel_path):
|
||||
full = BASE_DIR / rel_path
|
||||
try:
|
||||
full = full.resolve(strict=False)
|
||||
except (ValueError, OSError):
|
||||
abort(400)
|
||||
if not full.is_relative_to(BASE_DIR):
|
||||
abort(403)
|
||||
if not full.exists() or not full.is_file():
|
||||
abort(404)
|
||||
return full
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
files = sorted(
|
||||
f.name
|
||||
for f in BASE_DIR.iterdir()
|
||||
if f.is_file() and f.suffix.lower() == ".mp4"
|
||||
)
|
||||
return render_template("index.html", files=files, dirname=str(BASE_DIR))
|
||||
|
||||
|
||||
@app.route("/play/<path:path>")
|
||||
def play(path):
|
||||
_validate_path(path)
|
||||
return render_template("play.html", filename=Path(path).name, path=path)
|
||||
|
||||
|
||||
@app.route("/video/<path:path>")
|
||||
def serve_video(path):
|
||||
full = _validate_path(path)
|
||||
return send_file(full, mimetype="video/mp4")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host=FLASK_HOST, port=FLASK_PORT, debug=True)
|
||||
@@ -0,0 +1,2 @@
|
||||
flask
|
||||
python-dotenv
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>MP4 Viewer</title>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 2em auto; padding: 0 1em; }
|
||||
ul { list-style: none; padding: 0; }
|
||||
li { padding: 0.5em 0; border-bottom: 1px solid #eee; }
|
||||
a { color: #2563eb; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>MP4 Viewer</h1>
|
||||
<p>{{ files|length }} file(s) in {{ dirname }}</p>
|
||||
<ul>
|
||||
{% for f in files %}
|
||||
<li><a href="/play/{{ f|e }}">{{ f|e }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ filename|e }} — MP4 Viewer</title>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; max-width: 900px; margin: 2em auto; padding: 0 1em; }
|
||||
video { width: 100%; max-height: 70vh; background: #000; }
|
||||
a { color: #2563eb; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p><a href="/">← Back to listing</a></p>
|
||||
<h2>{{ filename|e }}</h2>
|
||||
<video controls autoplay>
|
||||
<source src="/video/{{ path|e }}" type="video/mp4">
|
||||
</video>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user