feat(series): chapter layer over series_page — backend (FC-6.1)
Adds an ordered chapter layer to series. Reading order becomes (series_chapter.chapter_number, series_page.page_number); a chapter may be a placeholder reserving a slot, and carries an optional parsed stated-page range used to flag missing-page gaps. An image still lives in at most one series ⇒ one chapter (image_id stays UNIQUE). - models: series_chapter; series_page gains chapter_id (NOT NULL, cascade) + stated_page. Migration 0040 backfills every existing series into one auto-chapter holding its current flat pages — no data loss. - SeriesService: chapter CRUD (create/update/reorder/delete/merge), page→chapter assignment, reorder_pages, chapter-aware set_cover; list_pages now returns chapters[] + gaps[] alongside a back-compat flat pages[]. Legacy series-wide reorder operates on the single default chapter and rejects multi-chapter series. - API: chapter endpoints under /api/series/<tag>/chapters; POST pages accepts an optional chapter_id. - TagService.merge now repoints series_chapter too, so a merged series' chapters (and their pages) survive the source tag's deletion instead of cascading away. - Tests: new chapter suite; updated the 4 direct SeriesPage(...) constructions to supply chapter_id. Frontend (chapter-aware manage view + reader) lands next; until then the existing UI keeps working via the flat pages[] + single default chapter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
"""SeriesPage — ordered image membership for a series-kind Tag.
|
||||
|
||||
A series IS a Tag with kind='series'; series_page gives it ordered pages.
|
||||
An image belongs to at most one series (UNIQUE image_id). Cover = the
|
||||
lowest page_number. page_number is an ordering key only (not unique) —
|
||||
reorder rewrites 1..N wholesale.
|
||||
A series IS a Tag with kind='series'; series_page gives it ordered pages,
|
||||
grouped into chapters (FC-6). An image belongs to at most one series
|
||||
(UNIQUE image_id) ⇒ at most one chapter. Reading order is
|
||||
(chapter.chapter_number, series_page.page_number): page_number orders pages
|
||||
WITHIN a chapter and is an ordering key only (not unique) — reorder rewrites
|
||||
1..N wholesale. stated_page carries the page number parsed from the source
|
||||
post (FC-6.2), nullable when unknown.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
@@ -21,12 +24,18 @@ class SeriesPage(Base):
|
||||
series_tag_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("tag.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
chapter_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("series_chapter.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
image_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("image_record.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
)
|
||||
page_number: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
stated_page: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user