479 lines
14 KiB
Python
479 lines
14 KiB
Python
"""
|
|
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"
|
|
}
|