diff --git a/internal/services/media_scanner.go b/internal/services/media_scanner.go index 5a4399d..ce66543 100644 --- a/internal/services/media_scanner.go +++ b/internal/services/media_scanner.go @@ -1068,7 +1068,15 @@ func (s *MediaScanner) extractMetadata(path string) (*MediaMetadata, error) { case ".epub": metadata, err := s.extractEPUBMetadata(path) if err != nil { - return metadata, err + // 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: mimeType, + }} } // Try to extract embedded cover coverPath, err := s.extractEPUBCover(path) @@ -1181,6 +1189,165 @@ func (s *MediaScanner) extractEPUBMetadata(path string) (*MediaMetadata, error) return metadata, nil } +// 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, `