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
This commit is contained in:
2026-03-29 19:12:05 -04:00
parent 9e3e0ff931
commit 55ae91a147
2 changed files with 108 additions and 0 deletions
+88
View File
@@ -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
}
+20
View File
@@ -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