rapid interations of server side app and firefox extension
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
"""Source management API endpoints."""
|
||||
|
||||
from quart import Blueprint, request, jsonify, current_app
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.models.source import Source
|
||||
from app.models.subscription import Subscription
|
||||
from app.tasks.downloads import download_source
|
||||
|
||||
bp = Blueprint("sources", __name__)
|
||||
|
||||
|
||||
@bp.route("", methods=["GET"])
|
||||
async def list_sources():
|
||||
"""List all sources with optional filtering and pagination."""
|
||||
# Query parameters
|
||||
platform = request.args.get("platform")
|
||||
subscription_id = request.args.get("subscription_id")
|
||||
enabled = request.args.get("enabled")
|
||||
page = int(request.args.get("page", 1))
|
||||
per_page = min(int(request.args.get("per_page", 50)), 100)
|
||||
|
||||
async with current_app.db_engine.connect() as conn:
|
||||
session = AsyncSession(bind=conn)
|
||||
|
||||
# Build query with subscription eager loading
|
||||
query = select(Source).options(selectinload(Source.subscription))
|
||||
|
||||
if platform:
|
||||
query = query.where(Source.platform == platform)
|
||||
if subscription_id:
|
||||
query = query.where(Source.subscription_id == int(subscription_id))
|
||||
if enabled is not None:
|
||||
query = query.where(Source.enabled == (enabled.lower() == "true"))
|
||||
|
||||
# Get total count
|
||||
count_query = select(func.count()).select_from(Source)
|
||||
if platform:
|
||||
count_query = count_query.where(Source.platform == platform)
|
||||
if subscription_id:
|
||||
count_query = count_query.where(Source.subscription_id == int(subscription_id))
|
||||
if enabled is not None:
|
||||
count_query = count_query.where(Source.enabled == (enabled.lower() == "true"))
|
||||
|
||||
total_result = await session.execute(count_query)
|
||||
total = total_result.scalar()
|
||||
|
||||
# Apply pagination - order by subscription name, then platform
|
||||
query = query.join(Subscription).order_by(Subscription.name, Source.platform)
|
||||
query = query.offset((page - 1) * per_page).limit(per_page)
|
||||
|
||||
result = await session.execute(query)
|
||||
sources = result.scalars().all()
|
||||
|
||||
return jsonify({
|
||||
"items": [s.to_dict() for s in sources],
|
||||
"total": total,
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"pages": (total + per_page - 1) // per_page if total > 0 else 0,
|
||||
})
|
||||
|
||||
|
||||
@bp.route("", methods=["POST"])
|
||||
async def create_source():
|
||||
"""Create a new source for a subscription."""
|
||||
data = await request.get_json()
|
||||
|
||||
# Validate required fields
|
||||
required = ["subscription_id", "platform", "url"]
|
||||
missing = [f for f in required if not data.get(f)]
|
||||
if missing:
|
||||
return jsonify({"error": f"Missing required fields: {', '.join(missing)}"}), 400
|
||||
|
||||
# Validate platform
|
||||
valid_platforms = ["patreon", "subscribestar", "hentaifoundry", "discord"]
|
||||
if data["platform"] not in valid_platforms:
|
||||
return jsonify({"error": f"Invalid platform. Must be one of: {', '.join(valid_platforms)}"}), 400
|
||||
|
||||
async with current_app.db_engine.connect() as conn:
|
||||
session = AsyncSession(bind=conn)
|
||||
|
||||
# Check subscription exists
|
||||
sub_result = await session.execute(
|
||||
select(Subscription).where(Subscription.id == data["subscription_id"])
|
||||
)
|
||||
subscription = sub_result.scalar()
|
||||
if not subscription:
|
||||
return jsonify({"error": "Subscription not found"}), 404
|
||||
|
||||
# Check for duplicate platform+url
|
||||
existing = await session.execute(
|
||||
select(Source).where(
|
||||
Source.platform == data["platform"],
|
||||
Source.url == data["url"]
|
||||
)
|
||||
)
|
||||
if existing.scalar():
|
||||
return jsonify({"error": "Source with this platform and URL already exists"}), 409
|
||||
|
||||
source = Source(
|
||||
subscription_id=data["subscription_id"],
|
||||
platform=data["platform"],
|
||||
url=data["url"],
|
||||
enabled=data.get("enabled", True),
|
||||
check_interval=data.get("check_interval", 3600),
|
||||
metadata_=data.get("metadata", {}),
|
||||
)
|
||||
|
||||
session.add(source)
|
||||
await session.commit()
|
||||
await session.refresh(source)
|
||||
|
||||
current_app.logger.info(f"Created source: {subscription.name}/{source.platform}")
|
||||
return jsonify(source.to_dict()), 201
|
||||
|
||||
|
||||
@bp.route("/<int:source_id>", methods=["GET"])
|
||||
async def get_source(source_id: int):
|
||||
"""Get a single source by ID."""
|
||||
async with current_app.db_engine.connect() as conn:
|
||||
session = AsyncSession(bind=conn)
|
||||
|
||||
result = await session.execute(
|
||||
select(Source)
|
||||
.options(selectinload(Source.subscription))
|
||||
.where(Source.id == source_id)
|
||||
)
|
||||
source = result.scalar()
|
||||
|
||||
if not source:
|
||||
return jsonify({"error": "Source not found"}), 404
|
||||
|
||||
return jsonify(source.to_dict())
|
||||
|
||||
|
||||
@bp.route("/<int:source_id>", methods=["PATCH"])
|
||||
async def update_source(source_id: int):
|
||||
"""Update a source."""
|
||||
data = await request.get_json()
|
||||
|
||||
async with current_app.db_engine.connect() as conn:
|
||||
session = AsyncSession(bind=conn)
|
||||
|
||||
result = await session.execute(
|
||||
select(Source)
|
||||
.options(selectinload(Source.subscription))
|
||||
.where(Source.id == source_id)
|
||||
)
|
||||
source = result.scalar()
|
||||
|
||||
if not source:
|
||||
return jsonify({"error": "Source not found"}), 404
|
||||
|
||||
# Update allowed fields
|
||||
allowed_fields = ["enabled", "check_interval", "metadata"]
|
||||
for field in allowed_fields:
|
||||
if field in data:
|
||||
if field == "metadata":
|
||||
source.metadata_ = data[field]
|
||||
else:
|
||||
setattr(source, field, data[field])
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(source)
|
||||
|
||||
current_app.logger.info(f"Updated source: {source.subscription.name}/{source.platform}")
|
||||
return jsonify(source.to_dict())
|
||||
|
||||
|
||||
@bp.route("/<int:source_id>", methods=["DELETE"])
|
||||
async def delete_source(source_id: int):
|
||||
"""Delete a source."""
|
||||
async with current_app.db_engine.connect() as conn:
|
||||
session = AsyncSession(bind=conn)
|
||||
|
||||
result = await session.execute(
|
||||
select(Source)
|
||||
.options(selectinload(Source.subscription))
|
||||
.where(Source.id == source_id)
|
||||
)
|
||||
source = result.scalar()
|
||||
|
||||
if not source:
|
||||
return jsonify({"error": "Source not found"}), 404
|
||||
|
||||
info = f"{source.subscription.name}/{source.platform}"
|
||||
await session.delete(source)
|
||||
await session.commit()
|
||||
|
||||
current_app.logger.info(f"Deleted source: {info}")
|
||||
return "", 204
|
||||
|
||||
|
||||
@bp.route("/<int:source_id>/check", methods=["POST"])
|
||||
async def trigger_check(source_id: int):
|
||||
"""Trigger an immediate download check for a source."""
|
||||
async with current_app.db_engine.connect() as conn:
|
||||
session = AsyncSession(bind=conn)
|
||||
|
||||
result = await session.execute(
|
||||
select(Source)
|
||||
.options(selectinload(Source.subscription))
|
||||
.where(Source.id == source_id)
|
||||
)
|
||||
source = result.scalar()
|
||||
|
||||
if not source:
|
||||
return jsonify({"error": "Source not found"}), 404
|
||||
|
||||
# Queue Celery task for immediate download
|
||||
task = download_source.delay(source_id)
|
||||
|
||||
current_app.logger.info(f"Triggered check for source: {source.subscription.name}/{source.platform}")
|
||||
return jsonify({
|
||||
"message": "Check queued",
|
||||
"source_id": source_id,
|
||||
"subscription_name": source.subscription.name,
|
||||
"platform": source.platform,
|
||||
"task_id": task.id,
|
||||
}), 202
|
||||
Reference in New Issue
Block a user