it mostly works

This commit is contained in:
Evan Buss
2024-07-04 23:48:11 -04:00
parent 9daf7bf8b9
commit 7c63b06e05
11 changed files with 328 additions and 63 deletions

51
.air.toml Normal file
View File

@@ -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

2
go.mod
View File

@@ -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

2
go.sum
View File

@@ -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=

View File

@@ -1,11 +1,22 @@
{{define "content"}}
{{range .Feed.Links}}
{{template "link" .}}
<a href="{{.Href}}">{{.Title}}</a>
{{end}}
<ul>
{{range .Feed.Links}}
<li><a href="{{.Href}}" rel="{{.Rel}}">{{.Title}}{{.Rel}}</a></li>
{{end}}
{{range .Feed.Entries}}
{{$link := index .Links 0}}
<a href="{{$link.Href}}">{{.Title}}</a>
{{end}}
{{range .Feed.Entries}}
<li class="link">
{{$title := .Title}}
{{range .Links}}
{{if .IsImage "image/thumbnail"}}
<img src="/acquisition{{.Href}}" alt="{{$title}}" height="40">
{{else if .IsDownload}}
<a href="/acquisition{{.Href}}" download>{{$title}}</a>
{{else if .IsNavigation }}
<a href="{{.Href}}">{{$title}}</a>
{{end}}
{{end}}
</li>
{{end}}
</ul>
{{end}}

View File

@@ -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)
}
}
func StaticFiles() embed.FS {
return files
}

View File

@@ -3,9 +3,11 @@
<head>
<meta charset="UTF-8">
<title>{{block "title" .}}Kobo OPDS Proxy{{end}}</title>
<link rel="stylesheet" href="./static/style.css">
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
{{block "content" .}}{{end}}
<main id="container">
{{block "content" .}}{{end}}
</main>
</body>
</html>

40
html/static/style.css Normal file
View File

@@ -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;
}

95
main.go
View File

@@ -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)
}

87
opds/models.go Normal file
View File

@@ -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"`
}

40
opds/opds.go Normal file
View File

@@ -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"
}

View File

@@ -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;
}