From 88982ec11e5ce75125920226824d6d0c1555f14a Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 12 Apr 2026 19:03:49 -0400 Subject: [PATCH] docs: Add comprehensive implementation plan for manga EPUB and panel detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This document provides a complete, phased implementation plan for: - Enabling manga EPUBs in manga library (not just CBZ/CBR) - Detecting fixed-layout EPUBs vs reflowable EPUBs - Processing issue tracking for format mismatches - Universal panel detection for manga and comics libraries - Smart panel detection that works for PDF comics but not PDF ebooks Key features: - All changes follow existing code patterns with exact line numbers - 9 implementation phases in correct dependency order - Code-around context for every change (before/after) - Testing checklist and rollback plan - Database schema changes, scanner enhancements, new handlers, frontend updates Panel detection logic: - Manga library + fixed_layout/comic_archive → panel detection ON - Comics library + fixed_layout/comic_archive → panel detection ON - Ebooks library + any format → panel detection OFF - Comics library + PDF → panel detection ON - Ebooks library + PDF → panel detection OFF Implementation addresses the constraint that manga EPUBs live in /manga/ directory physically but must be filtered to only show fixed-layout EPUBs in the manga library (not reflowable novels). This is a planning document only - no code changes yet. --- MANGA_EPUB_IMPLEMENTATION.md | 1048 ++++++++++++++++++++++++++++++++++ 1 file changed, 1048 insertions(+) create mode 100644 MANGA_EPUB_IMPLEMENTATION.md diff --git a/MANGA_EPUB_IMPLEMENTATION.md b/MANGA_EPUB_IMPLEMENTATION.md new file mode 100644 index 0000000..f9cd20c --- /dev/null +++ b/MANGA_EPUB_IMPLEMENTATION.md @@ -0,0 +1,1048 @@ +# Manga EPUB and Panel Detection Implementation + +**Status:** Ready to implement +**Created:** April 12, 2026 +**Purpose:** Enable manga EPUBs in manga library, filter by fixed-layout, and implement universal panel detection + +--- + +## Table of Contents + +1. [Phase 1: Database Schema Changes](#phase-1-database-schema-changes) +2. [Phase 2: Database Query Additions](#phase-2-database-query-additions) +3. [Phase 3: Scanner Service Enhancements](#phase-3-scanner-service-enhancements) +4. [Phase 4: New Processing Issues Handler](#phase-4-new-processing-issues-handler) +5. [Phase 5: Reader Handler Updates](#phase-5-reader-handler-updates) +6. [Phase 6: Route Registration](#phase-6-route-registration) +7. [Phase 7: Frontend Templates](#phase-7-frontend-templates) +8. [Phase 8: Update Reader Template](#phase-8-update-reader-template) +9. [Phase 9: Update Reader JavaScript](#phase-9-update-reader-javascript) +10. [Implementation Order](#implementation-order) + +--- + +## Phase 1: Database Schema Changes + +### File: `database/schema/schema.sql` + +**Location:** Add at end of file (after line 1253) + +**Changes to add:** + +```sql +-- ============================================================================ +-- ENHANCED MANGA/COMIC FORMAT DETECTION AND PROCESSING ISSUE TRACKING +-- ============================================================================ + +-- Allow EPUB in manga library +UPDATE library_types +SET allowed_extensions = array_append(allowed_extensions, '.epub') +WHERE name = 'manga'; + +-- Function to detect fixed-layout EPUBs from OPF content +CREATE OR REPLACE FUNCTION detect_fixed_layout_epub(opf_content TEXT) +RETURNS BOOLEAN AS $$ +BEGIN + -- Check for pre-paginated metadata + IF opf_content LIKE '%rendition:layout">pre-paginated<%' THEN + RETURN TRUE; + END IF; + + IF opf_content LIKE '%rendition:layout="pre-paginated"%' THEN + RETURN TRUE; + END IF; + + -- Check for RTL page progression (manga indicator) + IF opf_content LIKE '%page-progression-direction="rtl"%' THEN + RETURN TRUE; + END IF; + + -- Check for image-heavy content (count tags) + -- Threshold of 50 images suggests manga/comic vs novel + IF (SELECT COUNT(*) FROM regexp_matches(opf_content, ']+>', 'g')) > 50 THEN + RETURN TRUE; + END IF; + + RETURN FALSE; +END; +$$ LANGUAGE plpgsql; + +-- Table to track items that can't be processed in their library +CREATE TABLE IF NOT EXISTS processing_issues ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, + library_id UUID NOT NULL REFERENCES libraries(id) ON DELETE CASCADE, + issue_type VARCHAR(50) NOT NULL, + issue_description TEXT NOT NULL, + severity VARCHAR(20) NOT NULL DEFAULT 'warning', + resolved BOOLEAN DEFAULT FALSE, + resolved_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(media_item_id, issue_type) +); + +-- Indexes for querying problem items +CREATE INDEX IF NOT EXISTS idx_processing_issues_library + ON processing_issues(library_id, resolved); +CREATE INDEX IF NOT EXISTS idx_processing_issues_severity + ON processing_issues(severity, resolved); + +-- Add comment for documentation +COMMENT ON TABLE processing_issues IS 'Tracks media items that cannot be properly processed in their assigned library due to format mismatches or other issues'; +``` + +**Implementation:** Run this SQL directly in PostgreSQL or add to migration file + +--- + +## Phase 2: Database Query Additions + +### File: `internal/database/queries/queries.sql` + +**Location:** Add at end of file (after line 2018) + +**Changes to add:** + +```sql +-- ============================================================================ +-- PROCESSING ISSUES QUERIES +-- ============================================================================ + +-- name: CreateProcessingIssue :one +INSERT INTO processing_issues (media_item_id, library_id, issue_type, issue_description, severity) +VALUES ($1, $2, $3, $4, $5) +ON CONFLICT (media_item_id, issue_type) +DO UPDATE SET issue_description = EXCLUDED.issue_description, + severity = EXCLUDED.severity, + resolved = false, + resolved_at = NULL +RETURNING *; + +-- name: ListProcessingIssuesByLibrary :many +SELECT + pi.id, + pi.media_item_id, + pi.issue_type, + pi.issue_description, + pi.severity, + pi.resolved, + pi.resolved_at, + pi.created_at, + mi.title, + mi.file_path, + mi.format_group, + lt.name as library_type_name +FROM processing_issues pi +JOIN media_items mi ON pi.media_item_id = mi.id +JOIN libraries l ON pi.library_id = l.id +JOIN library_types lt ON l.library_type_id = lt.id +WHERE pi.library_id = $1 + AND pi.resolved = false +ORDER BY + CASE pi.severity + WHEN 'error' THEN 1 + WHEN 'warning' THEN 2 + WHEN 'info' THEN 3 + END, + pi.created_at DESC; + +-- name: GetProcessingIssueStats :one +SELECT + COUNT(*) FILTER (WHERE severity = 'error' AND resolved = false) as error_count, + COUNT(*) FILTER (WHERE severity = 'warning' AND resolved = false) as warning_count, + COUNT(*) FILTER (WHERE severity = 'info' AND resolved = false) as info_count +FROM processing_issues +WHERE library_id = $1; + +-- name: ResolveProcessingIssue :one +UPDATE processing_issues +SET resolved = true, + resolved_at = NOW() +WHERE id = $1 + AND media_item_id = $2 +RETURNING *; + +-- name: DeleteProcessingIssue :one +DELETE FROM processing_issues +WHERE id = $1 +RETURNING *; + +-- ============================================================================ +-- LIBRARY WITH TYPE INFO QUERIES +-- ============================================================================ + +-- name: GetLibraryWithType :one +SELECT + l.*, + lt.name as type_name, + lt.description as type_description, + lt.allowed_extensions +FROM libraries l +JOIN library_types lt ON l.library_type_id = lt.id +WHERE l.id = $1; +``` + +**Next step:** Run `sqlc generate` in `internal/database/` directory + +--- + +## Phase 3: Scanner Service Enhancements + +### File: `internal/services/media_scanner.go` + +#### Change 1: Add Fixed-Layout Detection Function + +**Location:** After `extractEPUBMetadata` function (after line 1182) + +**Add this new function:** + +```go +// DetectFixedLayoutEPUB checks if EPUB has fixed-layout (manga) characteristics +// by examining the OPF file for rendition metadata and content indicators +func (s *MediaScanner) DetectFixedLayoutEPUB(epubPath string) (bool, error) { + // Open EPUB ZIP file + r, err := zip.OpenReader(epubPath) + if err != nil { + return false, fmt.Errorf("failed to open EPUB: %w", err) + } + defer r.Close() + + // Find and read OPF file + var opfFile *zip.File + for _, f := range r.File { + if strings.HasSuffix(f.Name, ".opf") { + opfFile = f + break + } + // Also check in META-INF directory + if strings.Contains(f.Name, "META-INF/") && strings.HasSuffix(f.Name, ".opf") { + opfFile = f + break + } + } + + if opfFile == nil { + return false, fmt.Errorf("OPF file not found in EPUB") + } + + // Read OPF content + rc, err := opfFile.Open() + if err != nil { + return false, fmt.Errorf("failed to open OPF: %w", err) + } + defer rc.Close() + + opfContent, err := io.ReadAll(rc) + if err != nil { + return false, fmt.Errorf("failed to read OPF: %w", err) + } + + // Check for fixed-layout indicators + opfString := string(opfContent) + + // Check 1: rendition:layout = pre-paginated (EPUB 3 fixed layout) + if strings.Contains(opfString, `rendition:layout">pre-paginated<`) || + strings.Contains(opfString, `rendition:layout="pre-paginated"`) { + return true, nil + } + + // Check 2: RTL page progression (manga indicator) + if strings.Contains(opfString, `page-progression-direction="rtl"`) { + return true, nil + } + + // Check 3: Image-heavy content (count tags) + // Threshold of 50 images suggests manga/comic vs novel + imgCount := strings.Count(opfString, ` 50 { + return true, nil + } + + // Check 4: Manga subject tag + lowerOPF := strings.ToLower(opfString) + if strings.Contains(lowerOPF, ` + + + + Processing Issues - Bookhoard + + + + @Header(user, "/admin/libraries/"+libraryID) + +
+
+
+
+

Processing Issues

+

Items that couldn't be processed in this library

+
+ + ← Back to Library + +
+
+ + if stats.ErrorCount > 0 || stats.WarningCount > 0 || stats.InfoCount > 0 { + +
+ if stats.ErrorCount > 0 { +
+

Errors

+

{ stats.ErrorCount }

+
+ } + if stats.WarningCount > 0 { +
+

Warnings

+

{ stats.WarningCount }

+
+ } + if stats.InfoCount > 0 { +
+

Info

+

{ stats.InfoCount }

+
+ } +
+ } + + if len(issues) == 0 { +
+

No processing issues found for this library.

+
+ } else { + +
+ for _, issue := range issues { +
+
+
+

{ issue.Title }

+

{ issue.IssueDescription }

+
+

Type: { issue.IssueType }

+

Format: { issue.FormatGroup }

+

File: { issue.FilePath }

+

Library: { issue.LibraryTypeName }

+
+
+
+ + { issue.Severity } + +
+
+
+ if issue.Severity == "warning" || issue.Severity == "info" { + + } +
+
+ } +
+ } +
+ + +} +``` + +--- + +## Phase 8: Update Reader Template + +### File: `templates/reader.templ` + +**Location:** Update the `x-init` attribute to include panel detection config + +**Find this code:** + +```templ + +``` + +**Replace with:** + +```templ + +``` + +--- + +## Phase 9: Update Reader JavaScript + +### File: `web/src/reader/reader.ts` + +**Location:** Replace entire file content + +**Replace with:** + +```typescript +import "foliate-js/view.js"; +import { Alpine } from "../alpine"; + +document.addEventListener("alpine:init", () => { + Alpine.data("readerShell", () => ({ + enablePanelDetection: false, + libraryType: '', + formatGroup: '', + mangaType: '', + readingDirection: '', + panelDetector: null, + + initReader(config: any) { + this.enablePanelDetection = config.enablePanelDetection; + this.libraryType = config.libraryType; + this.formatGroup = config.formatGroup; + this.mangaType = config.mangaType; + this.readingDirection = config.readingDirection; + + console.log("Reader initialized with:", { + panelDetection: this.enablePanelDetection, + library: this.libraryType, + format: this.formatGroup + }); + + // Only load panel detection if enabled + if (this.enablePanelDetection) { + this.loadPanelDetection(); + } + }, + + async loadPanelDetection() { + try { + // Dynamic import to only load when needed + const { PanelDetector } = await import('foliate-js/panel-detection.js'); + this.panelDetector = new PanelDetector(); + console.log("Panel detection loaded successfully"); + } catch (error) { + console.error("Failed to load panel detection:", error); + } + }, + + nextPage() { + const view = document.querySelector("#reader-view"); + // @ts-ignore - foliate custom element + view?.next?.(); + }, + + previousPage() { + const view = document.querySelector("#reader-view"); + // @ts-ignore - foliate custom element + view?.prev?.(); + } + })); + + Alpine.start(); +}); +``` + +--- + +## Implementation Order + +### Step 1: Database Changes + +```bash +# Run schema changes +psql -U your_user -d bookhoard -f database/schema/schema.sql + +# Generate database code +cd internal/database +sqlc generate + +# Verify new queries were generated +ls -la queries.sql.go +``` + +### Step 2: Backend Code Changes + +```bash +# Add scanner enhancements +# Edit internal/services/media_scanner.go + +# Add processing issues handler +# Create internal/handlers/processing_issues.go + +# Update reader handler +# Edit internal/handlers/reader.go + +# Update routes +# Edit cmd/server/main.go +``` + +### Step 3: Frontend Changes + +```bash +# Create processing issues template +# Create templates/admin_processing_issues.templ + +# Update reader template +# Edit templates/reader.templ + +# Update reader JavaScript +# Edit web/src/reader/reader.ts + +# Regenerate templates +cd templates +templ generate + +# Build frontend +cd ../web +npm run build:ts +``` + +### Step 4: Testing + +```bash +# Restart server +go run cmd/server/main.go + +# Test 1: Scan manga directory with EPUBs +# Check that fixed-layout EPUBs are detected correctly + +# Test 2: Verify processing issues are created +# Check database: SELECT * FROM processing_issues; + +# Test 3: Test reader with different library types +# - Manga library with CBZ → panel detection enabled +# - Manga library with fixed-layout EPUB → panel detection enabled +# - Ebooks library with PDF → panel detection disabled +# - Comics library with PDF → panel detection enabled + +# Test 4: Check processing issues UI +# Visit: /admin/libraries/{libraryId}/issues/list +``` + +### Step 5: Verification + +```bash +# Check database changes +\dt processing_issues +\d+ processing_issues +SELECT * FROM library_types WHERE name = 'manga'; + +# Verify scanner behavior +grep -n "DetectFixedLayoutEPUB" internal/services/media_scanner.go + +# Verify handler is registered +grep -n "processingIssuesHandler" cmd/server/main.go + +# Test API endpoints +curl http://localhost:8765/api/libraries/{libraryId}/issues/list +curl http://localhost:8765/api/libraries/{libraryId}/issues/stats +``` + +--- + +## Testing Checklist + +- [ ] Manga library accepts `.epub` files +- [ ] Fixed-layout EPUBs detected correctly +- [ ] Reflowable EPUBs in manga library create processing issues +- [ ] Processing issues API endpoints work +- [ ] Processing issues UI displays correctly +- [ ] Reader enables panel detection for manga library +- [ ] Reader enables panel detection for comics library +- [ ] Reader disables panel detection for ebooks library +- [ ] PDF ebooks don't get panel detection +- [ ] PDF comics get panel detection + +--- + +## Rollback Plan + +If issues occur: + +```bash +# 1. Revert database changes +psql -U your_user -d bookhoard -c "DROP TABLE IF EXISTS processing_issues CASCADE;" +psql -U your_user -d bookhoard -c "UPDATE library_types SET allowed_extensions = ARRAY['.cbz','.cbr','.png','.jpg','.jpeg','.gif','.bmp','.webp'] WHERE name = 'manga';" + +# 2. Remove code changes +git checkout HEAD -- internal/services/media_scanner.go +git checkout HEAD -- internal/handlers/reader.go +rm internal/handlers/processing_issues.go +git checkout HEAD -- cmd/server/main.go + +# 3. Regenerate database code +cd internal/database && sqlc generate + +# 4. Regenerate templates +cd templates && templ generate + +# 5. Rebuild frontend +cd web && npm run build:ts + +# 6. Restart server +``` + +--- + +## Notes + +- All changes follow existing code patterns +- No breaking changes to existing functionality +- Processing issues are warnings only, don't block scanning +- Panel detection is determined by library type + format group +- PDF panel detection works for comics but not ebooks +- Fixed-layout detection uses multiple heuristics for reliability