98 lines
2.7 KiB
Python
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)
|