188 lines
4.3 KiB
Go
188 lines
4.3 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)
|
|
|
|
_ = 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, _ := zip.OpenReader(path)
|
|
for i := range zf.File {
|
|
if strings.ToLower(zf.File[i].Name) == "comicinfo.xml" {
|
|
cif, err := zf.File[i].Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cib, _ := io.ReadAll(cif)
|
|
_ = cif.Close()
|
|
_ = xml.Unmarshal(cib, &ComicInfo)
|
|
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
|
|
})
|
|
|
|
_ = defaults.Set(&feed)
|
|
data, _ := xml.MarshalIndent(feed, "", " ")
|
|
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))
|
|
}
|