Files
2026-07-29 09:17:15 +07:00

98 lines
2.7 KiB
Python

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/<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)
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)