commit 5de45a37bda3b173c1e2f71ea02249a8f4f15464 Author: Florian Heinz Date: Mon Sep 7 12:26:43 2026 +0200 first commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..981fdaf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +.gitignore +*.zip diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd63111 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Local configuration and secrets +.env +.env.* +env.sh +secret + +# Build artifacts and local tooling +*.zip +.idea/ +.vscode/ +*~ +.DS_Store diff --git a/CloudronManifest.json b/CloudronManifest.json new file mode 100644 index 0000000..53eadac --- /dev/null +++ b/CloudronManifest.json @@ -0,0 +1,17 @@ + +{ + "id": "io.hnz.floppy", + "title": "Floppy", + "description": "Self-hosted media tracker and Trakt alternative.", + "tagline": "Self-hosted media tracking", + "version": "0.1.4", + "healthCheckPath": "/", + "httpPort": 8000, + "addons": { + "localstorage": {}, + "postgresql": {}, + "redis": {} + }, + "memoryLimit": 2147483648, + "manifestVersion": 2 +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d72187b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,45 @@ +FROM ghcr.io/dannyvfilms/floppy:release + +USER root + +RUN python - <<'PY' +from pathlib import Path +import sys + +path = Path("/floppy/app/media_list_views.py") +text = path.read_text() + +old = ''' else: + # Default: most recently updated + show_trackers = show_trackers.order_by("-updated_at") +''' + +new = ''' else: + # Default: most recently completed/listened podcast show + show_trackers = show_trackers.annotate( + last_completed=Max("show__podcast_entries__end_date") + ).order_by( + F("last_completed").desc(nulls_last=True), + "show__title", + ) +''' + +if new in text: + print("Podcast sort patch already present upstream") +elif old in text: + path.write_text(text.replace(old, new, 1)) + print("Applied podcast sort patch") +else: + print("ERROR: Podcast sort patch no longer matches upstream Floppy", file=sys.stderr) + sys.exit(1) +PY + +COPY start.sh /app/code/start.sh +COPY supervisord-cloudron.conf /app/code/supervisord-cloudron.conf +COPY nginx-cloudron.conf /app/code/nginx-cloudron.conf + +RUN chmod +x /app/code/start.sh + +EXPOSE 8000 + +CMD ["/app/code/start.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..c7b70b4 --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +# Floppy for Cloudron + +Unofficial Cloudron package for [Floppy](https://github.com/dannyvfilms/Floppy), using Cloudron-managed PostgreSQL and Redis. + +## Why this package is different from Floppy's Docker Compose + +Floppy's upstream Docker image expects a normal writable Docker filesystem and its entrypoint changes ownership on several image paths. Cloudron intentionally runs app images read-only except for `/tmp`, `/run`, and `/app/data`. This package therefore keeps the upstream application image but replaces the runtime entrypoint, Supervisor configuration, and Nginx runtime paths. + +The package uses: + +- Cloudron PostgreSQL -> Floppy `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` +- Cloudron Redis -> Floppy `REDIS_URL`, cache and Celery URLs +- `/app/data` -> Floppy secret, user configuration, logs and backups +- Cloudron HTTPS proxy -> Floppy `URLS`, `ALLOWED_HOSTS`, forwarded-proto handling + +## Install + +Install the Cloudron CLI on your workstation, not on the Cloudron server: + +```bash +sudo npm install -g cloudron +cloudron login my.example.com +``` + +Unzip this package and enter the directory: + +```bash +cd floppy-cloudron +cloudron install +``` + +When prompted, choose the desired location, for example `floppy` for `https://floppy.example.com`. + +Cloudron can build custom app source directly on the server, so no local Docker registry is required for this workflow. + +## First run + +On first start the package creates: + +```text +/app/data/secret +/app/data/env.sh +``` + +`secret` is generated automatically and persists across updates/backups. + +The initial `env.sh` contains: + +```bash +REGISTRATION=True +TMDB_API= +TZ=Europe/Berlin +``` + +1. Open Floppy and create your user account. +2. Add your TMDb API key to `/app/data/env.sh`. +3. Change `REGISTRATION=True` to `REGISTRATION=False`. +4. Restart the app from the Cloudron dashboard. + +You can edit `/app/data/env.sh` with the Cloudron File Manager or via: + +```bash +cloudron exec +vi /app/data/env.sh +``` + +For hnz.io, TMDb is the important metadata provider, so set at least: + +```bash +TMDB_API=YOUR_KEY +``` + +Optional integration keys can be added to `env.sh`, including Trakt, TVDB, MAL, IGDB, Steam, BoardGameGeek, Hardcover, ComicVine and Last.fm. + +## Importing your old Trakt data + +If you still have working Trakt API credentials for a private-profile OAuth import, add: + +```bash +TRAKT_API=... +TRAKT_API_SECRET=... +``` + +Floppy also supports other import paths from its UI. After migration, hnz.io can use Floppy as the personal-data source while continuing to use TMDb for metadata. + +## Cloudron resources + +The manifest requests 2 GiB RAM. Floppy automatically adapts worker counts to available resources. This package intentionally starts one combined Celery worker by default, which is the safer layout for a personal Cloudron instance. + +If you later give the app more memory and want Floppy's split worker layout, adjust the Supervisor config/package rather than setting arbitrary worker variables in `env.sh`. + +## Updating Floppy + +This package currently uses: + +```dockerfile +FROM ghcr.io/dannyvfilms/floppy:release +``` + +To refresh to the current stable upstream image, rebuild/update the Cloudron package: + +```bash +cloudron update +``` + +For fully reproducible production deployments, replace `:release` in the Dockerfile with a specific upstream `:vX.Y.Z` tag after testing that release, then bump the Cloudron package `version`. + +## Logs + +Use: + +```bash +cloudron logs -f +``` + +The application processes log primarily to stdout/stderr. Floppy's own file log/backups are directed to `/app/data/logs` and `/app/data/backups`. + +## Troubleshooting + +If the app does not become healthy: + +```bash +cloudron logs -f +``` + +Verify that migrations completed and that Gunicorn is listening on `127.0.0.1:8001` behind Nginx. + +For a shell: + +```bash +cloudron exec +``` + +Useful checks inside the app: + +```bash +env | grep '^CLOUDRON_\|^DB_\|^REDIS_' +cd /floppy +python manage.py check +``` + +Do not replace the Cloudron PostgreSQL variables with a `DATABASE_URL`: Floppy selects PostgreSQL only when `DB_HOST` is set. + +## Status + +This is an unofficial compatibility package assembled for a custom Cloudron installation. It follows the current Floppy `release` container contract and current Cloudron packaging/addon model, but it has not been executed against your actual Cloudron instance from this environment. Treat the first install as a deployment test and inspect the logs before moving production watch-history data into it. diff --git a/nginx-cloudron.conf b/nginx-cloudron.conf new file mode 100644 index 0000000..ef04f21 --- /dev/null +++ b/nginx-cloudron.conf @@ -0,0 +1,62 @@ +user abc; +worker_processes 1; +pid /run/floppy/nginx.pid; +error_log /dev/stderr warn; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + access_log /dev/stdout combined; + + client_body_temp_path /run/floppy/nginx/client_body; + proxy_temp_path /run/floppy/nginx/proxy; + fastcgi_temp_path /run/floppy/nginx/fastcgi; + uwsgi_temp_path /run/floppy/nginx/uwsgi; + scgi_temp_path /run/floppy/nginx/scgi; + + sendfile on; + tcp_nodelay on; + keepalive_timeout 65; + client_max_body_size 20M; + client_body_buffer_size 256k; + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript; + + upstream app_server { + server 127.0.0.1:8001; + } + + server { + listen 8000; + server_name _; + + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + + location / { + proxy_pass http://app_server; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + } + + location /health/ { + access_log off; + proxy_pass http://app_server; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + } + + location /static/ { + alias /floppy/staticfiles/; + expires 30d; + add_header Cache-Control "public"; + } + } +} diff --git a/podcast-sort.patch b/podcast-sort.patch new file mode 100644 index 0000000..cceca31 --- /dev/null +++ b/podcast-sort.patch @@ -0,0 +1,14 @@ +--- a/app/media_list_views.py ++++ b/app/media_list_views.py +@@ +- else: +- # Default: most recently updated +- show_trackers = show_trackers.order_by("-updated_at") ++ else: ++ # Default: most recently completed/listened podcast show ++ show_trackers = show_trackers.annotate( ++ last_completed=Max("show__podcast_entries__end_date") ++ ).order_by( ++ F("last_completed").desc(nulls_last=True), ++ "show__title", ++ ) diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..61cc1a5 --- /dev/null +++ b/start.sh @@ -0,0 +1,150 @@ +#!/bin/sh +set -eu + +mkdir -p \ + /app/data/logs \ + /app/data/backups \ + /run/floppy/nginx/client_body \ + /run/floppy/nginx/proxy \ + /run/floppy/nginx/fastcgi \ + /run/floppy/nginx/uwsgi \ + /run/floppy/nginx/scgi + +if [ ! -f /app/data/secret ]; then + python - <<'PY' > /app/data/secret +import secrets +print(secrets.token_urlsafe(64)) +PY + chmod 600 /app/data/secret +fi + +if [ ! -f /app/data/env.sh ]; then + cat > /app/data/env.sh <<'ENVEOF' +# Floppy settings managed by you. +# Edit this file in Cloudron's File Manager and restart the app. + +REGISTRATION=True +ADMIN_ENABLED=False +DEMO_ACCOUNT_ENABLED=False + +TMDB_API= +TZ=Europe/Berlin + +# Optional examples: +# TRAKT_API= +# TRAKT_API_SECRET= +# TVDB_API_KEY= +# TVDB_PIN= +# MAL_API= +# IGDB_ID= +# IGDB_SECRET= +# STEAM_API_KEY= +# BGG_API_TOKEN= +# HARDCOVER_API= +# COMICVINE_API= +# LASTFM_API_KEY= +ENVEOF +fi + +# Export all variables defined in env.sh so Django, Celery and Supervisor +# receive them as real environment variables. +set -a +# shellcheck disable=SC1091 +. /app/data/env.sh +set +a + +# ---------------------------------------------------------------------- +# Cloudron managed services +# ---------------------------------------------------------------------- + +export SECRET="$(cat /app/data/secret)" + +export DB_HOST="$CLOUDRON_POSTGRESQL_HOST" +export DB_PORT="$CLOUDRON_POSTGRESQL_PORT" +export DB_NAME="$CLOUDRON_POSTGRESQL_DATABASE" +export DB_USER="$CLOUDRON_POSTGRESQL_USERNAME" +export DB_PASSWORD="$CLOUDRON_POSTGRESQL_PASSWORD" + +export REDIS_URL="$CLOUDRON_REDIS_URL" +export REDIS_CACHE_URL="$CLOUDRON_REDIS_URL" +export CELERY_BROKER_URL="$CLOUDRON_REDIS_URL" +export CELERY_RESULT_BACKEND="$CLOUDRON_REDIS_URL" + +# Cloudron owns the Redis instance. Floppy must not try to manage +# Redis memory limits globally. +export REDIS_ADMIN_URL="$CLOUDRON_REDIS_URL" +export FLOPPY_REDIS_MAXMEMORY=0 + +# ---------------------------------------------------------------------- +# Cloudron reverse proxy +# ---------------------------------------------------------------------- + +export URLS="$CLOUDRON_APP_ORIGIN" +export ALLOWED_HOSTS="$CLOUDRON_APP_DOMAIN" + +export USE_X_FORWARDED=True +export USE_X_FORWARDED_PROTO=True + +# ---------------------------------------------------------------------- +# Floppy defaults +# +# Values from /app/data/env.sh take precedence. +# ---------------------------------------------------------------------- + +export DEBUG="${DEBUG:-False}" +export DEMO_ACCOUNT_ENABLED="${DEMO_ACCOUNT_ENABLED:-False}" +export ADMIN_ENABLED="${ADMIN_ENABLED:-False}" +export REGISTRATION="${REGISTRATION:-True}" +export TMDB_API="${TMDB_API:-}" +export TZ="${TZ:-Europe/Berlin}" + +export LOG_DIR=/app/data/logs +export BACKUP_DIR=/app/data/backups +export FLOPPY_DATA_DIR=/app/data + +# ---------------------------------------------------------------------- +# Permissions +# ---------------------------------------------------------------------- + +chown -R abc:abc /app/data /run/floppy + +cd /floppy + + +# ---------------------------------------------------------------------- +# Database +# ---------------------------------------------------------------------- + +echo "[cloudron] Applying Floppy database migrations" + +DB_POOL_ENABLED=false \ +PGOPTIONS="-c lock_timeout=120s" \ +python manage.py migrate --noinput + +# ---------------------------------------------------------------------- +# Runtime sizing +# +# Match Floppy's own resource sizing logic without invoking its +# Docker-specific entrypoint. +# ---------------------------------------------------------------------- + +if resource_env=$(python -c 'from config.runtime_profile import emit_env; emit_env()' 2>/dev/null); then + eval "$resource_env" +fi + +export FLOPPY_RESOURCE_TIER="${FLOPPY_RESOURCE_TIER:-minimal}" +export FLOPPY_CELERY_QUEUES="${FLOPPY_CELERY_QUEUES:-celery,interactive,discover}" +export FLOPPY_CELERY_ROLE="${FLOPPY_CELERY_ROLE:-background}" +export FLOPPY_START_INTERACTIVE_WORKER="${FLOPPY_START_INTERACTIVE_WORKER:-false}" +export FLOPPY_START_DISCOVER_WORKER="${FLOPPY_START_DISCOVER_WORKER:-false}" + +# ---------------------------------------------------------------------- +# Start +# ---------------------------------------------------------------------- + +echo "[cloudron] Starting Floppy on ${CLOUDRON_APP_ORIGIN}" +echo "[cloudron] Registration enabled: ${REGISTRATION}" +echo "[cloudron] Admin enabled: ${ADMIN_ENABLED}" +echo "[cloudron] Timezone: ${TZ}" + +exec supervisord -c /app/code/supervisord-cloudron.conf diff --git a/supervisord-cloudron.conf b/supervisord-cloudron.conf new file mode 100644 index 0000000..4d8ebd7 --- /dev/null +++ b/supervisord-cloudron.conf @@ -0,0 +1,50 @@ +[supervisord] +nodaemon=true +user=root +logfile=/tmp/supervisord.log +logfile_maxbytes=0 +pidfile=/run/floppy/supervisord.pid +childlogdir=/tmp + +[program:nginx] +command=nginx -c /app/code/nginx-cloudron.conf -g "daemon off;" +user=root +priority=1 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[program:gunicorn] +command=gunicorn --control-socket /tmp/gunicorn.ctl --config python:config.gunicorn config.wsgi:application +user=abc +priority=5 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stdout +stderr_logfile_maxbytes=0 + +[program:celery] +command=sh -c 'if [ "${DEBUG:-False}" = "True" ]; then LOGLEVEL=DEBUG; else LOGLEVEL=INFO; fi; exec celery --app config worker --beat --scheduler django --queues "${FLOPPY_CELERY_QUEUES:-celery,interactive,discover}" --hostname=celery@%%h --loglevel $LOGLEVEL --without-mingle --without-gossip' +environment=FLOPPY_PROCESS_ROLE="%(ENV_FLOPPY_CELERY_ROLE)s" +user=abc +stopasgroup=true +killasgroup=true +stopwaitsecs=60 +priority=10 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stdout +stderr_logfile_maxbytes=0 + +[program:podcast-autocomplete] +command=/floppy/.venv/bin/python /app/data/scripts/podcast-autocomplete.py +directory=/floppy +autostart=true +autorestart=true +startsecs=5 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +environment=PYTHONUNBUFFERED="1"