forked from Ivasoft/opds-proxy
refactor
This commit is contained in:
53
.gitignore
vendored
Normal file
53
.gitignore
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
.DS_Store
|
||||
*.[56789ao]
|
||||
*.a[56789o]
|
||||
*.so
|
||||
*.pyc
|
||||
._*
|
||||
.nfs.*
|
||||
[56789a].out
|
||||
*~
|
||||
*.orig
|
||||
*.rej
|
||||
*.exe
|
||||
.*.swp
|
||||
core
|
||||
*.cgo*.go
|
||||
*.cgo*.c
|
||||
_cgo_*
|
||||
_obj
|
||||
_test
|
||||
_testmain.go
|
||||
tmp
|
||||
|
||||
/VERSION.cache
|
||||
/bin/
|
||||
/build.out
|
||||
/doc/articles/wiki/*.bin
|
||||
/goinstall.log
|
||||
/last-change
|
||||
/misc/cgo/life/run.out
|
||||
/misc/cgo/stdio/run.out
|
||||
/misc/cgo/testso/main
|
||||
/pkg/
|
||||
/src/*.*/
|
||||
/src/cmd/cgo/zdefaultcc.go
|
||||
/src/cmd/dist/dist
|
||||
/src/cmd/go/internal/cfg/zdefaultcc.go
|
||||
/src/cmd/internal/objabi/zbootstrap.go
|
||||
/src/go/build/zcgo.go
|
||||
/src/go/doc/headscan
|
||||
/src/internal/buildcfg/zbootstrap.go
|
||||
/src/runtime/internal/sys/zversion.go
|
||||
/src/unicode/maketables
|
||||
/src/time/tzdata/zzipdata.go
|
||||
/test.out
|
||||
/test/garbage/*.out
|
||||
/test/pass.out
|
||||
/test/run.out
|
||||
/test/times.out
|
||||
|
||||
# This file includes artifacts of Go build that should not be checked in.
|
||||
# For files created by specific development environment (e.g. editor),
|
||||
# use alternative ways to exclude files from git.
|
||||
# For example, set up .git/info/exclude or use a global .gitignore.
|
||||
109
html/feed.go
Normal file
109
html/feed.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/evan-buss/kobo-opds-proxy/opds"
|
||||
)
|
||||
|
||||
type FeedViewModel struct {
|
||||
Title string
|
||||
Search string
|
||||
Navigation []NavigationViewModel
|
||||
Links []LinkViewModel
|
||||
}
|
||||
type NavigationViewModel struct {
|
||||
Href string
|
||||
Label string
|
||||
}
|
||||
|
||||
type LinkViewModel struct {
|
||||
Title string
|
||||
Author string
|
||||
ImageURL string
|
||||
Content string
|
||||
Href string
|
||||
IsDownload bool
|
||||
}
|
||||
|
||||
func convertFeed(p *FeedParams) FeedViewModel {
|
||||
vm := FeedViewModel{
|
||||
Title: p.Feed.Title,
|
||||
Search: "",
|
||||
Links: make([]LinkViewModel, 0),
|
||||
Navigation: make([]NavigationViewModel, 0),
|
||||
}
|
||||
|
||||
for _, link := range p.Feed.Links {
|
||||
if link.Rel == "search" {
|
||||
vm.Search = resolveHref(p.URL, link.Href)
|
||||
}
|
||||
|
||||
if link.TypeLink == "application/atom+xml;type=feed;profile=opds-catalog" {
|
||||
vm.Navigation = append(vm.Navigation, NavigationViewModel{
|
||||
Href: resolveHref(p.URL, link.Href),
|
||||
Label: strings.ToUpper(link.Rel[:1]) + link.Rel[1:],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, entry := range p.Feed.Entries {
|
||||
vm.Links = append(vm.Links, constructLink(p.URL, entry))
|
||||
}
|
||||
|
||||
return vm
|
||||
}
|
||||
|
||||
func constructLink(url string, entry opds.Entry) LinkViewModel {
|
||||
vm := LinkViewModel{
|
||||
Title: entry.Title,
|
||||
Content: entry.Content.Content,
|
||||
}
|
||||
|
||||
authors := make([]string, 0)
|
||||
for _, author := range entry.Author {
|
||||
authors = append(authors, author.Name)
|
||||
}
|
||||
vm.Author = strings.Join(authors, " & ")
|
||||
|
||||
for _, link := range entry.Links {
|
||||
vm.IsDownload = link.IsDownload()
|
||||
if link.IsNavigation() || link.IsDownload() {
|
||||
vm.Href = resolveHref(url, link.Href)
|
||||
}
|
||||
|
||||
// Prefer the first "thumbnail" image we find
|
||||
if vm.ImageURL == "" && link.IsImage("thumbnail") {
|
||||
vm.ImageURL = resolveHref(url, link.Href)
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't find a thumbnail, use the first image we find
|
||||
if vm.ImageURL == "" {
|
||||
for _, link := range entry.Links {
|
||||
if link.IsImage("") {
|
||||
vm.ImageURL = resolveHref(url, link.Href)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -3,7 +3,11 @@
|
||||
<nav class="navigation">
|
||||
{{if .Search }}
|
||||
<div class="search">
|
||||
<a href="{{.Search}}" tabindex="-1">
|
||||
<a
|
||||
href="{{.Search}}"
|
||||
tabindex="-1"
|
||||
hx-get="?q={{.Search}}&partial=search.html"
|
||||
>
|
||||
<svg
|
||||
width="30"
|
||||
height="30"
|
||||
@@ -50,4 +54,5 @@
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
|
||||
{{end}}
|
||||
|
||||
119
html/html.go
119
html/html.go
@@ -2,12 +2,8 @@ package html
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/evan-buss/kobo-opds-proxy/opds"
|
||||
)
|
||||
@@ -17,11 +13,12 @@ var files embed.FS
|
||||
|
||||
var (
|
||||
home = parse("home.html")
|
||||
feed = parse("feed.html")
|
||||
feed = parse("feed.html", "partials/search.html")
|
||||
)
|
||||
|
||||
func parse(file string) *template.Template {
|
||||
return template.Must(template.New("layout.html").ParseFS(files, "layout.html", file))
|
||||
func parse(file ...string) *template.Template {
|
||||
file = append(file, "layout.html")
|
||||
return template.Must(template.New("layout.html").ParseFS(files, file...))
|
||||
}
|
||||
|
||||
type FeedParams struct {
|
||||
@@ -29,111 +26,12 @@ type FeedParams struct {
|
||||
Feed *opds.Feed
|
||||
}
|
||||
|
||||
type FeedViewModel struct {
|
||||
Title string
|
||||
Search string
|
||||
Navigation []NavigationViewModel
|
||||
Links []LinkViewModel
|
||||
}
|
||||
type NavigationViewModel struct {
|
||||
Href string
|
||||
Label string
|
||||
}
|
||||
|
||||
type LinkViewModel struct {
|
||||
Title string
|
||||
Author string
|
||||
ImageURL string
|
||||
Content string
|
||||
Href string
|
||||
IsDownload bool
|
||||
}
|
||||
|
||||
func convertFeed(url string, feed *opds.Feed) FeedViewModel {
|
||||
vm := FeedViewModel{
|
||||
Title: feed.Title,
|
||||
Search: "",
|
||||
Links: make([]LinkViewModel, 0),
|
||||
Navigation: make([]NavigationViewModel, 0),
|
||||
}
|
||||
|
||||
for _, link := range feed.Links {
|
||||
if link.Rel == "search" {
|
||||
vm.Search = resolveHref(url, link.Href)
|
||||
}
|
||||
|
||||
if link.TypeLink == "application/atom+xml;type=feed;profile=opds-catalog" {
|
||||
vm.Navigation = append(vm.Navigation, NavigationViewModel{
|
||||
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(url, entry))
|
||||
}
|
||||
|
||||
return vm
|
||||
}
|
||||
|
||||
func constructLink(url string, entry opds.Entry) LinkViewModel {
|
||||
vm := LinkViewModel{
|
||||
Title: entry.Title,
|
||||
Content: entry.Content.Content,
|
||||
}
|
||||
|
||||
authors := make([]string, 0)
|
||||
for _, author := range entry.Author {
|
||||
authors = append(authors, author.Name)
|
||||
}
|
||||
vm.Author = strings.Join(authors, " & ")
|
||||
|
||||
for _, link := range entry.Links {
|
||||
vm.IsDownload = link.IsDownload()
|
||||
if link.IsNavigation() || link.IsDownload() {
|
||||
vm.Href = resolveHref(url, link.Href)
|
||||
}
|
||||
|
||||
// Prefer the first "thumbnail" image we find
|
||||
if vm.ImageURL == "" && link.IsImage("thumbnail") {
|
||||
vm.ImageURL = resolveHref(url, link.Href)
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't find a thumbnail, use the first image we find
|
||||
if vm.ImageURL == "" {
|
||||
for _, link := range entry.Links {
|
||||
if link.IsImage("") {
|
||||
vm.ImageURL = resolveHref(url, link.Href)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.URL, p.Feed)
|
||||
vm := convertFeed(&p)
|
||||
|
||||
return feed.ExecuteTemplate(w, partial, vm)
|
||||
}
|
||||
@@ -143,8 +41,11 @@ type FeedInfo struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
func Home(w io.Writer, p []FeedInfo) error {
|
||||
return home.ExecuteTemplate(w, "layout.html", p)
|
||||
func Home(w io.Writer, vm []FeedInfo, partial string) error {
|
||||
if partial == "" {
|
||||
partial = "layout.html"
|
||||
}
|
||||
return home.ExecuteTemplate(w, partial, vm)
|
||||
}
|
||||
|
||||
func StaticFiles() embed.FS {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>{{block "title" .}}Kobo OPDS Proxy{{end}}</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<main id="container">
|
||||
{{block "content" .}}{{end}}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
<script
|
||||
src="https://unpkg.com/htmx.org@2.0.0"
|
||||
integrity="sha384-wS5l5IKJBvK6sPTKa2WZ1js3d947pvWXbPJ1OmWfEuxLgeHcEbjUUA5i9V5ZkpCw"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
</head>
|
||||
<body>
|
||||
<main id="container">{{block "content" .}}{{end}}</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
16
html/partials/search.html
Normal file
16
html/partials/search.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{{define "search-partial"}}
|
||||
|
||||
<form method="get">
|
||||
<input type="hidden" name="q" value="{{.Search}}" />
|
||||
<input type="search" placeholder="Search" name="search" />
|
||||
|
||||
<script>
|
||||
const searchInput = document.querySelector("input[name=search]");
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
if (searchParams.has("search") && !!searchInput) {
|
||||
searchInput.value = searchParams.get("search");
|
||||
}
|
||||
</script>
|
||||
</form>
|
||||
|
||||
{{end}}
|
||||
43
main.go
43
main.go
@@ -1,11 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/evan-buss/kobo-opds-proxy/html"
|
||||
@@ -13,13 +16,10 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
router := http.NewServeMux()
|
||||
router.HandleFunc("GET /{$}", handleHome())
|
||||
router.HandleFunc("GET /feed", handleFeed())
|
||||
router.Handle("GET /static/", http.FileServer(http.FS(html.StaticFiles())))
|
||||
server := NewServer()
|
||||
|
||||
slog.Info("Starting server", slog.String("port", "8080"))
|
||||
log.Fatal(http.ListenAndServe(":8080", router))
|
||||
slog.Info("Starting server", slog.String("port", server.addr))
|
||||
log.Fatal(http.ListenAndServe(server.addr, server.router))
|
||||
}
|
||||
|
||||
func handleHome() http.HandlerFunc {
|
||||
@@ -35,7 +35,7 @@ func handleHome() http.HandlerFunc {
|
||||
URL: "https://m.gutenberg.org/ebooks.opds/",
|
||||
},
|
||||
}
|
||||
html.Home(w, feeds)
|
||||
html.Home(w, feeds, partial(r))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,18 @@ func handleFeed() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("Fetching feed", slog.String("path", queryURL))
|
||||
parsedUrl, err := url.PathUnescape(queryURL)
|
||||
if err != nil {
|
||||
handleError(r, w, "Failed to parse URL", err)
|
||||
return
|
||||
}
|
||||
queryURL = parsedUrl
|
||||
|
||||
searchTerm := r.URL.Query().Get("search")
|
||||
if searchTerm != "" {
|
||||
fmt.Println("Search term", searchTerm)
|
||||
queryURL = replaceSearchPlaceHolder(queryURL, searchTerm)
|
||||
}
|
||||
|
||||
resp, err := fetchFromUrl(queryURL)
|
||||
if err != nil {
|
||||
@@ -65,13 +76,17 @@ func handleFeed() http.HandlerFunc {
|
||||
|
||||
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}, "")
|
||||
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
|
||||
@@ -105,3 +120,11 @@ func handleError(r *http.Request, w http.ResponseWriter, message string, err err
|
||||
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")
|
||||
}
|
||||
|
||||
30
server.go
Normal file
30
server.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/evan-buss/kobo-opds-proxy/html"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
addr string
|
||||
router *http.ServeMux
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
port := os.Getenv("PORT")
|
||||
if os.Getenv("PORT") == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
router := http.NewServeMux()
|
||||
router.HandleFunc("GET /{$}", handleHome())
|
||||
router.HandleFunc("GET /feed", handleFeed())
|
||||
router.Handle("GET /static/", http.FileServer(http.FS(html.StaticFiles())))
|
||||
|
||||
return &Server{
|
||||
addr: ":" + port,
|
||||
router: router,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user