Skip to content

Commit

Permalink
refactor: dry conversion code
Browse files Browse the repository at this point in the history
  • Loading branch information
evan-buss committed Jul 12, 2024
1 parent b683d0c commit f4408ab
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 98 deletions.
7 changes: 1 addition & 6 deletions convert/convert.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
package convert

const (
MOBI_MIME = "application/x-mobipocket-ebook"
EPUB_MIME = "application/epub+zip"
)

type Converter interface {
// Whether or not the converter is available
// Usually based on the availability of the underlying tool
Available() bool
// Convert the input file to the output file
Convert(input string, output string) error
Convert(input string) (string, error)
}
31 changes: 21 additions & 10 deletions convert/kepub.go
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
package convert

import (
"fmt"
"os/exec"
"path/filepath"
"strings"
"sync"
)

type KepubConverter struct {
mutex sync.Mutex
mutex sync.Mutex
available bool
availableOnce sync.Once
}

func (kc *KepubConverter) Available() bool {
path, err := exec.LookPath("kepubify")
if err != nil {
return false
}
return path != ""
kc.availableOnce.Do(func() {
fmt.Println("TEST")
path, err := exec.LookPath("kepubify")
kc.available = err == nil && path != ""
})

return kc.available
}

func (kc *KepubConverter) Convert(input string, output string) error {
func (kc *KepubConverter) Convert(input string) (string, error) {
kc.mutex.Lock()
defer kc.mutex.Unlock()
cmd := exec.Command("kepubify", "-v", "-u", "-o", output, input)

dir := filepath.Dir(input)
kepubFile := filepath.Join(dir, strings.Replace(filepath.Base(input), ".epub", ".kepub.epub", 1))

cmd := exec.Command("kepubify", "-v", "-u", "-o", kepubFile, input)
if err := cmd.Run(); err != nil {
return err
return "", err
}

return nil
return kepubFile, nil
}
26 changes: 15 additions & 11 deletions convert/mobi.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,38 @@ import (
"fmt"
"os/exec"
"path/filepath"
"strings"
"sync"
)

type MobiConverter struct {
mutex sync.Mutex
mutex sync.Mutex
available bool
availableOnce sync.Once
}

func (mc *MobiConverter) Available() bool {
path, err := exec.LookPath("kindlegen")
if err != nil {
return false
}
return path != ""
mc.availableOnce.Do(func() {
path, err := exec.LookPath("kindlegen")
mc.available = err == nil && path != ""
})
return mc.available
}

func (mc *MobiConverter) Convert(input string, output string) error {
func (mc *MobiConverter) Convert(input string) (string, error) {
mc.mutex.Lock()
defer mc.mutex.Unlock()

// KindleGen doesn't allow the input file to be in a different directory
// So set the working directory to the input file.
outDir, _ := filepath.Abs(filepath.Dir(input))
mobiFile := filepath.Join(outDir, strings.Replace(filepath.Base(input), ".epub", ".mobi", 1))

// And remove the directory from file paths
cmd := exec.Command("kindlegen",
filepath.Base(input),
"-dont_append_source", "-c1", "-o",
filepath.Base(output),
filepath.Base(mobiFile),
)
cmd.Dir = outDir

Expand All @@ -45,13 +49,13 @@ func (mc *MobiConverter) Convert(input string, output string) error {
if exiterr, ok := err.(*exec.ExitError); ok {
if exiterr.ExitCode() != 1 {
fmt.Println(fmt.Sprint(err) + ": " + out.String() + ":" + stderr.String())
return err
return "", err
}
} else {
fmt.Println(fmt.Sprint(err) + ": " + out.String() + ":" + stderr.String())
return err
return "", err
}
}

return nil
return mobiFile, nil
}
150 changes: 79 additions & 71 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,23 @@ import (
"github.com/evan-buss/opds-proxy/opds"
)

type Middleware func(http.HandlerFunc) http.HandlerFunc

type Server struct {
addr string
router *http.ServeMux
}

const (
MOBI_MIME = "application/x-mobipocket-ebook"
EPUB_MIME = "application/epub+zip"
ATOM_MIME = "application/atom+xml"
)

var (
_ = mime.AddExtensionType(".epub", EPUB_MIME)
_ = mime.AddExtensionType(".kepub.epub", EPUB_MIME)
_ = mime.AddExtensionType(".mobi", MOBI_MIME)
)

func NewServer(config *config) *Server {
router := http.NewServeMux()
router.HandleFunc("GET /{$}", handleHome(config.Feeds))
Expand Down Expand Up @@ -56,7 +66,7 @@ func handleHome(feeds []feedConfig) http.HandlerFunc {
}
}

func handleFeed(dir string) http.HandlerFunc {
func handleFeed(outputDir string) http.HandlerFunc {
kepubConverter := &convert.KepubConverter{}
mobiConverter := &convert.MobiConverter{}

Expand Down Expand Up @@ -93,7 +103,7 @@ func handleFeed(dir string) http.HandlerFunc {
handleError(r, w, "Failed to parse content type", err)
}

if mimeType == "application/atom+xml" {
if mimeType == ATOM_MIME {
feed, err := opds.ParseFeed(resp.Body)
if err != nil {
handleError(r, w, "Failed to parse feed", err)
Expand All @@ -105,82 +115,41 @@ func handleFeed(dir string) http.HandlerFunc {
Feed: feed,
}

err = html.Feed(w, feedParams, partial(r))
if err != nil {
if err = html.Feed(w, feedParams, partial(r)); err != nil {
handleError(r, w, "Failed to render feed", err)
return
}
}

if mimeType != convert.EPUB_MIME {
for k, v := range resp.Header {
w.Header()[k] = v
}

io.Copy(w, resp.Body)
return
var converter convert.Converter
if strings.Contains(r.UserAgent(), "Kobo") && kepubConverter.Available() {
converter = kepubConverter
} else if strings.Contains(r.UserAgent(), "Kindle") && mobiConverter.Available() {
converter = mobiConverter
}

if strings.Contains(r.Header.Get("User-Agent"), "Kobo") && kepubConverter.Available() {
epubFile := filepath.Join(dir, parseFileName(resp))
downloadFile(epubFile, resp)

kepubFile := filepath.Join(dir, strings.Replace(parseFileName(resp), ".epub", ".kepub.epub", 1))
kepubConverter.Convert(epubFile, kepubFile)
if err != nil {
handleError(r, w, "Failed to convert to kepub", err)
}

outFile, _ := os.Open(kepubFile)
defer outFile.Close()

outInfo, _ := outFile.Stat()

w.Header().Set("Content-Length", fmt.Sprintf("%d", outInfo.Size()))
w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filepath.Base(kepubFile)}))
w.Header().Set("Content-Type", convert.EPUB_MIME)

io.Copy(w, outFile)

os.Remove(epubFile)
os.Remove(kepubFile)

if mimeType != EPUB_MIME || converter == nil {
forwardResponse(w, resp)
return
}

if strings.Contains(r.Header.Get("User-Agent"), "Kindle") && mobiConverter.Available() {
epubFile := filepath.Join(dir, parseFileName(resp))
downloadFile(epubFile, resp)

mobiFile := filepath.Join(dir, strings.Replace(parseFileName(resp), ".epub", ".mobi", 1))
err := mobiConverter.Convert(epubFile, mobiFile)
if err != nil {
handleError(r, w, "Failed to convert to mobi", err)
return
}

outFile, _ := os.Open(mobiFile)
defer outFile.Close()

outInfo, _ := outFile.Stat()

w.Header().Set("Content-Length", fmt.Sprintf("%d", outInfo.Size()))
w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filepath.Base(mobiFile)}))
w.Header().Set("Content-Type", convert.MOBI_MIME)

io.Copy(w, outFile)
filename, err := parseFileName(resp)
if err != nil {
handleError(r, w, "Failed to parse file name", err)
}

os.Remove(epubFile)
os.Remove(mobiFile)
epubFile := filepath.Join(outputDir, filename)
downloadFile(epubFile, resp)
defer os.Remove(epubFile)

return
outputFile, err := converter.Convert(epubFile)
if err != nil {
handleError(r, w, "Failed to convert epub", err)
}

for k, v := range resp.Header {
w.Header()[k] = v
if err = sendConvertedFile(w, outputFile); err != nil {
handleError(r, w, "Failed to send converted file", err)
}

io.Copy(w, resp.Body)
}
}

Expand Down Expand Up @@ -211,25 +180,64 @@ func partial(req *http.Request) string {
return req.URL.Query().Get("partial")
}

func downloadFile(path string, resp *http.Response) {
func downloadFile(path string, resp *http.Response) error {
file, err := os.Create(path)
if err != nil {
log.Fatal(err)
return err
}
defer file.Close()

_, err = io.Copy(file, resp.Body)
if err != nil {
log.Fatal(err)
return err
}

return nil
}

func parseFileName(resp *http.Response) string {
func parseFileName(resp *http.Response) (string, error) {
contentDisposition := resp.Header.Get("Content-Disposition")
_, params, err := mime.ParseMediaType(contentDisposition)
if err != nil {
log.Fatal(err)
return "", err
}

return params["filename"], nil
}

func forwardResponse(w http.ResponseWriter, resp *http.Response) {
for k, v := range resp.Header {
w.Header()[k] = v
}

io.Copy(w, resp.Body)
}

func sendConvertedFile(w http.ResponseWriter, filePath string) error {
defer os.Remove(filePath)
file, err := os.Open(filePath)
if err != nil {
return err
}

info, err := file.Stat()
if err != nil {
return err
}

w.Header().Set("Content-Length", fmt.Sprintf("%d", info.Size()))
w.Header().Set("Content-Disposition",
mime.FormatMediaType(
"attachment",
map[string]string{"filename": filepath.Base(filePath)},
),
)
w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(filePath)))

_, err = io.Copy(w, file)
if err != nil {
return err
}

return params["filename"]
return nil
}

0 comments on commit f4408ab

Please sign in to comment.