"""API endpoints for setup and unresolved folder management. Provides endpoints to: - List unresolved folders that couldn't be auto-resolved during setup - Get suggestions/search results for an unresolved folder - Resolve an unresolved folder by providing a provider key """ import json import logging from typing import Any, Optional from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel, Field from src.server.database.connection import get_db_session from src.server.database.service import AnimeSeriesService, UnresolvedFolderService from src.server.utils.dependencies import ( get_database_session, get_series_app, require_auth, ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/setup", tags=["setup"]) class UnresolvedFolderResponse(BaseModel): """Response model for an unresolved folder.""" folder_name: str = Field(..., description="Original filesystem folder name") title: str = Field(..., description="Extracted title from folder name") year: Optional[int] = Field(None, description="Extracted release year") search_attempts: int = Field(..., description="Number of search attempts made") search_suggestions: list[dict[str, Any]] = Field( default_factory=list, description="Cached search results for potential matches" ) class Config: from_attributes = True class ResolveFolderRequest(BaseModel): """Request model for resolving an unresolved folder.""" provider_key: str = Field( ..., min_length=1, max_length=255, description="Provider key to associate with this folder" ) class ResolveFolderResponse(BaseModel): """Response model for resolving an unresolved folder.""" status: str = Field(..., description="Operation status") message: str = Field(..., description="Human-readable message") folder_name: str = Field(..., description="Folder name that was resolved") key: str = Field(..., description="Provider key that was used") series_id: int = Field(..., description="Database ID of the created series") @router.get("/unresolved", response_model=list[UnresolvedFolderResponse]) async def list_unresolved_folders( db=Depends(get_database_session), ) -> list[UnresolvedFolderResponse]: """List all unresolved folders that need manual key resolution. Returns folders that couldn't be auto-resolved during setup, including cached search suggestions when available. Returns: List of UnresolvedFolderResponse objects """ folders = await UnresolvedFolderService.get_all_unresolved(db) result = [] for folder in folders: suggestions = [] if folder.last_search_result: try: suggestions = json.loads(folder.last_search_result) except json.JSONDecodeError: logger.warning( "Failed to parse search result for folder: %s", folder.folder_name ) result.append(UnresolvedFolderResponse( folder_name=folder.folder_name, title=folder.title, year=folder.year, search_attempts=folder.search_attempts, search_suggestions=suggestions, )) return result @router.get("/unresolved/{folder_name}", response_model=UnresolvedFolderResponse) async def get_unresolved_folder( folder_name: str, db=Depends(get_database_session), ) -> UnresolvedFolderResponse: """Get details for a specific unresolved folder. Args: folder_name: URL-encoded folder name to look up Returns: UnresolvedFolderResponse for the specified folder Raises: HTTPException: 404 if folder not found or already resolved """ folder = await UnresolvedFolderService.get_by_folder_name(db, folder_name) if not folder: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Unresolved folder not found: {folder_name}" ) if folder.is_resolved: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Folder already resolved: {folder_name}" ) suggestions = [] if folder.last_search_result: try: suggestions = json.loads(folder.last_search_result) except json.JSONDecodeError: pass return UnresolvedFolderResponse( folder_name=folder.folder_name, title=folder.title, year=folder.year, search_attempts=folder.search_attempts, search_suggestions=suggestions, ) @router.post("/unresolved/{folder_name}/resolve", response_model=ResolveFolderResponse) async def resolve_unresolved_folder( folder_name: str, request: ResolveFolderRequest, db=Depends(get_database_session), ) -> ResolveFolderResponse: """Resolve an unresolved folder by providing the correct provider key. This endpoint: 1. Validates the provider key format 2. Updates the UnresolvedFolder record as resolved 3. Creates the AnimeSeries record in the database 4. Returns the created series information Args: folder_name: URL-encoded folder name to resolve request: ResolveFolderRequest with the provider_key Returns: ResolveFolderResponse with created series details Raises: HTTPException: 404 if folder not found HTTPException: 400 if key is invalid or series already exists """ # Check if folder exists and is unresolved unresolved = await UnresolvedFolderService.get_by_folder_name(db, folder_name) if not unresolved: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Unresolved folder not found: {folder_name}" ) if unresolved.is_resolved: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Folder already resolved: {folder_name}" ) # Check if a series with this key already exists existing_series = await AnimeSeriesService.get_by_key(db, request.provider_key) if existing_series: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Series with key '{request.provider_key}' already exists" ) # Mark as resolved await UnresolvedFolderService.resolve(db, folder_name, request.provider_key) # Create the AnimeSeries record series = await AnimeSeriesService.create( db=db, key=request.provider_key, name=unresolved.title, site="https://aniworld.to", folder=folder_name, year=unresolved.year, loading_status="pending", episodes_loaded=False, logo_loaded=False, images_loaded=False, ) logger.info( "Resolved unresolved folder via API: %s -> key=%s (series_id=%d)", folder_name, request.provider_key, series.id ) return ResolveFolderResponse( status="success", message=f"Successfully resolved and added series: {unresolved.title}", folder_name=folder_name, key=request.provider_key, series_id=series.id, ) @router.post("/unresolved/{folder_name}/search", response_model=UnresolvedFolderResponse) async def search_unresolved_folder( folder_name: str, db=Depends(get_database_session), ) -> UnresolvedFolderResponse: """Re-search for a specific unresolved folder to get fresh suggestions. Performs a new search using the folder's title and caches the results. Args: folder_name: URL-encoded folder name to search for Returns: UnresolvedFolderResponse with updated search suggestions Raises: HTTPException: 404 if folder not found or already resolved """ from pathlib import Path folder = await UnresolvedFolderService.get_by_folder_name(db, folder_name) if not folder: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Unresolved folder not found: {folder_name}" ) if folder.is_resolved: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Folder already resolved: {folder_name}" ) # Perform search series_app = get_series_app() try: results = await series_app.search(folder.title) search_result_json = json.dumps(results) if results else "[]" except Exception as e: logger.warning( "Search failed for unresolved folder: %s, error: %s", folder_name, str(e) ) search_result_json = "[]" results = [] # Update the folder with new search results await UnresolvedFolderService.update_search_result(db, folder_name, search_result_json) return UnresolvedFolderResponse( folder_name=folder.folder_name, title=folder.title, year=folder.year, search_attempts=folder.search_attempts, search_suggestions=results, ) @router.delete("/unresolved/{folder_name}") async def delete_unresolved_folder( folder_name: str, db=Depends(get_database_session), ) -> dict[str, str]: """Delete an unresolved folder tracking record. Use this when you've manually added the series outside of this flow (e.g., via POST /api/anime/add) to clean up the unresolved tracker. Args: folder_name: URL-encoded folder name to delete Returns: Dict with status message Raises: HTTPException: 404 if folder not found """ deleted = await UnresolvedFolderService.delete(db, folder_name) if not deleted: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Unresolved folder not found: {folder_name}" ) return {"status": "success", "message": f"Deleted unresolved folder: {folder_name}"}