import os import json import socket import hashlib from datetime import datetime from flask import ( Flask, render_template, send_from_directory, redirect, request, abort ) from flask_login import ( LoginManager, login_user, login_required, logout_user, UserMixin ) from werkzeug.utils import secure_filename # ------------------------------------------------- # Grundkonfiguration # ------------------------------------------------- BASE_DIR = os.path.dirname(os.path.abspath(__file__)) MEDIA_DIR = os.path.join(BASE_DIR, "media") CONFIG_FILE = os.path.join(BASE_DIR, "config.json") APP_VERSION = "2.0.0" UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"} app = Flask(__name__) app.secret_key = "CHANGE_THIS_SECRET" login_manager = LoginManager(app) login_manager.login_view = "login" # ------------------------------------------------- # Config Helpers # ------------------------------------------------- def load_config(): if not os.path.exists(CONFIG_FILE): return {"admin": {}, "screens": {}} with open(CONFIG_FILE) as f: return json.load(f) def save_config(cfg): with open(CONFIG_FILE, "w") as f: json.dump(cfg, f, indent=2) f.flush() os.fsync(f.fileno()) print("✅ config.json gespeichert") # ------------------------------------------------- # Auth / Benutzer # ------------------------------------------------- class Admin(UserMixin): id = 1 @login_manager.user_loader def load_user(user_id): return Admin() # ------------------------------------------------- # Login / Logout # ------------------------------------------------- @app.route("/login", methods=["GET", "POST"]) def login(): config = load_config() if request.method == "POST": if ( request.form.get("username") == config["admin"].get("username") and request.form.get("password") == config["admin"].get("password") ): login_user(Admin()) return redirect("/admin") return render_template("login.html") @app.route("/logout") def logout(): logout_user() return redirect("/login") # ------------------------------------------------- # Medien ausliefern # ------------------------------------------------- @app.route("/media//") def media(screen, filename): path = os.path.join(MEDIA_DIR, screen) return send_from_directory(path, filename) # ------------------------------------------------- # Player # ------------------------------------------------- @app.route("/player/") def player(screen): config = load_config() screens_cfg = config.setdefault("screens", {}) screen_cfg = screens_cfg.get(screen) if not screen_cfg: abort(404) folder = os.path.join(MEDIA_DIR, screen) playlist = screen_cfg.get("playlist", []) show_images = screen_cfg.get("show_images", True) show_videos = screen_cfg.get("show_videos", True) def allowed_file(name): if name.startswith("._"): return False ext = name.lower() if ext.endswith((".jpg", ".jpeg", ".png")): return show_images if ext.endswith(".mp4"): return show_videos return False ordered = [] # 1. Playlist-Reihenfolge for name in playlist: if os.path.exists(os.path.join(folder, name)) and allowed_file(name): ordered.append(name) # 2. Neue Dateien hintendran for f in sorted(os.listdir(folder)): if f not in ordered and allowed_file(f): ordered.append(f) return render_template( "player.html", screen=screen, files=ordered, interval=screen_cfg.get("interval", 10) ) # ------------------------------------------------- # Hash für automatisches Player-Reload # ------------------------------------------------- @app.route("/playlist//hash") def playlist_hash(screen): screen_dir = os.path.join(MEDIA_DIR, screen) if not os.path.isdir(screen_dir): return "" entries = [] for f in os.listdir(screen_dir): if f.startswith("._"): continue p = os.path.join(screen_dir, f) try: entries.append(f"{f}:{int(os.path.getmtime(p))}") except OSError: pass h = hashlib.md5("|".join(sorted(entries)).encode()).hexdigest() return h # ------------------------------------------------- # Admin Dashboard # ------------------------------------------------- @app.route("/admin") @login_required def admin(): config = load_config() screens_cfg = config.setdefault("screens", {}) screens = {} media_files = {} screen_status = {} for screen in sorted(os.listdir(MEDIA_DIR)): screen_dir = os.path.join(MEDIA_DIR, screen) if not os.path.isdir(screen_dir): continue screens_cfg.setdefault(screen, { "interval": 10, "show_images": True, "show_videos": True, "playlist": [] }) screens[screen] = screens_cfg[screen] files = [] for f in sorted(os.listdir(screen_dir)): if f.startswith("._"): continue ext = os.path.splitext(f)[1].lower() ftype = "video" if ext == ".mp4" else "image" size = os.path.getsize(os.path.join(screen_dir, f)) // 1024 files.append({ "name": f, "type": ftype, "size": size }) media_files[screen] = files screen_status[screen] = "active" if files else "empty" save_config(config) return render_template( "admin.html", screens=screens, media_files=media_files, screen_status=screen_status, hostname=socket.gethostname(), version=APP_VERSION, year=datetime.now().year ) # ------------------------------------------------- # Admin: Screen-Einstellungen speichern # ------------------------------------------------- @app.route("/admin/update/", methods=["POST"]) @login_required def update_screen(screen): config = load_config() cfg = config["screens"][screen] cfg["interval"] = int(request.form.get("interval", 10)) cfg["show_images"] = "show_images" in request.form cfg["show_videos"] = "show_videos" in request.form save_config(config) return redirect("/admin") # ------------------------------------------------- # Admin: Upload # ------------------------------------------------- @app.route("/admin/upload/", methods=["POST"]) @login_required def upload(screen): file = request.files.get("file") if not file: return redirect("/admin") ext = os.path.splitext(file.filename)[1].lower() if ext not in UPLOAD_EXTENSIONS: return "Ungültiger Dateityp", 400 filename = secure_filename(file.filename) file.save(os.path.join(MEDIA_DIR, screen, filename)) return redirect("/admin") # ------------------------------------------------- # Admin: Datei löschen # ------------------------------------------------- @app.route("/admin/delete//", methods=["POST"]) @login_required def delete_file(screen, filename): path = os.path.join(MEDIA_DIR, screen, filename) if os.path.exists(path): os.remove(path) return redirect("/admin") # ------------------------------------------------- # Admin: Playlist speichern # ------------------------------------------------- @app.route("/admin/playlist/", methods=["POST"]) @login_required def update_playlist(screen): config = load_config() cfg = config["screens"][screen] data = request.get_json() cfg["playlist"] = data.get("playlist", []) save_config(config) return "", 204 # ------------------------------------------------- # Main # ------------------------------------------------- if __name__ == "__main__": app.run(debug=True, host="0.0.0.0", port=5005)