forked from Ivasoft/opds-proxy
feat: set up configuration
A YAML file is used to set the available feeds. The CLI args can set the port and the path to the YAML file.
This commit is contained in:
5
config.yml
Normal file
5
config.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
feeds:
|
||||||
|
- name: "Evan's Library"
|
||||||
|
url: "http://calibre.terminus/opds"
|
||||||
|
- name: "Project Gutenberg"
|
||||||
|
url: "http://gutenberg.org/opds"
|
||||||
17
go.mod
17
go.mod
@@ -1,3 +1,20 @@
|
|||||||
module github.com/evan-buss/opds-proxy
|
module github.com/evan-buss/opds-proxy
|
||||||
|
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/knadh/koanf/parsers/yaml v0.1.0
|
||||||
|
github.com/knadh/koanf/providers/file v1.0.0
|
||||||
|
github.com/knadh/koanf/v2 v2.1.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
|
||||||
|
github.com/knadh/koanf/maps v0.1.1 // indirect
|
||||||
|
github.com/knadh/koanf/providers/basicflag v1.0.0
|
||||||
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|||||||
30
go.sum
30
go.sum
@@ -0,0 +1,30 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
|
||||||
|
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||||
|
github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w=
|
||||||
|
github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY=
|
||||||
|
github.com/knadh/koanf/providers/basicflag v1.0.0 h1:qB0es/9fYsLuYnrKazxNCuWtkv3JFX1lI1druUsDDvY=
|
||||||
|
github.com/knadh/koanf/providers/basicflag v1.0.0/go.mod h1:n0NlnaxXUCER/WIzRroT9q3Np+FiZ9pSjrC6A/OozI8=
|
||||||
|
github.com/knadh/koanf/providers/file v1.0.0 h1:DtPvSQBeF+N0QLPMz0yf2bx0nFSxUcncpqQvzCxfCyk=
|
||||||
|
github.com/knadh/koanf/providers/file v1.0.0/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI=
|
||||||
|
github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM=
|
||||||
|
github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es=
|
||||||
|
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||||
|
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
|
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
245
main.go
245
main.go
@@ -1,221 +1,54 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"flag"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"log/slog"
|
|
||||||
"mime"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/evan-buss/opds-proxy/convert"
|
"github.com/knadh/koanf/parsers/yaml"
|
||||||
"github.com/evan-buss/opds-proxy/html"
|
"github.com/knadh/koanf/providers/basicflag"
|
||||||
"github.com/evan-buss/opds-proxy/opds"
|
"github.com/knadh/koanf/providers/file"
|
||||||
|
"github.com/knadh/koanf/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Port string `koanf:"port"`
|
||||||
|
Feeds []feedConfig `koanf:"feeds" `
|
||||||
|
}
|
||||||
|
|
||||||
|
type feedConfig struct {
|
||||||
|
Name string `koanf:"name"`
|
||||||
|
Url string `koanf:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var k = koanf.New(".")
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
server := NewServer()
|
fs := flag.NewFlagSet("", flag.ContinueOnError)
|
||||||
|
fs.String("port", "8080", "port to listen on")
|
||||||
slog.Info("Starting server", slog.String("port", server.addr))
|
configPath := fs.String("config", "config.yml", "config file to load")
|
||||||
log.Fatal(http.ListenAndServe(server.addr, server.router))
|
if err := fs.Parse(os.Args[1:]); err != nil {
|
||||||
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleHome() http.HandlerFunc {
|
// Load config file from disk.
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
// Feed options must be defined here.
|
||||||
slog.Info("Rendering home page")
|
if err := k.Load(file.Provider(*configPath), yaml.Parser()); err != nil && !os.IsNotExist(err) {
|
||||||
feeds := []html.FeedInfo{
|
|
||||||
{
|
|
||||||
Title: "Evan's Library",
|
|
||||||
URL: "http://calibre.terminus/opds",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Title: "Project Gutenberg",
|
|
||||||
URL: "https://m.gutenberg.org/ebooks.opds/",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
html.Home(w, feeds, partial(r))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 == "" {
|
|
||||||
http.Error(w, "No feed specified", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedUrl, err := url.PathUnescape(queryURL)
|
|
||||||
queryURL = parsedUrl
|
|
||||||
if err != nil {
|
|
||||||
handleError(r, w, "Failed to parse URL", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
searchTerm := r.URL.Query().Get("search")
|
|
||||||
if searchTerm != "" {
|
|
||||||
fmt.Println("Search term", searchTerm)
|
|
||||||
queryURL = replaceSearchPlaceHolder(queryURL, searchTerm)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := fetchFromUrl(queryURL)
|
|
||||||
if err != nil {
|
|
||||||
handleError(r, w, "Failed to fetch", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
contentType := resp.Header.Get("Content-Type")
|
|
||||||
mimeType, _, err := mime.ParseMediaType(contentType)
|
|
||||||
if err != nil {
|
|
||||||
handleError(r, w, "Failed to parse content type", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mimeType == "application/atom+xml" {
|
|
||||||
feed, err := opds.ParseFeed(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
handleError(r, w, "Failed to parse feed", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
feedParams := html.FeedParams{
|
|
||||||
URL: queryURL,
|
|
||||||
Feed: feed,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = html.Feed(w, feedParams, partial(r))
|
|
||||||
if 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
os.Remove(epubFile)
|
|
||||||
os.Remove(mobiFile)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range resp.Header {
|
|
||||||
w.Header()[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
io.Copy(w, resp.Body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchFromUrl(url string) (*http.Response, error) {
|
|
||||||
client := &http.Client{
|
|
||||||
Timeout: 2 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.SetBasicAuth("public", "evanbuss")
|
|
||||||
|
|
||||||
return client.Do(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleError(r *http.Request, w http.ResponseWriter, message string, err error) {
|
|
||||||
slog.Error(message, slog.String("path", r.URL.RawPath), slog.Any("error", err))
|
|
||||||
http.Error(w, "An unexpected error occurred", http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
func replaceSearchPlaceHolder(url string, searchTerm string) string {
|
|
||||||
return strings.Replace(url, "{searchTerms}", searchTerm, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return params["filename"]
|
// Flags take precedence over config file.
|
||||||
|
if err := k.Load(basicflag.Provider(fs, "."), nil); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := config{}
|
||||||
|
k.Unmarshal("", &config)
|
||||||
|
|
||||||
|
if len(config.Feeds) == 0 {
|
||||||
|
log.Fatal("No feeds defined in config")
|
||||||
|
}
|
||||||
|
|
||||||
|
server := NewServer(&config)
|
||||||
|
server.Serve()
|
||||||
}
|
}
|
||||||
|
|||||||
219
server.go
219
server.go
@@ -1,10 +1,21 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"log/slog"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/evan-buss/opds-proxy/convert"
|
||||||
"github.com/evan-buss/opds-proxy/html"
|
"github.com/evan-buss/opds-proxy/html"
|
||||||
|
"github.com/evan-buss/opds-proxy/opds"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Middleware func(http.HandlerFunc) http.HandlerFunc
|
type Middleware func(http.HandlerFunc) http.HandlerFunc
|
||||||
@@ -14,19 +25,211 @@ type Server struct {
|
|||||||
router *http.ServeMux
|
router *http.ServeMux
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer() *Server {
|
func NewServer(config *config) *Server {
|
||||||
port := os.Getenv("PORT")
|
|
||||||
if os.Getenv("PORT") == "" {
|
|
||||||
port = "8080"
|
|
||||||
}
|
|
||||||
|
|
||||||
router := http.NewServeMux()
|
router := http.NewServeMux()
|
||||||
router.HandleFunc("GET /{$}", handleHome())
|
router.HandleFunc("GET /{$}", handleHome(config.Feeds))
|
||||||
router.HandleFunc("GET /feed", handleFeed("tmp/"))
|
router.HandleFunc("GET /feed", handleFeed("tmp/"))
|
||||||
router.Handle("GET /static/", http.FileServer(http.FS(html.StaticFiles())))
|
router.Handle("GET /static/", http.FileServer(http.FS(html.StaticFiles())))
|
||||||
|
|
||||||
return &Server{
|
return &Server{
|
||||||
addr: ":" + port,
|
addr: ":" + config.Port,
|
||||||
router: router,
|
router: router,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) Serve() {
|
||||||
|
slog.Info("Starting server", slog.String("port", s.addr))
|
||||||
|
log.Fatal(http.ListenAndServe(s.addr, s.router))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleHome(feeds []feedConfig) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vmFeeds := make([]html.FeedInfo, len(feeds))
|
||||||
|
for i, feed := range feeds {
|
||||||
|
vmFeeds[i] = html.FeedInfo{
|
||||||
|
Title: feed.Name,
|
||||||
|
URL: feed.Url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html.Home(w, vmFeeds, partial(r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 == "" {
|
||||||
|
http.Error(w, "No feed specified", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedUrl, err := url.PathUnescape(queryURL)
|
||||||
|
queryURL = parsedUrl
|
||||||
|
if err != nil {
|
||||||
|
handleError(r, w, "Failed to parse URL", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
searchTerm := r.URL.Query().Get("search")
|
||||||
|
if searchTerm != "" {
|
||||||
|
fmt.Println("Search term", searchTerm)
|
||||||
|
queryURL = replaceSearchPlaceHolder(queryURL, searchTerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := fetchFromUrl(queryURL)
|
||||||
|
if err != nil {
|
||||||
|
handleError(r, w, "Failed to fetch", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
contentType := resp.Header.Get("Content-Type")
|
||||||
|
mimeType, _, err := mime.ParseMediaType(contentType)
|
||||||
|
if err != nil {
|
||||||
|
handleError(r, w, "Failed to parse content type", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mimeType == "application/atom+xml" {
|
||||||
|
feed, err := opds.ParseFeed(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
handleError(r, w, "Failed to parse feed", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
feedParams := html.FeedParams{
|
||||||
|
URL: queryURL,
|
||||||
|
Feed: feed,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = html.Feed(w, feedParams, partial(r))
|
||||||
|
if 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
os.Remove(epubFile)
|
||||||
|
os.Remove(mobiFile)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range resp.Header {
|
||||||
|
w.Header()[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
io.Copy(w, resp.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchFromUrl(url string) (*http.Response, error) {
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 2 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.SetBasicAuth("public", "evanbuss")
|
||||||
|
|
||||||
|
return client.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleError(r *http.Request, w http.ResponseWriter, message string, err error) {
|
||||||
|
slog.Error(message, slog.String("path", r.URL.RawPath), slog.Any("error", err))
|
||||||
|
http.Error(w, "An unexpected error occurred", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceSearchPlaceHolder(url string, searchTerm string) string {
|
||||||
|
return strings.Replace(url, "{searchTerms}", searchTerm, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
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"]
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user