From 9358308a326c9b00840674aacd5fc897da68085f Mon Sep 17 00:00:00 2001 From: Jan Schaufuss Date: Fri, 15 Aug 2025 13:27:29 +0200 Subject: [PATCH] prettier README / better duplicated notification handling --- .gitignore | 3 +- README.md | 134 +++++++++++++++++++++++++++++++++++++++ arr_api/notifications.py | 8 +-- db.sqlite3 | Bin 0 -> 172032 bytes mailmap.txt | 3 + 5 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 db.sqlite3 create mode 100644 mailmap.txt diff --git a/.gitignore b/.gitignore index 46c76b2..437dd60 100644 --- a/.gitignore +++ b/.gitignore @@ -69,7 +69,8 @@ staticfiles/ media/ # Database -/db.sqlite3 +.data/ +data/ # Environment files .env.local diff --git a/README.md b/README.md index 663328b..70cabe7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,139 @@ # Subscribarr +

+ License MIT + Python 3.13 + Django 5 + Docker ready + ntfy supported + Apprise supported +

+ + + +Ein leichtgewichtiges Web‑Frontend für Benachrichtigungen und Abos rund um Sonarr/Radarr – mit Jellyfin‑Login, Kalender, Abo‑Verwaltung und flexiblen Notifications per E‑Mail, ntfy und Apprise. + +## Features +- Jellyfin‑Login (kein eigener Userstore nötig) +- Kalender im Sonarr/Radarr‑Stil (kommende Episoden/Filme) +- Abonnieren/Abbestellen direkt aus dem UI (Serien & Filme) +- Admin‑Übersicht aller Abos je Nutzer inkl. Poster +- Benachrichtigungen pro Nutzer wählbar: + - E‑Mail (SMTP) + - ntfy (Token oder Basic Auth) + - Apprise (zahlreiche Ziele wie Discord, Gotify, Pushover, Webhooks u. v. m.) +- Docker‑fertig, env‑gesteuerte Security‑Settings (ALLOWED_HOSTS, CSRF, Proxy) + +## Schnellstart + +## Screenshots +

+ Screenshot 1
+ Screenshot 2
+ Screenshot 3
+ Screenshot 4
+ Screenshot 5
+ Screenshot 6 +

+ +### Mit Docker Compose +1) Lockfile aktuell halten (wenn `Pipfile` geändert wurde): +```bash +pipenv lock +``` +2) Image bauen/Starten: +```bash +docker compose build +docker compose up -d +``` +3) Öffne die App und führe das First‑Run‑Setup (Jellyfin + Arr‑URLs/Keys) durch. + +Wichtige Umgebungsvariablen (Beispiele): +- `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` + +> Hinweis: In `DJANGO_CSRF_TRUSTED_ORIGINS` muss Schema+Host (und ggf. Port) exakt stimmen. + +### Lokal (Pipenv) +```bash +pipenv sync +pipenv run python manage.py migrate +pipenv run python manage.py runserver +``` + +## Konfiguration im UI +- Einstellungen → Jellyfin: Server‑URL + API‑Key +- Einstellungen → Sonarr/Radarr: Base‑URLs + API‑Keys (inkl. „Test“-Knopf) +- Einstellungen → Mailserver: SMTP (Host/Port/TLS/SSL/Benutzer/Passwort/From) +- Einstellungen → Notifications: + - ntfy: Server‑URL, Default‑Topic, Basic‑Auth oder Bearer‑Token + - Apprise: Default‑URL(s) (eine pro Zeile) +- Profil (pro Nutzer): + - Kanal wählen: E‑Mail, ntfy oder Apprise + - ntfy Topic (optional, überschreibt Default) + - Apprise URL(s) (optional, ergänzen die Defaults) + +## ntfy – Hinweise +- Server‑URL: z. B. `https://ntfy.sh` oder eigener Server +- Auth: + - Bearer‑Token (Header) + - Basic‑Auth (Benutzer/Passwort) +- Topic: + - pro Nutzer frei wählbar (Profil) oder globales Default‑Topic (Einstellungen) + +## Apprise – Hinweise +- Trag eine oder mehrere Ziel‑URLs ein (pro Zeile), z. B.: + - `gotify://TOKEN@gotify.example.com/` + - `discord://webhook_id/webhook_token` + - `mailto://user:pass@smtp.example.com` + - `pover://user@token` + - `json://webhook.example.com/path` +- Nutzer können eigene URLs ergänzen; die globalen Defaults bleiben zusätzlich aktiv. + +## Benachrichtigungslogik +- Serien: Es wird pro Abo am Release‑Tag geprüft, ob die Episode bereits als Datei vorhanden ist (Sonarr `hasFile`). +- Filme: Analog über Radarr `hasFile` und Release‑Datum (Digital/Disc/Kino‐Tag). +- Doppelversand wird per `SentNotification` unterdrückt (täglich pro Item/Nutzer). +- Fallback: Wenn ntfy/Apprise scheitern, wird E‑Mail versendet (falls konfiguriert). + +## Jobs / Manuell anstoßen +- Regelmäßiger Check per Management Command (z. B. via Cron): +```bash +pipenv run python manage.py check_new_media +``` +- In Docker: +```bash +docker compose exec web python manage.py check_new_media +``` + +## Sicherheit & Proxy +- Setze `DJANGO_ALLOWED_HOSTS` auf deine(n) Hostnamen. +- Füge alle genutzten Ursprünge in `DJANGO_CSRF_TRUSTED_ORIGINS` hinzu (http/https und Port beachten). +- Hinter Reverse‑Proxy TLS aktivieren: `USE_X_FORWARDED_HOST`, `DJANGO_SECURE_PROXY_SSL_HEADER`, Cookie‑Flags. + +## Tech‑Stack +- Backend: Django 5 + DRF +- Integrationen: Sonarr/Radarr (API v3) +- Auth: Jellyfin +- Notifications: SMTP, ntfy (HTTP), Apprise +- Frontend: Templates + FullCalendar +- DB: SQLite (default) + +## Lizenz +MIT +# Subscribarr + # Subscribarr 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. diff --git a/arr_api/notifications.py b/arr_api/notifications.py index 5092913..4c4a5cf 100644 --- a/arr_api/notifications.py +++ b/arr_api/notifications.py @@ -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, diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..1cfcbc39f5e956e3d58ae41d377f7a028274278c GIT binary patch literal 172032 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%0AB^f-Qq9S^eO0e0L^ zk0J7VfaJ)tW1Jmh^cb9G$0>H4WXC8yj!m#*gdM}|I35Z{xez@B>0vAwj7A;%|MXVC z_@3`W1PB5k00JNY0w4eaAOHd&00JNY0?#1BtkB~tC1V8`;KmY_l00ck) z1V8`;o+|>l{(r8z8^MAA2!H?xfB*=900@8p2!H?xJf{S3{r{YHJi-P65C8!X009sH z0T2KI5C8!Xc&-T0|NnnB`gVZ-NB)caFY~YSOZ=huzsJ87|6E*)Ux^=y{W$iwvCqbS zC0311Pk(dzx2E5iK0Wm>Q=gq`Pvs|nI{8LjTGnj?WN@Q*%+N(Xb4V&#Z1iAa&;#Cs^y!_O>c(n$p@-Ehk8sU>06DLxeLI zCk$si;)YKyh10r%;hRUnaM7udIzJ2u=D8}jv{qYFHk7)g zDeac!KCKX(HKh|o?P4Wj#NelB1k$g1Tvs)s@l!QI>Ql`fBdUeV5gM1D9u2NfsdSXJ z7d9g7D6*<{YihltYQQ=0M>Whbodg)47 zkJe9DkGEg(5K|my5pT80KhB`6PGvQ&crP9#ikDs;H{$hGHNy2#KAcha)&@8O9|Dig9g(UP)yPM3@8a#n9ya#k0y z5+O$9W*0(ww^%}YU$CzW(?sI@Vvu=NZ?`ludbG#evPIq~6KLUuDI%P?7&P4Th#M}t z6ee{A!%vTb;j2?26(tG_g&=j#=f5%Co*T|$EZt|0Ck3anGcNzq6<0Z#J1eoQY^@{NU)0*f<*Ab zqT!8QPMtPfvI`f+n6TlNT~37!-|WIUjtHkR)IYnBN>V57672tvxI9A)1V8`;KmY_l z00ck)1V8`;KmY`uVFHfp{|ORCAOHd&00JNY0w4eaAOHd& z00K`Z;CTN3>B5iz0T2KI5C8!X009sH0T2KI5C8!XcqR$Z{eM3AD8QeMe>)zIU7Y&2 z$^V(W8U2lkdy(G^|1i8BJ{$U8@X^@sjs>|l0=Ioa?A<&bNvH*GC#bX<@*}BU--(=> zNc8qsxt+^qQM=pHRQAqU{b0X;&?r@$Zz&YXx2#kvWQz5y*lj5fh|wVyj$K);l-DZ4 zot5gXI~Ae2a<%dyAvs(}cVl>~N#W+o)9fULb4mJoUgC1{MImX&MGI2B*^`mPdV%Xz zd_b@lMXLJCBWc97MkXUIrt@ycb*ZC9h`zB6GUVsKE=#>;pL8#U6OqIRiI?TUUK%lq zg>0>u&14q+oIEmulU_T+eKg6Y`jgC&4@Uf^X)`dfP^zbEx$K5d3h63`Mo65IjE@!t z$q&=FWNK&gSR_%ca!;z7RBOs^o-7nQm(=042aPb+$}cZhMoCzbnu0>!>AFc?E4^}a zO<1|Jyez!7TD?(TT^Fub)`jw&wVTxyV)jO5WsSIP_JdGUHi@FGv|4$kvRYZWQn~GE zmu46p0m4-(vC2HFu2k2m<>lpdqp)(dZ^ZPw4lr1ymL2QZa3T#W-l9R$isY70c+c}phq*HdD>Ptha zclJOe@x}tzyP?l??tC&|EiBTP8q*m$yOCck%J~}e*dC6PhTG<3Jfan2l5k7YNR8JQ zo6VACxa&Y10NOgplXNVsz~ zTPWDMn(Dp8M-rPQt|!^Oua9gSfa;hAX{Ija>xFtLE8AnsA)v;65_VLJj=t_wp+Ib( z^mB=YI2mEYdB+Ir({r5jWU}dcZKLGxcWflTb@TKD$}sjcmrVsS(C)>^nQE}a?aU5i zZ7MjJQt0V=BbP1I()Muci*$PMsOyupskoTBn;AK_zNIy_voaY;luF!_MSJ}9IpNM+r@E^_sax?qA^E>7S$)wDAMBpVXS9Ky>Rk&*673w<`!Gu$pBaPxbcNWIYI2kO zBmjAxySP}aHFAwyjiro_LdZ8?d<2JE*0aP%jYekqL@!vFTVxUmpxyDt_VDp=jbH;BGF9s z*11UH!Ub+ea?;sSHaqM$MpPqmyI{;^H`!OeTQ2tLjcdsmzMzm=g0!i~)XQ9N-Q%+N$4RY5vW-Hq zkeAZ7(_Yb;L2i2m`)qn0_bRwuzduiIR&!-;=gg?SlLr&XFQ?Lb)j27h&q$?=ey`dh zGW;dvUBay5Z4xUztRqDO%I96Y0HGx7}7Xx#PH%$M%NE7CcdHKa@N47ebSQCO@+G z*-6c&-0H}xr(C#lt#akMaPHg(Zr@xHUoS6LuaXBlbjg%(bJbR>-e$Y}sT48QAwMB4 zsj^6K20CvE>MqnfGASp&x~w_tjHd*Mc6XcV4XHPthIzN26-a%oEJPCX^W58IJtLgY z1i3cwPkMUZDY&cN-j-DL&31?Mwza#QU9xxV2$ZIzYGSiZ9*%HA(}60gsCLPg4xNLN zLap6yT6bS5PEzXL4fNZ&CW0GEhqN(7)O2IG2=zvjHa7Y_AvIE6Qy!9>@H$C*T)ap2 z`+e^rlZNFjNom@6vW2{j$9OJ_NY#|KCxJ;JZMLyR!FgM~CpVjqHILtdn zj3uPC@5{u9*+T_C8FLx1q%saE=Z}Mn^yhS2vLmF7X9HK;cr0q|ZKZBEnROT=cicM) z8DrfJv7wJ;k`1Ze&!34T-Xu@ParzYQOig_*wfFwKd%T%Uu2#$EbFN(PQ#xTJzqy9) zrUhfx9;!+QlI^R$_j~PhByqpW^^{S9Gx|c5^BZX?Q_Ktx>-akg%WHeofXw_z#p{7j zCwjA&xE=i=7ju>I{$#tg{%y$(MWPF>{((I~A}Jj<$BK6k1P`vb|&b`BC%HM8D3~Mct=Ueb22v>yH#ZIde9WD3`gnj7JIwp0(N8>^=XR z@GPAJP3&qNCG)s`I_i7cWYj+8?8%TCb`oWX3_h1K1V%rlax3U(R_-#vd4lybdkR%b zh7&jD2R_K>n((u(3HfwsW^`5eTz-mND}Ev&UFLdsT-PmqXYAHwx>&3)6*r`snWue{ zXFRLHpmX~^i{|wR9#L1?Ew*T5|KELY1K&UZ1V8`;KmY_l00ck)1V8`;KwvKt!1e!L z>{Y}B0w4eaAOHd&00JNY0w4eaAOHex0=WKn^T0O{009sH0T2KI5C8!X009sH0T9@W z1aSSo7kd>kfdB}A00@8p2!H?xfB*=900@A9n*ja&zra3s8NPu42!H?xfB*=900@8p z2!H?xfB*>WEdtp8-&_5OXg~l2KmY_l00ck)1V8`;KmY_lz>ff~|NW?-3KmY_l00ck)1V8`;KmY_l00cn5 zj{vU!{ivV}1V8`;KmY_l00ck)1V8`;KmY{x76Cl}zqk4m(SQI5fB*=900@8p2!H?x zfB*=9fFA+u|NBuv83=#?2!H?xfB*=900@8p2!H?x>@5PZ=m!GR!OsNv3$LwKZ(n zDp#(PhTMAZyCkGj)LOeGbvk0V(@Y8vrB3~>)H#<;r&G)iyI$H>#QXANhfJYh$#kT~ zP#Z@5P#as4(iHEuRn5N%R=wTskZKc_9WW(kM(GeVLu5s&T<>;d+jzzfk=1uqxntL6 z9nt9)B~^X1-66m&NM4bRPJ7E)owxW%^}0yyx1=qn2L;;&Dk0N`{0(%s8niGO_mgK<)J*J&s*BYV{s%Qt-Y<(#fH2gb(_wfb0iQm zHkdA}-X2PqiD>Qna?44_PCqgtJBlisZs@tJ$&WOa!>L>8a6-P!1(a4pexyEVDw-@x zU9HW&lQ0JiZZR{k>V@8+_eK)8Q(Ujaj5O{^t<5$urK$=Umhz)*r9<2y9VJSIdNErp z=G-FZjQOFua<%dyA?dF4)lUjHSGV?Ew@?sw>kF=<*uZL z&0t)EK2I~&XO5!tgb_T6T1pB!eM6GeI^FEg;MB$Q;l$0hcpj>+YnH?de@L zNSv}YJV>T?k}pgodb5|f9X%TdDOo;m$qhxK&g&nv$J->ul#ZTjjasp|)R0SJx1~H7 zWNOxk%2*tX3>+lAi-t!JpX7xZPty8KL!#fzsFJCV#!`{QrAyq$&auJBHofWQRd9{B z-u-zp-ncTib7oZE$z(yoqC+kxrSlo7l<^ENZ-c^92AEggifBMf?<7E$6NbNJImo{! zl8`QQy*sX8x7rU$M1K9WSgbD8@`PkoYS{JW`7&Wg#j=nkS!-rY&{xk%z8m${w9 zh;B1;tcNxV8^zqB +Jan Schaufuss Jan Schaufuss +