diff --git a/Dockerfile b/Dockerfile index 196c0d5..af3bfb0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,17 @@ FROM python:3.11-slim WORKDIR /app -COPY . . + +# 1. Install System Deps (ffmpeg) FIRST +# These rarely change, so Docker will cache this layer forever. RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/* -RUN pip install --no-cache-dir requests flask -RUN mkdir -p /app/data -EXPOSE 5000 -CMD ["python", "ta_symlink.py"] + +# 2. Install Python Deps SECOND +# Only re-runs if requirements.txt changes +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# 3. Copy Code LAST +# Now, if you change code, only this fast step re-runs. +COPY . . + +CMD ["python", "ta_symlink.py"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..30692b7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask +requests diff --git a/ta_symlink.py b/ta_symlink.py index a122db9..c41c352 100644 --- a/ta_symlink.py +++ b/ta_symlink.py @@ -6,7 +6,8 @@ import sys import threading import time import ipaddress -from flask import Flask, jsonify, render_template, request, abort +from functools import wraps +from flask import Flask, jsonify, render_template, request, abort, Response # Load config from environment variables API_URL = os.getenv("API_URL", "http://localhost:8457/api") @@ -14,6 +15,8 @@ VIDEO_URL = os.getenv("VIDEO_URL", "http://localhost:8457/video/") API_TOKEN = os.getenv("API_TOKEN", "") SCAN_INTERVAL = int(os.getenv("SCAN_INTERVAL", 60)) # Default 60 minutes ALLOWED_IPS = [ip.strip() for ip in os.getenv("ALLOWED_IPS", "127.0.0.1").split(",")] +UI_USERNAME = os.getenv("UI_USERNAME", "admin") +UI_PASSWORD = os.getenv("UI_PASSWORD", "password") SOURCE_DIR = Path("/app/source") TARGET_DIR = Path("/app/target") HEADERS = {"Authorization": f"Token {API_TOKEN}"} @@ -557,11 +560,33 @@ def limit_remote_addr(): log(f"⛔ Invalid IP format: {client_ip}, Error: {e}") abort(403) +def check_auth(username, password): + """Checks whether a username/password combination is valid.""" + return username == UI_USERNAME and password == UI_PASSWORD + +def authenticate(): + """Sends a 401 response that enables basic auth""" + return Response( + 'Could not verify your access level for that URL.\n' + 'You have to login with proper credentials', 401, + {'WWW-Authenticate': 'Basic realm="Login Required"'}) + +def requires_auth(f): + @wraps(f) + def decorated(*args, **kwargs): + auth = request.authorization + if not auth or not check_auth(auth.username, auth.password): + return authenticate() + return f(*args, **kwargs) + return decorated + @app.route("/") +@requires_auth def index(): return render_template('dashboard.html') @app.route("/api/status") +@requires_auth def api_status(): with get_db() as conn: # Get all videos from DB @@ -589,6 +614,7 @@ def api_status(): }) @app.route("/api/logs") +@requires_auth def api_logs(): start = request.args.get('start', 0, type=int) with log_lock: @@ -598,26 +624,31 @@ def api_logs(): }) @app.route("/api/scan", methods=["POST"]) +@requires_auth def api_scan(): # Run in background to avoid blocking threading.Thread(target=process_videos).start() return jsonify({"status": "started"}) @app.route("/api/cleanup", methods=["POST"]) +@requires_auth def api_cleanup(): threading.Thread(target=cleanup_old_folders).start() return jsonify({"status": "started"}) @app.route("/api/check-orphans", methods=["POST"]) +@requires_auth def api_check_orphans(): orphaned = check_orphaned_links() return jsonify({"status": "complete", "orphaned": orphaned, "count": len(orphaned)}) @app.route("/transcode") +@requires_auth def transcode_page(): return render_template('transcoding.html') @app.route("/api/transcode/videos") +@requires_auth def api_transcode_videos(): """Get all videos that need transcoding.""" page = request.args.get('page', 1, type=int) @@ -650,6 +681,7 @@ def api_transcode_videos(): }) @app.route("/api/transcode/start", methods=["POST"]) +@requires_auth def api_transcode_start(): """Start transcoding a video.""" data = request.get_json() @@ -669,6 +701,7 @@ def api_transcode_start(): return jsonify({"message": "Transcode started", "encoder": encoder}) @app.route("/api/transcode/logs") +@requires_auth def api_transcode_logs(): """Get transcode logs.""" start = request.args.get('start', 0, type=int)