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, _ := 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 }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } _ = 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)) }