diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..c4ef1e4 --- /dev/null +++ b/.air.toml @@ -0,0 +1,51 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ." + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[proxy] + app_port = 0 + enabled = false + proxy_port = 0 + +[screen] + clear_on_rebuild = true + keep_scroll = true diff --git a/go.mod b/go.mod index 42b9353..de434cd 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module github.com/evan-buss/kobo-opds-proxy go 1.22 - -require github.com/opds-community/libopds2-go v0.0.0-20170628075933-9c163cf60f6e // indirect diff --git a/go.sum b/go.sum index 6ac4ed2..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +0,0 @@ -github.com/opds-community/libopds2-go v0.0.0-20170628075933-9c163cf60f6e h1:kjurmIVxVypqhb5CUAG9jLhYL1TLsUE47KfoEm7cdlE= -github.com/opds-community/libopds2-go v0.0.0-20170628075933-9c163cf60f6e/go.mod h1:U/OpXIq9O6FgLfzvun31PZt8iIlbG93BieaxjOEIAd0= diff --git a/html/feed.html b/html/feed.html index dcc6b75..ff51bbe 100644 --- a/html/feed.html +++ b/html/feed.html @@ -1,11 +1,22 @@ {{define "content"}} -{{range .Feed.Links}} - {{template "link" .}} - {{.Title}} -{{end}} + {{end}} diff --git a/html/html.go b/html/html.go index f637f37..0d00e85 100644 --- a/html/html.go +++ b/html/html.go @@ -1,27 +1,36 @@ -package html +package html import ( + "embed" "html/template" "io" - "github.com/opds-community/libopds2-go/opds1" + + "github.com/evan-buss/kobo-opds-proxy/opds" ) +//go:embed * +var files embed.FS + var ( feed = parse("feed.html") ) func parse(file string) *template.Template { - return template.Must(template.New("layout.html").ParseFiles("html/layout.html", "html/" + file)) + return template.Must(template.New("layout.html").ParseFS(files, "layout.html", file)) } type FeedParams struct { - Feed *opds1.Feed + Feed *opds.Feed } func Feed(w io.Writer, p FeedParams, partial string) error { - if (partial == "" ) { + if partial == "" { partial = "layout.html" } return feed.ExecuteTemplate(w, partial, p) -} \ No newline at end of file +} + +func StaticFiles() embed.FS { + return files +} diff --git a/html/layout.html b/html/layout.html index d3d1fd6..9ab6822 100644 --- a/html/layout.html +++ b/html/layout.html @@ -3,9 +3,11 @@ {{block "title" .}}Kobo OPDS Proxy{{end}} - + - {{block "content" .}}{{end}} +
+ {{block "content" .}}{{end}} +
\ No newline at end of file diff --git a/html/static/style.css b/html/static/style.css new file mode 100644 index 0000000..5bb72c3 --- /dev/null +++ b/html/static/style.css @@ -0,0 +1,40 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + + font-family: sans-serif; + font-size: 1.25rem; +} + +#container { + border: 1px solid rgba(0, 0, 0, 0.5); + min-width: 1200px; +} + +#container > ul { + list-style-type: none; +} + +#container > ul > li { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 1rem; +} + +#container > ul > li:not(:last-child) { + border-bottom: 1px solid rgba(0, 0, 0, 0.5); +} + +body { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; +} + +a { + text-decoration: none; + color: black; +} diff --git a/main.go b/main.go index 75357bd..373eb00 100644 --- a/main.go +++ b/main.go @@ -1,39 +1,88 @@ package main import ( - "encoding/xml" - "github.com/opds-community/libopds2-go/opds1" + "io" "log" + "log/slog" "net/http" - "os" + "time" + "github.com/evan-buss/kobo-opds-proxy/html" + "github.com/evan-buss/kobo-opds-proxy/opds" ) +const baseUrl = "http://calibre.terminus" + func main() { router := http.NewServeMux() - router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - feed := parseFeed() - html.Feed(w, html.FeedParams{feed}, "") - }) + router.HandleFunc("GET /", handleFeed()) + router.HandleFunc("GET /acquisition/{path...}", handleAcquisition()) + router.Handle("GET /static/", http.FileServer(http.FS(html.StaticFiles()))) - router.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, r.URL.Path[1:]) - }) - - log.Panic(http.ListenAndServe(":8080", router)) + slog.Info("Starting server", slog.String("port", "8080")) + log.Fatal(http.ListenAndServe(":8080", router)) } -func parseFeed() *opds1.Feed { - var feed opds1.Feed +func handleFeed() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + opdsUrl := baseUrl + r.URL.RequestURI() - bytes, err := os.ReadFile("/home/evan/Downloads/opds.atom") - if err != nil { - panic(err) - } - err = xml.Unmarshal(bytes, &feed) - if err != nil { - panic(err) - } + slog.Info("Fetching feed", slog.String("path", opdsUrl)) - return &feed + feed, err := fetchFeed(opdsUrl) + if err != nil { + slog.Error("Failed to parse feed", slog.String("path", r.URL.RawPath), slog.Any("error", err)) + http.Error(w, "An unexpected error occurred", http.StatusInternalServerError) + return + } + + err = html.Feed(w, html.FeedParams{Feed: feed}, "") + if err != nil { + slog.Error("Failed to render feed", slog.String("path", r.URL.RawPath), slog.Any("error", err)) + http.Error(w, "An unexpected error occurred", http.StatusInternalServerError) + return + } + } +} + +func handleAcquisition() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + opdsUrl := baseUrl + "/" + r.PathValue("path") + resp, err := fetchFromUrl(opdsUrl) + if err != nil { + slog.Error("Failed to fetch acquisition", slog.String("path", opdsUrl), slog.Any("error", err)) + http.Error(w, "An unexpected error occurred", http.StatusInternalServerError) + 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("user", "password") + + return client.Do(req) +} + +func fetchFeed(url string) (*opds.Feed, error) { + r, err := fetchFromUrl(url) + if err != nil { + return nil, err + } + defer r.Body.Close() + + return opds.ParseFeed(r.Body) } diff --git a/opds/models.go b/opds/models.go new file mode 100644 index 0000000..81e1df8 --- /dev/null +++ b/opds/models.go @@ -0,0 +1,87 @@ +package opds + +import ( + "time" +) + +// Feed root element for acquisition or navigation feed +type Feed struct { + ID string `xml:"id"` + Title string `xml:"title"` + Updated time.Time `xml:"updated"` + Entries []Entry `xml:"entry"` + Links []Link `xml:"link"` + TotalResults int `xml:"totalResults"` + ItemsPerPage int `xml:"itemsPerPage"` +} + +// Link link to different resources +type Link struct { + Rel string `xml:"rel,attr"` + Href string `xml:"href,attr"` + TypeLink string `xml:"type,attr"` + Title string `xml:"title,attr"` + FacetGroup string `xml:"facetGroup,attr"` + Count int `xml:"count,attr"` + Price Price `xml:"price"` + IndirectAcquisition []IndirectAcquisition `xml:"indirectAcquisition"` +} + +// Author represent the feed author or the entry author +type Author struct { + Name string `xml:"name"` + URI string `xml:"uri"` +} + +// Entry an atom entry in the feed +type Entry struct { + Title string `xml:"title"` + ID string `xml:"id"` + Identifier string `xml:"identifier"` + Updated *time.Time `xml:"updated"` + Rights string `xml:"rights"` + Publisher string `xml:"publisher"` + Author []Author `xml:"author,omitempty"` + Language string `xml:"language"` + Issued string `xml:"issued"` // Check for format + Published *time.Time `xml:"published"` + Category []Category `xml:"category,omitempty"` + Links []Link `xml:"link,omitempty"` + Summary Content `xml:"summary"` + Content Content `xml:"content"` + Series []Serie `xml:"Series"` +} + +// Content content tag in an entry, the type will be html or text +type Content struct { + Content string `xml:",cdata"` + ContentType string `xml:"type,attr"` +} + +// Category represent the book category with scheme and term to machine +// handling +type Category struct { + Scheme string `xml:"scheme,attr"` + Term string `xml:"term,attr"` + Label string `xml:"label,attr"` +} + +// Price represent the book price +type Price struct { + CurrencyCode string `xml:"currencycode,attr"` + Value float64 `xml:",cdata"` +} + +// IndirectAcquisition represent the link mostly for buying or borrowing +// a book +type IndirectAcquisition struct { + TypeAcquisition string `xml:"type,attr"` + IndirectAcquisition []IndirectAcquisition `xml:"indirectAcquisition"` +} + +// Serie store serie information from schema.org +type Serie struct { + Name string `xml:"name,attr"` + URL string `xml:"url,attr"` + Position float32 `xml:"position,attr"` +} diff --git a/opds/opds.go b/opds/opds.go new file mode 100644 index 0000000..29bf2b4 --- /dev/null +++ b/opds/opds.go @@ -0,0 +1,40 @@ +package opds + +import ( + "encoding/xml" + "io" + "strings" +) + +func ParseFeed(r io.Reader) (*Feed, error) { + var feed Feed + + err := xml.NewDecoder(r).Decode(&feed) + if err != nil { + return nil, err + } + + return &feed, nil +} + +func (link Link) IsDownload() string { + if link.Rel == "http://opds-spec.org/acquisition" { + return link.Href + } + + return "" +} + +func (link Link) IsImage(category string) string { + if strings.HasPrefix(link.TypeLink, "image") { + if strings.Contains(link.Rel, category) { + return link.Href + } + } + + return "" +} + +func (link Link) IsNavigation() bool { + return link.TypeLink == "application/atom+xml;type=feed;profile=opds-catalog" +} diff --git a/static/style.css b/static/style.css deleted file mode 100644 index 22e5183..0000000 --- a/static/style.css +++ /dev/null @@ -1,20 +0,0 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; - - font-family: sans-serif; - line-height: 1.6; - font-size: 1rem; -} - -body { - display: flex; - justify-content: center; - align-items: start; - flex-flow: column nowrap; -} - -a { - display: block; -}