212 lines
7.2 KiB
Python
212 lines
7.2 KiB
Python
"""Download history API endpoints."""
|
|
|
|
from datetime import datetime
|
|
from quart import Blueprint, request, jsonify, current_app
|
|
from sqlalchemy import select, func, and_
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
from app.models.download import Download, DownloadStatus
|
|
from app.models.source import Source
|
|
from app.tasks.downloads import process_download
|
|
|
|
bp = Blueprint("downloads", __name__)
|
|
|
|
|
|
@bp.route("", methods=["GET"])
|
|
async def list_downloads():
|
|
"""List download history with filtering and pagination."""
|
|
# Query parameters
|
|
source_id = request.args.get("source_id", type=int)
|
|
status = request.args.get("status")
|
|
from_date = request.args.get("from_date")
|
|
to_date = request.args.get("to_date")
|
|
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
|
|
query = select(Download)
|
|
count_query = select(func.count()).select_from(Download)
|
|
|
|
filters = []
|
|
if source_id:
|
|
filters.append(Download.source_id == source_id)
|
|
if status:
|
|
filters.append(Download.status == status)
|
|
if from_date:
|
|
filters.append(Download.created_at >= datetime.fromisoformat(from_date))
|
|
if to_date:
|
|
filters.append(Download.created_at <= datetime.fromisoformat(to_date))
|
|
|
|
if filters:
|
|
query = query.where(and_(*filters))
|
|
count_query = count_query.where(and_(*filters))
|
|
|
|
# Get total count
|
|
total_result = await session.execute(count_query)
|
|
total = total_result.scalar()
|
|
|
|
# Apply pagination and ordering
|
|
query = query.order_by(Download.created_at.desc())
|
|
query = query.offset((page - 1) * per_page).limit(per_page)
|
|
|
|
result = await session.execute(query)
|
|
downloads = result.scalars().all()
|
|
|
|
return jsonify({
|
|
"items": [d.to_dict() for d in downloads],
|
|
"total": total,
|
|
"page": page,
|
|
"per_page": per_page,
|
|
"pages": (total + per_page - 1) // per_page,
|
|
})
|
|
|
|
|
|
@bp.route("/<int:download_id>", methods=["GET"])
|
|
async def get_download(download_id: int):
|
|
"""Get download details."""
|
|
async with current_app.db_engine.connect() as conn:
|
|
session = AsyncSession(bind=conn)
|
|
|
|
result = await session.execute(select(Download).where(Download.id == download_id))
|
|
download = result.scalar()
|
|
|
|
if not download:
|
|
return jsonify({"error": "Download not found"}), 404
|
|
|
|
# Include source info if available
|
|
response = download.to_dict()
|
|
if download.source_id:
|
|
source_result = await session.execute(
|
|
select(Source)
|
|
.options(selectinload(Source.subscription))
|
|
.where(Source.id == download.source_id)
|
|
)
|
|
source = source_result.scalar()
|
|
if source:
|
|
response["source"] = {
|
|
"id": source.id,
|
|
"subscription_name": source.subscription.name if source.subscription else None,
|
|
"platform": source.platform,
|
|
}
|
|
|
|
return jsonify(response)
|
|
|
|
|
|
@bp.route("/<int:download_id>/retry", methods=["POST"])
|
|
async def retry_download(download_id: int):
|
|
"""Retry a failed download."""
|
|
async with current_app.db_engine.connect() as conn:
|
|
session = AsyncSession(bind=conn)
|
|
|
|
result = await session.execute(select(Download).where(Download.id == download_id))
|
|
download = result.scalar()
|
|
|
|
if not download:
|
|
return jsonify({"error": "Download not found"}), 404
|
|
|
|
if download.status != DownloadStatus.FAILED:
|
|
return jsonify({"error": "Can only retry failed downloads"}), 400
|
|
|
|
# Reset status to pending
|
|
download.status = DownloadStatus.PENDING
|
|
download.error_type = None
|
|
download.error_message = None
|
|
await session.commit()
|
|
|
|
# Queue Celery task to retry the download
|
|
task = process_download.delay(download_id)
|
|
|
|
current_app.logger.info(f"Queued retry for download: {download_id}")
|
|
return jsonify({
|
|
"message": "Retry queued",
|
|
"download_id": download_id,
|
|
"task_id": task.id,
|
|
}), 202
|
|
|
|
|
|
@bp.route("/recent-activity", methods=["GET"])
|
|
async def recent_activity():
|
|
"""Get recent noteworthy downloads: failures and completions with new files.
|
|
|
|
These are downloads that warrant user attention, not routine "no new content" runs.
|
|
Results are limited to the last 7 days by default.
|
|
"""
|
|
limit = min(int(request.args.get("limit", 10)), 50)
|
|
days = int(request.args.get("days", 7))
|
|
|
|
from datetime import timedelta
|
|
cutoff = datetime.utcnow() - timedelta(days=days)
|
|
|
|
async with current_app.db_engine.connect() as conn:
|
|
session = AsyncSession(bind=conn)
|
|
|
|
# Get failed downloads OR completed with files (noteworthy activity)
|
|
from sqlalchemy import or_
|
|
query = select(Download).where(
|
|
and_(
|
|
Download.created_at >= cutoff,
|
|
or_(
|
|
Download.status == DownloadStatus.FAILED,
|
|
and_(
|
|
Download.status == DownloadStatus.COMPLETED,
|
|
Download.file_count > 0
|
|
)
|
|
)
|
|
)
|
|
).order_by(Download.created_at.desc()).limit(limit)
|
|
|
|
result = await session.execute(query)
|
|
downloads = result.scalars().all()
|
|
|
|
return jsonify({
|
|
"items": [d.to_dict() for d in downloads],
|
|
"count": len(downloads),
|
|
})
|
|
|
|
|
|
@bp.route("/stats", methods=["GET"])
|
|
async def get_stats():
|
|
"""Get download statistics."""
|
|
period = request.args.get("period", "week") # day, week, month
|
|
|
|
async with current_app.db_engine.connect() as conn:
|
|
session = AsyncSession(bind=conn)
|
|
|
|
# Total counts by status
|
|
status_query = select(
|
|
Download.status,
|
|
func.count(Download.id)
|
|
).group_by(Download.status)
|
|
|
|
status_result = await session.execute(status_query)
|
|
status_counts = {row[0]: row[1] for row in status_result}
|
|
|
|
# Counts by platform (through source)
|
|
platform_query = select(
|
|
Source.platform,
|
|
func.count(Download.id)
|
|
).join(Source, Download.source_id == Source.id).group_by(Source.platform)
|
|
|
|
platform_result = await session.execute(platform_query)
|
|
platform_counts = {row[0]: row[1] for row in platform_result}
|
|
|
|
# Recent downloads count
|
|
recent_query = select(func.count()).select_from(Download).where(
|
|
Download.status == DownloadStatus.COMPLETED
|
|
)
|
|
recent_result = await session.execute(recent_query)
|
|
recent_completed = recent_result.scalar()
|
|
|
|
return jsonify({
|
|
"total": sum(status_counts.values()),
|
|
"by_status": status_counts,
|
|
"by_platform": platform_counts,
|
|
"completed": status_counts.get(DownloadStatus.COMPLETED, 0),
|
|
"failed": status_counts.get(DownloadStatus.FAILED, 0),
|
|
"pending": status_counts.get(DownloadStatus.PENDING, 0),
|
|
})
|