Skip to content

Commit 39636de

Browse files
fix: Manga import chapter population, cover caching, and series bulk actions (#115)
* fix: Manga import chapter population, cover caching, and series bulk actions Manga imported via MangaDex or MAL all showed placeholder cover images and 0/0 issues because: (1) cover art was never downloaded to local cache during import, and (2) MangaDex chapter API filtered by English only — most popular manga had English translations DMCA'd, returning 0 chapters. The `if chapters:` guard then silently skipped Total/Have. Changes: - Add get_total_chapter_count() to mangadex.py that calls the aggregate endpoint WITHOUT translatedLanguage[] filter for accurate totals - Extract shared _populate_manga_chapters() helper eliminating ~70 lines of duplicated code between addMangaToDB() and addMangaToDB_MAL() - Multi-source chapter population: language-filtered → unfiltered aggregate → MAL num_chapters → explicit 0 (never NULL) - Cache cover images locally via helpers.getImage() in both import paths - Fix myanimelist.py truthiness bug where num_chapters=0 became None - Add bulk selection (checkboxes) to SeriesTable with action bar - Add POST /series/bulk-delete, bulk-pause, bulk-resume endpoints - Add useBulkDeleteSeries, useBulkPauseSeries, useBulkResumeSeries hooks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Address code review findings — row selection, input validation, raw SQL - Use getRowId to key row selection by ComicID instead of positional index, preventing wrong-series bulk operations after sort/filter - Reset confirmDelete state when selection changes to prevent stale confirmation targeting unintended series - Add _validate_bulk_ids() shared helper with: max 100 IDs cap, string type validation, dict body type guard - Replace raw text() SQL with SQLAlchemy table object query - Remove dead ternary in bulk action bar count display Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3f8ac30 commit 39636de

6 files changed

Lines changed: 513 additions & 160 deletions

File tree

comicarr/app/series/router.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,80 @@ def resume_series(comic_id: str, ctx: AppContext = Depends(get_context)):
9595
return series_service.resume_comic(ctx, comic_id)
9696

9797

98+
# ---------------------------------------------------------------------------
99+
# Bulk series operations
100+
# ---------------------------------------------------------------------------
101+
102+
MAX_BULK_IDS = 100
103+
104+
105+
def _validate_bulk_ids(request_body):
106+
"""Validate and extract IDs from a bulk operation request body."""
107+
if not isinstance(request_body, dict):
108+
return None, JSONResponse(status_code=422, content={"detail": "Request body must be a JSON object"})
109+
ids = request_body.get("ids")
110+
if not ids or not isinstance(ids, list):
111+
return None, JSONResponse(status_code=400, content={"detail": "Missing ids array"})
112+
if len(ids) > MAX_BULK_IDS:
113+
return None, JSONResponse(
114+
status_code=422, content={"detail": "Maximum %d IDs per bulk operation" % MAX_BULK_IDS}
115+
)
116+
if not all(isinstance(i, str) and i.strip() for i in ids):
117+
return None, JSONResponse(status_code=422, content={"detail": "All IDs must be non-empty strings"})
118+
return ids, None
119+
120+
121+
@router.post("/series/bulk-delete", dependencies=[Depends(require_session)])
122+
def bulk_delete_series(
123+
request_body: dict = None,
124+
ctx: AppContext = Depends(get_context),
125+
):
126+
"""Delete multiple series at once."""
127+
ids, error = _validate_bulk_ids(request_body)
128+
if error:
129+
return error
130+
131+
results = []
132+
for comic_id in ids:
133+
result = series_service.delete_comic(ctx, comic_id)
134+
results.append({"id": comic_id, "success": result.get("success", False)})
135+
136+
succeeded = sum(1 for r in results if r["success"])
137+
return {"success": succeeded > 0, "deleted": succeeded, "total": len(ids), "results": results}
138+
139+
140+
@router.post("/series/bulk-pause", dependencies=[Depends(require_session)])
141+
def bulk_pause_series(
142+
request_body: dict = None,
143+
ctx: AppContext = Depends(get_context),
144+
):
145+
"""Pause multiple series at once."""
146+
ids, error = _validate_bulk_ids(request_body)
147+
if error:
148+
return error
149+
150+
for comic_id in ids:
151+
series_service.pause_comic(ctx, comic_id)
152+
153+
return {"success": True, "count": len(ids)}
154+
155+
156+
@router.post("/series/bulk-resume", dependencies=[Depends(require_session)])
157+
def bulk_resume_series(
158+
request_body: dict = None,
159+
ctx: AppContext = Depends(get_context),
160+
):
161+
"""Resume multiple series at once."""
162+
ids, error = _validate_bulk_ids(request_body)
163+
if error:
164+
return error
165+
166+
for comic_id in ids:
167+
series_service.resume_comic(ctx, comic_id)
168+
169+
return {"success": True, "count": len(ids)}
170+
171+
98172
@router.post("/series/{comic_id}/refresh", dependencies=[Depends(require_session)])
99173
def refresh_series(comic_id: str, ctx: AppContext = Depends(get_context)):
100174
"""Refresh series metadata from provider."""

0 commit comments

Comments
 (0)