Skip to content

Commit 951c46c

Browse files
committed
mail: use epub title metadata instead of file name when available
1 parent 578ce41 commit 951c46c

File tree

187 files changed

+62583
-190945
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

187 files changed

+62583
-190945
lines changed

.github/copilot-instructions.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
applyTo: "**"
3+
---
4+
5+
# Project general coding standards
6+
7+
This project uses Go (Golang) and follows standard community practices, prioritizing simplicity and clarity.
8+
When running prompt always assume that sources could be changed.
9+
10+
## Naming Conventions
11+
* **Variables/Functions:** Use `camelCase` for variable names and function names.
12+
* **Structs/Interfaces:** Use `PascalCase` for struct names and interfaces.
13+
* **Constants:** Use `ALL_CAPS` for constants.
14+
* **Acronyms:** Acronyms (like `URL` or `HTTP`) should be all uppercase in names (e.g., `makeHTTPRequest`, not `makeHttpRequest`).
15+
* **Private/Public:** Use lowercase first letter for package-private items; use uppercase first letter for publicly exported items.
16+
17+
## Code Style and Formatting
18+
* **Indentation:** Use tabs for indentation, not spaces (standard Go practice).
19+
* **Formatting:** Always run `goimports-reviser -format -company-prefixes github.com/rupor-github ./...` before committing. Generated code should be explicitly marked and excluded from style checks.
20+
* **Imports:** Use standard grouping for imports: standard library first, then third-party libraries, then local project packages, separated by blank lines.
21+
* **Control flow:** Prefer early funcion exits. When possible prefer if/then to full if/then/else to avoid nesting.
22+
* **Language** Always use latest Go features.
23+
24+
## Error Handling
25+
* **Error Values:** Errors should be the last return value and have type `error`.
26+
* **Error Wrapping:** Wrap errors using `fmt.Errorf` with `%w` where appropriate to preserve the original error chain.
27+
* **Panics:** Avoid `panic` except for truly unrecoverable situations (e.g., a critical configuration issue at startup). Use error returns for expected failures.
28+
* **Logging:** Use uber zap structured logging library for all logging (avoid `fmt.Println` in production code).
29+
30+
## Project Structure
31+
* **Dependencies:** We use [Go Modules](go.dev) for dependency management.
32+
* **Testing:** Write unit tests for all new functions. Test files should have the `_test.go` suffix. Use the built-in `testing` package and `go test` runner.
33+
34+
## Copilot Directives
35+
* **Prefer standard library:** Where possible, prefer the Go standard library over external dependencies.
36+
* **Concurrency:** When writing concurrent code, prioritize using channels and goroutines following Go idioms.
37+
* **Code Review:** When performing a code review, focus on idiomatic Go practices, test coverage, and clear error handling.
38+
* **Binaries and temporary artifacts:** Never build anything in project directory, use /tmp. Always generate temporary artifacts in the /tmp directory

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
- name: Set up Go
3434
uses: actions/setup-go@v5
3535
with:
36-
go-version: '1.25.1'
36+
go-version: '1.25.5'
3737

3838
- name: Build everything
3939
run: task release

common/epub.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package common
2+
3+
import (
4+
"archive/zip"
5+
"encoding/xml"
6+
"io"
7+
"strings"
8+
)
9+
10+
type epubContainer struct {
11+
Rootfile struct {
12+
Path string `xml:"full-path,attr"`
13+
} `xml:"rootfiles>rootfile"`
14+
}
15+
16+
type epubPackage struct {
17+
Metadata struct {
18+
Title string `xml:"http://purl.org/dc/elements/1.1/ title"`
19+
} `xml:"metadata"`
20+
}
21+
22+
// GetEPUBTitle extracts the title from an EPUB file
23+
// Returns the title if successful, empty string otherwise
24+
func GetEPUBTitle(epubPath string) (string, error) {
25+
r, err := zip.OpenReader(epubPath)
26+
if err != nil {
27+
return "", err
28+
}
29+
defer r.Close()
30+
31+
// Find and read the container.xml to get the content.opf path
32+
var contentPath string
33+
for _, f := range r.File {
34+
if f.Name == "META-INF/container.xml" {
35+
rc, err := f.Open()
36+
if err != nil {
37+
return "", err
38+
}
39+
data, err := io.ReadAll(rc)
40+
rc.Close()
41+
if err != nil {
42+
return "", err
43+
}
44+
45+
var container epubContainer
46+
if err := xml.Unmarshal(data, &container); err != nil {
47+
return "", err
48+
}
49+
contentPath = container.Rootfile.Path
50+
break
51+
}
52+
}
53+
54+
if contentPath == "" {
55+
return "", nil
56+
}
57+
58+
// Read the content.opf to get metadata
59+
// Need to handle both forward and backslashes since some EPUB files use backslashes
60+
contentPathAlt := strings.ReplaceAll(contentPath, "/", "\\")
61+
for _, f := range r.File {
62+
if f.Name == contentPath || f.Name == contentPathAlt {
63+
rc, err := f.Open()
64+
if err != nil {
65+
return "", err
66+
}
67+
data, err := io.ReadAll(rc)
68+
rc.Close()
69+
if err != nil {
70+
return "", err
71+
}
72+
73+
var pkg epubPackage
74+
if err := xml.Unmarshal(data, &pkg); err != nil {
75+
return "", err
76+
}
77+
return strings.TrimSpace(pkg.Metadata.Title), nil
78+
}
79+
}
80+
81+
return "", nil
82+
}

common/epub_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package common
2+
3+
import (
4+
"archive/zip"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
)
9+
10+
func TestGetEPUBTitle(t *testing.T) {
11+
// Create a temporary EPUB file for testing
12+
tmpDir := t.TempDir()
13+
epubPath := filepath.Join(tmpDir, "test.epub")
14+
15+
// Create a minimal EPUB structure
16+
zipFile, err := os.Create(epubPath)
17+
if err != nil {
18+
t.Fatalf("Failed to create test EPUB: %v", err)
19+
}
20+
defer zipFile.Close()
21+
22+
w := zip.NewWriter(zipFile)
23+
24+
// Add mimetype file
25+
mimetypeFile, _ := w.Create("mimetype")
26+
mimetypeFile.Write([]byte("application/epub+zip"))
27+
28+
// Add container.xml
29+
containerFile, _ := w.Create("META-INF/container.xml")
30+
containerXML := `<?xml version="1.0"?>
31+
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
32+
<rootfiles>
33+
<rootfile full-path="content.opf" media-type="application/oebps-package+xml"/>
34+
</rootfiles>
35+
</container>`
36+
containerFile.Write([]byte(containerXML))
37+
38+
// Add content.opf with metadata
39+
contentFile, _ := w.Create("content.opf")
40+
contentOPF := `<?xml version="1.0" encoding="UTF-8"?>
41+
<package xmlns="http://www.idpf.org/2007/opf" version="2.0">
42+
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
43+
<dc:title>Test Book Title</dc:title>
44+
<dc:creator>Test Author</dc:creator>
45+
</metadata>
46+
</package>`
47+
contentFile.Write([]byte(contentOPF))
48+
49+
w.Close()
50+
51+
// Test the function
52+
title, err := GetEPUBTitle(epubPath)
53+
if err != nil {
54+
t.Fatalf("GetEPUBTitle failed: %v", err)
55+
}
56+
57+
expectedTitle := "Test Book Title"
58+
if title != expectedTitle {
59+
t.Errorf("Expected title %q, got %q", expectedTitle, title)
60+
}
61+
}
62+
63+
func TestGetEPUBTitle_NonExistent(t *testing.T) {
64+
_, err := GetEPUBTitle("/nonexistent/file.epub")
65+
if err == nil {
66+
t.Error("Expected error for non-existent file, got nil")
67+
}
68+
}
69+
70+
func TestGetEPUBTitle_InvalidZip(t *testing.T) {
71+
tmpDir := t.TempDir()
72+
invalidPath := filepath.Join(tmpDir, "invalid.epub")
73+
74+
// Create a non-zip file
75+
if err := os.WriteFile(invalidPath, []byte("not a zip file"), 0644); err != nil {
76+
t.Fatalf("Failed to create invalid file: %v", err)
77+
}
78+
79+
_, err := GetEPUBTitle(invalidPath)
80+
if err == nil {
81+
t.Error("Expected error for invalid zip file, got nil")
82+
}
83+
}

go.mod

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module sync2kindle
22

3-
go 1.25.1
3+
go 1.25.5
44

55
tool honnef.co/go/tools/cmd/staticcheck
66

@@ -9,46 +9,43 @@ require (
99
github.com/disintegration/imaging v1.6.2
1010
github.com/dustin/go-humanize v1.0.1
1111
github.com/go-ole/go-ole v1.3.0
12-
github.com/go-playground/validator/v10 v10.27.0
12+
github.com/go-playground/validator/v10 v10.29.0
1313
github.com/gosimple/slug v1.15.0
14-
github.com/rupor-github/gencfg v1.0.7
15-
github.com/urfave/cli/v3 v3.4.1
16-
go.uber.org/zap v1.27.0
17-
golang.org/x/sys v0.36.0
18-
golang.org/x/term v0.35.0
14+
github.com/rupor-github/gencfg v1.0.13
15+
github.com/urfave/cli/v3 v3.6.1
16+
go.uber.org/zap v1.27.1
17+
golang.org/x/sys v0.39.0
18+
golang.org/x/term v0.38.0
1919
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
2020
gopkg.in/yaml.v3 v3.0.1
2121
zombiezen.com/go/sqlite v1.4.2
2222
)
2323

2424
require (
2525
github.com/BurntSushi/toml v1.5.0 // indirect
26-
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
26+
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
2727
github.com/go-playground/locales v0.14.1 // indirect
2828
github.com/go-playground/universal-translator v0.18.1 // indirect
2929
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
3030
github.com/google/uuid v1.6.0 // indirect
3131
github.com/gosimple/unidecode v1.0.1 // indirect
3232
github.com/leodido/go-urn v1.4.0 // indirect
3333
github.com/mattn/go-isatty v0.0.20 // indirect
34-
github.com/ncruces/go-strftime v0.1.9 // indirect
34+
github.com/ncruces/go-strftime v1.0.0 // indirect
3535
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
36-
github.com/stretchr/testify v1.11.1 // indirect
3736
go.uber.org/multierr v1.11.0 // indirect
38-
golang.org/x/crypto v0.42.0 // indirect
39-
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
40-
golang.org/x/exp/typeparams v0.0.0-20250911091902-df9299821621 // indirect
41-
golang.org/x/image v0.31.0 // indirect
42-
golang.org/x/mod v0.28.0 // indirect
43-
golang.org/x/sync v0.17.0 // indirect
44-
golang.org/x/text v0.29.0 // indirect
45-
golang.org/x/tools v0.37.0 // indirect
37+
golang.org/x/crypto v0.46.0 // indirect
38+
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
39+
golang.org/x/exp/typeparams v0.0.0-20251209150349-8475f28825e9 // indirect
40+
golang.org/x/image v0.34.0 // indirect
41+
golang.org/x/mod v0.31.0 // indirect
42+
golang.org/x/sync v0.19.0 // indirect
43+
golang.org/x/text v0.32.0 // indirect
44+
golang.org/x/tools v0.40.0 // indirect
4645
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
4746
honnef.co/go/tools v0.6.1 // indirect
48-
modernc.org/cc/v4 v4.26.5 // indirect
49-
modernc.org/fileutil v1.3.33 // indirect
50-
modernc.org/libc v1.66.9 // indirect
47+
modernc.org/libc v1.67.1 // indirect
5148
modernc.org/mathutil v1.7.1 // indirect
5249
modernc.org/memory v1.11.0 // indirect
53-
modernc.org/sqlite v1.39.0 // indirect
50+
modernc.org/sqlite v1.40.1 // indirect
5451
)

0 commit comments

Comments
 (0)