feat(series): /api/series pages/add/remove/reorder/cover routes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 23:35:12 -04:00
parent 0db76c70e8
commit d8e585a76e
2 changed files with 157 additions and 3 deletions
+85 -3
View File
@@ -8,6 +8,7 @@ from ..extensions import get_session
from ..models import Tag, TagKind
from ..models.tag_allowlist import TagAllowlist
from ..services.bulk_tag_service import BulkTagService
from ..services.series_service import SeriesError, SeriesService
from ..services.tag_directory_service import TagDirectoryService
from ..services.tag_service import (
TagMergeConflict,
@@ -27,7 +28,9 @@ def _coerce_kind(raw: str | None) -> TagKind | None:
return None
def _parse_bulk_ids(body) -> tuple[list[int] | None, tuple | None]:
def _parse_bulk_ids(
body, *, max_ids: int = 200
) -> tuple[list[int] | None, tuple | None]:
"""Returns (image_ids, error). error is (json, status) or None."""
if not body or "image_ids" not in body:
return None, (jsonify({"error": "image_ids required"}), 400)
@@ -44,9 +47,9 @@ def _parse_bulk_ids(body) -> tuple[list[int] | None, tuple | None]:
jsonify({"error": "image_ids must be integers"}),
400,
)
if len(ids) > 200:
if len(ids) > max_ids:
return None, (
jsonify({"error": "selection too large (max 200)"}),
jsonify({"error": f"selection too large (max {max_ids})"}),
400,
)
return ids, None
@@ -282,3 +285,82 @@ async def bulk_remove_tag():
)
await session.commit()
return jsonify({"removed_count": removed})
def _series_err(exc: SeriesError):
msg = str(exc)
status = 404 if ("not found" in msg or "not a series" in msg) else 400
return jsonify({"error": msg}), status
@tags_bp.route("/series/<int:tag_id>/pages", methods=["GET"])
async def series_pages(tag_id: int):
async with get_session() as session:
try:
data = await SeriesService(session).list_pages(tag_id)
except SeriesError as exc:
return _series_err(exc)
return jsonify(data)
@tags_bp.route("/series/<int:tag_id>/pages", methods=["POST"])
async def series_add(tag_id: int):
body = await request.get_json()
ids, err = _parse_bulk_ids(body, max_ids=500)
if err:
return err
async with get_session() as session:
try:
n = await SeriesService(session).add_images(tag_id, ids)
except SeriesError as exc:
return _series_err(exc)
await session.commit()
return jsonify({"added_count": n})
@tags_bp.route("/series/<int:tag_id>/pages/remove", methods=["POST"])
async def series_remove(tag_id: int):
body = await request.get_json()
ids, err = _parse_bulk_ids(body, max_ids=500)
if err:
return err
async with get_session() as session:
try:
n = await SeriesService(session).remove_images(tag_id, ids)
except SeriesError as exc:
return _series_err(exc)
await session.commit()
return jsonify({"removed_count": n})
@tags_bp.route("/series/<int:tag_id>/reorder", methods=["POST"])
async def series_reorder(tag_id: int):
body = await request.get_json()
ids, err = _parse_bulk_ids(body, max_ids=500)
if err:
return err
async with get_session() as session:
try:
await SeriesService(session).reorder(tag_id, ids)
except SeriesError as exc:
return _series_err(exc)
await session.commit()
return jsonify({"ok": True})
@tags_bp.route("/series/<int:tag_id>/cover", methods=["POST"])
async def series_cover(tag_id: int):
body = await request.get_json()
if not body or "image_id" not in body:
return jsonify({"error": "image_id required"}), 400
try:
image_id = int(body["image_id"])
except (TypeError, ValueError):
return jsonify({"error": "image_id must be an integer"}), 400
async with get_session() as session:
try:
await SeriesService(session).set_cover(tag_id, image_id)
except SeriesError as exc:
return _series_err(exc)
await session.commit()
return jsonify({"ok": True})