diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..fc7fb6d
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,16 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Launch Package",
+ "type": "go",
+ "request": "launch",
+ "mode": "auto",
+ "program": "${workspaceFolder}"
+ }
+
+ ]
+}
\ No newline at end of file
diff --git a/html/feed.html b/html/feed.html
index 3d55daf..1aaedcb 100644
--- a/html/feed.html
+++ b/html/feed.html
@@ -25,8 +25,9 @@
{{end}}
@@ -35,14 +36,14 @@
{{range .Links}}
{{if .ImageURL}}
-
+
{{end}}
diff --git a/html/home.html b/html/home.html
new file mode 100644
index 0000000..cc691eb
--- /dev/null
+++ b/html/home.html
@@ -0,0 +1,9 @@
+{{define "content"}}
+
+{{end}}
diff --git a/html/html.go b/html/html.go
index 0d2b53c..d46fad7 100644
--- a/html/html.go
+++ b/html/html.go
@@ -2,8 +2,11 @@ package html
import (
"embed"
+ "fmt"
"html/template"
"io"
+ "log"
+ "net/url"
"strings"
"github.com/evan-buss/kobo-opds-proxy/opds"
@@ -13,6 +16,7 @@ import (
var files embed.FS
var (
+ home = parse("home.html")
feed = parse("feed.html")
)
@@ -21,6 +25,7 @@ func parse(file string) *template.Template {
}
type FeedParams struct {
+ URL string
Feed *opds.Feed
}
@@ -44,7 +49,7 @@ type LinkViewModel struct {
IsDownload bool
}
-func convertFeed(feed *opds.Feed) FeedViewModel {
+func convertFeed(url string, feed *opds.Feed) FeedViewModel {
vm := FeedViewModel{
Title: feed.Title,
Search: "",
@@ -54,25 +59,25 @@ func convertFeed(feed *opds.Feed) FeedViewModel {
for _, link := range feed.Links {
if link.Rel == "search" {
- vm.Search = link.Href
+ vm.Search = resolveHref(url, link.Href)
}
if link.TypeLink == "application/atom+xml;type=feed;profile=opds-catalog" {
vm.Navigation = append(vm.Navigation, NavigationViewModel{
- Href: link.Href,
+ Href: resolveHref(url, link.Href),
Label: strings.ToUpper(link.Rel[:1]) + link.Rel[1:],
})
}
}
for _, entry := range feed.Entries {
- vm.Links = append(vm.Links, constructLink(entry))
+ vm.Links = append(vm.Links, constructLink(url, entry))
}
return vm
}
-func constructLink(entry opds.Entry) LinkViewModel {
+func constructLink(url string, entry opds.Entry) LinkViewModel {
vm := LinkViewModel{
Title: entry.Title,
Content: entry.Content.Content,
@@ -87,13 +92,12 @@ func constructLink(entry opds.Entry) LinkViewModel {
for _, link := range entry.Links {
vm.IsDownload = link.IsDownload()
if link.IsNavigation() || link.IsDownload() {
- vm.Href = link.Href
-
+ vm.Href = resolveHref(url, link.Href)
}
// Prefer the first "thumbnail" image we find
if vm.ImageURL == "" && link.IsImage("thumbnail") {
- vm.ImageURL = link.Href
+ vm.ImageURL = resolveHref(url, link.Href)
}
}
@@ -101,7 +105,7 @@ func constructLink(entry opds.Entry) LinkViewModel {
if vm.ImageURL == "" {
for _, link := range entry.Links {
if link.IsImage("") {
- vm.ImageURL = link.Href
+ vm.ImageURL = resolveHref(url, link.Href)
break
}
}
@@ -109,17 +113,40 @@ func constructLink(entry opds.Entry) LinkViewModel {
return vm
}
+func resolveHref(feedUrl string, relativePath string) string {
+ baseUrl, err := url.Parse(feedUrl)
+ if err != nil {
+ log.Fatal(err)
+ }
+ relativeUrl, err := url.Parse(relativePath)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ resolved := baseUrl.ResolveReference(relativeUrl).String()
+ fmt.Println("Resolved URL: ", resolved)
+ return resolved
+}
func Feed(w io.Writer, p FeedParams, partial string) error {
if partial == "" {
partial = "layout.html"
}
- vm := convertFeed(p.Feed)
+ vm := convertFeed(p.URL, p.Feed)
return feed.ExecuteTemplate(w, partial, vm)
}
+type FeedInfo struct {
+ Title string
+ URL string
+}
+
+func Home(w io.Writer, p []FeedInfo) error {
+ return home.ExecuteTemplate(w, "layout.html", p)
+}
+
func StaticFiles() embed.FS {
return files
}
diff --git a/main.go b/main.go
index 373eb00..39f79bd 100644
--- a/main.go
+++ b/main.go
@@ -4,6 +4,7 @@ import (
"io"
"log"
"log/slog"
+ "mime"
"net/http"
"time"
@@ -11,55 +12,78 @@ import (
"github.com/evan-buss/kobo-opds-proxy/opds"
)
-const baseUrl = "http://calibre.terminus"
-
func main() {
router := http.NewServeMux()
- router.HandleFunc("GET /", handleFeed())
- router.HandleFunc("GET /acquisition/{path...}", handleAcquisition())
+ router.HandleFunc("GET /{$}", handleHome())
+ router.HandleFunc("GET /feed", handleFeed())
router.Handle("GET /static/", http.FileServer(http.FS(html.StaticFiles())))
slog.Info("Starting server", slog.String("port", "8080"))
log.Fatal(http.ListenAndServe(":8080", router))
}
-func handleFeed() http.HandlerFunc {
+func handleHome() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
- opdsUrl := baseUrl + r.URL.RequestURI()
-
- slog.Info("Fetching feed", slog.String("path", opdsUrl))
-
- 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
+ slog.Info("Rendering home page")
+ 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)
}
}
-func handleAcquisition() http.HandlerFunc {
+func handleFeed() 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)
+ queryURL := r.URL.Query().Get("q")
+ if queryURL == "" {
+ http.Error(w, "No feed specified", http.StatusBadRequest)
return
}
+ slog.Info("Fetching feed", slog.String("path", queryURL))
+
+ resp, err := fetchFromUrl(queryURL)
+ if err != nil {
+ handleError(r, w, "Failed to fetch feed", 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
+ }
+
+ err = html.Feed(w, html.FeedParams{URL: queryURL, Feed: feed}, "")
+ if err != nil {
+ handleError(r, w, "Failed to render feed", err)
+ return
+ }
+ }
+
for k, v := range resp.Header {
w.Header()[k] = v
}
io.Copy(w, resp.Body)
+
}
}
@@ -77,12 +101,7 @@ func fetchFromUrl(url string) (*http.Response, error) {
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)
+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)
}
diff --git a/opds/opds.go b/opds/opds.go
index c2c98fc..9c3b9d2 100644
--- a/opds/opds.go
+++ b/opds/opds.go
@@ -22,7 +22,7 @@ func (link Link) IsDownload() bool {
}
func (link Link) IsImage(category string) bool {
- if strings.HasPrefix(link.TypeLink, "image") {
+ if strings.HasPrefix(link.TypeLink, "image") && !strings.HasPrefix(link.Href, "data") {
return strings.Contains(link.Rel, category)
}
@@ -30,5 +30,5 @@ func (link Link) IsImage(category string) bool {
}
func (link Link) IsNavigation() bool {
- return link.TypeLink == "application/atom+xml;type=feed;profile=opds-catalog"
+ return link.TypeLink == "application/atom+xml;type=feed;profile=opds-catalog" || link.Rel == "subsection"
}