import uuid from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from ..dependencies import get_db, get_current_user, require_admin from ..models.user import User from ..models.library import Library from ..models.media_item import LibraryItem from ..models.podcast import Podcast, PodcastEpisode from ..services.podcast_feed import fetch_and_update_feed, search_podcast_feeds router = APIRouter(prefix="/api/podcasts", tags=["podcasts"]) def _episode_out(ep: PodcastEpisode) -> dict: return { "id": ep.id, "podcastId": ep.podcast_id, "title": ep.title, "description": ep.description, "episode": ep.episode_number, "season": ep.season_number, "pubDate": ep.pub_date.isoformat() if ep.pub_date else None, "duration": ep.duration_seconds, "size": ep.size_bytes, "path": ep.path, "feedEpisodeId": ep.feed_episode_id, "feedEpisodeUrl": ep.feed_episode_url, "explicit": ep.explicit, "addedAt": int(ep.created_at.timestamp() * 1000) if ep.created_at else 0, } def _podcast_out(podcast: Podcast, item: LibraryItem, episodes: list[PodcastEpisode]) -> dict: return { "id": item.id, "libraryId": item.library_id, "title": item.title, "author": item.author or podcast.author, "description": item.description, "cover": f"/api/items/{item.id}/cover" if item.cover_path else None, "feedUrl": podcast.feed_url, "feedLastChecked": podcast.feed_last_checked.isoformat() if podcast.feed_last_checked else None, "updateIntervalHours": podcast.update_interval_hours, "tags": item.tags or [], "episodes": [_episode_out(ep) for ep in episodes], "numEpisodes": len(episodes), "matchedSource": item.matched_source, } @router.get("") async def list_podcasts( library_id: str | None = None, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): query = select(LibraryItem).where(LibraryItem.media_type == "podcast") if library_id: query = query.where(LibraryItem.library_id == library_id) items_result = await db.execute(query) items = items_result.scalars().all() result = [] for item in items: podcast_result = await db.execute(select(Podcast).where(Podcast.library_item_id == item.id)) podcast = podcast_result.scalar_one_or_none() if not podcast: continue ep_result = await db.execute( select(PodcastEpisode).where(PodcastEpisode.podcast_id == podcast.id) .order_by(PodcastEpisode.pub_date.desc()) ) episodes = ep_result.scalars().all() result.append(_podcast_out(podcast, item, episodes)) return {"podcasts": result} @router.get("/search") async def search_feeds( q: str, current_user: User = Depends(get_current_user), ): results = await search_podcast_feeds(q) return {"results": results} @router.get("/{item_id}") async def get_podcast( item_id: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): item_result = await db.execute(select(LibraryItem).where(LibraryItem.id == item_id)) item = item_result.scalar_one_or_none() if not item or item.media_type != "podcast": raise HTTPException(status_code=404, detail="Podcast not found") podcast_result = await db.execute(select(Podcast).where(Podcast.library_item_id == item_id)) podcast = podcast_result.scalar_one_or_none() if not podcast: raise HTTPException(status_code=404, detail="Podcast data not found") ep_result = await db.execute( select(PodcastEpisode).where(PodcastEpisode.podcast_id == podcast.id) .order_by(PodcastEpisode.pub_date.desc()) ) episodes = ep_result.scalars().all() return _podcast_out(podcast, item, episodes) @router.patch("/{item_id}/feed") async def set_feed_url( item_id: str, body: dict, background_tasks: BackgroundTasks, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): item_result = await db.execute(select(LibraryItem).where(LibraryItem.id == item_id)) item = item_result.scalar_one_or_none() if not item: raise HTTPException(status_code=404, detail="Item not found") podcast_result = await db.execute(select(Podcast).where(Podcast.library_item_id == item_id)) podcast = podcast_result.scalar_one_or_none() if not podcast: podcast = Podcast(library_item_id=item_id) db.add(podcast) feed_url = body.get("feedUrl", "") if not feed_url: raise HTTPException(status_code=400, detail="feedUrl required") podcast.feed_url = feed_url if body.get("updateIntervalHours"): podcast.update_interval_hours = int(body["updateIntervalHours"]) await db.commit() background_tasks.add_task(fetch_and_update_feed, item_id) return {"success": True, "message": "Feed wird aktualisiert..."} @router.post("/{item_id}/update-feed") async def update_feed( item_id: str, background_tasks: BackgroundTasks, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): podcast_result = await db.execute(select(Podcast).where(Podcast.library_item_id == item_id)) podcast = podcast_result.scalar_one_or_none() if not podcast or not podcast.feed_url: raise HTTPException(status_code=400, detail="Kein Feed konfiguriert") background_tasks.add_task(fetch_and_update_feed, item_id) return {"success": True} @router.get("/{item_id}/episodes") async def get_episodes( item_id: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): podcast_result = await db.execute(select(Podcast).where(Podcast.library_item_id == item_id)) podcast = podcast_result.scalar_one_or_none() if not podcast: raise HTTPException(status_code=404, detail="Podcast not found") ep_result = await db.execute( select(PodcastEpisode).where(PodcastEpisode.podcast_id == podcast.id) .order_by(PodcastEpisode.pub_date.desc()) ) return {"episodes": [_episode_out(ep) for ep in ep_result.scalars().all()]}