d4d8d8a3b6
- 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
254 lines
8.6 KiB
Python
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))
|