forked from Ivasoft/opds-proxy
feat: kepub conversions
Working *.epub to *.kepub.epub file conversions when using a Kobo reader. Updated docker file to include `kepubify` to convert to kepub. If not available the file is just sent without conversion.
This commit is contained in:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"EPUB",
|
||||
"kepub",
|
||||
"mobi",
|
||||
"opds"
|
||||
]
|
||||
}
|
||||
14
convert/convert.go
Normal file
14
convert/convert.go
Normal file
@@ -0,0 +1,14 @@
|
||||
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
|
||||
}
|
||||
29
convert/kepub.go
Normal file
29
convert/kepub.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type KepubConverter struct {
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func (kc *KepubConverter) Available() bool {
|
||||
path, err := exec.LookPath("kepubify")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return path != ""
|
||||
}
|
||||
|
||||
func (kc *KepubConverter) Convert(input string, output string) error {
|
||||
kc.mutex.Lock()
|
||||
defer kc.mutex.Unlock()
|
||||
cmd := exec.Command("kepubify", "-v", "-u", "-o", output, input)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
25
convert/mobi.go
Normal file
25
convert/mobi.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type MobiConverter struct {
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func (mc *MobiConverter) Available() bool {
|
||||
path, err := exec.LookPath("kindlegen")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return path != ""
|
||||
}
|
||||
|
||||
func (mc *MobiConverter) Convert(input string, output string) error {
|
||||
mc.mutex.Lock()
|
||||
defer mc.mutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
15
dockerfile
15
dockerfile
@@ -1,6 +1,19 @@
|
||||
# Dockerfile.distroless
|
||||
FROM golang:1.22 as base
|
||||
|
||||
RUN
|
||||
# Download and install kepubify
|
||||
RUN wget https://github.com/pgaskin/kepubify/releases/download/v4.0.4/kepubify-linux-64bit && \
|
||||
mv kepubify-linux-64bit /usr/local/bin/kepubify && \
|
||||
chmod +x /usr/local/bin/kepubify
|
||||
|
||||
# Download and install kindlegen
|
||||
RUN wget https://web.archive.org/web/20150803131026if_/https://kindlegen.s3.amazonaws.com/kindlegen_linux_2.6_i386_v2_9.tar.gz && \
|
||||
mkdir kindlegen && \
|
||||
tar xvf kindlegen_linux_2.6_i386_v2_9.tar.gz --directory kindlegen && \
|
||||
cp kindlegen/kindlegen /usr/local/bin/kindlegen && \
|
||||
chmod +x /usr/local/bin/kindlegen
|
||||
|
||||
WORKDIR /src/kobo-opds-proxy/app/
|
||||
|
||||
COPY go.mod .
|
||||
@@ -15,6 +28,8 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build .
|
||||
|
||||
FROM gcr.io/distroless/static
|
||||
|
||||
COPY --from=base /usr/local/bin/kepubify /usr/local/bin/kepubify
|
||||
COPY --from=base /usr/local/bin/kindlegen /usr/local/bin/kindlegen
|
||||
COPY --from=base /src/kobo-opds-proxy/app/kobo-opds-proxy .
|
||||
|
||||
CMD ["./kobo-opds-proxy"]
|
||||
@@ -1,7 +1,6 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -103,7 +102,5 @@ func resolveHref(feedUrl string, relativePath string) string {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
resolved := baseUrl.ResolveReference(relativeUrl).String()
|
||||
fmt.Println("Resolved URL: ", resolved)
|
||||
return resolved
|
||||
return baseUrl.ResolveReference(relativeUrl).String()
|
||||
}
|
||||
|
||||
96
main.go
96
main.go
@@ -8,9 +8,12 @@ import (
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/evan-buss/kobo-opds-proxy/convert"
|
||||
"github.com/evan-buss/kobo-opds-proxy/html"
|
||||
"github.com/evan-buss/kobo-opds-proxy/opds"
|
||||
)
|
||||
@@ -39,7 +42,10 @@ func handleHome() http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func handleFeed() http.HandlerFunc {
|
||||
func handleFeed(dir string) http.HandlerFunc {
|
||||
kepubConverter := &convert.KepubConverter{}
|
||||
mobiConverter := &convert.MobiConverter{}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
queryURL := r.URL.Query().Get("q")
|
||||
if queryURL == "" {
|
||||
@@ -48,11 +54,11 @@ func handleFeed() http.HandlerFunc {
|
||||
}
|
||||
|
||||
parsedUrl, err := url.PathUnescape(queryURL)
|
||||
queryURL = parsedUrl
|
||||
if err != nil {
|
||||
handleError(r, w, "Failed to parse URL", err)
|
||||
return
|
||||
}
|
||||
queryURL = parsedUrl
|
||||
|
||||
searchTerm := r.URL.Query().Get("search")
|
||||
if searchTerm != "" {
|
||||
@@ -62,10 +68,9 @@ func handleFeed() http.HandlerFunc {
|
||||
|
||||
resp, err := fetchFromUrl(queryURL)
|
||||
if err != nil {
|
||||
handleError(r, w, "Failed to fetch feed", err)
|
||||
handleError(r, w, "Failed to fetch", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
@@ -93,12 +98,68 @@ func handleFeed() http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
if mimeType != convert.EPUB_MIME {
|
||||
for k, v := range resp.Header {
|
||||
w.Header()[k] = v
|
||||
}
|
||||
|
||||
io.Copy(w, resp.Body)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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": strings.Replace(parseFileName(resp), ".epub", ".kepub.epub", 1)}))
|
||||
w.Header().Set("Content-Type", convert.EPUB_MIME)
|
||||
|
||||
io.Copy(w, outFile)
|
||||
|
||||
os.Remove(epubFile)
|
||||
os.Remove(kepubFile)
|
||||
|
||||
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))
|
||||
kepubConverter.Convert(epubFile, mobiFile)
|
||||
|
||||
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": strings.Replace(parseFileName(resp), ".epub", ".kepub.epub", 1)}))
|
||||
w.Header().Set("Content-Type", convert.MOBI_MIME)
|
||||
|
||||
io.Copy(w, outFile)
|
||||
|
||||
os.Remove(epubFile)
|
||||
os.Remove(mobiFile)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for k, v := range resp.Header {
|
||||
w.Header()[k] = v
|
||||
}
|
||||
|
||||
io.Copy(w, resp.Body)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +172,7 @@ func fetchFromUrl(url string) (*http.Response, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SetBasicAuth("user", "password")
|
||||
req.SetBasicAuth("public", "evanbuss")
|
||||
|
||||
return client.Do(req)
|
||||
}
|
||||
@@ -128,3 +189,26 @@ func replaceSearchPlaceHolder(url string, searchTerm string) string {
|
||||
func partial(req *http.Request) string {
|
||||
return req.URL.Query().Get("partial")
|
||||
}
|
||||
|
||||
func downloadFile(path string, resp *http.Response) {
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(file, resp.Body)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func parseFileName(resp *http.Response) string {
|
||||
contentDisposition := resp.Header.Get("Content-Disposition")
|
||||
_, params, err := mime.ParseMediaType(contentDisposition)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return params["filename"]
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/evan-buss/kobo-opds-proxy/html"
|
||||
)
|
||||
|
||||
type Middleware func(http.HandlerFunc) http.HandlerFunc
|
||||
|
||||
type Server struct {
|
||||
addr string
|
||||
router *http.ServeMux
|
||||
@@ -18,9 +21,11 @@ func NewServer() *Server {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
fmt.Println(os.Getenv("PATH"))
|
||||
|
||||
router := http.NewServeMux()
|
||||
router.HandleFunc("GET /{$}", handleHome())
|
||||
router.HandleFunc("GET /feed", handleFeed())
|
||||
router.HandleFunc("GET /feed", handleFeed("tmp/"))
|
||||
router.Handle("GET /static/", http.FileServer(http.FS(html.StaticFiles())))
|
||||
|
||||
return &Server{
|
||||
|
||||
Reference in New Issue
Block a user