diff --git a/internal/services/media_scanner.go b/internal/services/media_scanner.go index ceb7faf..17e5354 100644 --- a/internal/services/media_scanner.go +++ b/internal/services/media_scanner.go @@ -75,6 +75,9 @@ type MediaMetadata struct { WebURL string // URL to info page (Goodreads, ComicVine, etc.) MetadataNotes string // Notes from metadata files (not user notes) CommunityRating float64 // Pre-existing community rating (0-10) + PageCount int32 // Actual page count (images for comics, pages for PDF) + TotalCharacters int64 // Total text characters (for reflowable EPUBs) + ChapterCount int32 // Number of chapters detected // Comic-specific fields StoryArc string // Story arc name @@ -707,6 +710,7 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool, ScanInformation: pgtype.Text{String: metadata.ScanInformation, Valid: metadata.ScanInformation != ""}, Summary: pgtype.Text{String: metadata.Summary, Valid: metadata.Summary != ""}, CommunityRating: pgtype.Float8{Float64: metadata.CommunityRating, Valid: metadata.CommunityRating > 0}, + PageCount: pgtype.Int4{Int32: metadata.PageCount, Valid: metadata.PageCount > 0}, }) if err != nil { return false, fmt.Errorf("failed to create media item: %v", err) @@ -726,6 +730,46 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool, } } + // Set format group, total characters, and chapter count + mimeType := s.getMimeType(path) + ext := strings.ToLower(filepath.Ext(path)) + var formatGroup string + var isReflowable, hasFixedLayout bool + switch ext { + case ".epub": + isFixed, fixedErr := s.DetectFixedLayoutEPUB(path) + if fixedErr == nil && isFixed { + formatGroup = "fixed_layout" + hasFixedLayout = true + } else { + formatGroup = "reflowable" + isReflowable = true + } + case ".mobi", ".azw", ".azw3", ".fb2", ".txt": + formatGroup = "reflowable" + isReflowable = true + case ".pdf", ".djvu": + formatGroup = "fixed_layout" + hasFixedLayout = true + case ".cbz", ".cbr", ".cb7", ".cbt": + formatGroup = "comic_archive" + hasFixedLayout = true + default: + formatGroup = "unknown" + } + err = s.db.UpdateMediaItemFormatGroup(ctx, database.UpdateMediaItemFormatGroupParams{ + ID: createdItem.ID, + FormatGroup: formatGroup, + FormatMimetype: pgtype.Text{String: mimeType, Valid: mimeType != ""}, + IsReflowable: pgtype.Bool{Bool: isReflowable, Valid: true}, + HasFixedLayout: pgtype.Bool{Bool: hasFixedLayout, Valid: true}, + TotalCharacters: pgtype.Int8{Int64: metadata.TotalCharacters, Valid: metadata.TotalCharacters > 0}, + ChapterCount: pgtype.Int4{Int32: metadata.ChapterCount, Valid: metadata.ChapterCount > 0}, + }) + if err != nil { + fmt.Printf("Warning: failed to update format info for %s: %v\n", path, err) + } + // Store format information in the database for _, format := range metadata.FileFormats { _, err = s.db.CreateMediaItemFormat(ctx, database.CreateMediaItemFormatParams{ @@ -783,6 +827,16 @@ func (s *MediaScanner) mergeMetadata(path string, calibreMetadata *MediaMetadata if err == nil { genreTags := extractGenreTagsFromEPUB(book) processGenresAndTags(metadata, genreTags) + if allText := book.AllChaptersText(); len(allText) > 0 { + metadata.TotalCharacters = int64(len(allText)) + } + metadata.ChapterCount = int32(book.ChapterCount()) + } + isFixed, fixedErr := s.DetectFixedLayoutEPUB(path) + if fixedErr == nil && isFixed { + if pageCount, imgErr := countArchiveImages(path); imgErr == nil && pageCount > 0 { + metadata.PageCount = int32(pageCount) + } } } @@ -899,6 +953,10 @@ func (s *MediaScanner) mergeMetadata(path string, calibreMetadata *MediaMetadata fmt.Printf("Merged comic metadata from %s: title=%s, series=%s, issue=%d, manga=%s, direction=%s\n", path, comicInfo.Title, comicInfo.Series, comicInfo.Number, comicInfo.Manga, metadata.ReadingDirection) } + + if pageCount, err := countArchiveImages(path); err == nil && pageCount > 0 { + metadata.PageCount = int32(pageCount) + } } return metadata, nil @@ -1089,12 +1147,14 @@ func (s *MediaScanner) extractMetadata(path string) (*MediaMetadata, error) { // Enhanced format detection for EPUBs isFixedLayout, detectErr := s.DetectFixedLayoutEPUB(path) if detectErr == nil && isFixedLayout { - // Override format group for manga EPUBs metadata.FileFormats = []*FormatInfo{{ FormatType: "fixed_layout", FilePath: path, MimeType: s.getMimeType(path), }} + if pageCount, imgErr := countArchiveImages(path); imgErr == nil && pageCount > 0 { + metadata.PageCount = int32(pageCount) + } } // Try to extract embedded cover coverPath, err := s.extractEPUBCover(path) @@ -1832,6 +1892,8 @@ func (s *MediaScanner) extractPDFMetadata(path string) (*MediaMetadata, error) { metadata.Publisher = pdfInfo.Producer } + metadata.PageCount = int32(pdfInfo.PageCount) + // Try to extract cover image coverPath, err := s.extractPDFCover(path) if err != nil { @@ -2276,7 +2338,77 @@ func (t *tarFileAdapter) Open() (io.ReadCloser, error) { // isImageFile checks if a file is an image based on extension func isImageFile(filename string) bool { ext := strings.ToLower(filepath.Ext(filename)) - return ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif" + return ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif" || ext == ".webp" +} + +// countArchiveImages counts image files in a comic archive +func countArchiveImages(filePath string) (int, error) { + ext := strings.ToLower(filepath.Ext(filePath)) + count := 0 + + switch ext { + case ".cbz", ".epub": + r, err := zip.OpenReader(filePath) + if err != nil { + return 0, err + } + defer r.Close() + for _, f := range r.File { + if !f.FileInfo().IsDir() && isImageFile(f.Name) { + count++ + } + } + case ".cbr": + r, err := rardecode.OpenReader(filePath, "") + if err != nil { + return 0, err + } + defer r.Close() + for { + header, err := r.Next() + if err == io.EOF { + break + } + if err != nil { + break + } + if !header.IsDir && isImageFile(header.Name) { + count++ + } + } + case ".cb7": + sz, err := sevenzip.OpenReader(filePath) + if err != nil { + return 0, err + } + defer sz.Close() + for _, f := range sz.File { + if !f.FileInfo().IsDir() && isImageFile(f.Name) { + count++ + } + } + case ".cbt": + f, err := os.Open(filePath) + if err != nil { + return 0, err + } + defer f.Close() + tr := tar.NewReader(f) + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + break + } + if !header.FileInfo().IsDir() && isImageFile(header.Name) { + count++ + } + } + } + + return count, nil } func (s *MediaScanner) updateMediaItem(ctx context.Context, mediaItemID pgtype.UUID, path string, _ os.FileInfo) error {