diff --git a/internal/service/internal_test.go b/internal/service/internal_test.go index b7a5e11..296a09b 100644 --- a/internal/service/internal_test.go +++ b/internal/service/internal_test.go @@ -86,8 +86,8 @@ func TestSortEntries(t *testing.T) { func TestExtractMetadata(t *testing.T) { t.Run("Extract EPUB", func(t *testing.T) { path := filepath.Join("testdata", "mybook", "mybook.epub") - title, author, coverPath, description, series, seriesIndex := extractEpubMetadata(path) - t.Logf("EPUB Title: %q, Author: %q, CoverPath: %q, Description: %q, Series: %q, SeriesIndex: %q", title, author, coverPath, description, series, seriesIndex) + title, author, coverPath, description, series, seriesIndex, subjects := extractEpubMetadata(path) + t.Logf("EPUB Title: %q, Author: %q, CoverPath: %q, Description: %q, Series: %q, SeriesIndex: %q, Subjects: %v", title, author, coverPath, description, series, seriesIndex, subjects) }) t.Run("Parse PDF value", func(t *testing.T) { diff --git a/internal/service/service.go b/internal/service/service.go index a7d5065..e61c79f 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -96,6 +96,7 @@ type CatalogEntry struct { Description string Series string SeriesIndex string + Subjects []string } type IsDirer interface { @@ -206,7 +207,7 @@ func (s OPDS) Scan(fPath string, urlPath string, page int) (*Catalog, error) { if s.ExtractMetadata && !entry.IsDir() { idx := len(catalog.Entries) - 1 - title, author, coverPath, description, series, seriesIndex := extractMetadata(entryPath) + title, author, coverPath, description, series, seriesIndex, subjects := extractMetadata(entryPath) if title != "" { catalog.Entries[idx].Title = title } @@ -225,6 +226,9 @@ func (s OPDS) Scan(fPath string, urlPath string, page int) (*Catalog, error) { if seriesIndex != "" { catalog.Entries[idx].SeriesIndex = seriesIndex } + if len(subjects) > 0 { + catalog.Entries[idx].Subjects = subjects + } } } @@ -261,22 +265,22 @@ func (s OPDS) Scan(fPath string, urlPath string, page int) (*Catalog, error) { return catalog, nil } -func extractMetadata(path string) (string, string, string, string, string, string) { +func extractMetadata(path string) (string, string, string, string, string, string, []string) { ext := strings.ToLower(filepath.Ext(path)) switch ext { case ".epub": return extractEpubMetadata(path) case ".pdf": title, author := extractPdfMetadata(path) - return title, author, "", "", "", "" + return title, author, "", "", "", "", nil } - return "", "", "", "", "", "" + return "", "", "", "", "", "", nil } -func extractEpubMetadata(path string) (string, string, string, string, string, string) { +func extractEpubMetadata(path string) (string, string, string, string, string, string, []string) { r, err := zip.OpenReader(path) if err != nil { - return "", "", "", "", "", "" + return "", "", "", "", "", "", nil } defer r.Close() @@ -289,25 +293,26 @@ func extractEpubMetadata(path string) (string, string, string, string, string, s } if opfPath == "" { - return "", "", "", "", "", "" + return "", "", "", "", "", "", nil } f, err := r.Open(opfPath) if err != nil { - return "", "", "", "", "", "" + return "", "", "", "", "", "", nil } defer f.Close() opfContent, err := io.ReadAll(f) if err != nil { - return "", "", "", "", "", "" + return "", "", "", "", "", "", nil } var opf struct { Metadata struct { - Title string `xml:"title"` - Creator string `xml:"creator"` - Description string `xml:"description"` + Title string `xml:"title"` + Creator string `xml:"creator"` + Description string `xml:"description"` + Subject []string `xml:"subject"` Meta []struct { Name string `xml:"name,attr"` Content string `xml:"content,attr"` @@ -324,7 +329,7 @@ func extractEpubMetadata(path string) (string, string, string, string, string, s decoder := xml.NewDecoder(bytes.NewReader(opfContent)) if err := decoder.Decode(&opf); err != nil { - return "", "", "", "", "", "" + return "", "", "", "", "", "", nil } // If standard unmarshal fails to get values due to namespaces @@ -333,9 +338,10 @@ func extractEpubMetadata(path string) (string, string, string, string, string, s decoder.DefaultSpace = "http://purl.org/dc/elements/1.1/" var opf2 struct { Metadata struct { - Title string `xml:"title"` - Creator string `xml:"creator"` - Description string `xml:"description"` + Title string `xml:"title"` + Creator string `xml:"creator"` + Description string `xml:"description"` + Subject []string `xml:"subject"` Meta []struct { Name string `xml:"name,attr"` Content string `xml:"content,attr"` @@ -352,6 +358,9 @@ func extractEpubMetadata(path string) (string, string, string, string, string, s if opf2.Metadata.Description != "" { opf.Metadata.Description = opf2.Metadata.Description } + if len(opf2.Metadata.Subject) > 0 && len(opf.Metadata.Subject) == 0 { + opf.Metadata.Subject = opf2.Metadata.Subject + } if len(opf2.Metadata.Meta) > 0 && len(opf.Metadata.Meta) == 0 { opf.Metadata.Meta = opf2.Metadata.Meta } @@ -370,7 +379,7 @@ func extractEpubMetadata(path string) (string, string, string, string, string, s // Find cover image in manifest coverPath := findEpubCover(r, opf.Manifest.Items, opfPath) - return opf.Metadata.Title, opf.Metadata.Creator, coverPath, opf.Metadata.Description, series, seriesIndex + return opf.Metadata.Title, opf.Metadata.Creator, coverPath, opf.Metadata.Description, series, seriesIndex, opf.Metadata.Subject } func findEpubCover(r *zip.ReadCloser, items []struct { @@ -1008,6 +1017,13 @@ func (s OPDS) makeFeed(catalog *Catalog, req *http.Request) opds.Feed { Build()) } + for _, subject := range entry.Subjects { + entryBuilder = entryBuilder.AddCategory(opds.Category{ + Term: subject, + Label: subject, + }) + } + feedBuilder = feedBuilder.AddEntry(entryBuilder.Build()) } return feedBuilder.Build() diff --git a/opds/atom.go b/opds/atom.go index 1594ce1..dcee017 100644 --- a/opds/atom.go +++ b/opds/atom.go @@ -25,18 +25,24 @@ type Feed struct { Opds string `xml:"xmlns:opds,attr,omitempty"` } -// Entry is an Atom entry. type Entry struct { - Title string `xml:"title"` - ID string `xml:"id"` - Link []Link `xml:"link"` - Published TimeStr `xml:"published"` - Updated TimeStr `xml:"updated"` - Author *Person `xml:"author"` - Summary *Text `xml:"summary"` - Content *Text `xml:"content"` - DcSeries string `xml:"dc:series,omitempty"` - DcSeriesPosition string `xml:"dc:seriesPosition,omitempty"` + Title string `xml:"title"` + ID string `xml:"id"` + Link []Link `xml:"link"` + Published TimeStr `xml:"published"` + Updated TimeStr `xml:"updated"` + Author *Person `xml:"author"` + Summary *Text `xml:"summary"` + Content *Text `xml:"content"` + DcSeries string `xml:"dc:series,omitempty"` + DcSeriesPosition string `xml:"dc:seriesPosition,omitempty"` + Categories []Category `xml:"category"` +} + +type Category struct { + Term string `xml:"term,attr"` + Label string `xml:"label,attr,omitempty"` + Scheme string `xml:"scheme,attr,omitempty"` } // Link is an Atom link with optional OPDS 1.2 facet attributes. diff --git a/opds/entry_builder.go b/opds/entry_builder.go index 0b72739..eddddf1 100644 --- a/opds/entry_builder.go +++ b/opds/entry_builder.go @@ -48,6 +48,10 @@ func (e entryBuilder) SeriesPosition(pos string) entryBuilder { return builder.Set(e, "DcSeriesPosition", pos).(entryBuilder) } +func (e entryBuilder) AddCategory(category Category) entryBuilder { + return builder.Append(e, "Categories", category).(entryBuilder) +} + func (e entryBuilder) Build() Entry { return builder.GetStruct(e).(Entry) }