import requests from PIL import Image, ImageDraw from io import BytesIO import random import math import os import threading import time from flask import Flask, send_file, render_template_string, request # ============================================================================== # CONFIGURATIE (UIT ENV) # ============================================================================== app = Flask(__name__) # --- MAP CONFIGURATIE (VOOR DOCKER VOLUME) --- # We slaan de image op in /data, deze map moet je mappen in Unraid! DATA_DIR = os.getenv('DATA_DIR', '/data') OUTPUT_FILENAME = os.path.join(DATA_DIR, "custom_splash.jpg") # --- SERVER INSTELLINGEN --- PORT = 5000 BIND_ADDRESS = '0.0.0.0' # --- EMBY / JELLYFIN CONNECTIE --- # Haal waarden uit Unraid settings, of gebruik standaardwaarden als fallback EMBY_URL = os.getenv('EMBY_URL', "http://YOUR-IP:8096").rstrip('/') API_KEY = os.getenv('API_KEY', "") SERVER_ACCESS_KEY = os.getenv('SERVER_ACCESS_KEY', "geheim123") # --- SPLASH SCREEN CONFIG --- SPLASH_REGEN_INTERVAL_HOURS = int(os.getenv('SPLASH_REGEN_INTERVAL_HOURS', 24)) OUTPUT_WIDTH = int(os.getenv('OUTPUT_WIDTH', 3840)) OUTPUT_HEIGHT = int(os.getenv('OUTPUT_HEIGHT', 2160)) ROTATION_ANGLE = int(os.getenv('ROTATION_ANGLE', -12)) POSTER_SCALE = int(os.getenv('POSTER_SCALE', 8)) BORDER_SIZE = int(os.getenv('BORDER_SIZE', 15)) CORNER_RADIUS = int(os.getenv('CORNER_RADIUS', 15)) USE_BACKDROPS = os.getenv('USE_BACKDROPS', 'True').lower() == 'true' # --- LIVE WALL CONFIG --- LIVE_WALL_REFRESH_MINUTES = int(os.getenv('LIVE_WALL_REFRESH_MINUTES', 15)) LIVE_WALL_ROWS = int(os.getenv('LIVE_WALL_ROWS', 10)) LIVE_WALL_ROW_HEIGHT = int(os.getenv('LIVE_WALL_ROW_HEIGHT', 200)) LIVE_WALL_SPEED = int(os.getenv('LIVE_WALL_SPEED', 200)) LIVE_WALL_MAX_ITEMS = int(os.getenv('LIVE_WALL_MAX_ITEMS', 1000)) # ============================================================================== # EINDE CONFIGURATIE # ============================================================================== # Zorg dat de output map bestaat if not os.path.exists(DATA_DIR): os.makedirs(DATA_DIR) # ------------------------------------------------------------------------------ # HELPER FUNCTIES (GENERATOR) # ------------------------------------------------------------------------------ def get_emby_items(limit): headers = {"X-Emby-Token": API_KEY} params = { "IncludeItemTypes": "Movie,Series", "Recursive": "true", "ImageTypes": "Backdrop" if USE_BACKDROPS else "Primary", "SortBy": "Random", "Limit": limit, "Fields": "Id,Name,Width,Height" } try: url = f"{EMBY_URL}/Items" response = requests.get(url, headers=headers, params=params) response.raise_for_status() data = response.json() return [item for item in data.get("Items", []) if item.get('Width') and item.get('Height')] except Exception as e: print(f"[Generator] Fout bij ophalen items van {EMBY_URL}: {e}") return [] def download_image(item_id, target_height): image_type = "Backdrop" if USE_BACKDROPS else "Primary" url = f"{EMBY_URL}/Items/{item_id}/Images/{image_type}/0?Height={int(target_height * 1.5)}&Quality=90" try: response = requests.get(url, stream=True) if response.status_code == 200: return Image.open(BytesIO(response.content)) except: return None return None def create_splash(): print(f"[Generator] Start genereren splash screen ({OUTPUT_WIDTH}x{OUTPUT_HEIGHT})...") try: angle_rad = math.radians(abs(ROTATION_ANGLE)) needed_width = int(OUTPUT_WIDTH * math.cos(angle_rad) + OUTPUT_HEIGHT * math.sin(angle_rad)) needed_height = int(OUTPUT_WIDTH * math.sin(angle_rad) + OUTPUT_HEIGHT * math.cos(angle_rad)) temp_width = int(needed_width * 1.2) temp_height = int(needed_height * 1.2) row_height = int(OUTPUT_HEIGHT / POSTER_SCALE) avg_aspect = 1.77 if USE_BACKDROPS else 0.67 avg_item_width = int(row_height * avg_aspect) items_per_row = int(temp_width / (avg_item_width + BORDER_SIZE)) + 3 rows_needed = int(temp_height / (row_height + BORDER_SIZE)) + 3 total_items = items_per_row * rows_needed * 2 items = get_emby_items(total_items) if not items: print("[Generator] Geen items gevonden. Check URL en API Key.") return large_canvas = Image.new('RGB', (temp_width, temp_height), color=(0, 0, 0)) current_y = -int(row_height * 2) item_idx = 0 images_placed = 0 while current_y < temp_height: current_x = -int(avg_item_width * 2) while current_x < temp_width: if item_idx >= len(items): item_idx = 0 item = items[item_idx] item_idx += 1 aspect = item['Width'] / item['Height'] img_width = int(row_height * aspect) img = download_image(item['Id'], row_height) if img: img = img.resize((img_width, row_height), Image.LANCZOS) if CORNER_RADIUS > 0: mask = Image.new("L", img.size, 0) draw = ImageDraw.Draw(mask) draw.rounded_rectangle([(0, 0), (img.width - 1, img.height - 1)], radius=CORNER_RADIUS, fill=255) large_canvas.paste(img, (current_x, current_y), mask=mask) else: large_canvas.paste(img, (current_x, current_y)) current_x += img_width + BORDER_SIZE images_placed += 1 current_y += row_height + BORDER_SIZE print(f"[Generator] Canvas gevuld. Draaien en bijsnijden...") rotated_canvas = large_canvas.rotate(ROTATION_ANGLE, resample=Image.BICUBIC, expand=True) center_x = rotated_canvas.width // 2 center_y = rotated_canvas.height // 2 left = center_x - (OUTPUT_WIDTH // 2) top = center_y - (OUTPUT_HEIGHT // 2) right = center_x + (OUTPUT_WIDTH // 2) bottom = center_y + (OUTPUT_HEIGHT // 2) final_image = rotated_canvas.crop((left, top, right, bottom)) final_image.save(OUTPUT_FILENAME, quality=95, optimize=True) print(f"[Generator] Succes! Opgeslagen als '{OUTPUT_FILENAME}'") except Exception as e: print(f"[Generator] Kritieke fout: {e}") def background_scheduler(): while True: if not os.path.exists(OUTPUT_FILENAME): create_splash() time.sleep(SPLASH_REGEN_INTERVAL_HOURS * 3600) create_splash() # ------------------------------------------------------------------------------ # WEBSERVER ROUTES # ------------------------------------------------------------------------------ @app.route('/splashscreen') def serve_splash(): if request.args.get('key') != SERVER_ACCESS_KEY: return "Toegang geweigerd.", 401 if os.path.exists(OUTPUT_FILENAME): return send_file(OUTPUT_FILENAME, mimetype='image/jpeg') else: return "Splash screen wordt gegenereerd...", 503 @app.route('/') def index(): if request.args.get('key') != SERVER_ACCESS_KEY: return "Toegang geweigerd.", 401 html_content = """ Jellyfin Live Wall
Laden...
--:--
""" return render_template_string(html_content, emby_url=EMBY_URL, api_key=API_KEY, rows=LIVE_WALL_ROWS, refresh_min=LIVE_WALL_REFRESH_MINUTES, img_type="Backdrop" if USE_BACKDROPS else "Primary", card_radius=CORNER_RADIUS, gap_size=BORDER_SIZE, row_height=LIVE_WALL_ROW_HEIGHT, rotation=ROTATION_ANGLE, speed=LIVE_WALL_SPEED, max_items=LIVE_WALL_MAX_ITEMS ) if __name__ == "__main__": print("----------------------------------------------------------------") print(" JELLYFIN DASHBOARD SERVER GESTART") print(f" Live Wall op: http://localhost:{PORT}/?key={SERVER_ACCESS_KEY}") print("----------------------------------------------------------------") regen_thread = threading.Thread(target=background_scheduler, daemon=True) regen_thread.start() app.run(host=BIND_ADDRESS, port=PORT, debug=False)