""" Sonarr integration routes for Ohm Stream Downloader API. """ import logging from typing import Optional from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query from fastapi.requests import Request from fastapi.responses import JSONResponse from pydantic import BaseModel from app.models import DownloadRequest from app.models.auth import User from app.models.sonarr import SonarrConfig, SonarrDownloadRequest, SonarrMapping from app.routers.router_auth import get_current_user_from_token logger = logging.getLogger(__name__) router = APIRouter(prefix="/api", tags=["sonarr"]) def get_sonarr_handler(): from app.sonarr_handler import get_sonarr_handler return get_sonarr_handler() @router.post("/webhook/sonarr") async def sonarr_webhook(request: Request): """Receive and process Sonarr webhook events""" from app.models.sonarr import SonarrWebhookPayload sonarr_handler = get_sonarr_handler() body = await request.body() signature = request.headers.get("X-Sonarr-Event", "") if not sonarr_handler.verify_hmac(body, signature): logger.warning("Invalid HMAC signature for Sonarr webhook") raise HTTPException(status_code=403, detail="Invalid signature") try: payload_data = await request.json() payload = SonarrWebhookPayload(**payload_data) result = await sonarr_handler.process_webhook(payload) return JSONResponse(content=result, status_code=200) except Exception as e: logger.error(f"Error processing Sonarr webhook: {e}", exc_info=True) raise HTTPException(status_code=422, detail=f"Invalid payload: {str(e)}") @router.post("/webhook/test/sonarr") async def test_sonarr_webhook(request: Request): """Test endpoint for Sonarr webhook configuration""" try: payload = await request.json() logger.info( f"Received test Sonarr webhook: {payload.get('eventType', 'unknown')}" ) return { "status": "ok", "message": "Test webhook received successfully", "received_payload": payload, } except Exception as e: logger.error(f"Error in test webhook: {e}") return {"status": "error", "message": str(e)} @router.get("/sonarr/config") async def get_sonarr_config( current_user: User = Depends(get_current_user_from_token), ): """Get Sonarr webhook configuration""" sonarr_handler = get_sonarr_handler() return sonarr_handler.get_config() @router.put("/sonarr/config") async def update_sonarr_config( config: SonarrConfig, current_user: User = Depends(get_current_user_from_token), ): """Update Sonarr webhook configuration""" sonarr_handler = get_sonarr_handler() try: updated_config = sonarr_handler.update_config(config) return {"status": "success", "config": updated_config} except Exception as e: logger.error(f"Error updating Sonarr config: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/sonarr/mappings") async def get_sonarr_mappings( current_user: User = Depends(get_current_user_from_token), ): """Get all Sonarr to anime mappings""" sonarr_handler = get_sonarr_handler() return sonarr_handler.get_mappings() @router.get("/sonarr/mappings/{series_id}") async def get_sonarr_mapping( series_id: int, current_user: User = Depends(get_current_user_from_token), ): """Get specific mapping by Sonarr series ID""" sonarr_handler = get_sonarr_handler() mapping = sonarr_handler.get_mapping(series_id) if not mapping: raise HTTPException(status_code=404, detail="Mapping not found") return mapping @router.post("/sonarr/mappings") async def create_sonarr_mapping( mapping: SonarrMapping, current_user: User = Depends(get_current_user_from_token), ): """Create or update a Sonarr to anime mapping""" sonarr_handler = get_sonarr_handler() try: mapping = sonarr_handler.add_mapping(mapping) return {"status": "success", "mapping": mapping} except Exception as e: logger.error(f"Error creating mapping: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.delete("/sonarr/mappings/{series_id}") async def delete_sonarr_mapping( series_id: int, current_user: User = Depends(get_current_user_from_token), ): """Delete a Sonarr mapping""" sonarr_handler = get_sonarr_handler() success = sonarr_handler.delete_mapping(series_id) if not success: raise HTTPException(status_code=404, detail="Mapping not found") return {"status": "success", "message": f"Mapping for series {series_id} deleted"} @router.get("/sonarr/search") async def search_anime_for_sonarr( q: str = Query(..., description="Series title to search"), provider: str = Query("anime-sama", description="Anime provider to search"), lang: str = Query("vostfr", description="Language (vostfr, vf)"), current_user: User = Depends(get_current_user_from_token), ): """Search for anime on providers to create Sonarr mappings""" sonarr_handler = get_sonarr_handler() try: results = await sonarr_handler.search_anime_by_title(q, provider, lang) return { "status": "success", "query": q, "provider": provider, "lang": lang, "results": results, } except Exception as e: logger.error(f"Error searching anime: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/sonarr/episodes") async def get_anime_episodes( url: str = Query(..., description="Anime URL from provider"), provider: str = Query("anime-sama", description="Anime provider"), lang: str = Query("vostfr", description="Language (vostfr, vf)"), current_user: User = Depends(get_current_user_from_token), ): """Get episode list for anime""" sonarr_handler = get_sonarr_handler() try: episodes = await sonarr_handler.get_episodes_for_anime(url, provider, lang) return { "status": "success", "url": url, "provider": provider, "lang": lang, "episodes": episodes, } except Exception as e: logger.error(f"Error getting episodes: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/sonarr/suggest") async def suggest_anime_mapping( sonarr_title: str = Query(..., description="Sonarr series title"), provider: str = Query("anime-sama", description="Anime provider"), lang: str = Query("vostfr", description="Language"), current_user: User = Depends(get_current_user_from_token), ): """Suggest possible anime mappings based on Sonarr series title""" sonarr_handler = get_sonarr_handler() try: suggestions = await sonarr_handler.suggest_mapping(sonarr_title, provider, lang) return { "status": "success", "sonarr_title": sonarr_title, "provider": provider, "lang": lang, "suggestions": suggestions, } except Exception as e: logger.error(f"Error getting suggestions: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/sonarr/download") async def trigger_sonarr_download( request: SonarrDownloadRequest, background_tasks: BackgroundTasks, current_user: User = Depends(get_current_user_from_token), ): """Manually trigger a download based on Sonarr information""" from main import download_manager sonarr_handler = get_sonarr_handler() mapping = sonarr_handler.get_mapping(request.sonarr_series_id) if not mapping: raise HTTPException( status_code=404, detail=f"No mapping found for series {request.sonarr_series_id}. Create a mapping first.", ) try: episodes = await sonarr_handler.get_episodes_for_anime( mapping.anime_url, request.provider or mapping.anime_provider, request.lang or mapping.lang, ) target_episode = None for ep in episodes: ep_num = ep.get("episode", 0) season_num = ep.get("season", 1) if ep_num == request.episode_number and season_num == request.season_number: target_episode = ep break if not target_episode: raise HTTPException( status_code=404, detail=f"Episode S{request.season_number}E{request.episode_number} not found", ) episode_url = target_episode.get("url") if not episode_url: raise HTTPException(status_code=400, detail="Episode URL not found") download_request = DownloadRequest( url=episode_url, filename=f"{mapping.anime_title} - S{request.season_number}E{request.episode_number}.mp4", ) task = download_manager.create_task(download_request) background_tasks.add_task(download_manager.start_download, task.id) return { "status": "success", "task_id": task.id, "message": f"Download started for {mapping.anime_title} S{request.season_number}E{request.episode_number}", } except HTTPException: raise except Exception as e: logger.error(f"Error triggering download: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e))