478
web/backend/routers/easynews.py
Normal file
478
web/backend/routers/easynews.py
Normal file
@@ -0,0 +1,478 @@
|
||||
"""
|
||||
Easynews Router
|
||||
|
||||
Handles Easynews integration:
|
||||
- Configuration management (credentials, proxy settings)
|
||||
- Search term management
|
||||
- Manual check triggers
|
||||
- Results browsing and downloads
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
|
||||
from ..core.dependencies import get_current_user, require_admin, get_app_state
|
||||
from ..core.exceptions import handle_exceptions
|
||||
from modules.universal_logger import get_logger
|
||||
|
||||
logger = get_logger('API')
|
||||
|
||||
router = APIRouter(prefix="/api/easynews", tags=["Easynews"])
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
|
||||
# Thread pool for blocking operations
|
||||
_executor = ThreadPoolExecutor(max_workers=2)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PYDANTIC MODELS
|
||||
# ============================================================================
|
||||
|
||||
class EasynewsConfigUpdate(BaseModel):
|
||||
username: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
enabled: Optional[bool] = None
|
||||
check_interval_hours: Optional[int] = None
|
||||
auto_download: Optional[bool] = None
|
||||
min_quality: Optional[str] = None
|
||||
proxy_enabled: Optional[bool] = None
|
||||
proxy_type: Optional[str] = None
|
||||
proxy_host: Optional[str] = None
|
||||
proxy_port: Optional[int] = None
|
||||
proxy_username: Optional[str] = None
|
||||
proxy_password: Optional[str] = None
|
||||
notifications_enabled: Optional[bool] = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
def _get_monitor():
|
||||
"""Get the Easynews monitor instance."""
|
||||
from modules.easynews_monitor import EasynewsMonitor
|
||||
app_state = get_app_state()
|
||||
db_path = str(app_state.db.db_path) # Convert Path to string
|
||||
return EasynewsMonitor(db_path)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CONFIGURATION ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/config")
|
||||
@limiter.limit("30/minute")
|
||||
@handle_exceptions
|
||||
async def get_config(
|
||||
request: Request,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Get Easynews configuration (passwords masked)."""
|
||||
monitor = _get_monitor()
|
||||
config = monitor.get_config()
|
||||
|
||||
# Mask password for security
|
||||
if config.get('password'):
|
||||
config['password'] = '********'
|
||||
if config.get('proxy_password'):
|
||||
config['proxy_password'] = '********'
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"config": config
|
||||
}
|
||||
|
||||
|
||||
@router.put("/config")
|
||||
@limiter.limit("10/minute")
|
||||
@handle_exceptions
|
||||
async def update_config(
|
||||
request: Request,
|
||||
config: EasynewsConfigUpdate,
|
||||
current_user: Dict = Depends(require_admin)
|
||||
):
|
||||
"""Update Easynews configuration."""
|
||||
monitor = _get_monitor()
|
||||
|
||||
# Build update kwargs
|
||||
kwargs = {}
|
||||
if config.username is not None:
|
||||
kwargs['username'] = config.username
|
||||
if config.password is not None and config.password != '********':
|
||||
kwargs['password'] = config.password
|
||||
if config.enabled is not None:
|
||||
kwargs['enabled'] = config.enabled
|
||||
if config.check_interval_hours is not None:
|
||||
kwargs['check_interval_hours'] = config.check_interval_hours
|
||||
if config.auto_download is not None:
|
||||
kwargs['auto_download'] = config.auto_download
|
||||
if config.min_quality is not None:
|
||||
kwargs['min_quality'] = config.min_quality
|
||||
if config.proxy_enabled is not None:
|
||||
kwargs['proxy_enabled'] = config.proxy_enabled
|
||||
if config.proxy_type is not None:
|
||||
kwargs['proxy_type'] = config.proxy_type
|
||||
if config.proxy_host is not None:
|
||||
kwargs['proxy_host'] = config.proxy_host
|
||||
if config.proxy_port is not None:
|
||||
kwargs['proxy_port'] = config.proxy_port
|
||||
if config.proxy_username is not None:
|
||||
kwargs['proxy_username'] = config.proxy_username
|
||||
if config.proxy_password is not None and config.proxy_password != '********':
|
||||
kwargs['proxy_password'] = config.proxy_password
|
||||
if config.notifications_enabled is not None:
|
||||
kwargs['notifications_enabled'] = config.notifications_enabled
|
||||
|
||||
if not kwargs:
|
||||
return {"success": False, "message": "No updates provided"}
|
||||
|
||||
success = monitor.update_config(**kwargs)
|
||||
|
||||
return {
|
||||
"success": success,
|
||||
"message": "Configuration updated" if success else "Update failed"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/test")
|
||||
@limiter.limit("5/minute")
|
||||
@handle_exceptions
|
||||
async def test_connection(
|
||||
request: Request,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Test Easynews connection with current credentials."""
|
||||
monitor = _get_monitor()
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(_executor, monitor.test_connection)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CELEBRITY ENDPOINTS (uses tracked celebrities from Appearances)
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/celebrities")
|
||||
@limiter.limit("30/minute")
|
||||
@handle_exceptions
|
||||
async def get_celebrities(
|
||||
request: Request,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Get all tracked celebrities that will be searched on Easynews."""
|
||||
monitor = _get_monitor()
|
||||
celebrities = monitor.get_celebrities()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"celebrities": celebrities,
|
||||
"count": len(celebrities)
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# RESULTS ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/results")
|
||||
@limiter.limit("30/minute")
|
||||
@handle_exceptions
|
||||
async def get_results(
|
||||
request: Request,
|
||||
status: Optional[str] = None,
|
||||
celebrity_id: Optional[int] = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Get discovered results with optional filters."""
|
||||
monitor = _get_monitor()
|
||||
results = monitor.get_results(
|
||||
status=status,
|
||||
celebrity_id=celebrity_id,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
# Get total count
|
||||
total = monitor.get_result_count(status=status)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"results": results,
|
||||
"count": len(results),
|
||||
"total": total
|
||||
}
|
||||
|
||||
|
||||
@router.post("/results/{result_id}/status")
|
||||
@limiter.limit("30/minute")
|
||||
@handle_exceptions
|
||||
async def update_result_status(
|
||||
request: Request,
|
||||
result_id: int,
|
||||
status: str,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Update a result's status (e.g., mark as ignored)."""
|
||||
valid_statuses = ['new', 'downloaded', 'ignored', 'failed']
|
||||
if status not in valid_statuses:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid status. Must be one of: {valid_statuses}")
|
||||
|
||||
monitor = _get_monitor()
|
||||
success = monitor.update_result_status(result_id, status)
|
||||
|
||||
return {
|
||||
"success": success,
|
||||
"message": f"Status updated to {status}" if success else "Update failed"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/results/{result_id}/download")
|
||||
@limiter.limit("10/minute")
|
||||
@handle_exceptions
|
||||
async def download_result(
|
||||
request: Request,
|
||||
result_id: int,
|
||||
background_tasks: BackgroundTasks,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Start downloading a result."""
|
||||
monitor = _get_monitor()
|
||||
|
||||
def do_download():
|
||||
return monitor.download_result(result_id)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(_executor, do_download)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CHECK ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/status")
|
||||
@limiter.limit("60/minute")
|
||||
@handle_exceptions
|
||||
async def get_status(
|
||||
request: Request,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Get current check status."""
|
||||
monitor = _get_monitor()
|
||||
status = monitor.get_status()
|
||||
config = monitor.get_config()
|
||||
celebrity_count = monitor.get_celebrity_count()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"status": status,
|
||||
"last_check": config.get('last_check'),
|
||||
"enabled": config.get('enabled', False),
|
||||
"has_credentials": config.get('has_credentials', False),
|
||||
"celebrity_count": celebrity_count,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/check")
|
||||
@limiter.limit("5/minute")
|
||||
@handle_exceptions
|
||||
async def trigger_check(
|
||||
request: Request,
|
||||
background_tasks: BackgroundTasks,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Trigger a manual check for all tracked celebrities."""
|
||||
monitor = _get_monitor()
|
||||
status = monitor.get_status()
|
||||
|
||||
if status.get('is_running'):
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Check already in progress"
|
||||
}
|
||||
|
||||
def do_check():
|
||||
return monitor.check_all_celebrities()
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
background_tasks.add_task(loop.run_in_executor, _executor, do_check)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Check started"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/check/{search_id}")
|
||||
@limiter.limit("5/minute")
|
||||
@handle_exceptions
|
||||
async def trigger_single_check(
|
||||
request: Request,
|
||||
search_id: int,
|
||||
background_tasks: BackgroundTasks,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Trigger a manual check for a specific search term."""
|
||||
monitor = _get_monitor()
|
||||
status = monitor.get_status()
|
||||
|
||||
if status.get('is_running'):
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Check already in progress"
|
||||
}
|
||||
|
||||
# Verify search exists
|
||||
search = monitor.get_search(search_id)
|
||||
if not search:
|
||||
raise HTTPException(status_code=404, detail="Search not found")
|
||||
|
||||
def do_check():
|
||||
return monitor.check_single_search(search_id)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
background_tasks.add_task(loop.run_in_executor, _executor, do_check)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Check started for: {search['search_term']}"
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SEARCH MANAGEMENT ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
class EasynewsSearchCreate(BaseModel):
|
||||
search_term: str
|
||||
media_type: Optional[str] = 'any'
|
||||
tmdb_id: Optional[int] = None
|
||||
tmdb_title: Optional[str] = None
|
||||
poster_url: Optional[str] = None
|
||||
|
||||
|
||||
class EasynewsSearchUpdate(BaseModel):
|
||||
search_term: Optional[str] = None
|
||||
media_type: Optional[str] = None
|
||||
enabled: Optional[bool] = None
|
||||
tmdb_id: Optional[int] = None
|
||||
tmdb_title: Optional[str] = None
|
||||
poster_url: Optional[str] = None
|
||||
|
||||
|
||||
@router.get("/searches")
|
||||
@limiter.limit("30/minute")
|
||||
@handle_exceptions
|
||||
async def get_searches(
|
||||
request: Request,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Get all saved search terms."""
|
||||
monitor = _get_monitor()
|
||||
searches = monitor.get_all_searches()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"searches": searches,
|
||||
"count": len(searches)
|
||||
}
|
||||
|
||||
|
||||
@router.post("/searches")
|
||||
@limiter.limit("10/minute")
|
||||
@handle_exceptions
|
||||
async def add_search(
|
||||
request: Request,
|
||||
search: EasynewsSearchCreate,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Add a new search term."""
|
||||
monitor = _get_monitor()
|
||||
|
||||
search_id = monitor.add_search(
|
||||
search_term=search.search_term,
|
||||
media_type=search.media_type,
|
||||
tmdb_id=search.tmdb_id,
|
||||
tmdb_title=search.tmdb_title,
|
||||
poster_url=search.poster_url
|
||||
)
|
||||
|
||||
if search_id:
|
||||
return {
|
||||
"success": True,
|
||||
"id": search_id,
|
||||
"message": f"Search term '{search.search_term}' added"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to add search term"
|
||||
}
|
||||
|
||||
|
||||
@router.put("/searches/{search_id}")
|
||||
@limiter.limit("10/minute")
|
||||
@handle_exceptions
|
||||
async def update_search(
|
||||
request: Request,
|
||||
search_id: int,
|
||||
updates: EasynewsSearchUpdate,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Update an existing search term."""
|
||||
monitor = _get_monitor()
|
||||
|
||||
# Build update kwargs
|
||||
kwargs = {}
|
||||
if updates.search_term is not None:
|
||||
kwargs['search_term'] = updates.search_term
|
||||
if updates.media_type is not None:
|
||||
kwargs['media_type'] = updates.media_type
|
||||
if updates.enabled is not None:
|
||||
kwargs['enabled'] = updates.enabled
|
||||
if updates.tmdb_id is not None:
|
||||
kwargs['tmdb_id'] = updates.tmdb_id
|
||||
if updates.tmdb_title is not None:
|
||||
kwargs['tmdb_title'] = updates.tmdb_title
|
||||
if updates.poster_url is not None:
|
||||
kwargs['poster_url'] = updates.poster_url
|
||||
|
||||
if not kwargs:
|
||||
return {"success": False, "message": "No updates provided"}
|
||||
|
||||
success = monitor.update_search(search_id, **kwargs)
|
||||
|
||||
return {
|
||||
"success": success,
|
||||
"message": "Search updated" if success else "Update failed"
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/searches/{search_id}")
|
||||
@limiter.limit("10/minute")
|
||||
@handle_exceptions
|
||||
async def delete_search(
|
||||
request: Request,
|
||||
search_id: int,
|
||||
current_user: Dict = Depends(get_current_user)
|
||||
):
|
||||
"""Delete a search term."""
|
||||
monitor = _get_monitor()
|
||||
success = monitor.delete_search(search_id)
|
||||
|
||||
return {
|
||||
"success": success,
|
||||
"message": "Search deleted" if success else "Delete failed"
|
||||
}
|
||||
Reference in New Issue
Block a user