mirror of
https://github.com/R0m1k3/Priceflow.git
synced 2026-10-11 17:29:14 +02:00
179 lines
6.9 KiB
Python
179 lines
6.9 KiB
Python
import logging
|
|
import os
|
|
from datetime import UTC, datetime
|
|
|
|
from fastapi import HTTPException
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app import models, schemas
|
|
from app.services.settings_service import SettingsService
|
|
from app.url_validation import URLValidationError, validate_url
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ItemService:
|
|
@staticmethod
|
|
def get_items(db: Session):
|
|
import glob
|
|
items = db.query(models.Item).all()
|
|
result = []
|
|
for item in items:
|
|
# Strategy: Look for the latest screenshot file on disk
|
|
# Format: item_{id}_{timestamp}.png
|
|
screenshot_url = None
|
|
|
|
try:
|
|
# Find all timestamped screenshots for this item
|
|
files = glob.glob(f"screenshots/item_{item.id}_*.png")
|
|
|
|
if files:
|
|
# Sort by timestamp valid in filename
|
|
# We expect format item_ID_TIMESTAMP.png
|
|
def get_timestamp(f):
|
|
try:
|
|
# Extract timestamp part: remove extension, split by _, take last part
|
|
ts_str = os.path.splitext(f)[0].split('_')[-1]
|
|
return float(ts_str)
|
|
except (ValueError, IndexError):
|
|
return 0.0
|
|
|
|
latest_file = max(files, key=get_timestamp)
|
|
screenshot_url = f"/screenshots/{os.path.basename(latest_file)}"
|
|
|
|
# Primary fallback: Legacy static file
|
|
elif os.path.exists(f"screenshots/item_{item.id}.png"):
|
|
screenshot_url = f"/screenshots/item_{item.id}.png"
|
|
|
|
# Secondary fallback: use DB history if for some reason file scan missed but DB has record
|
|
# (This is less likely to be useful if we assume files exist, but good for safety)
|
|
if not screenshot_url:
|
|
latest_history = (
|
|
db.query(models.PriceHistory)
|
|
.filter(models.PriceHistory.item_id == item.id)
|
|
.filter(models.PriceHistory.screenshot_path.isnot(None))
|
|
.order_by(models.PriceHistory.timestamp.desc())
|
|
.first()
|
|
)
|
|
if latest_history and latest_history.screenshot_path:
|
|
filename = os.path.basename(latest_history.screenshot_path)
|
|
# Verify it exists
|
|
if os.path.exists(f"screenshots/{filename}"):
|
|
screenshot_url = f"/screenshots/{filename}"
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error determining screenshot for item {item.id}: {e}")
|
|
# Ultimate fallback
|
|
if os.path.exists(f"screenshots/item_{item.id}.png"):
|
|
screenshot_url = f"/screenshots/item_{item.id}.png"
|
|
|
|
result.append({**item.__dict__, "screenshot_url": screenshot_url})
|
|
return result
|
|
|
|
@staticmethod
|
|
def create_item(db: Session, item: schemas.ItemCreate):
|
|
logger.info(f"Creating item: {item.name} - {item.url}")
|
|
try:
|
|
validate_url(item.url)
|
|
except URLValidationError as e:
|
|
raise HTTPException(status_code=400, detail=f"Invalid URL: {e}") from e
|
|
|
|
db_item = models.Item(**item.model_dump())
|
|
db.add(db_item)
|
|
db.commit()
|
|
db.refresh(db_item)
|
|
return db_item
|
|
|
|
@staticmethod
|
|
def update_item(db: Session, item_id: int, item_update: schemas.ItemCreate):
|
|
db_item = db.query(models.Item).filter(models.Item.id == item_id).first()
|
|
if not db_item:
|
|
raise HTTPException(status_code=404, detail="Item not found")
|
|
|
|
for key, value in item_update.model_dump().items():
|
|
setattr(db_item, key, value)
|
|
|
|
db.commit()
|
|
db.refresh(db_item)
|
|
return db_item
|
|
|
|
@staticmethod
|
|
def delete_item(db: Session, item_id: int):
|
|
import glob
|
|
item = db.query(models.Item).filter(models.Item.id == item_id).first()
|
|
if not item:
|
|
raise HTTPException(status_code=404, detail="Item not found")
|
|
|
|
# Delete all associated screenshots (legacy and timestamped)
|
|
for file_path in glob.glob(f"screenshots/item_{item_id}*.png"):
|
|
try:
|
|
os.remove(file_path)
|
|
logger.info(f"Deleted screenshot: {file_path}")
|
|
except OSError as e:
|
|
logger.warning(f"Failed to delete {file_path}: {e}")
|
|
|
|
db.delete(item)
|
|
db.commit()
|
|
return {"ok": True}
|
|
|
|
@staticmethod
|
|
def get_item(db: Session, item_id: int):
|
|
return db.query(models.Item).filter(models.Item.id == item_id).first()
|
|
|
|
@staticmethod
|
|
def get_item_data_for_checking(db: Session, item_id: int):
|
|
item = db.query(models.Item).filter(models.Item.id == item_id).first()
|
|
if not item:
|
|
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,
|
|
"url": item.url,
|
|
"selector": item.selector,
|
|
"name": item.name,
|
|
"current_price": item.current_price,
|
|
"in_stock": item.in_stock,
|
|
"target_price": item.target_price,
|
|
"notification_channel": channel,
|
|
}
|
|
|
|
config = {
|
|
"smart_scroll": settings.get("smart_scroll_enabled", "false").lower() == "true",
|
|
"smart_scroll_pixels": int(settings.get("smart_scroll_pixels", "350")),
|
|
"text_context_enabled": settings.get("text_context_enabled", "false").lower() == "true",
|
|
"text_length": int(settings.get("text_context_length", "5000"))
|
|
if settings.get("text_context_enabled", "false").lower() == "true"
|
|
else 0,
|
|
"scraper_timeout": int(settings.get("scraper_timeout", "90000")),
|
|
}
|
|
return item_data, config
|
|
|
|
@staticmethod
|
|
def get_due_items(db: Session):
|
|
items = db.query(models.Item).filter(models.Item.is_active).all()
|
|
global_interval = int(SettingsService.get_setting_value(db, "refresh_interval_minutes", "60"))
|
|
due_items = []
|
|
now = datetime.now(UTC)
|
|
|
|
for item in items:
|
|
if item.is_refreshing:
|
|
continue
|
|
|
|
interval = item.check_interval_minutes if item.check_interval_minutes else global_interval
|
|
|
|
if not item.last_checked:
|
|
due_items.append((item.id, interval, -1))
|
|
continue
|
|
|
|
last_checked = (
|
|
item.last_checked.replace(tzinfo=UTC) if item.last_checked.tzinfo is None else item.last_checked
|
|
)
|
|
time_since = (now - last_checked).total_seconds() / 60
|
|
if time_since >= interval:
|
|
due_items.append((item.id, interval, int(time_since)))
|
|
|
|
return due_items
|