From 49de22d7885b06384cb3f4a4474ed09d84fa0c20 Mon Sep 17 00:00:00 2001 From: Blackwhitebear8 Date: Tue, 23 Dec 2025 14:29:27 +0100 Subject: [PATCH] Upload files to "/" --- .dockerignore | 15 +++ Dockerfile | 17 +++ app.py | 341 +++++++++++++++++++++++++++++++++++++++++++++++ build.sh | 56 ++++++++ requirements.txt | 3 + 5 files changed, 432 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 app.py create mode 100644 build.sh create mode 100644 requirements.txt diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f14a65b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +.git +.gitignore +.env +__pycache__ +*.pyc +*.pyo +venv/ +.venv/ +logs/ +data/ +custom_splash.jpg +docker-compose.yml +Dockerfile +build.sh +README.md \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f882898 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.9-slim + +WORKDIR /app + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt && \ + find /usr/local -name '__pycache__' -type d -exec rm -rf {} + + +COPY app.py . +RUN mkdir -p /data + +EXPOSE 5000 + +CMD ["python", "app.py"] \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..9a5bc73 --- /dev/null +++ b/app.py @@ -0,0 +1,341 @@ +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) \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..12aad53 --- /dev/null +++ b/build.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +set -e + +DOCKER_USER="blackwhitebear8" +REPO_NAME="emby-splash-wall" + +echo "Checking if you are logged in to Docker Hub..." +if ! docker system info | grep -q "Username"; then + echo "Not logged in. Starting 'docker login'..." + docker login +else + echo "Logged in as $(docker system info | grep "Username" | awk '{print $2}')" +fi + +echo "" +read -p "Enter the version/tag (e.g., 1.0.0 or latest): " VERSION + +if [ -z "$VERSION" ]; then + echo "Error: No version specified. Script aborted." + exit 1 +fi + +FULL_IMAGE_NAME="$DOCKER_USER/$REPO_NAME:$VERSION" + +if ! docker buildx inspect multiarch-builder > /dev/null 2>&1; then + echo "Creating new buildx builder 'multiarch-builder'..." + docker buildx create --use --name multiarch-builder +else + echo "Using existing builder 'multiarch-builder'..." + docker buildx use multiarch-builder +fi + +TAG_ARGS="-t $FULL_IMAGE_NAME" + +if [ "$VERSION" != "latest" ]; then + echo "Adding extra tag 'latest'..." + TAG_ARGS="$TAG_ARGS -t $DOCKER_USER/$REPO_NAME:latest" +fi + +echo "" +echo "Building and pushing for linux/amd64 and linux/arm64..." +echo "This may take a while..." + +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + $TAG_ARGS \ + --push \ + . + +echo "" +echo "=======================================================" +echo "Done! Your multi-arch image is now on Docker Hub:" +echo "$FULL_IMAGE_NAME" +echo "Architectures: AMD64 & ARM64" +echo "=======================================================" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4594a61 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask +requests +Pillow