Compare commits
	
		
			3 Commits
		
	
	
		
			fb9b8390a3
			...
			11193677cf
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 11193677cf | |||
| c03606e31d | |||
| b36f42a7b9 | 
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -69,7 +69,12 @@ staticfiles/ | ||||
| media/ | ||||
|  | ||||
| # Database | ||||
| /db.sqlite3 | ||||
| .data/ | ||||
| data/ | ||||
| data/db.sqlite3 | ||||
| ./db.sqlite3 | ||||
| db.sqlite3 | ||||
| .db.sqlite3 | ||||
|  | ||||
| # Environment files | ||||
| .env.local | ||||
|   | ||||
							
								
								
									
										311
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										311
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,228 +1,133 @@ | ||||
| # Subscribarr | ||||
|  | ||||
| # Subscribarr | ||||
| <p align="center"> | ||||
|   <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License MIT"></a> | ||||
|   <img src="https://img.shields.io/badge/python-3.13-blue.svg" alt="Python 3.13"> | ||||
|   <img src="https://img.shields.io/badge/django-5.x-092e20?logo=django&logoColor=white" alt="Django 5"> | ||||
|   <img src="https://img.shields.io/badge/docker-ready-2496ED?logo=docker&logoColor=white" alt="Docker ready"> | ||||
|   <img src="https://img.shields.io/badge/ntfy-supported-4c1" alt="ntfy supported"> | ||||
|   <img src="https://img.shields.io/badge/Apprise-supported-4c1" alt="Apprise supported"> | ||||
| </p> | ||||
|  | ||||
| Subscribarr is a notification tool for the *Arr ecosystem (Sonarr, Radarr) and Jellyfin. Users can subscribe to shows/movies; when new episodes/releases are available (and actually present), Subscribarr sends email notifications. | ||||
| <!-- Optional dynamic badges (uncomment and replace OWNER/REPO / IMAGE if you want): | ||||
| <p align="center"> | ||||
|   <a href="https://github.com/OWNER/REPO/releases"><img src="https://img.shields.io/github/v/release/OWNER/REPO?sort=semver" alt="latest release"></a> | ||||
|   <a href="https://hub.docker.com/r/OWNER/IMAGE"><img src="https://img.shields.io/docker/pulls/OWNER/IMAGE" alt="docker pulls"></a> | ||||
|   <a href="https://github.com/OWNER/REPO/commits/main"><img src="https://img.shields.io/github/commit-activity/m/OWNER/REPO" alt="commit activity"></a> | ||||
| </p> | ||||
| --> | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## Screenshots | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| --- | ||||
| Lightweight web UI for Sonarr/Radarr subscriptions with Jellyfin login, calendar, and flexible notifications via Email, ntfy, and Apprise. | ||||
|  | ||||
| ## Features | ||||
| - Sign in with Jellyfin (no separate user store) | ||||
| - Sonarr/Radarr‑style calendar (upcoming episodes/movies) | ||||
| - Subscribe/unsubscribe from the UI (series & movies) | ||||
| - Admin overview of all users’ subscriptions with posters | ||||
| - Per‑user notification channels: | ||||
|   - Email (SMTP) | ||||
|   - ntfy (Bearer token or Basic Auth) | ||||
|   - Apprise (Discord, Gotify, Pushover, Webhooks, and many more) | ||||
| - Docker‑ready; environment‑driven security (ALLOWED_HOSTS, CSRF, proxy) | ||||
|  | ||||
| - **Login via Jellyfin** (use your Jellyfin account; admin status respected) | ||||
| - **Subscriptions** for series and movies; duplicate-send protection per user/day | ||||
| - **Email notifications** (SMTP configurable) | ||||
| - **Sonarr/Radarr integration** (calendar/status; optional file-presence check) | ||||
| - **Settings UI** for Jellyfin/Arr/Mail/Account | ||||
| - **Periodic check via cron** calling `manage.py check_new_media` | ||||
| ## Screenshots | ||||
| <p align="center"> | ||||
|   <img src="./screenshots/SCR-20250811-lfrm.png" alt="Settings" width="800"><br/> | ||||
|   <img src="./screenshots/SCR-20250811-lfvc.png" alt="Subscriptions" width="800"><br/> | ||||
|   <img src="./screenshots/SCR-20250811-lfod.png" alt="Overview" width="800"><br/> | ||||
|   <img src="./screenshots/SCR-20250811-lfyq.png" alt="Search" width="800"><br/> | ||||
|   <img src="./screenshots/SCR-20250811-lgau.png" alt="Details" width="800"><br/> | ||||
|   <img src="./screenshots/SCR-20250811-lgcz.png" alt="Notifications" width="800"> | ||||
| </p> | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## Architecture / Tech Stack | ||||
|  | ||||
| - **Backend:** Django + Django REST Framework   | ||||
| - **Apps (examples):** `arr_api`, `accounts`, `settingspanel`   | ||||
| - **Database:** SQLite by default (path configurable via env)   | ||||
| - **Auth:** Jellyfin API (admin mirrored from Jellyfin policy) | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## Quickstart (Docker) | ||||
|  | ||||
| ### 1) Clone & run | ||||
| ## Quickstart (Docker Compose) | ||||
| 1) Ensure the lockfile matches your Pipfile (e.g., after adding packages): | ||||
| ```bash | ||||
| git clone https://gitea.js-devop.de/jschaufuss/Subscribarr.git | ||||
| cd Subscribarr | ||||
| docker compose up -d --build | ||||
| pipenv lock | ||||
| ``` | ||||
|  | ||||
| - Default app port inside the container: **8000**   | ||||
| - Optional: set `CRON_SCHEDULE` (e.g., `*/30 * * * *`) to enable periodic checks | ||||
|  | ||||
| ### 2) Minimal `docker-compose.yml` (example) | ||||
| ```yaml | ||||
| --- | ||||
| services: | ||||
|   subscribarr: | ||||
|     build: . | ||||
|     container_name: subscribarr | ||||
|     ports: | ||||
|       - "8081:8000" | ||||
|     environment: | ||||
|       # Django | ||||
|       - DJANGO_DEBUG=true | ||||
|       - USE_X_FORWARDED_HOST=true | ||||
|       - DJANGO_SECURE_PROXY_SSL_HEADER=true | ||||
|       - DJANGO_CSRF_COOKIE_SECURE=true | ||||
|       - DJANGO_SESSION_COOKIE_SECURE=true | ||||
|       - DJANGO_ALLOWED_HOSTS=* | ||||
|       - DJANGO_SECRET_KEY=change-me | ||||
|       - DB_PATH=/app/data/db.sqlite3 | ||||
|       - NOTIFICATIONS_ALLOW_DUPLICATES=false | ||||
|       - DJANGO_CSRF_TRUSTED_ORIGINS="https://subscribarr.local.js-devop.de" | ||||
|       # App Settings (optional, otherwise use first-run setup) | ||||
|       #- JELLYFIN_URL= | ||||
|       #- JELLYFIN_API_KEY= | ||||
|       #- SONARR_URL= | ||||
|       #- SONARR_API_KEY= | ||||
|       #- RADARR_URL= | ||||
|       #- RADARR_API_KEY= | ||||
|       #- MAIL_HOST= | ||||
|       #- MAIL_PORT= | ||||
|       #- MAIL_SECURE= | ||||
|       #- MAIL_USER= | ||||
|       #- MAIL_PASSWORD= | ||||
|       #- MAIL_FROM= | ||||
|       # Admin bootstrap (optional) | ||||
|       #- ADMIN_USERNAME= | ||||
|       #- ADMIN_PASSWORD= | ||||
|       #- ADMIN_EMAIL= | ||||
|       # Cron schedule (default every 30min) | ||||
|       - CRON_SCHEDULE=*/30 * * * * | ||||
|     volumes: | ||||
|       - ./data:/app/data | ||||
|     restart: unless-stopped | ||||
| ``` | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## Environment Variables (selection) | ||||
|  | ||||
| | Variable | Purpose | | ||||
| |---|---| | ||||
| | `DJANGO_DEBUG` | `true` / `false` (disable in production). | | ||||
| | `DJANGO_ALLOWED_HOSTS` | Comma list of allowed hosts (e.g., `example.com,localhost`). | | ||||
| | `DJANGO_SECRET_KEY` | Django secret key. | | ||||
| | `DB_PATH` | SQLite path, e.g., `/app/data/db.sqlite3`. | | ||||
| | `NOTIFICATIONS_ALLOW_DUPLICATES` | Allow duplicate sends (`true`/`false`). | | ||||
| | `ADMIN_USERNAME` / `ADMIN_PASSWORD` / `ADMIN_EMAIL` | Optional: bootstrap an admin user on first run. | | ||||
| | `JELLYFIN_URL` / `JELLYFIN_API_KEY` | Base URL + API key for Jellyfin. | | ||||
| | `SONARR_URL` / `SONARR_API_KEY` | Base URL + API key for Sonarr. | | ||||
| | `RADARR_URL` / `RADARR_API_KEY` | Base URL + API key for Radarr. | | ||||
| | `MAIL_HOST` / `MAIL_PORT` / `MAIL_SECURE` | SMTP host/port/security (`starttls` / `ssl` / empty). | | ||||
| | `MAIL_USER` / `MAIL_PASSWORD` / `MAIL_FROM` | SMTP auth + sender address. | | ||||
| | `CRON_SCHEDULE` | Cron interval for periodic checks (e.g., `*/30 * * * *`). | | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## First Run | ||||
|  | ||||
| 1. Start the container (or dev server) and open `http://<host>:8081`.   | ||||
| 2. Complete the **first-time setup**: Jellyfin URL/API key (required), optional Sonarr/Radarr, SMTP.   | ||||
| 3. **Sign in** with Jellyfin credentials (admin users in Jellyfin become admins in Subscribarr).   | ||||
| 4. Adjust settings later at `/settings/`. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## Notifications & Cron | ||||
|  | ||||
| - The periodic job calls `check_new_media` which determines today’s items via Sonarr/Radarr calendars.   | ||||
| - Email is sent only if the item is **present** (e.g., `hasFile`/downloaded) and not already recorded in the sent-log (duplicate guard).   | ||||
| - Cron is configured using `CRON_SCHEDULE` and runs `python manage.py check_new_media`. Output is typically logged to `/app/cron.log` in the container. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## Routes / Endpoints (selected) | ||||
|  | ||||
| - `GET /` — Overview page with search/filter and subscribe actions   | ||||
| - `GET/POST /settings/` — Jellyfin/Arr/Mail/Account configuration (auth required; admin for some actions)   | ||||
| - Example subscribe endpoints (subject to change):   | ||||
|   - `POST /api/series/subscribe/<series_id>/`, `POST /api/series/unsubscribe/<series_id>/`   | ||||
|   - `POST /api/movies/subscribe/<movie_id>/`, `POST /api/movies/unsubscribe/<movie_id>/` | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## Local Development (without Docker) | ||||
|  | ||||
| > Requires Python 3.12+ (recommended). | ||||
|  | ||||
| ### 1) Clone | ||||
| 2) Build and run: | ||||
| ```bash | ||||
| git clone https://gitea.js-devop.de/jschaufuss/Subscribarr.git | ||||
| cd Subscribarr | ||||
| docker compose build | ||||
| docker compose up -d | ||||
| ``` | ||||
| 3) Open the app and complete the first‑run setup (Jellyfin + Arr URLs/keys). | ||||
|  | ||||
| ### 2) Create & activate a virtualenv | ||||
| Important environment variables (examples): | ||||
| - `DJANGO_ALLOWED_HOSTS=subscribarr.example.com,localhost,127.0.0.1` | ||||
| - `DJANGO_CSRF_TRUSTED_ORIGINS=https://subscribarr.example.com,http://subscribarr.example.com` | ||||
| - Reverse proxy/TLS: | ||||
|   - `USE_X_FORWARDED_HOST=true` | ||||
|   - `DJANGO_SECURE_PROXY_SSL_HEADER=true` | ||||
|   - `DJANGO_CSRF_COOKIE_SECURE=true` | ||||
|   - `DJANGO_SESSION_COOKIE_SECURE=true` | ||||
|  | ||||
| > Note: `DJANGO_CSRF_TRUSTED_ORIGINS` must include the exact scheme+host (+port if used). | ||||
|  | ||||
| ### Local (Pipenv) | ||||
| ```bash | ||||
| python -m venv .venv | ||||
| # Linux/macOS: | ||||
| source .venv/bin/activate | ||||
| # Windows (PowerShell): | ||||
| # .venv\Scripts\Activate.ps1 | ||||
| pipenv sync | ||||
| pipenv run python manage.py migrate | ||||
| pipenv run python manage.py runserver | ||||
| ``` | ||||
|  | ||||
| ### 3) Install dependencies (including Django) | ||||
| If the repository provides `requirements.txt`: | ||||
| ## In‑App Configuration | ||||
| - Settings → Jellyfin: server URL + API key | ||||
| - Settings → Sonarr/Radarr: base URLs + API keys (with “Test” button) | ||||
| - Settings → Mail server: SMTP (host/port/TLS/SSL/user/password/from) | ||||
| - Settings → Notifications: | ||||
|   - ntfy: server URL, default topic, Basic Auth or Bearer token | ||||
|   - Apprise: default URL(s) (one per line) | ||||
| - Profile (per user): | ||||
|   - Choose channel: Email, ntfy, or Apprise | ||||
|   - ntfy topic (optional, overrides default) | ||||
|   - Apprise URL(s) (optional, appended to defaults) | ||||
|  | ||||
| ## ntfy Notes | ||||
| - Server URL: e.g., `https://ntfy.sh` or your own server | ||||
| - Auth: | ||||
|   - Bearer token (Authorization header) | ||||
|   - Basic Auth (username/password) | ||||
| - Topic selection: | ||||
|   - Per user in the profile, or a global default topic in Settings | ||||
|  | ||||
| ## Apprise Notes | ||||
| Provide one or more destination URLs (one per line), e.g.: | ||||
| - `gotify://TOKEN@gotify.example.com/` | ||||
| - `discord://webhook_id/webhook_token` | ||||
| - `mailto://user:pass@smtp.example.com` | ||||
| - `pover://user@token` | ||||
| - `json://webhook.example.com/path` | ||||
|  | ||||
| User URLs are added in addition to global defaults. | ||||
|  | ||||
| ## Notification Logic | ||||
| - Series: on the air date, Subscribarr checks Sonarr for the episode and only notifies when `hasFile` is true (downloaded/present). | ||||
| - Movies: similar via Radarr `hasFile` and matching the release date (Digital/Disc/Cinema) for today. | ||||
| - Duplicate suppression: entries are recorded in `SentNotification` per user/title/day; if sending fails, no record is stored. | ||||
| - Fallback: if ntfy/Apprise fail, Subscribarr falls back to Email (when configured). | ||||
|  | ||||
| ## Jobs / Manual Trigger | ||||
| - Periodic check via management command (e.g., cron): | ||||
| ```bash | ||||
| pip install --upgrade pip wheel | ||||
| pip install -r requirements.txt | ||||
| pipenv run python manage.py check_new_media | ||||
| ``` | ||||
| If not, install the core stack explicitly: | ||||
| - In Docker: | ||||
| ```bash | ||||
| pip install --upgrade pip wheel | ||||
| pip install "Django>=5" djangorestframework python-dotenv | ||||
| # add any additional libs your project uses as needed | ||||
| docker compose exec web python manage.py check_new_media | ||||
| ``` | ||||
|  | ||||
| ### 4) Configure environment (dev) | ||||
| Create a `.env` (or export env vars) with at least: | ||||
| ```env | ||||
| DJANGO_DEBUG=true | ||||
| DJANGO_SECRET_KEY=dev-secret | ||||
| DJANGO_ALLOWED_HOSTS=* | ||||
| DB_PATH=./data/db.sqlite3 | ||||
| ``` | ||||
| Create the `data/` directory if it doesn’t exist. | ||||
| ## Security & Proxy | ||||
| - Set `DJANGO_ALLOWED_HOSTS` to your hostnames. | ||||
| - Include all used origins in `DJANGO_CSRF_TRUSTED_ORIGINS` (http/https and port where applicable). | ||||
| - Behind a reverse proxy with TLS: enable `USE_X_FORWARDED_HOST`, `DJANGO_SECURE_PROXY_SSL_HEADER`, and secure cookie flags. | ||||
|  | ||||
| ### 5) Database setup | ||||
| ```bash | ||||
| python manage.py makemigrations | ||||
| python manage.py migrate | ||||
| ``` | ||||
|  | ||||
| ### 6) (Optional) Create a superuser for the Django admin | ||||
| ```bash | ||||
| python manage.py createsuperuser | ||||
| ``` | ||||
|  | ||||
| ### 7) Run the dev server | ||||
| ```bash | ||||
| python manage.py runserver 0.0.0.0:8000 | ||||
| ``` | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## Data Model (high level) | ||||
|  | ||||
| - **User** (`accounts.User`): custom user with Jellyfin link and admin flag.   | ||||
| - **Subscriptions** (`arr_api.SeriesSubscription`, `arr_api.MovieSubscription`): unique per user/title.   | ||||
| - **SentNotification**: records delivered emails to avoid duplicates.   | ||||
| - **AppSettings**: singleton for Jellyfin/Arr/Mail/Account configuration. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## Production Notes | ||||
|  | ||||
| - Set **`DEBUG=false`**, a strong **`DJANGO_SECRET_KEY`**, and proper **`DJANGO_ALLOWED_HOSTS`**.   | ||||
| - Run behind a reverse proxy with HTTPS.   | ||||
| - Collect static files if served by Django:   | ||||
|   ```bash | ||||
|   python manage.py collectstatic --noinput | ||||
|   ``` | ||||
| - Use a persistent database volume (or switch to Postgres/MySQL) for production. | ||||
|  | ||||
| --- | ||||
| ## Tech Stack | ||||
| - Backend: Django 5 + DRF | ||||
| - Integrations: Sonarr/Radarr (API v3) | ||||
| - Auth: Jellyfin | ||||
| - Notifications: SMTP, ntfy (HTTP), Apprise | ||||
| - Frontend: Templates + FullCalendar | ||||
| - DB: SQLite (default) | ||||
|  | ||||
| ## License | ||||
|  | ||||
| MIT (see `LICENSE`). | ||||
|   | ||||
| @@ -361,9 +361,9 @@ def check_and_notify_users(): | ||||
|                     html = render_to_string('arr_api/email/new_media_notification.html', ctx) | ||||
|                 except Exception: | ||||
|                     pass | ||||
|                 _dispatch_user_notification(sub.user, subject=subj, body_text=body, html_message=html) | ||||
|                 ok = _dispatch_user_notification(sub.user, subject=subj, body_text=body, html_message=html) | ||||
|                 # mark as sent unless duplicates are allowed | ||||
|                 if not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): | ||||
|                 if ok and not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): | ||||
|                     SentNotification.objects.create( | ||||
|                         user=sub.user, | ||||
|                         media_id=sub.series_id, | ||||
| @@ -423,8 +423,8 @@ def check_and_notify_users(): | ||||
|                 html = render_to_string('arr_api/email/new_media_notification.html', ctx) | ||||
|             except Exception: | ||||
|                 pass | ||||
|             _dispatch_user_notification(sub.user, subject=subj, body_text=body, html_message=html) | ||||
|             if not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): | ||||
|             ok = _dispatch_user_notification(sub.user, subject=subj, body_text=body, html_message=html) | ||||
|             if ok and not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): | ||||
|                 SentNotification.objects.create( | ||||
|                     user=sub.user, | ||||
|                     media_id=sub.movie_id, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user