mirror of
https://github.com/R0m1k3/Priceflow.git
synced 2026-10-22 22:58:11 +02:00
feat: Initialize core application structure with item tracking, price history, and notification channel management.
This commit is contained in:
1 parent
40225676fe
commit
9637d77cef
10 files changed
+484
-263
No files matched your search
@@ -80,6 +80,7 @@ class ItemService:
|
||||
return None, None
|
||||
|
||||
settings = {s.key: s.value for s in db.query(models.Settings).all()}
|
||||
channel = item.notification_channel
|
||||
|
||||
item_data = {
|
||||
"id": item.id,
|
||||
@@ -89,6 +90,7 @@ class ItemService:
|
||||
"current_price": item.current_price,
|
||||
"in_stock": item.in_stock,
|
||||
"target_price": item.target_price,
|
||||
"notification_channel": channel,
|
||||
}
|
||||
|
||||
config = {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import logging
|
||||
import json
|
||||
import apprise
|
||||
from app import models
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class NotificationService:
|
||||
@staticmethod
|
||||
async def send_notification(channel: models.NotificationChannel, title: str, body: str):
|
||||
"""Send a notification via the specified channel using Apprise."""
|
||||
if not channel.is_active:
|
||||
logger.info(f"Channel {channel.name} is inactive, skipping notification.")
|
||||
return False
|
||||
|
||||
apobj = apprise.Apprise()
|
||||
|
||||
# Construct Apprise URL based on channel type and configuration
|
||||
config_url = NotificationService._get_config_url(channel)
|
||||
|
||||
if not config_url:
|
||||
logger.error(f"Failed to construct Apprise URL for channel {channel.name}")
|
||||
return False
|
||||
|
||||
apobj.add(config_url)
|
||||
|
||||
try:
|
||||
await apobj.async_notify(
|
||||
body=body,
|
||||
title=title,
|
||||
)
|
||||
logger.info(f"Notification sent to {channel.name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send notification to {channel.name}: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _get_config_url(channel: models.NotificationChannel) -> str | None:
|
||||
"""Construct Apprise URL from channel configuration."""
|
||||
try:
|
||||
# If configuration is already a URL (e.g. for simple webhooks), use it
|
||||
if channel.configuration.startswith("http") or "://" in channel.configuration:
|
||||
return channel.configuration
|
||||
|
||||
# If it's JSON, parse it
|
||||
config = json.loads(channel.configuration)
|
||||
|
||||
if channel.type == "email":
|
||||
# mailto://user:password@smtp.example.com:2525
|
||||
user = config.get("user", "")
|
||||
password = config.get("password", "")
|
||||
host = config.get("host", "")
|
||||
port = config.get("port", "")
|
||||
|
||||
auth = f"{user}:{password}@" if user and password else ""
|
||||
port_str = f":{port}" if port else ""
|
||||
|
||||
return f"mailto://{auth}{host}{port_str}"
|
||||
|
||||
elif channel.type == "discord":
|
||||
# discord://webhook_id/webhook_token
|
||||
# Or just the full webhook URL which Apprise supports
|
||||
return config.get("webhook_url")
|
||||
|
||||
elif channel.type == "mattermost":
|
||||
# mattermost://hostname/token
|
||||
# Or the full webhook URL
|
||||
return config.get("webhook_url")
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing configuration for channel {channel.name}: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def send_test_notification(channel: models.NotificationChannel):
|
||||
return await NotificationService.send_notification(
|
||||
channel,
|
||||
title="Test Priceflow",
|
||||
body="Ceci est une notification de test de Priceflow ! 🚀"
|
||||
)
|
||||
@@ -8,6 +8,7 @@ from app import database, models
|
||||
from app.ai_schema import AIExtractionMetadata, AIExtractionResponse
|
||||
from app.services.ai_service import AIService
|
||||
from app.services.item_service import ItemService
|
||||
from app.services.notification_service import NotificationService
|
||||
from app.services.scraper_service import ScraperService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -133,6 +134,32 @@ async def process_item_check(item_id: int):
|
||||
None, _update_db_result, item_id, extraction, metadata, thresholds, screenshot_path
|
||||
)
|
||||
|
||||
# Check for notifications
|
||||
if item_data["notification_channel"] and extraction.price:
|
||||
channel = item_data["notification_channel"]
|
||||
|
||||
# 1. Target Price Reached
|
||||
if item_data["target_price"] and extraction.price <= item_data["target_price"]:
|
||||
# Only notify if we haven't already notified for this price (or if price dropped further)
|
||||
# For simplicity, we notify if current price <= target.
|
||||
# Ideally we should check if we already notified recently, but let's keep it simple.
|
||||
if not old_price or old_price > item_data["target_price"]:
|
||||
await NotificationService.send_notification(
|
||||
channel,
|
||||
title=f"🎯 Prix cible atteint : {item_data['name']}",
|
||||
body=f"Le prix de {item_data['name']} est passé à {extraction.price}€ (Cible: {item_data['target_price']}€)\n{item_data['url']}"
|
||||
)
|
||||
|
||||
# 2. Price Drop
|
||||
elif old_price and extraction.price < old_price:
|
||||
drop_percent = (old_price - extraction.price) / old_price * 100
|
||||
if drop_percent >= 5: # Notify only for significant drops (> 5%)
|
||||
await NotificationService.send_notification(
|
||||
channel,
|
||||
title=f"📉 Baisse de prix : {item_data['name']}",
|
||||
body=f"Le prix de {item_data['name']} a baissé de {drop_percent:.1f}% !\nNouveau prix : {extraction.price}€ (Ancien: {old_price}€)\n{item_data['url']}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
except Exception as e:
|
||||
|
||||
Reference in new issue
Block a user