import os import re import sys from pathlib import Path from dotenv import load_dotenv from flask import Flask, Response, abort, render_template, request, 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/") def play(path): _validate_path(path) return render_template("play.html", filename=Path(path).name, path=path) @app.route("/video/") def serve_video(path): full = _validate_path(path) range_header = request.headers.get("Range") if not range_header: return send_file(full, mimetype="video/mp4", conditional=True) file_size = full.stat().st_size match = re.match(r"bytes=(\d+)-(\d*)", range_header) if not match: abort(416) start = int(match.group(1)) end_str = match.group(2) end = int(end_str) if end_str else file_size - 1 if start >= file_size or end >= file_size or start > end: abort(416) content_length = end - start + 1 return _stream_range(full, start, end, content_length, file_size) def _stream_range(path, start, end, content_length, file_size): def generate(): with open(path, "rb") as f: f.seek(start) remaining = content_length while remaining > 0: chunk_size = min(65536, remaining) data = f.read(chunk_size) if not data: break remaining -= len(data) yield data headers = { "Content-Range": f"bytes {start}-{end}/{file_size}", "Accept-Ranges": "bytes", "Content-Length": str(content_length), "Content-Type": "video/mp4", } return Response(generate(), status=206, headers=headers) if __name__ == "__main__": app.run(host=FLASK_HOST, port=FLASK_PORT, debug=True, threaded=True)