package services import ( "archive/zip" "bytes" "image" "image/jpeg" "os" "path/filepath" "testing" "time" ) // createTestJPEGBytes returns the bytes of a minimal valid JPEG. func createTestJPEGBytes() string { var buf bytes.Buffer img := image.NewRGBA(image.Rect(0, 0, 1, 1)) if err := jpeg.Encode(&buf, img, nil); err != nil { return "" } return buf.String() } // createPragmaticStyleEPUB builds an EPUB modeled on Pragmatic Bookshelf // output: the dc namespace declared on the element (not on // ), a scheme-less ISBN identifier, an OPF-declared cover, and a // deliberately malformed chapter body. The malformed chapter is the // regression trigger: the previous go-epub-based extractor failed the whole // book when any chapter was unparseable and wrote blank metadata. func createPragmaticStyleEPUB(epubPath string) error { file, err := os.Create(epubPath) if err != nil { return err } defer file.Close() zipWriter := zip.NewWriter(file) defer zipWriter.Close() mimetypeW, err := zipWriter.CreateHeader(&zip.FileHeader{ Name: "mimetype", Method: zip.Store, }) if err != nil { return err } mimetypeW.Write([]byte("application/epub+zip")) files := map[string]string{ "META-INF/container.xml": ``, // dc namespace declared on ; identifiers carry no scheme attr "OEBPS/content.opf": ` en A Common-Sense Guide Jay Wengrow The Pragmatic Bookshelf, LLC Content that makes you a better programmer. Programming 978-1-68050-722-8 `, // Malformed on purpose: unclosed tags "OEBPS/ch1.xhtml": `

unclosed paragraph`, "OEBPS/images/cover.jpg": createTestJPEGBytes(), } for name, content := range files { w, err := zipWriter.Create(name) if err != nil { return err } if _, err := w.Write([]byte(content)); err != nil { return err } } return zipWriter.Close() } // TestExtractEPUBMetadataBrokenChapter guards the regression where one // unparseable chapter made the extractor return nothing at all: metadata must // come from the OPF regardless of chapter-body damage. func TestExtractEPUBMetadataBrokenChapter(t *testing.T) { tmpDir := t.TempDir() epubPath := filepath.Join(tmpDir, "book.epub") if err := createPragmaticStyleEPUB(epubPath); err != nil { t.Fatalf("failed to create test EPUB: %v", err) } s := NewMediaScanner(nil) metadata, err := s.extractEPUBMetadata(epubPath) if err != nil { t.Fatalf("extractEPUBMetadata() error: %v", err) } if metadata.Title != "A Common-Sense Guide" { t.Errorf("Title = %q, want %q", metadata.Title, "A Common-Sense Guide") } if metadata.Author != "Jay Wengrow" { t.Errorf("Author = %q, want %q", metadata.Author, "Jay Wengrow") } if metadata.Publisher != "The Pragmatic Bookshelf, LLC" { t.Errorf("Publisher = %q, want %q", metadata.Publisher, "The Pragmatic Bookshelf, LLC") } if metadata.Description == "" { t.Error("Description missing") } if metadata.Language != "en" { t.Errorf("Language = %q, want %q", metadata.Language, "en") } // Scheme-less identifier that normalizes to a valid ISBN must be picked up if metadata.ISBN == "" { t.Error("ISBN missing (scheme-less dc:identifier fallback failed)") } } func TestParseOPFContentCalibreSeries(t *testing.T) { opf := ` Test Book Some Author 2020-03-15 Fiction Classic 978-3-16-148410-0 ` metadata, err := parseOPFContent([]byte(opf)) if err != nil { t.Fatalf("parseOPFContent() error: %v", err) } if metadata.Series != "Great Series" || metadata.SeriesNumber != 2 { t.Errorf("Series = %q/%d, want Great Series/2", metadata.Series, metadata.SeriesNumber) } if metadata.ISBN == "" { t.Error("schemed ISBN not extracted") } wantDate := time.Date(2020, 3, 15, 0, 0, 0, 0, time.UTC) if !metadata.PublishDate.Equal(wantDate) { t.Errorf("PublishDate = %v, want %v", metadata.PublishDate, wantDate) } if len(metadata.Tags) != 2 { t.Errorf("Tags = %v, want 2 subjects", metadata.Tags) } } func TestExtractAudiobookshelfSidecar(t *testing.T) { tests := []struct { name string json string validate func(t *testing.T, m *MediaMetadata) }{ { name: "full sidecar", json: `{ "title": "An Book", "authors": ["Author One", "Author Two"], "series": [{"series": "The Series", "sequence": "4.5"}], "genres": ["Fantasy"], "tags": ["tag1"], "publishedYear": 2019, "publisher": "ACME Books", "description": "A very good book.", "isbn": "978-3-16-148410-0", "asin": "B08XYZ", "language": "en" }`, validate: func(t *testing.T, m *MediaMetadata) { if m.Title != "An Book" || m.Author != "Author One" { t.Errorf("Title/Author = %q/%q", m.Title, m.Author) } if m.Series != "The Series" || m.SeriesNumber != 4 { t.Errorf("Series = %q/%d, want The Series/4", m.Series, m.SeriesNumber) } if len(m.Tags) != 2 { t.Errorf("Tags = %v, want genres+tags merged", m.Tags) } if m.PublishDate.Year() != 2019 { t.Errorf("PublishDate year = %d, want 2019", m.PublishDate.Year()) } if m.Publisher != "ACME Books" || m.Description != "A very good book." { t.Errorf("Publisher/Description = %q/%q", m.Publisher, m.Description) } if m.ISBN == "" || m.ASIN != "B08XYZ" || m.Language != "en" { t.Errorf("ISBN/ASIN/Language = %q/%q/%q", m.ISBN, m.ASIN, m.Language) } }, }, { name: "sparse sidecar (real-world Audiobookshelf export)", json: `{"title": "Sparse (1234)", "authors": ["X"], "tags": [], "description": null}`, validate: func(t *testing.T, m *MediaMetadata) { if m.Title != "Sparse (1234)" || m.Author != "X" { t.Errorf("Title/Author = %q/%q", m.Title, m.Author) } if m.Description != "" || m.Tags != nil { t.Error("null/empty sidecar fields must stay unset") } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "metadata.json"), []byte(tt.json), 0644); err != nil { t.Fatal(err) } m := extractAudiobookshelfSidecar(filepath.Join(dir, "book.epub")) if m == nil { t.Fatal("extractAudiobookshelfSidecar() = nil, want metadata") } tt.validate(t, m) }) } t.Run("no sidecar returns nil", func(t *testing.T) { dir := t.TempDir() if m := extractAudiobookshelfSidecar(filepath.Join(dir, "book.epub")); m != nil { t.Errorf("extractAudiobookshelfSidecar() = %v, want nil", m) } }) }