Skip to content

Commit ba851f6

Browse files
author
CoderLuii
committed
Release v0.7
1 parent f8fdef5 commit ba851f6

10 files changed

Lines changed: 209 additions & 111 deletions

File tree

README.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,20 @@
2929

3030
## 📈 Version History
3131

32+
- **v0.7 (May 01, 2025)** - Notification Enhancements & Bug Fixes
33+
- **Apprise Module Improvements**:
34+
- 📧 **Enhanced Email Logic** - Improved email configuration with support for custom SMTP servers
35+
- 🔗 **Advanced Discord Integration** - Better formatting and reliability for Discord notifications
36+
- 🖼️ **UI Enhancements** - Clearer documentation and examples for notification configuration
37+
- 🧹 **Removed MQTT** - Streamlined notification options by removing deprecated MQTT support
38+
- **Live TV Improvements**:
39+
- 🖼️ **Fixed Image Selection** - Corrected case-sensitive bug in channel/program image selection
40+
- 📊 **Better UI Documentation** - Added detailed formatting guides for notification services
41+
- **Bug Fixes**:
42+
- Fixed email formatting issues with various SMTP providers
43+
- Improved error handling for notification delivery
44+
- Enhanced security for authentication credentials
45+
3246
- **v0.6 (April 26, 2024)** - Project Restructuring, Web UI & Configuration Improvements
3347
- **Complete Project Restructuring**:
3448
- Organized codebase with clearer separation of concerns
@@ -382,12 +396,11 @@ Configuration is managed through the web UI at `http://your-server-ip:8501`
382396
|----------|-------------|---------------|
383397
| Pushover | Mobile/Desktop notifications | User Key, API Token |
384398
| Discord | Chat channel notifications | Webhook URL |
385-
| Telegram | Messaging service | Bot Token, Chat ID |
399+
| Telegram | Chat messaging | Bot Token/Chat ID |
386400
| Email | Standard email | SMTP Settings |
387-
| Slack | Team collaboration | Webhook Tokens |
388-
| Gotify | Self-hosted notifications | URL, Token |
389-
| Matrix | Open chat platform | User/Pass, Room |
390-
| MQTT | IoT messaging protocol | Broker URL |
401+
| Slack | Chat messaging | Webhook URL (token format) |
402+
| Gotify | Self-hosted notifications | Server URL & Token |
403+
| Matrix | Decentralized chat | Room/User credentials |
391404
| Custom | Any Apprise-supported service | URL |
392405

393406
<p align="right">

SECURITY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ ChannelWatch is currently maintained with security updates for the following ver
66

77
| Version | Supported |
88
| ------- | ------------------ |
9+
| 0.7 | :white_check_mark: |
910
| 0.6 | :white_check_mark: |
1011
| 0.5 | :white_check_mark: |
1112
| 0.4 | :white_check_mark: |

core/alerts/channel_watching.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -280,10 +280,7 @@ def _process_watching_event(self, event_data: Dict[str, Any], tracking_key: str)
280280
channel_info["program_title"] = program_info["title"]
281281

282282
if program_info.get("icon_url"):
283-
if self.image_source == "Program":
284-
channel_info["program_icon_url"] = program_info["icon_url"]
285-
elif self.image_source == "Channel":
286-
channel_info["program_icon_url"] = program_info["icon_url"]
283+
channel_info["program_icon_url"] = program_info["icon_url"]
287284

288285
success = self._send_alert(channel_info)
289286

@@ -345,7 +342,7 @@ def _send_alert(self, channel_info: Dict[str, Any]) -> bool:
345342
if program_info and program_info.get("icon_url"):
346343
program_image_url = program_info["icon_url"]
347344

348-
if self.image_source == "Channel":
345+
if self.image_source.upper() == "CHANNEL":
349346
image_url = channel_logo_url if channel_logo_url else program_image_url
350347
log(f"Using channel image: {image_url}", level=LOG_VERBOSE)
351348
else:

core/docker-entrypoint.sh

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ if [ ! -f "$SETTINGS_FILE" ]; then
7474
\"apprise_slack\": \"\",
7575
\"apprise_gotify\": \"\",
7676
\"apprise_matrix\": \"\",
77-
\"apprise_mqtt\": \"\",
7877
\"apprise_custom\": \"\"
7978
}
8079
EOF"

core/helpers/config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ class CoreSettings:
8888
apprise_slack: Optional[str] = ""
8989
apprise_gotify: Optional[str] = ""
9090
apprise_matrix: Optional[str] = ""
91-
apprise_mqtt: Optional[str] = ""
9291
apprise_custom: Optional[str] = ""
9392

9493
# Singleton

core/helpers/initialize.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def initialize_notifications(settings: CoreSettings, test_mode=False) -> Optiona
5050
settings.apprise_discord, settings.apprise_email,
5151
settings.apprise_telegram, settings.apprise_slack,
5252
settings.apprise_gotify, settings.apprise_matrix,
53-
settings.apprise_mqtt, settings.apprise_custom
53+
settings.apprise_custom
5454
])
5555

5656
if apprise_configured:

core/notifications/providers/apprise.py

Lines changed: 148 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ class AppriseProvider(NotificationProvider):
2323
"apprise_slack": "slack://{}",
2424
"apprise_gotify": "gotify://{}",
2525
"apprise_matrix": "matrix://{}",
26-
"apprise_mqtt": "mqtt://{}",
2726
"apprise_custom": "{}"
2827
}
2928

@@ -47,28 +46,28 @@ def initialize(self, settings: CoreSettings, **kwargs) -> bool:
4746
for url in self.urls:
4847
add_result = self.apprise.add(url)
4948
if not add_result:
50-
log(f"Failed to add Apprise URL: {url}", LOG_STANDARD)
49+
log(f"Failed to add notification URL: {url}", LOG_STANDARD)
5150
else:
5251
service_type = url.split('://')[0] if '://' in url else 'custom'
53-
log(f"Added {service_type} notification service", LOG_VERBOSE)
52+
log(f"Added {service_type} service", LOG_VERBOSE)
5453

5554
if self.is_configured():
5655
services = [url.split('://')[0] for url in self.urls if '://' in url]
5756
service_counts = {}
5857
for service in services:
5958
service_counts[service] = service_counts.get(service, 0) + 1
6059
service_summary = ', '.join([f"{count} {name}" for name, count in service_counts.items()])
61-
log(f"Apprise ready with {len(self.urls)} service(s): {service_summary}", LOG_VERBOSE)
60+
log(f"Notification services ready: {service_summary}", LOG_VERBOSE)
6261
return True
6362

64-
log("Apprise initialized but no valid service URLs were configured.", LOG_STANDARD)
63+
log("No valid notification services configured", LOG_STANDARD)
6564
return False
6665

6766
except ImportError:
6867
log("Apprise package not installed. Run: pip install apprise")
6968
return False
7069
except Exception as e:
71-
log(f"Error initializing Apprise: {e}")
70+
log(f"Error initializing notification services: {e}")
7271
return False
7372

7473
def _collect_urls_from_settings(self) -> List[str]:
@@ -82,18 +81,54 @@ def _collect_urls_from_settings(self) -> List[str]:
8281
for setting_attr, url_template in self.SERVICE_MAP.items():
8382
value = getattr(settings, setting_attr, "")
8483
if value and isinstance(value, str):
85-
if setting_attr == "apprise_custom" and "://" in value:
84+
if setting_attr == "apprise_email" and "=" in value and "://" not in value and (
85+
value.strip().startswith(("user=", "pass=", "smtp=", "port=")) or
86+
any(param in value for param in ["user=", "pass=", "smtp=", "port="])
87+
):
88+
89+
if "from=" not in value:
90+
url = f"mailtos://_?{value}&from=ChannelWatch"
91+
else:
92+
url = f"mailtos://_?{value}"
93+
94+
elif setting_attr == "apprise_discord" and ("discord.com/api/webhooks/" in value or "discordapp.com/api/webhooks/" in value):
95+
96+
try:
97+
parts = value.split("/api/webhooks/")
98+
if len(parts) == 2 and "/" in parts[1]:
99+
webhook_parts = parts[1].split("/", 1)
100+
if len(webhook_parts) >= 2:
101+
webhook_id, webhook_token = webhook_parts[0], webhook_parts[1]
102+
103+
if "?" in webhook_token:
104+
webhook_token = webhook_token.split("?", 1)[0]
105+
106+
url = f"discord://{webhook_id}/{webhook_token}"
107+
else:
108+
url = url_template.format(value)
109+
log(f"Could not extract token from Discord webhook URL", LOG_STANDARD)
110+
else:
111+
url = url_template.format(value)
112+
log(f"Invalid Discord webhook URL format", LOG_STANDARD)
113+
except Exception as e:
114+
log(f"Error parsing Discord webhook URL: {e}", LOG_STANDARD)
115+
url = url_template.format(value)
116+
elif setting_attr == "apprise_custom" and "://" in value:
86117
url = value
87118
else:
88119
url = url_template.format(value)
120+
if setting_attr == "apprise_email" and "from=" not in url:
121+
separator = '&' if '?' in url else '?'
122+
url = f"{url}{separator}from=ChannelWatch"
123+
log(f"Added ChannelWatch as sender name for email", LOG_VERBOSE)
89124
urls.append(url)
90125

91126
email_to = settings.apprise_email_to
92127
if email_to:
93128
updated_urls = []
94129
found_mailto = False
95130
for url in urls:
96-
if url.startswith("mailto://"):
131+
if url.startswith(("mailto://", "mailtos://")):
97132
separator = '&' if '?' in url else '?'
98133
updated_urls.append(f"{url}{separator}to={email_to}")
99134
found_mailto = True
@@ -102,7 +137,7 @@ def _collect_urls_from_settings(self) -> List[str]:
102137
if found_mailto:
103138
urls = updated_urls
104139
else:
105-
log("APPRISE_EMAIL_TO provided, but no APPRISE_EMAIL configured.", LOG_STANDARD)
140+
pass
106141

107142
return urls
108143

@@ -113,51 +148,121 @@ def is_configured(self) -> bool:
113148
# NOTIFICATION DELIVERY
114149

115150
def send_notification(self, title: str, message: str, **kwargs) -> bool:
116-
"""Delivers notification to all configured Apprise services with optional image attachment and timeout."""
117-
log(f"AP: Entering send_notification (Title: {title})", level=LOG_VERBOSE)
151+
"""Delivers notification to all configured services."""
152+
log(f"Sending notification: {title}", level=LOG_VERBOSE)
118153
if not self.is_configured():
119-
log("AP: Apprise not configured or no services enabled.", level=LOG_VERBOSE)
154+
log("No notification services configured", level=LOG_VERBOSE)
120155
return False
121156

122157
success = False
123158
try:
124159
image_url = kwargs.get('image_url')
125-
attach = [image_url] if image_url else None
126-
log(f"AP: Image URL: {image_url}, Attach: {attach}", level=LOG_VERBOSE)
160+
discord_urls = []
161+
other_urls = []
162+
163+
for url in self.urls:
164+
if url.startswith('discord://'):
165+
discord_urls.append(url)
166+
else:
167+
other_urls.append(url)
168+
169+
apprise_module = importlib.import_module('apprise')
170+
if discord_urls:
171+
try:
172+
discord_success = False
173+
try:
174+
import requests
175+
176+
for discord_url in discord_urls:
177+
if discord_url.startswith('discord://') and '/' in discord_url[10:]:
178+
parts = discord_url[10:].split('/', 1)
179+
if len(parts) == 2:
180+
webhook_id, webhook_token = parts
181+
webhook_url = f"https://discord.com/api/webhooks/{webhook_id}/{webhook_token}"
182+
embed = {
183+
"title": title,
184+
"description": message,
185+
"color": 3447003,
186+
}
187+
if image_url:
188+
embed["image"] = {"url": image_url}
189+
190+
payload = {
191+
"username": "ChannelWatch Bot",
192+
"content": "",
193+
"embeds": [embed]
194+
}
195+
196+
log(f"Sending Discord notification", level=LOG_VERBOSE)
197+
response = requests.post(webhook_url, json=payload)
198+
199+
if response.status_code == 204:
200+
discord_success = True
201+
log(f"Discord notification sent successfully", level=LOG_VERBOSE)
202+
else:
203+
log(f"Discord notification failed: {response.status_code} {response.text}", level=LOG_STANDARD)
204+
except ImportError:
205+
log("Requests library not available, using Apprise fallback for Discord", level=LOG_STANDARD)
206+
discord_message = message
207+
discord_apprise = apprise_module.Apprise()
208+
for url in discord_urls:
209+
discord_apprise.add(url)
210+
211+
try:
212+
body_format = apprise_module.NotifyFormat.TEXT if hasattr(apprise_module, 'NotifyFormat') else None
213+
except (ImportError, AttributeError):
214+
body_format = None
215+
discord_success = discord_apprise.notify(
216+
title=title,
217+
body=discord_message,
218+
body_format=body_format,
219+
attach=[image_url] if image_url else None
220+
)
221+
if discord_success:
222+
log("Discord notification sent via Apprise fallback", level=LOG_VERBOSE)
223+
except Exception as e:
224+
log(f"Error sending Discord notification: {e}", level=LOG_STANDARD)
225+
except Exception as e:
226+
log(f"Discord notification error: {e}", level=LOG_STANDARD)
227+
discord_success = False
228+
else:
229+
discord_success = True
230+
if other_urls:
231+
try:
232+
try:
233+
body_format = apprise_module.NotifyFormat.HTML if 'NotifyFormat' in dir(apprise_module) else None
234+
html_message = message.replace("\n", "<br />")
235+
except (ImportError, AttributeError):
236+
body_format = None
237+
html_message = message
238+
other_apprise = apprise_module.Apprise()
239+
for url in other_urls:
240+
other_apprise.add(url)
241+
attach = [image_url] if image_url else None
242+
other_success = other_apprise.notify(
243+
title=title,
244+
body=html_message,
245+
attach=attach,
246+
body_format=body_format
247+
)
248+
if other_success:
249+
log("Other notification services: delivery successful", level=LOG_VERBOSE)
250+
else:
251+
log("Other notification services: delivery failed", level=LOG_STANDARD)
252+
except Exception as e:
253+
log(f"Error with other notification services: {e}", level=LOG_STANDARD)
254+
other_success = False
255+
else:
256+
other_success = True
257+
success = (discord_success or other_success)
127258

128-
try:
129-
apprise_module = importlib.import_module('apprise')
130-
body_format = apprise_module.NotifyFormat.HTML
131-
html_message = message.replace("\n", "<br />")
132-
log(f"AP: Body format set to HTML.", level=LOG_VERBOSE)
133-
except (ImportError, AttributeError):
134-
body_format = None
135-
html_message = message
136-
log("AP: Could not set HTML format for Apprise, using default.", level=LOG_VERBOSE)
137-
138-
apprise = cast_optional(self.apprise)
139-
if not apprise:
140-
log("AP: Apprise object not initialized.", level=LOG_STANDARD)
141-
return False
142-
log(f"AP: Apprise object obtained. Preparing to call apprise.notify.", level=LOG_VERBOSE)
143-
144-
log(f"AP: Calling apprise.notify (Title: {title})", level=LOG_STANDARD)
145-
success = apprise.notify(
146-
title=title,
147-
body=html_message,
148-
attach=attach,
149-
body_format=body_format
150-
)
151-
log(f"AP: Returned from apprise.notify (Result: {success})", level=LOG_STANDARD)
152-
153259
if success:
154-
log("AP: Apprise library reported success.", level=LOG_VERBOSE)
260+
log("Notification sent successfully", level=LOG_VERBOSE)
155261
else:
156-
log("AP: Apprise library reported failure. Check Apprise logs if enabled.", level=LOG_STANDARD)
262+
log("All notification services failed", level=LOG_STANDARD)
157263

158264
except Exception as e:
159-
log(f"AP: Exception occurred during Apprise notification: {e}", level=LOG_STANDARD)
265+
log(f"Notification error: {e}", level=LOG_STANDARD)
160266
success = False
161267

162-
log(f"AP: Exiting send_notification (Result: {success})", level=LOG_VERBOSE)
163268
return success

ui/backend/schemas.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ def check_image_source(cls, v):
8383
apprise_slack: Optional[str] = StrEmpty()
8484
apprise_gotify: Optional[str] = StrEmpty()
8585
apprise_matrix: Optional[str] = StrEmpty()
86-
apprise_mqtt: Optional[str] = StrEmpty()
8786
apprise_custom: Optional[str] = StrEmpty()
8887

8988
model_config = {

0 commit comments

Comments
 (0)