Files
ohm_streaming/app/routers/router_sonarr.py
T
root d4d8d8a3b6
CI / Test (Python 3.11) (push) Has been cancelled
CI / Test (Python 3.12) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Type Check (push) Has been cancelled
CI / Summary (push) Has been cancelled
refactor: migrate main.py to modular routers and add project roadmap
- Migrated monolithic main.py to feature-scoped routers in app/routers/
- Added GEMINI.md for project context and AI instructional guidelines
- Updated README.md with a comprehensive modernization plan (SQL migration, robust scraping DSL, frontend modernization)
- Improved authentication with cookie support and modular JS
- Updated test suite and documentation
2026-03-24 10:12:04 +00:00

254 lines
8.6 KiB
Python

"""
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():
"""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):
"""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():
"""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):
"""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):
"""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):
"""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)"),
):
"""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)"),
):
"""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"),
):
"""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,
):
"""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))