Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions internal/service/internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 := extractEpubMetadata(path)
t.Logf("EPUB Title: %q, Author: %q, CoverPath: %q, Description: %q", title, author, coverPath, description)
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)
})

t.Run("Parse PDF value", func(t *testing.T) {
Expand Down
58 changes: 47 additions & 11 deletions internal/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ type CatalogEntry struct {
Author string
CoverPath string
Description string
Series string
SeriesIndex string
}

type IsDirer interface {
Expand Down Expand Up @@ -204,7 +206,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 := extractMetadata(entryPath)
title, author, coverPath, description, series, seriesIndex := extractMetadata(entryPath)
if title != "" {
catalog.Entries[idx].Title = title
}
Expand All @@ -217,6 +219,12 @@ func (s OPDS) Scan(fPath string, urlPath string, page int) (*Catalog, error) {
if description != "" {
catalog.Entries[idx].Description = description
}
if series != "" {
catalog.Entries[idx].Series = series
}
if seriesIndex != "" {
catalog.Entries[idx].SeriesIndex = seriesIndex
}
}
}

Expand Down Expand Up @@ -253,22 +261,22 @@ func (s OPDS) Scan(fPath string, urlPath string, page int) (*Catalog, error) {
return catalog, nil
}

func extractMetadata(path string) (string, string, string, string) {
func extractMetadata(path 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, "", "", "", ""
}
return "", "", "", ""
return "", "", "", "", "", ""
}

func extractEpubMetadata(path string) (string, string, string, string) {
func extractEpubMetadata(path string) (string, string, string, string, string, string) {
r, err := zip.OpenReader(path)
if err != nil {
return "", "", "", ""
return "", "", "", "", "", ""
}
defer r.Close()

Expand All @@ -281,25 +289,29 @@ func extractEpubMetadata(path string) (string, string, string, string) {
}

if opfPath == "" {
return "", "", "", ""
return "", "", "", "", "", ""
}

f, err := r.Open(opfPath)
if err != nil {
return "", "", "", ""
return "", "", "", "", "", ""
}
defer f.Close()

opfContent, err := io.ReadAll(f)
if err != nil {
return "", "", "", ""
return "", "", "", "", "", ""
}

var opf struct {
Metadata struct {
Title string `xml:"title"`
Creator string `xml:"creator"`
Description string `xml:"description"`
Meta []struct {
Name string `xml:"name,attr"`
Content string `xml:"content,attr"`
} `xml:"meta"`
} `xml:"metadata"`
Manifest struct {
Items []struct {
Expand All @@ -312,7 +324,7 @@ func extractEpubMetadata(path string) (string, string, string, string) {

decoder := xml.NewDecoder(bytes.NewReader(opfContent))
if err := decoder.Decode(&opf); err != nil {
return "", "", "", ""
return "", "", "", "", "", ""
}

// If standard unmarshal fails to get values due to namespaces
Expand All @@ -324,6 +336,10 @@ func extractEpubMetadata(path string) (string, string, string, string) {
Title string `xml:"title"`
Creator string `xml:"creator"`
Description string `xml:"description"`
Meta []struct {
Name string `xml:"name,attr"`
Content string `xml:"content,attr"`
} `xml:"meta"`
} `xml:"metadata"`
}
_ = decoder.Decode(&opf2)
Expand All @@ -336,12 +352,25 @@ func extractEpubMetadata(path string) (string, string, string, string) {
if opf2.Metadata.Description != "" {
opf.Metadata.Description = opf2.Metadata.Description
}
if len(opf2.Metadata.Meta) > 0 && len(opf.Metadata.Meta) == 0 {
opf.Metadata.Meta = opf2.Metadata.Meta
}
}

var series, seriesIndex string
for _, meta := range opf.Metadata.Meta {
switch meta.Name {
case "calibre:series":
series = meta.Content
case "calibre:series_index":
seriesIndex = meta.Content
}
}

// Find cover image in manifest
coverPath := findEpubCover(r, opf.Manifest.Items, opfPath)

return opf.Metadata.Title, opf.Metadata.Creator, coverPath, opf.Metadata.Description
return opf.Metadata.Title, opf.Metadata.Creator, coverPath, opf.Metadata.Description, series, seriesIndex
}

func findEpubCover(r *zip.ReadCloser, items []struct {
Expand Down Expand Up @@ -953,6 +982,13 @@ func (s OPDS) makeFeed(catalog *Catalog, req *http.Request) opds.Feed {
entryBuilder = entryBuilder.Summary(&text)
}

if s.ExtractMetadata && entry.Series != "" {
entryBuilder = entryBuilder.Series(entry.Series)
}
if s.ExtractMetadata && entry.SeriesIndex != "" {
entryBuilder = entryBuilder.SeriesPosition(entry.SeriesIndex)
}

if s.ExtractMetadata && entry.CoverPath != "" && entry.Type == pathTypeFile {
coverURL := s.joinURL("/cover?file=" + url.QueryEscape(entryPath))
ext := strings.ToLower(filepath.Ext(entry.CoverPath))
Expand Down
18 changes: 10 additions & 8 deletions opds/atom.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ type Feed struct {

// 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"`
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"`
}

// Link is an Atom link with optional OPDS 1.2 facet attributes.
Expand Down
8 changes: 8 additions & 0 deletions opds/entry_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ func (e entryBuilder) Content(content *Text) entryBuilder {
return builder.Set(e, "Content", content).(entryBuilder)
}

func (e entryBuilder) Series(series string) entryBuilder {
return builder.Set(e, "DcSeries", series).(entryBuilder)
}

func (e entryBuilder) SeriesPosition(pos string) entryBuilder {
return builder.Set(e, "DcSeriesPosition", pos).(entryBuilder)
}

func (e entryBuilder) Build() Entry {
return builder.GetStruct(e).(Entry)
}
Expand Down
Loading