First Commit

This commit is contained in:
proitlab committed 2026-07-29 09:06:11 +07:00
1 parent a9a0a0bf8c
commit 676a52199c
8 files changed
+177 -2

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
MP4_VIEWER_DIR=/path/to/your/mp4/folder
FLASK_HOST=127.0.0.1
FLASK_PORT=5000
+21
View File
@@ -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
-2
View File
@@ -1,2 +0,0 @@
# mp4-viewer
+51
View File
@@ -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 |
|----|------|-------|-----|
+58
View File
@@ -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)
+2
View File
@@ -0,0 +1,2 @@
flask
python-dotenv
+23
View File
@@ -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>
+19
View File
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>{{ filename|e }} &mdash; 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="/">&larr; Back to listing</a></p>
<h2>{{ filename|e }}</h2>
<video controls autoplay>
<source src="/video/{{ path|e }}" type="video/mp4">
</video>
</body>
</html>