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