136 lines
4.3 KiB
Python
136 lines
4.3 KiB
Python
|
|
"""Feeds router."""
|
||
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||
|
|
from sqlalchemy import func, select
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
|
||
|
|
from app.api.deps import get_current_user, get_db
|
||
|
|
from app.models.feed import Feed
|
||
|
|
from app.models.user import User
|
||
|
|
from app.schemas.common import MessageResponse, PaginatedResponse, PaginationParams
|
||
|
|
from app.schemas.feed import FeedCreate, FeedOut, FeedUpdate
|
||
|
|
|
||
|
|
router = APIRouter(prefix="/feeds", tags=["feeds"])
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("", response_model=PaginatedResponse)
|
||
|
|
async def list_feeds(
|
||
|
|
pagination: PaginationParams = Depends(),
|
||
|
|
category: str | None = Query(None),
|
||
|
|
search: str | None = Query(None),
|
||
|
|
is_active: bool | None = Query(None),
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
current_user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""List RSS feeds with pagination and filters."""
|
||
|
|
query = select(Feed)
|
||
|
|
|
||
|
|
if category:
|
||
|
|
query = query.where(Feed.category == category)
|
||
|
|
if search:
|
||
|
|
query = query.where(
|
||
|
|
Feed.title.ilike(f"%{search}%")
|
||
|
|
| Feed.url.ilike(f"%{search}%")
|
||
|
|
| Feed.description.ilike(f"%{search}%")
|
||
|
|
)
|
||
|
|
if is_active is not None:
|
||
|
|
query = query.where(Feed.is_active == is_active)
|
||
|
|
|
||
|
|
# Get total count
|
||
|
|
count_query = select(func.count()).select_from(query.subquery())
|
||
|
|
total = (await db.execute(count_query)).scalar_one()
|
||
|
|
|
||
|
|
# Get paginated items
|
||
|
|
query = query.offset(pagination.skip).limit(pagination.limit).order_by(Feed.created_at.desc())
|
||
|
|
result = await db.execute(query)
|
||
|
|
items = result.scalars().all()
|
||
|
|
|
||
|
|
return {
|
||
|
|
"total": total,
|
||
|
|
"items": [FeedOut.model_validate(item) for item in items],
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/{feed_id}", response_model=FeedOut)
|
||
|
|
async def get_feed(
|
||
|
|
feed_id: str,
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
current_user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""Get a single feed by ID."""
|
||
|
|
feed = await db.get(Feed, feed_id)
|
||
|
|
if not feed:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Feed not found")
|
||
|
|
return FeedOut.model_validate(feed)
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("", response_model=FeedOut, status_code=status.HTTP_201_CREATED)
|
||
|
|
async def create_feed(
|
||
|
|
feed_in: FeedCreate,
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
current_user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""Create a new RSS feed."""
|
||
|
|
# Check URL uniqueness
|
||
|
|
result = await db.execute(select(Feed).where(Feed.url == str(feed_in.url)))
|
||
|
|
existing = result.scalar_one_or_none()
|
||
|
|
if existing:
|
||
|
|
raise HTTPException(
|
||
|
|
status_code=status.HTTP_409_CONFLICT,
|
||
|
|
detail="Feed with this URL already exists",
|
||
|
|
)
|
||
|
|
|
||
|
|
feed = Feed(
|
||
|
|
url=str(feed_in.url),
|
||
|
|
title=feed_in.title or "",
|
||
|
|
description=feed_in.description or "",
|
||
|
|
category=feed_in.category or "",
|
||
|
|
is_active=feed_in.is_active,
|
||
|
|
fetch_interval_minutes=feed_in.fetch_interval_minutes,
|
||
|
|
priority=feed_in.priority,
|
||
|
|
parser_config=feed_in.parser_config,
|
||
|
|
proxy_policy=feed_in.proxy_policy,
|
||
|
|
)
|
||
|
|
db.add(feed)
|
||
|
|
await db.commit()
|
||
|
|
await db.refresh(feed)
|
||
|
|
return FeedOut.model_validate(feed)
|
||
|
|
|
||
|
|
|
||
|
|
@router.put("/{feed_id}", response_model=FeedOut)
|
||
|
|
async def update_feed(
|
||
|
|
feed_id: str,
|
||
|
|
feed_in: FeedUpdate,
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
current_user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""Update an existing feed."""
|
||
|
|
feed = await db.get(Feed, feed_id)
|
||
|
|
if not feed:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Feed not found")
|
||
|
|
|
||
|
|
update_data = feed_in.model_dump(exclude_unset=True)
|
||
|
|
for field, value in update_data.items():
|
||
|
|
if field == "url" and value is not None:
|
||
|
|
value = str(value)
|
||
|
|
setattr(feed, field, value)
|
||
|
|
|
||
|
|
await db.commit()
|
||
|
|
await db.refresh(feed)
|
||
|
|
return FeedOut.model_validate(feed)
|
||
|
|
|
||
|
|
|
||
|
|
@router.delete("/{feed_id}", response_model=MessageResponse)
|
||
|
|
async def delete_feed(
|
||
|
|
feed_id: str,
|
||
|
|
db: AsyncSession = Depends(get_db),
|
||
|
|
current_user: User = Depends(get_current_user),
|
||
|
|
):
|
||
|
|
"""Delete a feed."""
|
||
|
|
feed = await db.get(Feed, feed_id)
|
||
|
|
if not feed:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Feed not found")
|
||
|
|
|
||
|
|
await db.delete(feed)
|
||
|
|
await db.commit()
|
||
|
|
return {"message": "Feed deleted successfully"}
|