From 55ae91a1475e9f1974fc67f080a7ea5f8cb43eb7 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 29 Mar 2026 19:12:05 -0400 Subject: [PATCH] api: add comic metadata fields to API responses Update API layer to expose all 14 new comic metadata fields in media item responses for both list and detail endpoints. Handler Changes (internal/handlers/media.go): - Added encoding/json import for JSON unmarshaling - Updated ListMediaItems() to include 15 new fields in JSON response - Updated GetMediaItem() to include 15 new fields in JSON response - Added numericToFloat() helper: Convert pgtype.Numeric to float64 - Added jsonBytesToMap() helper: Convert JSONB []byte to map[string]interface{} - Field name mapping: WebUrl (not WebURL), proper pgtype handling TypeScript Types (web/src/types/api.d.ts): - Updated MediaItemSummary interface with 15 new optional fields - manga_type: 'unknown' | 'no' | 'yes' | 'yes_and_right_to_left' - reading_direction: 'auto' | 'ltr' | 'rtl' | 'vertical' - Universal fields: series_count, volume, imprint, age_rating, web_url, metadata_notes, community_rating - Comic-specific: story_arc, is_black_and_white, alternate_info (nested type), scan_information, summary - Proper TypeScript typing with string literals for type safety API Response Fields Added: Reading Direction: - manga_type: Raw Manga field from ComicInfo.xml - reading_direction: Computed direction (auto/ltr/rtl/vertical) Universal Metadata (all formats): - series_count, volume, imprint, age_rating, web_url, metadata_notes, community_rating Comic-Specific: - story_arc, is_black_and_white, alternate_info (JSONB object), scan_information, summary Part of Phase 5: API Layer Updates Implementation: IMPLEMENTATION_PLAN_MERGE_METADATA_READING_DIRECTION.md --- internal/handlers/media.go | 88 ++++++++++++++++++++++++++++++++++++++ web/src/types/api.d.ts | 20 +++++++++ 2 files changed, 108 insertions(+) diff --git a/internal/handlers/media.go b/internal/handlers/media.go index 6e08679..31ab72d 100644 --- a/internal/handlers/media.go +++ b/internal/handlers/media.go @@ -5,6 +5,7 @@ import ( "bookhoard/internal/services" "bookhoard/internal/utils" "context" + "encoding/json" "fmt" "io" "mime" @@ -39,6 +40,21 @@ type CreateMediaItemRequest struct { DatePublished string `json:"date_published"` Publisher string `json:"publisher"` Contributors []string `json:"contributors"` + // NEW: Reading direction and comic metadata fields + MangaType string `json:"manga_type"` // 'unknown' | 'no' | 'yes' | 'yes_and_right_to_left' + ReadingDirection string `json:"reading_direction"` // 'auto' | 'ltr' | 'rtl' | 'vertical' + SeriesCount int32 `json:"series_count"` + Volume int32 `json:"volume"` + Imprint string `json:"imprint"` + AgeRating string `json:"age_rating"` // 'Everyone' | 'Teen' | 'Mature' | 'Adult' + WebURL string `json:"web_url"` + MetadataNotes string `json:"metadata_notes"` + CommunityRating float64 `json:"community_rating"` + StoryArc string `json:"story_arc"` + IsBlackAndWhite bool `json:"is_black_and_white"` + AlternateInfo string `json:"alternate_info"` // JSON string + ScanInformation string `json:"scan_information"` + Summary string `json:"summary"` } // UpdateMediaItemRequest represents the request for updating a media item @@ -55,6 +71,21 @@ type UpdateMediaItemRequest struct { DatePublished string `json:"date_published"` Publisher string `json:"publisher"` Contributors []string `json:"contributors"` + // NEW: Reading direction and comic metadata fields + MangaType string `json:"manga_type"` // 'unknown' | 'no' | 'yes' | 'yes_and_right_to_left' + ReadingDirection string `json:"reading_direction"` // 'auto' | 'ltr' | 'rtl' | 'vertical' + SeriesCount int32 `json:"series_count"` + Volume int32 `json:"volume"` + Imprint string `json:"imprint"` + AgeRating string `json:"age_rating"` // 'Everyone' | 'Teen' | 'Mature' | 'Adult' + WebURL string `json:"web_url"` + MetadataNotes string `json:"metadata_notes"` + CommunityRating float64 `json:"community_rating"` + StoryArc string `json:"story_arc"` + IsBlackAndWhite bool `json:"is_black_and_white"` + AlternateInfo string `json:"alternate_info"` // JSON string + ScanInformation string `json:"scan_information"` + Summary string `json:"summary"` } // CreateMediaNoteRequest represents the request for creating a media note @@ -660,6 +691,23 @@ func (mh *MediaHandler) ListMediaItems(c *echo.Context) error { "created_at": item.CreatedAt.Time.Format("2006-01-02T15:04:05Z07:00"), "updated_at": item.UpdatedAt.Time.Format("2006-01-02T15:04:05Z07:00"), "format_group": item.FormatGroup, // already string + // NEW: Reading direction fields + "manga_type": textToString(item.MangaType), + "reading_direction": textToString(item.ReadingDirection), + // NEW: Universal metadata fields + "series_count": item.SeriesCount, + "volume": item.Volume, + "imprint": textToString(item.Imprint), + "age_rating": textToString(item.AgeRating), + "web_url": textToString(item.WebUrl), + "metadata_notes": textToString(item.MetadataNotes), + "community_rating": numericToFloat(item.CommunityRating), + // NEW: Comic-specific fields + "story_arc": textToString(item.StoryArc), + "is_black_and_white": item.IsBlackAndWhite, + "alternate_info": jsonBytesToMap(item.AlternateInfo), + "scan_information": textToString(item.ScanInformation), + "summary": textToString(item.Summary), } } return c.JSON(http.StatusOK, map[string]interface{}{"data": resolvedItems}) @@ -707,6 +755,23 @@ func (mh *MediaHandler) GetMediaItem(c *echo.Context) error { "updated_at": item.UpdatedAt.Time.Format("2006-01-02T15:04:05Z07:00"), "format_group": item.FormatGroup, "format_mimetype": textToString(item.FormatMimetype), + // NEW: Reading direction fields + "manga_type": textToString(item.MangaType), + "reading_direction": textToString(item.ReadingDirection), + // NEW: Universal metadata fields + "series_count": item.SeriesCount, + "volume": item.Volume, + "imprint": textToString(item.Imprint), + "age_rating": textToString(item.AgeRating), + "web_url": textToString(item.WebUrl), + "metadata_notes": textToString(item.MetadataNotes), + "community_rating": numericToFloat(item.CommunityRating), + // NEW: Comic-specific fields + "story_arc": textToString(item.StoryArc), + "is_black_and_white": item.IsBlackAndWhite, + "alternate_info": jsonBytesToMap(item.AlternateInfo), + "scan_information": textToString(item.ScanInformation), + "summary": textToString(item.Summary), }) } @@ -1577,3 +1642,26 @@ func (mh *MediaHandler) ServeFile(c *echo.Context) error { http.ServeFile(c.Response(), c.Request(), fullPath) return nil } + +// numericToFloat converts pgtype.Numeric to float64, returning 0 if invalid +func numericToFloat(n pgtype.Numeric) float64 { + if n.Valid { + f, err := n.Float64Value() + if err == nil { + return f.Float64 + } + } + return 0 +} + +// jsonBytesToMap converts []byte JSONB to map[string]interface{}, returning nil if invalid +func jsonBytesToMap(b []byte) map[string]interface{} { + if len(b) == 0 { + return nil + } + var result map[string]interface{} + if err := json.Unmarshal(b, &result); err != nil { + return nil + } + return result +} diff --git a/web/src/types/api.d.ts b/web/src/types/api.d.ts index 2a7f9f3..4e1b2ed 100644 --- a/web/src/types/api.d.ts +++ b/web/src/types/api.d.ts @@ -57,6 +57,26 @@ interface MediaItemSummary { hash_confidence?: string; library_name: string; library_type_name: string; + + // NEW: Reading direction fields for manga/comics + manga_type?: 'unknown' | 'no' | 'yes' | 'yes_and_right_to_left'; + reading_direction?: 'auto' | 'ltr' | 'rtl' | 'vertical'; + + // NEW: Universal metadata fields (apply to ebooks, audiobooks, comics) + series_count?: number; + volume?: number; + imprint?: string; + age_rating?: string; // 'Everyone' | 'Teen' | 'Mature' | 'Adult' + web_url?: string; + metadata_notes?: string; + community_rating?: number; + + // NEW: Comic-specific fields + story_arc?: string; + is_black_and_white?: boolean; + alternate_info?: { alternate_series?: string; alternate_number?: number; alternate_count?: number }; + scan_information?: string; + summary?: string; } // Matches handlers.CollectionData / CollectionResponse JSON response