Files
go-opds-pse/app/routes/routes.go
T

221 lines
4.7 KiB
Go

package routes
import (
"archive/zip"
"bytes"
"encoding/xml"
"fmt"
"github.com/creasty/defaults"
"github.com/gorilla/mux"
"github.com/patrickmn/go-cache"
"github.com/samber/lo"
"github.com/urfave/cli/v2"
"go-opds-pse/app/comicinfo"
"go-opds-pse/app/opds"
"go-opds-pse/app/utils"
"golang.org/x/tools/blog/atom"
"io"
"io/fs"
"net/http"
"path/filepath"
"slices"
"sort"
"strconv"
"strings"
"time"
)
var ctx *cli.Context
var c *cache.Cache
func Init(cctx *cli.Context, r *mux.Router, cc *cache.Cache) {
ctx = cctx
c = cc
r.HandleFunc("/", Index).Methods("GET")
r.HandleFunc("/stream/{manga:.*}/{page:[0-9]+}", Stream).Methods("GET")
}
func Index(w http.ResponseWriter, r *http.Request) {
feed := opds.Feed{
ID: "feed",
Title: "go-opds-pse",
Updated: atom.Time(time.Now()),
Author: &opds.Author{
Name: "go-opds-pse",
URI: "https://github.com/sclark-dev/go-opds-pse",
},
Link: []opds.Link{
{
Rel: "start",
Href: "/",
Type: "application/atom+xml;profile=opds-catalog;kind=navigation",
},
},
}
feed.Entry = append(feed.Entry)
err := filepath.WalkDir(ctx.String("manga"), func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() || filepath.Ext(d.Name()) != ".cbz" {
return err
}
hrefPath := strings.TrimPrefix(path, ctx.String("manga"))
// Check if ComicInfo is cached
var ComicInfo comicinfo.ComicInfo
ci, found := c.Get(path)
if found {
ComicInfo = ci.(comicinfo.ComicInfo)
} else {
zf, err := zip.OpenReader(path)
if err != nil {
return nil
}
defer zf.Close()
for i := range zf.File {
if strings.ToLower(zf.File[i].Name) == "comicinfo.xml" {
cif, err := zf.File[i].Open()
if err != nil {
return nil
}
cib, err := io.ReadAll(cif)
if err != nil {
return nil
}
err = cif.Close()
if err != nil {
return nil
}
err = xml.Unmarshal(cib, &ComicInfo)
if err != nil {
return nil
}
c.Set(path, ComicInfo, cache.NoExpiration)
}
}
}
entry := &opds.Entry{
ID: utils.FileBasename(d.Name()),
Title: utils.FileBasename(d.Name()),
Author: &opds.Author{
Name: ComicInfo.Writer,
},
Summary: &opds.Text{Body: ComicInfo.Summary},
DcPublisher: ComicInfo.Publisher,
Link: []opds.Link{
{
Rel: "http://opds-spec.org/acquisition",
Href: "/manga" + hrefPath,
Type: "application/x-cbz",
Title: utils.FileBasename(d.Name()),
},
{
Rel: "http://opds-spec.org/image",
Href: "/stream" + hrefPath + "/0",
Type: "image/png",
},
{
Rel: "http://opds-spec.org/image/thumbnail",
Href: "/stream" + hrefPath + "/0",
Type: "image/png",
},
},
}
if ComicInfo != (comicinfo.ComicInfo{}) {
entry.Link = append(entry.Link, opds.Link{
Rel: "http://vaemendis.net/opds-pse/stream",
Href: "/stream" + hrefPath + "/{pageNumber}",
Type: "image/png",
PseCount: uint(ComicInfo.PageCount),
})
}
feed.Entry = append(feed.Entry, entry)
return nil
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = defaults.Set(&feed)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data, err := xml.MarshalIndent(feed, "", " ")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.ServeContent(w, r, "feed.xml", time.Now(), bytes.NewReader(data))
}
func Stream(w http.ResponseWriter, r *http.Request) {
// Parse all variables
vars := mux.Vars(r)
path := ctx.String("manga") + "/" + vars["manga"]
page, err := strconv.Atoi(vars["page"])
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Open zip file
zf, err := zip.OpenReader(path)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer zf.Close()
// Filter to just accepted image files
pages := lo.Filter(zf.File, func(f *zip.File, i int) bool {
if slices.Contains([]string{".png", ".jpg", ".webp"}, filepath.Ext(f.Name)) {
return true
}
return false
})
// Check if page number exists
if page > len(pages)-1 {
http.Error(w, fmt.Sprintf("page %d out of range", page), http.StatusBadRequest)
return
}
// Sort pages lexically
sort.Slice(pages, func(i, j int) bool {
return pages[i].Name < pages[j].Name
})
// Open page for reading
imf, err := pages[page].Open()
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer imf.Close()
// Read all data from page
imageBytes, err := io.ReadAll(imf)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Serve page data
http.ServeContent(w, r, pages[page].Name, time.Now(), bytes.NewReader(imageBytes))
}