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
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
"""
|
||||
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))
|
||||
Reference in New Issue
Block a user