go-epub's ReadBook parses every spine chapter and fails the entire call if any single chapter (or the TOC) is malformed, discarding already-parsed OPF metadata. For books like Pragmatic's 'A Common-Sense Guide' the OPF holds good title/author/publisher/ISBN metadata that rescans then wrote as blanks - success toast, no (visible) change. Refactor to parse the EPUB's own OPF document with the same Dublin Core machinery used for Calibre sidecars: parseCalibreMetadataOPF is now a thin file wrapper around a reusable parseOPFContent([]byte), and the OPF lookup previously inline in extractEPUBCover is shared via findOPFPathInZip. Since metadata never touches chapter bodies, chapter damage cannot blank it. Also picked up along the way: dc:language mapping and scheme-less dc:identifier values that normalize to a valid ISBN (EPUB3 style). Tests cover a Pragmatic-style EPUB (dc namespace on <metadata>, no identifier scheme, deliberately malformed chapter) that must still yield full metadata, plus series/date/subject OPF parsing.
155 lines
5.3 KiB
Go
155 lines
5.3 KiB
Go
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 <metadata> element (not on
|
|
// <package>), 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": `<?xml version="1.0"?><container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"><rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/></rootfiles></container>`,
|
|
// dc namespace declared on <metadata>; identifiers carry no scheme attr
|
|
"OEBPS/content.opf": `<?xml version="1.0" encoding="UTF-8"?>
|
|
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="PubID">
|
|
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
|
<dc:language>en</dc:language>
|
|
<dc:title>A Common-Sense Guide</dc:title>
|
|
<dc:creator>Jay Wengrow</dc:creator>
|
|
<dc:publisher>The Pragmatic Bookshelf, LLC</dc:publisher>
|
|
<dc:description>Content that makes you a better programmer.</dc:description>
|
|
<dc:subject>Programming</dc:subject>
|
|
<dc:identifier id="PubID">978-1-68050-722-8</dc:identifier>
|
|
<meta name="cover" content="cover-image"/>
|
|
</metadata>
|
|
<manifest>
|
|
<item id="cover-image" href="images/cover.jpg" media-type="image/jpeg"/>
|
|
<item id="ch1" href="ch1.xhtml" media-type="application/xhtml+xml"/>
|
|
</manifest>
|
|
<spine><itemref idref="ch1"/></spine>
|
|
</package>`,
|
|
// Malformed on purpose: unclosed tags
|
|
"OEBPS/ch1.xhtml": `<html><body><p>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 := `<?xml version="1.0"?>
|
|
<package xmlns="http://www.idpf.org/2007/opf" version="2.0" unique-identifier="id">
|
|
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
|
|
<dc:title>Test Book</dc:title>
|
|
<dc:creator>Some Author</dc:creator>
|
|
<dc:date>2020-03-15</dc:date>
|
|
<dc:subject>Fiction</dc:subject>
|
|
<dc:subject>Classic</dc:subject>
|
|
<dc:identifier opf:scheme="ISBN">978-3-16-148410-0</dc:identifier>
|
|
<meta name="calibre:series" content="Great Series"/>
|
|
<meta name="calibre:series_index" content="2.5"/>
|
|
</metadata>
|
|
</package>`
|
|
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)
|
|
}
|
|
}
|