commit 1f7234997bd1e22f5bc518049ea1a4726450fc37 Author: Big Duckie Date: Fri Dec 6 11:53:25 2024 -0700 Commit of all existing code. diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100755 index 0000000..1c2fda5 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/dir2opdspse.iml b/.idea/dir2opdspse.iml new file mode 100755 index 0000000..338a266 --- /dev/null +++ b/.idea/dir2opdspse.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100755 index 0000000..c9758a5 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,37 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100755 index 0000000..d686ab4 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/cmd/cmd.go b/app/cmd/cmd.go new file mode 100755 index 0000000..b3843fd --- /dev/null +++ b/app/cmd/cmd.go @@ -0,0 +1,11 @@ +package cmd + +import "github.com/urfave/cli/v2" + +func intFlag(name string, value int, usage string) *cli.IntFlag { + return &cli.IntFlag{ + Name: name, + Value: value, + Usage: usage, + } +} diff --git a/app/cmd/serve.go b/app/cmd/serve.go new file mode 100755 index 0000000..c64250e --- /dev/null +++ b/app/cmd/serve.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "frostfire.io/dir2opdspse/app/routes" + "github.com/gorilla/mux" + "github.com/urfave/cli/v2" + "net/http" + "strconv" + "time" +) + +var Serve = cli.Command{ + Name: "serve", + Usage: "Starts the webserver", + Description: "Start the Flux webserver. That's all you need to run.", + Action: runServe, + Flags: []cli.Flag{ + intFlag("port, p", 3000, "Web server port"), + }, +} + +func runServe(c *cli.Context) error { + r := mux.NewRouter() + + fs := http.FileServer(http.Dir("books/")) + r.PathPrefix("/manga/").Handler(http.StripPrefix("/manga", fs)) + + routes.Init(c, r) + + srv := &http.Server{ + Handler: r, + Addr: ":" + strconv.Itoa(c.Int("port")), + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + return srv.ListenAndServe() +} diff --git a/app/opds/author_builder.go b/app/opds/author_builder.go new file mode 100644 index 0000000..a997707 --- /dev/null +++ b/app/opds/author_builder.go @@ -0,0 +1,31 @@ +package opds + +import ( + "github.com/lann/builder" + "golang.org/x/tools/blog/atom" +) + +type authorBuilder builder.Builder + +func (a authorBuilder) Name(name string) authorBuilder { + return builder.Set(a, "Name", name).(authorBuilder) +} + +func (a authorBuilder) URI(uri string) authorBuilder { + return builder.Set(a, "URI", uri).(authorBuilder) +} + +func (a authorBuilder) Email(email string) authorBuilder { + return builder.Set(a, "Email", email).(authorBuilder) +} + +func (a authorBuilder) InnerXML(inner string) authorBuilder { + return builder.Set(a, "InnerXML", inner).(authorBuilder) +} + +func (a authorBuilder) Build() atom.Person { + return builder.GetStruct(a).(atom.Person) +} + +// AuthorBuilder is a fluent immutable builder to build OPDS Authors +var AuthorBuilder = builder.Register(authorBuilder{}, atom.Person{}).(authorBuilder) diff --git a/app/opds/entry_builder.go b/app/opds/entry_builder.go new file mode 100644 index 0000000..4bb3a19 --- /dev/null +++ b/app/opds/entry_builder.go @@ -0,0 +1,49 @@ +package opds + +import ( + "time" + + "github.com/lann/builder" + "golang.org/x/tools/blog/atom" +) + +type entryBuilder builder.Builder + +func (e entryBuilder) Title(title string) entryBuilder { + return builder.Set(e, "Title", title).(entryBuilder) +} + +func (e entryBuilder) ID(id string) entryBuilder { + return builder.Set(e, "ID", id).(entryBuilder) +} + +func (e entryBuilder) AddLink(link atom.Link) entryBuilder { + return builder.Append(e, "Link", link).(entryBuilder) +} + +func (e entryBuilder) Published(published time.Time) entryBuilder { + return builder.Set(e, "Published", atom.Time(published)).(entryBuilder) +} + +func (e entryBuilder) Updated(updated time.Time) entryBuilder { + return builder.Set(e, "Updated", atom.Time(updated)).(entryBuilder) +} + +func (e entryBuilder) Author(author *atom.Person) entryBuilder { + return builder.Set(e, "Author", author).(entryBuilder) +} + +func (e entryBuilder) Summary(summary *atom.Text) entryBuilder { + return builder.Set(e, "Summary", summary).(entryBuilder) +} + +func (e entryBuilder) Content(content *atom.Text) entryBuilder { + return builder.Set(e, "Content", content).(entryBuilder) +} + +func (e entryBuilder) Build() atom.Entry { + return builder.GetStruct(e).(atom.Entry) +} + +// EntryBuilder is a fluent immutable builder to build OPDS entries +var EntryBuilder = builder.Register(entryBuilder{}, atom.Entry{}).(entryBuilder) diff --git a/app/opds/feed_builder.go b/app/opds/feed_builder.go new file mode 100644 index 0000000..e74fcc1 --- /dev/null +++ b/app/opds/feed_builder.go @@ -0,0 +1,47 @@ +package opds + +import ( + "time" + + "github.com/lann/builder" + "golang.org/x/tools/blog/atom" +) + +type AcquisitionFeed struct { + *atom.Feed + Dc string `xml:"xmlns:dc,attr"` + Opds string `xml:"xmlns:opds,attr"` +} + +type feedBuilder builder.Builder + +func (f feedBuilder) Title(title string) feedBuilder { + return builder.Set(f, "Title", title).(feedBuilder) +} + +func (f feedBuilder) ID(id string) feedBuilder { + return builder.Set(f, "ID", id).(feedBuilder) +} + +func (f feedBuilder) AddLink(link atom.Link) feedBuilder { + return builder.Append(f, "Link", link).(feedBuilder) +} + +func (f feedBuilder) Updated(updated time.Time) feedBuilder { + return builder.Set(f, "Updated", atom.Time(updated)).(feedBuilder) +} + +func (f feedBuilder) Author(author atom.Person) feedBuilder { + return builder.Set(f, "Author", &author).(feedBuilder) +} + +func (f feedBuilder) AddEntry(entry atom.Entry) feedBuilder { + return builder.Append(f, "Entry", &entry).(feedBuilder) +} + +func (f feedBuilder) Build() atom.Feed { + return builder.GetStruct(f).(atom.Feed) +} + +// FeedBuilder is a fluent immutable builder to build OPDS Feeds +var FeedBuilder = builder.Register(feedBuilder{}, atom.Feed{}).(feedBuilder) diff --git a/app/opds/link_builder.go b/app/opds/link_builder.go new file mode 100644 index 0000000..abd1969 --- /dev/null +++ b/app/opds/link_builder.go @@ -0,0 +1,43 @@ +package opds + +import ( + "github.com/lann/builder" + "golang.org/x/tools/blog/atom" +) + +type linkBuilder builder.Builder + +func (l linkBuilder) Rel(rel string) linkBuilder { + return builder.Set(l, "Rel", rel).(linkBuilder) +} + +func (l linkBuilder) Href(href string) linkBuilder { + return builder.Set(l, "Href", href).(linkBuilder) +} + +func (l linkBuilder) Type(typeName string) linkBuilder { + return builder.Set(l, "Type", typeName).(linkBuilder) +} + +func (l linkBuilder) HrefLang(lang string) linkBuilder { + return builder.Set(l, "HrefLang", lang).(linkBuilder) +} + +func (l linkBuilder) Title(title string) linkBuilder { + return builder.Set(l, "Title", title).(linkBuilder) +} + +func (l linkBuilder) Length(length uint) linkBuilder { + return builder.Set(l, "Length", length).(linkBuilder) +} + +func (l linkBuilder) PseCount(count uint) linkBuilder { + return builder.Set(l, "PseCount", count).(linkBuilder) +} + +func (l linkBuilder) Build() atom.Link { + return builder.GetStruct(l).(atom.Link) +} + +// LinkBuilder is a fluent immutable builder to build OPDS Links +var LinkBuilder = builder.Register(linkBuilder{}, atom.Link{}).(linkBuilder) diff --git a/app/opds/text_builder.go b/app/opds/text_builder.go new file mode 100644 index 0000000..cd2924e --- /dev/null +++ b/app/opds/text_builder.go @@ -0,0 +1,23 @@ +package opds + +import ( + "github.com/lann/builder" + "golang.org/x/tools/blog/atom" +) + +type textBuilder builder.Builder + +func (t textBuilder) Type(textType string) textBuilder { + return builder.Set(t, "Type", textType).(textBuilder) +} + +func (t textBuilder) Body(body string) textBuilder { + return builder.Set(t, "Body", body).(textBuilder) +} + +func (t textBuilder) Build() atom.Text { + return builder.GetStruct(t).(atom.Text) +} + +// TextBuilder is a fluent immutable builder to build OPDS texts +var TextBuilder = builder.Register(textBuilder{}, atom.Text{}).(textBuilder) diff --git a/app/routes/routes.go b/app/routes/routes.go new file mode 100755 index 0000000..fe3529f --- /dev/null +++ b/app/routes/routes.go @@ -0,0 +1,126 @@ +package routes + +import ( + "archive/zip" + "bytes" + "encoding/json" + "encoding/xml" + "errors" + "frostfire.io/dir2opdspse/app/opds" + "frostfire.io/dir2opdspse/app/utils" + "github.com/gorilla/mux" + "github.com/urfave/cli/v2" + "io" + "log" + "net/http" + "os" + "path" + "regexp" + "slices" + "strconv" + "time" +) + +func Init(c *cli.Context, r *mux.Router) { + r.HandleFunc("/", Home).Methods("GET") + r.HandleFunc("/stream/{manga}/{page}", Stream).Methods("GET") + r.HandleFunc("/test/{path:.*}/{page}", Test).Methods("GET") +} + +func Home(w http.ResponseWriter, r *http.Request) { + feed := opds.FeedBuilder. + ID("dir2opds"). + Title("dir2OPDS"). + Updated(time.Now()). + AddLink(opds.LinkBuilder. + Rel("start"). + Href("/"). + Type("application/atom+xml;profile=opds-catalog;kind=navigation"). + Build()) + + dirEntries, _ := os.ReadDir("books") + for _, entry := range dirEntries { + if !entry.IsDir() { + zFile, _ := zip.OpenReader(path.Join("books", entry.Name())) + //zCount := len(zFile.File) + _ = zFile.Close() + + feed = feed.AddEntry(opds.EntryBuilder. + ID(utils.FileBasename(entry.Name())). + Title(utils.FileBasename(entry.Name())). + AddLink(opds.LinkBuilder. + Rel("http://opds-spec.org/acquisition"). + Href("/manga/" + entry.Name()). + Type("application/x-cbz"). + Title(utils.FileBasename(entry.Name())). + Build()). + AddLink(opds.LinkBuilder. + Rel("http://opds-spec.org/image"). + Href("/stream/" + entry.Name() + "/0"). + Type("image/png"). + Build()). + AddLink(opds.LinkBuilder. + Rel("http://opds-spec.org/image/thumbnail"). + Href("/stream/" + entry.Name() + "/0"). + Type("image/png"). + Build()). + AddLink(opds.LinkBuilder. + Rel("http://vaemendis.net/opds-pse/stream"). + Href("/stream/" + entry.Name() + "/{pageNumber}"). + Type("image/png"). + //PseCount(10). + Build()). + Build()) + } + } + data, err := xml.MarshalIndent(feed.Build(), "", " ") + if err != nil { + panic(err) + } + http.ServeContent(w, r, "feed.xml", time.Now(), bytes.NewReader(data)) +} + +func Stream(w http.ResponseWriter, r *http.Request) { + re := regexp.MustCompile(`(\d+)(.*)`) + vars := mux.Vars(r) + if len(re.FindStringIndex(vars["page"])) <= 0 { + w.WriteHeader(404) + return + } + + pageNumber := re.FindStringSubmatch(vars["page"])[1] + + mangaPath := path.Join("books", vars["manga"]) + + if _, err := os.Stat(mangaPath); errors.Is(err, os.ErrNotExist) { + w.WriteHeader(404) + return + } + + zf, err := zip.OpenReader(mangaPath) + if err != nil { + log.Fatal(err) + } + + pageIndex := slices.IndexFunc(zf.File, func(f *zip.File) bool { + page, _ := strconv.Atoi(pageNumber) + return f.Name == (strconv.Itoa(page+1) + ".png") + }) + + if pageIndex == -1 { + w.WriteHeader(404) + return + } + + imf, _ := zf.File[pageIndex].Open() + imageBytes, _ := io.ReadAll(imf) + _ = imf.Close() + _ = zf.Close() + + http.ServeContent(w, r, zf.File[0].Name, time.Now(), bytes.NewReader(imageBytes)) +} + +func Test(w http.ResponseWriter, r *http.Request) { + data, _ := json.MarshalIndent(r.URL, "", " ") + http.ServeContent(w, r, "data.json", time.Now(), bytes.NewReader(data)) +} diff --git a/app/utils/utils.go b/app/utils/utils.go new file mode 100755 index 0000000..c360a21 --- /dev/null +++ b/app/utils/utils.go @@ -0,0 +1,10 @@ +package utils + +import ( + "path/filepath" + "strings" +) + +func FileBasename(path string) string { + return strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) +} diff --git a/books/.gitkeep b/books/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod new file mode 100755 index 0000000..8465ab1 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module frostfire.io/dir2opdspse + +go 1.20 + +require ( + github.com/gorilla/mux v1.8.0 + github.com/urfave/cli/v2 v2.25.7 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/tools v0.12.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100755 index 0000000..3c5fe85 --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= diff --git a/main.go b/main.go new file mode 100755 index 0000000..ec75d48 --- /dev/null +++ b/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "frostfire.io/dir2opdspse/app/cmd" + "github.com/urfave/cli/v2" + "log" + "mime" + "os" +) + +func main() { + _ = mime.AddExtensionType(".cbz", "application/x-cbz") + + app := cli.NewApp() + app.Name = "dir2opdspse" + app.Usage = "A lightweight OPDS server with OPDS-PS support." + app.Version = "0.0.1+dev" + app.Commands = []*cli.Command{ + &cmd.Serve, + } + + if err := app.Run(os.Args); err != nil { + log.Fatalf("Failed to start application: %v", err) + } +}