added Support for ntfy and apprise
This commit is contained in:
		
							
								
								
									
										1
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Pipfile
									
									
									
									
									
								
							| @@ -10,6 +10,7 @@ requests = "*" | |||||||
| python-dateutil = "*" | python-dateutil = "*" | ||||||
| django = "*" | django = "*" | ||||||
| jellyfin-apiclient-python = "*" | jellyfin-apiclient-python = "*" | ||||||
|  | apprise = "*" | ||||||
|  |  | ||||||
| [dev-packages] | [dev-packages] | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										102
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										102
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|     "_meta": { |     "_meta": { | ||||||
|         "hash": { |         "hash": { | ||||||
|             "sha256": "cea8d420f44ecbfb792b63223f9b9827af98bdff685d256b47053575dfb15b49" |             "sha256": "726c31f18af5284731c9d76b583e1d6d789a3a95277450b74c3deeb6836841e8" | ||||||
|         }, |         }, | ||||||
|         "pipfile-spec": 6, |         "pipfile-spec": 6, | ||||||
|         "requires": { |         "requires": { | ||||||
| @@ -16,6 +16,15 @@ | |||||||
|         ] |         ] | ||||||
|     }, |     }, | ||||||
|     "default": { |     "default": { | ||||||
|  |         "apprise": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:17dca8ad0a5b2063f6bae707979a51ca2cb374fcc66b0dd5c05a9d286dd40069", | ||||||
|  |                 "sha256:483122aee19a89a7b075ecd48ef11ae37d79744f7aeb450bcf985a9a6c28c988" | ||||||
|  |             ], | ||||||
|  |             "index": "pypi", | ||||||
|  |             "markers": "python_version >= '3.9'", | ||||||
|  |             "version": "==1.9.4" | ||||||
|  |         }, | ||||||
|         "asgiref": { |         "asgiref": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142", |                 "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142", | ||||||
| @@ -117,6 +126,14 @@ | |||||||
|             "markers": "python_version >= '3.7'", |             "markers": "python_version >= '3.7'", | ||||||
|             "version": "==3.4.3" |             "version": "==3.4.3" | ||||||
|         }, |         }, | ||||||
|  |         "click": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", | ||||||
|  |                 "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b" | ||||||
|  |             ], | ||||||
|  |             "markers": "python_version >= '3.10'", | ||||||
|  |             "version": "==8.2.1" | ||||||
|  |         }, | ||||||
|         "django": { |         "django": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0745b25681b129a77aae3d4f6549b62d3913d74407831abaa0d9021a03954bae", |                 "sha256:0745b25681b129a77aae3d4f6549b62d3913d74407831abaa0d9021a03954bae", | ||||||
| @@ -161,6 +178,22 @@ | |||||||
|             "markers": "python_version >= '3.6'", |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==1.11.0" |             "version": "==1.11.0" | ||||||
|         }, |         }, | ||||||
|  |         "markdown": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", | ||||||
|  |                 "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24" | ||||||
|  |             ], | ||||||
|  |             "markers": "python_version >= '3.9'", | ||||||
|  |             "version": "==3.8.2" | ||||||
|  |         }, | ||||||
|  |         "oauthlib": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", | ||||||
|  |                 "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1" | ||||||
|  |             ], | ||||||
|  |             "markers": "python_version >= '3.8'", | ||||||
|  |             "version": "==3.3.1" | ||||||
|  |         }, | ||||||
|         "python-dateutil": { |         "python-dateutil": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", |                 "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", | ||||||
| @@ -170,6 +203,65 @@ | |||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", |             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", | ||||||
|             "version": "==2.9.0.post0" |             "version": "==2.9.0.post0" | ||||||
|         }, |         }, | ||||||
|  |         "pyyaml": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", | ||||||
|  |                 "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", | ||||||
|  |                 "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", | ||||||
|  |                 "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", | ||||||
|  |                 "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", | ||||||
|  |                 "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", | ||||||
|  |                 "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", | ||||||
|  |                 "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", | ||||||
|  |                 "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", | ||||||
|  |                 "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", | ||||||
|  |                 "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", | ||||||
|  |                 "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", | ||||||
|  |                 "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", | ||||||
|  |                 "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", | ||||||
|  |                 "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", | ||||||
|  |                 "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", | ||||||
|  |                 "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", | ||||||
|  |                 "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", | ||||||
|  |                 "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", | ||||||
|  |                 "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", | ||||||
|  |                 "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", | ||||||
|  |                 "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", | ||||||
|  |                 "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", | ||||||
|  |                 "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", | ||||||
|  |                 "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", | ||||||
|  |                 "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", | ||||||
|  |                 "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", | ||||||
|  |                 "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", | ||||||
|  |                 "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", | ||||||
|  |                 "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", | ||||||
|  |                 "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", | ||||||
|  |                 "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", | ||||||
|  |                 "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", | ||||||
|  |                 "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", | ||||||
|  |                 "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", | ||||||
|  |                 "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", | ||||||
|  |                 "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", | ||||||
|  |                 "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", | ||||||
|  |                 "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", | ||||||
|  |                 "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", | ||||||
|  |                 "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", | ||||||
|  |                 "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", | ||||||
|  |                 "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", | ||||||
|  |                 "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", | ||||||
|  |                 "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", | ||||||
|  |                 "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", | ||||||
|  |                 "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", | ||||||
|  |                 "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", | ||||||
|  |                 "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", | ||||||
|  |                 "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", | ||||||
|  |                 "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", | ||||||
|  |                 "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", | ||||||
|  |                 "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" | ||||||
|  |             ], | ||||||
|  |             "markers": "python_version >= '3.8'", | ||||||
|  |             "version": "==6.0.2" | ||||||
|  |         }, | ||||||
|         "requests": { |         "requests": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", |                 "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", | ||||||
| @@ -179,6 +271,14 @@ | |||||||
|             "markers": "python_version >= '3.8'", |             "markers": "python_version >= '3.8'", | ||||||
|             "version": "==2.32.4" |             "version": "==2.32.4" | ||||||
|         }, |         }, | ||||||
|  |         "requests-oauthlib": { | ||||||
|  |             "hashes": [ | ||||||
|  |                 "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", | ||||||
|  |                 "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9" | ||||||
|  |             ], | ||||||
|  |             "markers": "python_version >= '3.4'", | ||||||
|  |             "version": "==2.0.0" | ||||||
|  |         }, | ||||||
|         "six": { |         "six": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", |                 "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", | ||||||
|   | |||||||
| @@ -14,9 +14,11 @@ class CustomUserChangeForm(UserChangeForm): | |||||||
|      |      | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = User |         model = User | ||||||
|         fields = ('email',) |         fields = ('email', 'notification_channel', 'ntfy_topic', 'apprise_url') | ||||||
|         widgets = { |         widgets = { | ||||||
|             'email': forms.EmailInput(attrs={'class': 'text-input', 'placeholder': 'Email address'}), |             'email': forms.EmailInput(attrs={'class': 'text-input', 'placeholder': 'Email address'}), | ||||||
|  |             'ntfy_topic': forms.TextInput(attrs={'class': 'text-input', 'placeholder': 'ntfy topic (optional)'}), | ||||||
|  |             'apprise_url': forms.Textarea(attrs={'rows': 2, 'placeholder': 'apprise://... or other URL'}), | ||||||
|         } |         } | ||||||
|  |  | ||||||
| class JellyfinLoginForm(forms.Form): | class JellyfinLoginForm(forms.Form): | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								accounts/migrations/0003_user_notifications.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								accounts/migrations/0003_user_notifications.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ('accounts', '0002_user_jellyfin_server_user_jellyfin_token_and_more'), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='user', | ||||||
|  |             name='notification_channel', | ||||||
|  |             field=models.CharField(choices=[('email', 'Email'), ('ntfy', 'ntfy'), ('apprise', 'Apprise')], default='email', max_length=10), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='user', | ||||||
|  |             name='ntfy_topic', | ||||||
|  |             field=models.CharField(blank=True, max_length=200, null=True), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='user', | ||||||
|  |             name='apprise_url', | ||||||
|  |             field=models.TextField(blank=True, null=True), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -16,6 +16,24 @@ class User(AbstractUser): | |||||||
|     jellyfin_user_id = models.CharField(max_length=100, blank=True, null=True) |     jellyfin_user_id = models.CharField(max_length=100, blank=True, null=True) | ||||||
|     jellyfin_token = models.CharField(max_length=500, blank=True, null=True) |     jellyfin_token = models.CharField(max_length=500, blank=True, null=True) | ||||||
|     jellyfin_server = models.CharField(max_length=200, blank=True, null=True) |     jellyfin_server = models.CharField(max_length=200, blank=True, null=True) | ||||||
|  |  | ||||||
|  |     # Notifications | ||||||
|  |     NOTIFY_EMAIL = 'email' | ||||||
|  |     NOTIFY_NTFY = 'ntfy' | ||||||
|  |     NOTIFY_APPRISE = 'apprise' | ||||||
|  |     NOTIFY_CHOICES = [ | ||||||
|  |         (NOTIFY_EMAIL, 'Email'), | ||||||
|  |         (NOTIFY_NTFY, 'ntfy'), | ||||||
|  |         (NOTIFY_APPRISE, 'Apprise'), | ||||||
|  |     ] | ||||||
|  |     notification_channel = models.CharField( | ||||||
|  |         max_length=10, | ||||||
|  |         choices=NOTIFY_CHOICES, | ||||||
|  |         default=NOTIFY_EMAIL, | ||||||
|  |     ) | ||||||
|  |     # Optional per-user targets/overrides | ||||||
|  |     ntfy_topic = models.CharField(max_length=200, blank=True, null=True) | ||||||
|  |     apprise_url = models.TextField(blank=True, null=True) | ||||||
|      |      | ||||||
|     def check_jellyfin_admin(self): |     def check_jellyfin_admin(self): | ||||||
|         """Check if user is Jellyfin admin on the server""" |         """Check if user is Jellyfin admin on the server""" | ||||||
|   | |||||||
| @@ -18,13 +18,26 @@ | |||||||
|     {% endif %} |     {% endif %} | ||||||
|  |  | ||||||
|     <div class="profile-section"> |     <div class="profile-section"> | ||||||
|     <h3>Email address</h3> |     <h3>Notifications</h3> | ||||||
|         <form method="post" class="profile-form compact-form"> |         <form method="post" class="profile-form compact-form"> | ||||||
|             {% csrf_token %} |             {% csrf_token %} | ||||||
|             <div class="form-row"> |             <div class="form-row"> | ||||||
|                 <label for="id_email">Email</label> |                 <label for="id_email">Email</label> | ||||||
|                 {{ form.email }} |                 {{ form.email }} | ||||||
|             </div> |             </div> | ||||||
|  |             <div class="form-row"> | ||||||
|  |                 <label for="id_notification_channel">Channel</label> | ||||||
|  |                 {{ form.notification_channel }} | ||||||
|  |                 <div class="help">Email, ntfy, or Apprise</div> | ||||||
|  |             </div> | ||||||
|  |             <div class="form-row"> | ||||||
|  |                 <label for="id_ntfy_topic">ntfy topic (optional)</label> | ||||||
|  |                 {{ form.ntfy_topic }} | ||||||
|  |             </div> | ||||||
|  |             <div class="form-row"> | ||||||
|  |                 <label for="id_apprise_url">Apprise URL(s)</label> | ||||||
|  |                 {{ form.apprise_url }} | ||||||
|  |             </div> | ||||||
|             <button type="submit" class="btn-primary">Save</button> |             <button type="submit" class="btn-primary">Save</button> | ||||||
|         </form> |         </form> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ def profile(request): | |||||||
|         form = CustomUserChangeForm(request.POST, instance=request.user) |         form = CustomUserChangeForm(request.POST, instance=request.user) | ||||||
|         if form.is_valid(): |         if form.is_valid(): | ||||||
|             form.save() |             form.save() | ||||||
|             messages.success(request, 'Email saved.') |             messages.success(request, 'Profile saved.') | ||||||
|             return redirect('accounts:profile') |             return redirect('accounts:profile') | ||||||
|     else: |     else: | ||||||
|         form = CustomUserChangeForm(instance=request.user) |         form = CustomUserChangeForm(instance=request.user) | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ from django.core.mail import send_mail | |||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.template.loader import render_to_string | from django.template.loader import render_to_string | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| from django.db import transaction |  | ||||||
| from settingspanel.models import AppSettings | from settingspanel.models import AppSettings | ||||||
| # from accounts.utils import JellyfinClient  # not needed for availability; use Sonarr/Radarr instead | # from accounts.utils import JellyfinClient  # not needed for availability; use Sonarr/Radarr instead | ||||||
| import requests | import requests | ||||||
| @@ -69,7 +68,7 @@ def send_notification_email( | |||||||
|     release_type=None, |     release_type=None, | ||||||
| ): | ): | ||||||
|     """ |     """ | ||||||
|     Sends a notification email to a user with extended details |     Sendet eine Benachrichtigungs-E-Mail an einen User mit erweiterten Details | ||||||
|     """ |     """ | ||||||
|     eff = _set_runtime_email_settings() |     eff = _set_runtime_email_settings() | ||||||
|     logger.info( |     logger.info( | ||||||
| @@ -95,7 +94,7 @@ def send_notification_email( | |||||||
|     context = { |     context = { | ||||||
|         'username': user.username, |         'username': user.username, | ||||||
|         'title': media_title, |         'title': media_title, | ||||||
|     'type': 'Series' if media_type == 'series' else 'Movie', |         'type': 'Serie' if media_type == 'series' else 'Film', | ||||||
|         'overview': overview, |         'overview': overview, | ||||||
|         'poster_url': poster_url, |         'poster_url': poster_url, | ||||||
|         'episode_title': episode_title, |         'episode_title': episode_title, | ||||||
| @@ -106,17 +105,89 @@ def send_notification_email( | |||||||
|         'release_type': release_type, |         'release_type': release_type, | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     subject = f"New {context['type']} available: {media_title}" |     subject = f"Neue {context['type']} verfügbar: {media_title}" | ||||||
|     message = render_to_string('arr_api/email/new_media_notification.html', context) |     message = render_to_string('arr_api/email/new_media_notification.html', context) | ||||||
|  |  | ||||||
|     send_mail( |     # Fallback to dispatch respecting user preference | ||||||
|         subject=subject, |     try: | ||||||
|         message=message, |         # strip HTML tags for body_text basic fallback | ||||||
|         from_email=settings.DEFAULT_FROM_EMAIL, |         import re | ||||||
|         recipient_list=[user.email], |         body_text = re.sub('<[^<]+?>', '', message) | ||||||
|         html_message=message, |     except Exception: | ||||||
|         fail_silently=False, |         body_text = message | ||||||
|     ) |     _dispatch_user_notification(user, subject=subject, body_text=body_text, html_message=message) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _send_ntfy(user, title: str, message: str, click_url: str | None = None): | ||||||
|  |     cfg = AppSettings.current() | ||||||
|  |     base = (cfg.ntfy_server_url or '').strip().rstrip('/') | ||||||
|  |     if not base: | ||||||
|  |         return False | ||||||
|  |     topic = (user.ntfy_topic or cfg.ntfy_topic_default or '').strip() | ||||||
|  |     if not topic: | ||||||
|  |         return False | ||||||
|  |     url = f"{base}/{topic}" | ||||||
|  |     headers = {"Title": title} | ||||||
|  |     if click_url: | ||||||
|  |         headers["Click"] = click_url | ||||||
|  |     if cfg.ntfy_token: | ||||||
|  |         headers["Authorization"] = f"Bearer {cfg.ntfy_token}" | ||||||
|  |     elif cfg.ntfy_user and cfg.ntfy_password: | ||||||
|  |         # basic auth via requests | ||||||
|  |         auth = (cfg.ntfy_user, cfg.ntfy_password) | ||||||
|  |     else: | ||||||
|  |         auth = None | ||||||
|  |     try: | ||||||
|  |         r = requests.post(url, data=message.encode('utf-8'), headers=headers, timeout=8, auth=auth if 'auth' in locals() else None) | ||||||
|  |         return r.status_code // 100 == 2 | ||||||
|  |     except Exception: | ||||||
|  |         return False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _send_apprise(user, title: str, message: str): | ||||||
|  |     # Lazy import apprise, optional dependency | ||||||
|  |     try: | ||||||
|  |         import apprise | ||||||
|  |     except Exception: | ||||||
|  |         return False | ||||||
|  |     cfg = AppSettings.current() | ||||||
|  |     urls = [] | ||||||
|  |     if user.apprise_url: | ||||||
|  |         urls.extend([u.strip() for u in str(user.apprise_url).splitlines() if u.strip()]) | ||||||
|  |     if cfg.apprise_default_url: | ||||||
|  |         urls.extend([u.strip() for u in str(cfg.apprise_default_url).splitlines() if u.strip()]) | ||||||
|  |     if not urls: | ||||||
|  |         return False | ||||||
|  |     app = apprise.Apprise() | ||||||
|  |     for u in urls: | ||||||
|  |         app.add(u) | ||||||
|  |     return app.notify(title=title, body=message) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _dispatch_user_notification(user, subject: str, body_text: str, html_message: str | None = None, click_url: str | None = None): | ||||||
|  |     channel = getattr(user, 'notification_channel', 'email') or 'email' | ||||||
|  |     if channel == 'ntfy': | ||||||
|  |         ok = _send_ntfy(user, title=subject, message=body_text, click_url=click_url) | ||||||
|  |         if ok: | ||||||
|  |             return True | ||||||
|  |         # fallback to email | ||||||
|  |     if channel == 'apprise': | ||||||
|  |         ok = _send_apprise(user, title=subject, message=body_text) | ||||||
|  |         if ok: | ||||||
|  |             return True | ||||||
|  |         # fallback to email | ||||||
|  |     try: | ||||||
|  |         send_mail( | ||||||
|  |             subject=subject, | ||||||
|  |             message=body_text, | ||||||
|  |             from_email=settings.DEFAULT_FROM_EMAIL, | ||||||
|  |             recipient_list=[user.email], | ||||||
|  |             html_message=html_message, | ||||||
|  |             fail_silently=False, | ||||||
|  |         ) | ||||||
|  |         return True | ||||||
|  |     except Exception: | ||||||
|  |         return False | ||||||
|  |  | ||||||
|  |  | ||||||
| def _get_arr_cfg(): | def _get_arr_cfg(): | ||||||
| @@ -210,8 +281,8 @@ def get_todays_radarr_calendar(): | |||||||
|  |  | ||||||
| def check_jellyfin_availability(user, media_id, media_type): | def check_jellyfin_availability(user, media_id, media_type): | ||||||
|     """ |     """ | ||||||
|     Replaced: We check availability via Sonarr/Radarr (hasFile), |     Ersetzt: Wir prüfen Verfügbarkeit über Sonarr/Radarr (hasFile), | ||||||
|     which is reliable if Jellyfin scans the same folders. |     was zuverlässig ist, wenn Jellyfin dieselben Ordner scannt. | ||||||
|     """ |     """ | ||||||
|     # user is unused here; kept for backward compatibility |     # user is unused here; kept for backward compatibility | ||||||
|     if media_type == 'series': |     if media_type == 'series': | ||||||
| @@ -255,56 +326,51 @@ def check_and_notify_users(): | |||||||
|             if season is None or number is None: |             if season is None or number is None: | ||||||
|                 continue |                 continue | ||||||
|  |  | ||||||
|  |             # duplicate guard (per series per day per user) | ||||||
|  |             if not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): | ||||||
|  |                 already_notified = SentNotification.objects.filter( | ||||||
|  |                     media_id=sub.series_id, | ||||||
|  |                     media_type='series', | ||||||
|  |                     air_date=today, | ||||||
|  |                     user=sub.user | ||||||
|  |                 ).exists() | ||||||
|  |                 if already_notified: | ||||||
|  |                     continue | ||||||
|  |  | ||||||
|             # check availability via Sonarr hasFile |             # check availability via Sonarr hasFile | ||||||
|             if sonarr_episode_has_file(sub.series_id, season, number): |             if sonarr_episode_has_file(sub.series_id, season, number): | ||||||
|                 if not sub.user.email: |                 if not sub.user.email: | ||||||
|                     continue |                     continue | ||||||
|                 # After confirming availability, reserve once per user/series/day |                 # Build subject/body | ||||||
|                 if not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): |                 subj = f"New episode available: {sub.series_title} S{season:02d}E{number:02d}" | ||||||
|                     try: |                 body = f"{sub.series_title} S{season:02d}E{number:02d} is now available." | ||||||
|                         with transaction.atomic(): |                 # Prefer HTML email rendering if channel falls back to email | ||||||
|                             obj, created = SentNotification.objects.get_or_create( |                 html = None | ||||||
|                                 user=sub.user, |  | ||||||
|                                 media_id=sub.series_id, |  | ||||||
|                                 media_type='series', |  | ||||||
|                                 air_date=today, |  | ||||||
|                                 defaults={ |  | ||||||
|                                     'media_title': sub.series_title, |  | ||||||
|                                 } |  | ||||||
|                             ) |  | ||||||
|                         if not created: |  | ||||||
|                             # already reserved/sent |  | ||||||
|                             continue |  | ||||||
|                     except Exception: |  | ||||||
|                         # if DB error (race), skip to avoid duplicates |  | ||||||
|                         continue |  | ||||||
|  |  | ||||||
|                 try: |                 try: | ||||||
|                     send_notification_email( |                     ctx = { | ||||||
|                     user=sub.user, |                         'username': sub.user.username, | ||||||
|                     media_title=sub.series_title, |                         'title': sub.series_title, | ||||||
|                     media_type='series', |                         'type': 'Serie', | ||||||
|                     overview=sub.series_overview, |                         'overview': sub.series_overview, | ||||||
|                     poster_url=ep.get('seriesPoster'), |                         'poster_url': ep.get('seriesPoster'), | ||||||
|                     episode_title=ep.get('title'), |                         'episode_title': ep.get('title'), | ||||||
|                     season=season, |                         'season': season, | ||||||
|                     episode=number, |                         'episode': number, | ||||||
|                     air_date=ep.get('airDateUtc'), |                         'air_date': ep.get('airDateUtc'), | ||||||
|                     ) |                     } | ||||||
|  |                     html = render_to_string('arr_api/email/new_media_notification.html', ctx) | ||||||
|                 except Exception: |                 except Exception: | ||||||
|                     # roll back reservation so we can retry next run |                     pass | ||||||
|                     if not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): |                 _dispatch_user_notification(sub.user, subject=subj, body_text=body, html_message=html) | ||||||
|                         try: |                 # mark as sent unless duplicates are allowed | ||||||
|                             SentNotification.objects.filter( |                 if not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): | ||||||
|                                 user=sub.user, |                     SentNotification.objects.create( | ||||||
|                                 media_id=sub.series_id, |                         user=sub.user, | ||||||
|                                 media_type='series', |                         media_id=sub.series_id, | ||||||
|                                 air_date=today, |                         media_type='series', | ||||||
|                             ).delete() |                         media_title=sub.series_title, | ||||||
|                         except Exception: |                         air_date=today | ||||||
|                             pass |                     ) | ||||||
|                     continue |  | ||||||
|                 # no-op: already reserved via get_or_create above |  | ||||||
|  |  | ||||||
|     # Film-Abos |     # Film-Abos | ||||||
|     for sub in MovieSubscription.objects.select_related('user').all(): |     for sub in MovieSubscription.objects.select_related('user').all(): | ||||||
| @@ -312,6 +378,16 @@ def check_and_notify_users(): | |||||||
|         if not it: |         if not it: | ||||||
|             continue |             continue | ||||||
|  |  | ||||||
|  |         if not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): | ||||||
|  |             already_notified = SentNotification.objects.filter( | ||||||
|  |                 media_id=sub.movie_id, | ||||||
|  |                 media_type='movie', | ||||||
|  |                 air_date=today, | ||||||
|  |                 user=sub.user | ||||||
|  |             ).exists() | ||||||
|  |             if already_notified: | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|         if radarr_movie_has_file(sub.movie_id): |         if radarr_movie_has_file(sub.movie_id): | ||||||
|             if not sub.user.email: |             if not sub.user.email: | ||||||
|                 continue |                 continue | ||||||
| @@ -329,47 +405,33 @@ def check_and_notify_users(): | |||||||
|             except Exception: |             except Exception: | ||||||
|                 pass |                 pass | ||||||
|  |  | ||||||
|             # After confirming availability, reserve once per user/movie/day |             subj = f"New movie available: {sub.title}" | ||||||
|             if not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): |             if rel: | ||||||
|                 try: |                 subj += f" ({rel})" | ||||||
|                     with transaction.atomic(): |             body = f"{sub.title} is now available." | ||||||
|                         obj, created = SentNotification.objects.get_or_create( |             html = None | ||||||
|                             user=sub.user, |  | ||||||
|                             media_id=sub.movie_id, |  | ||||||
|                             media_type='movie', |  | ||||||
|                             air_date=today, |  | ||||||
|                             defaults={ |  | ||||||
|                                 'media_title': sub.title, |  | ||||||
|                             } |  | ||||||
|                         ) |  | ||||||
|                     if not created: |  | ||||||
|                         continue |  | ||||||
|                 except Exception: |  | ||||||
|                     continue |  | ||||||
|  |  | ||||||
|             try: |             try: | ||||||
|                 send_notification_email( |                 ctx = { | ||||||
|                     user=sub.user, |                     'username': sub.user.username, | ||||||
|                     media_title=sub.title, |                     'title': sub.title, | ||||||
|                     media_type='movie', |                     'type': 'Film', | ||||||
|                     overview=sub.overview, |                     'overview': sub.overview, | ||||||
|                     poster_url=it.get('posterUrl'), |                     'poster_url': it.get('posterUrl'), | ||||||
|                     year=it.get('year'), |                     'year': it.get('year'), | ||||||
|                     release_type=rel, |                     'release_type': rel, | ||||||
|                 ) |                 } | ||||||
|  |                 html = render_to_string('arr_api/email/new_media_notification.html', ctx) | ||||||
|             except Exception: |             except Exception: | ||||||
|                 if not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): |                 pass | ||||||
|                     try: |             _dispatch_user_notification(sub.user, subject=subj, body_text=body, html_message=html) | ||||||
|                         SentNotification.objects.filter( |             if not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): | ||||||
|                             user=sub.user, |                 SentNotification.objects.create( | ||||||
|                             media_id=sub.movie_id, |                     user=sub.user, | ||||||
|                             media_type='movie', |                     media_id=sub.movie_id, | ||||||
|                             air_date=today, |                     media_type='movie', | ||||||
|                         ).delete() |                     media_title=sub.title, | ||||||
|                     except Exception: |                     air_date=today | ||||||
|                         pass |                 ) | ||||||
|                 continue |  | ||||||
|             # no-op: already reserved via get_or_create above |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def has_new_episode_today(series_id): | def has_new_episode_today(series_id): | ||||||
|   | |||||||
| @@ -64,6 +64,21 @@ class ArrSettingsForm(forms.Form): | |||||||
|     radarr_api_key = forms.CharField(label="Radarr API Key", required=False, |     radarr_api_key = forms.CharField(label="Radarr API Key", required=False, | ||||||
|                                     widget=forms.PasswordInput(render_value=True, attrs=WIDE)) |                                     widget=forms.PasswordInput(render_value=True, attrs=WIDE)) | ||||||
|  |  | ||||||
|  | class NotificationSettingsForm(forms.Form): | ||||||
|  |     # ntfy | ||||||
|  |     ntfy_server_url = forms.URLField(label="ntfy Server URL", required=False, widget=forms.URLInput(attrs=WIDE), | ||||||
|  |                                      help_text="e.g., https://ntfy.sh") | ||||||
|  |     ntfy_topic_default = forms.CharField(label="Default Topic", required=False, widget=forms.TextInput(attrs=WIDE)) | ||||||
|  |     ntfy_user = forms.CharField(label="ntfy Username", required=False) | ||||||
|  |     ntfy_password = forms.CharField(label="ntfy Password", required=False, widget=forms.PasswordInput(render_value=True)) | ||||||
|  |     ntfy_token = forms.CharField(label="ntfy Bearer Token", required=False, widget=forms.PasswordInput(render_value=True)) | ||||||
|  |  | ||||||
|  |     # Apprise | ||||||
|  |     apprise_default_url = forms.CharField( | ||||||
|  |         label="Apprise URL(s)", required=False, widget=forms.Textarea(attrs={"rows": 3, "class": "input-wide"}), | ||||||
|  |         help_text="One per line. See https://github.com/caronc/apprise/wiki for URL formats." | ||||||
|  |     ) | ||||||
|  |  | ||||||
| class MailSettingsForm(forms.Form): | class MailSettingsForm(forms.Form): | ||||||
|     mail_host = forms.CharField(label="Mail Host", required=False) |     mail_host = forms.CharField(label="Mail Host", required=False) | ||||||
|     mail_port = forms.IntegerField(label="Mail Port", required=False, min_value=1, max_value=65535) |     mail_port = forms.IntegerField(label="Mail Port", required=False, min_value=1, max_value=65535) | ||||||
| @@ -83,3 +98,23 @@ class AccountForm(forms.Form): | |||||||
|     email = forms.EmailField(label="Email", required=False) |     email = forms.EmailField(label="Email", required=False) | ||||||
|     new_password = forms.CharField(label="New password", required=False, widget=forms.PasswordInput) |     new_password = forms.CharField(label="New password", required=False, widget=forms.PasswordInput) | ||||||
|     repeat_password = forms.CharField(label="Repeat password", required=False, widget=forms.PasswordInput) |     repeat_password = forms.CharField(label="Repeat password", required=False, widget=forms.PasswordInput) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class NotificationSettingsForm(forms.Form): | ||||||
|  |     # ntfy | ||||||
|  |     ntfy_server_url = forms.URLField(label="ntfy Server URL", required=False, | ||||||
|  |                                      widget=forms.URLInput(attrs=WIDE), | ||||||
|  |                                      help_text="e.g. https://ntfy.sh or your self-hosted URL") | ||||||
|  |     ntfy_topic_default = forms.CharField(label="Default topic", required=False, | ||||||
|  |                                          widget=forms.TextInput(attrs=WIDE)) | ||||||
|  |     ntfy_user = forms.CharField(label="ntfy Username", required=False, | ||||||
|  |                                 widget=forms.TextInput(attrs=WIDE)) | ||||||
|  |     ntfy_password = forms.CharField(label="ntfy Password", required=False, | ||||||
|  |                                     widget=forms.PasswordInput(render_value=True, attrs=WIDE)) | ||||||
|  |     ntfy_token = forms.CharField(label="ntfy Bearer token", required=False, | ||||||
|  |                                  widget=forms.PasswordInput(render_value=True, attrs=WIDE)) | ||||||
|  |  | ||||||
|  |     # Apprise | ||||||
|  |     apprise_default_url = forms.CharField(label="Apprise URL(s)", required=False, | ||||||
|  |                                           widget=forms.Textarea(attrs={"rows": 3, **WIDE}), | ||||||
|  |                                           help_text="One URL per line. Will be used in addition to any user-provided URLs.") | ||||||
|   | |||||||
							
								
								
									
										41
									
								
								settingspanel/migrations/0005_appsettings_notifications.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								settingspanel/migrations/0005_appsettings_notifications.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ('settingspanel', '0004_alter_appsettings_mail_secure'), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='appsettings', | ||||||
|  |             name='ntfy_server_url', | ||||||
|  |             field=models.URLField(blank=True, null=True, help_text='Base URL of ntfy server, e.g. https://ntfy.sh'), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='appsettings', | ||||||
|  |             name='ntfy_topic_default', | ||||||
|  |             field=models.CharField(max_length=200, blank=True, null=True, help_text="Default topic if user hasn't set one"), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='appsettings', | ||||||
|  |             name='ntfy_user', | ||||||
|  |             field=models.CharField(max_length=255, blank=True, null=True), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='appsettings', | ||||||
|  |             name='ntfy_password', | ||||||
|  |             field=models.CharField(max_length=255, blank=True, null=True), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='appsettings', | ||||||
|  |             name='ntfy_token', | ||||||
|  |             field=models.CharField(max_length=255, blank=True, null=True, help_text='Bearer token, alternative to user/password'), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='appsettings', | ||||||
|  |             name='apprise_default_url', | ||||||
|  |             field=models.TextField(blank=True, null=True, help_text='Apprise URL(s). Multiple allowed, one per line.'), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -34,6 +34,16 @@ class AppSettings(models.Model): | |||||||
|     acc_username = models.CharField(max_length=150, blank=True, null=True) |     acc_username = models.CharField(max_length=150, blank=True, null=True) | ||||||
|     acc_email = models.EmailField(blank=True, null=True) |     acc_email = models.EmailField(blank=True, null=True) | ||||||
|  |  | ||||||
|  |     # Notifications - NTFY | ||||||
|  |     ntfy_server_url = models.URLField(blank=True, null=True, help_text="Base URL of ntfy server, e.g. https://ntfy.sh") | ||||||
|  |     ntfy_topic_default = models.CharField(max_length=200, blank=True, null=True, help_text="Default topic if user hasn't set one") | ||||||
|  |     ntfy_user = models.CharField(max_length=255, blank=True, null=True) | ||||||
|  |     ntfy_password = models.CharField(max_length=255, blank=True, null=True) | ||||||
|  |     ntfy_token = models.CharField(max_length=255, blank=True, null=True, help_text="Bearer token, alternative to user/password") | ||||||
|  |  | ||||||
|  |     # Notifications - Apprise (default target URLs, optional) | ||||||
|  |     apprise_default_url = models.TextField(blank=True, null=True, help_text="Apprise URL(s). Multiple allowed, one per line.") | ||||||
|  |  | ||||||
|     updated_at = models.DateTimeField(auto_now=True) |     updated_at = models.DateTimeField(auto_now=True) | ||||||
|  |  | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|   | |||||||
| @@ -90,6 +90,20 @@ | |||||||
|                     <div class="row"><label>From</label>{{ mail_form.mail_from }}</div> |                     <div class="row"><label>From</label>{{ mail_form.mail_from }}</div> | ||||||
|                 </div> |                 </div> | ||||||
|  |  | ||||||
|  |                 <div class="card"> | ||||||
|  |                     <h2>Notifications</h2> | ||||||
|  |                     <h3>ntfy</h3> | ||||||
|  |                     <div class="row"><label>Server URL</label>{{ notify_form.ntfy_server_url }}</div> | ||||||
|  |                     <div class="row"><label>Default topic</label>{{ notify_form.ntfy_topic_default }}</div> | ||||||
|  |                     <div class="row"><label>Username</label>{{ notify_form.ntfy_user }}</div> | ||||||
|  |                     <div class="row"><label>Password</label>{{ notify_form.ntfy_password }}</div> | ||||||
|  |                     <div class="row"><label>Bearer token</label>{{ notify_form.ntfy_token }}</div> | ||||||
|  |  | ||||||
|  |                     <h3 style="margin-top:12px;">Apprise</h3> | ||||||
|  |                     <div class="row"><label>Default URL(s)</label>{{ notify_form.apprise_default_url }}</div> | ||||||
|  |                     <div class="help">Users can also set their own ntfy topic or Apprise URLs in their profile.</div> | ||||||
|  |                 </div> | ||||||
|  |  | ||||||
|                 <div class="card"> |                 <div class="card"> | ||||||
|                     <h2>Account</h2> |                     <h2>Account</h2> | ||||||
|                     <div class="row"><label>Username</label>{{ account_form.username }}</div> |                     <div class="row"><label>Username</label>{{ account_form.username }}</div> | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ from django.shortcuts import render, redirect | |||||||
| from django.views import View | from django.views import View | ||||||
| from django.contrib import messages | from django.contrib import messages | ||||||
| from django.utils.decorators import method_decorator | from django.utils.decorators import method_decorator | ||||||
| from .forms import ArrSettingsForm, MailSettingsForm, AccountForm, FirstRunSetupForm, JellyfinSettingsForm | from .forms import ArrSettingsForm, MailSettingsForm, AccountForm, FirstRunSetupForm, JellyfinSettingsForm, NotificationSettingsForm | ||||||
| from .models import AppSettings | from .models import AppSettings | ||||||
| from django.http import JsonResponse | from django.http import JsonResponse | ||||||
| from accounts.utils import jellyfin_admin_required | from accounts.utils import jellyfin_admin_required | ||||||
| @@ -96,6 +96,14 @@ class SettingsView(View): | |||||||
|                 "mail_password": cfg.mail_password or "", |                 "mail_password": cfg.mail_password or "", | ||||||
|                 "mail_from": cfg.mail_from or "", |                 "mail_from": cfg.mail_from or "", | ||||||
|             }), |             }), | ||||||
|  |             "notify_form": NotificationSettingsForm(initial={ | ||||||
|  |                 "ntfy_server_url": cfg.ntfy_server_url or "", | ||||||
|  |                 "ntfy_topic_default": cfg.ntfy_topic_default or "", | ||||||
|  |                 "ntfy_user": cfg.ntfy_user or "", | ||||||
|  |                 "ntfy_password": cfg.ntfy_password or "", | ||||||
|  |                 "ntfy_token": cfg.ntfy_token or "", | ||||||
|  |                 "apprise_default_url": cfg.apprise_default_url or "", | ||||||
|  |             }), | ||||||
|             "account_form": AccountForm(initial={ |             "account_form": AccountForm(initial={ | ||||||
|                 "username": cfg.acc_username or "", |                 "username": cfg.acc_username or "", | ||||||
|                 "email": cfg.acc_email or "", |                 "email": cfg.acc_email or "", | ||||||
| @@ -106,13 +114,15 @@ class SettingsView(View): | |||||||
|         jellyfin_form = JellyfinSettingsForm(request.POST) |         jellyfin_form = JellyfinSettingsForm(request.POST) | ||||||
|         arr_form = ArrSettingsForm(request.POST) |         arr_form = ArrSettingsForm(request.POST) | ||||||
|         mail_form = MailSettingsForm(request.POST) |         mail_form = MailSettingsForm(request.POST) | ||||||
|  |         notify_form = NotificationSettingsForm(request.POST) | ||||||
|         acc_form = AccountForm(request.POST) |         acc_form = AccountForm(request.POST) | ||||||
|  |  | ||||||
|         if not (jellyfin_form.is_valid() and arr_form.is_valid() and mail_form.is_valid() and acc_form.is_valid()): |         if not (jellyfin_form.is_valid() and arr_form.is_valid() and mail_form.is_valid() and notify_form.is_valid() and acc_form.is_valid()): | ||||||
|             return render(request, self.template_name, { |             return render(request, self.template_name, { | ||||||
|                 "jellyfin_form": jellyfin_form, |                 "jellyfin_form": jellyfin_form, | ||||||
|                 "arr_form": arr_form, |                 "arr_form": arr_form, | ||||||
|                 "mail_form": mail_form, |                 "mail_form": mail_form, | ||||||
|  |                 "notify_form": notify_form, | ||||||
|                 "account_form": acc_form, |                 "account_form": acc_form, | ||||||
|             }) |             }) | ||||||
|  |  | ||||||
| @@ -136,6 +146,14 @@ class SettingsView(View): | |||||||
|         cfg.mail_password = mail_form.cleaned_data.get("mail_password") or None |         cfg.mail_password = mail_form.cleaned_data.get("mail_password") or None | ||||||
|         cfg.mail_from     = mail_form.cleaned_data.get("mail_from") or None |         cfg.mail_from     = mail_form.cleaned_data.get("mail_from") or None | ||||||
|  |  | ||||||
|  |         # Update Notification settings | ||||||
|  |         cfg.ntfy_server_url    = notify_form.cleaned_data.get("ntfy_server_url") or None | ||||||
|  |         cfg.ntfy_topic_default = notify_form.cleaned_data.get("ntfy_topic_default") or None | ||||||
|  |         cfg.ntfy_user          = notify_form.cleaned_data.get("ntfy_user") or None | ||||||
|  |         cfg.ntfy_password      = notify_form.cleaned_data.get("ntfy_password") or None | ||||||
|  |         cfg.ntfy_token         = notify_form.cleaned_data.get("ntfy_token") or None | ||||||
|  |         cfg.apprise_default_url = notify_form.cleaned_data.get("apprise_default_url") or None | ||||||
|  |  | ||||||
|         # Update account settings |         # Update account settings | ||||||
|         cfg.acc_username = acc_form.cleaned_data.get("username") or None |         cfg.acc_username = acc_form.cleaned_data.get("username") or None | ||||||
|         cfg.acc_email    = acc_form.cleaned_data.get("email") or None |         cfg.acc_email    = acc_form.cleaned_data.get("email") or None | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 jschaufuss@leitwerk.de
					jschaufuss@leitwerk.de