From b4e9727e8c27c60d6cbbfdca25e60d4335e23fd3 Mon Sep 17 00:00:00 2001 From: Big Duckie Date: Tue, 31 Dec 2024 03:38:40 -0700 Subject: [PATCH] Commit all code. Should be mostly functional. --- .gitignore | 142 ++++++++++++++++++++++++++++ .idea/.gitignore | 8 ++ .idea/go-opds-pse.iml | 9 ++ .idea/modules.xml | 8 ++ LICENSE | 21 +++++ README.md | 30 ++++++ app/cmd/cmd.go | 19 ++++ app/cmd/serve.go | 40 ++++++++ app/comicinfo/comicinfo.go | 18 ++++ app/opds/opds.go | 55 +++++++++++ app/routes/routes.go | 187 +++++++++++++++++++++++++++++++++++++ app/utils/utils.go | 10 ++ go.mod | 23 +++++ go.sum | 24 +++++ main.go | 28 ++++++ 15 files changed, 622 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/go-opds-pse.iml create mode 100644 .idea/modules.xml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app/cmd/cmd.go create mode 100644 app/cmd/serve.go create mode 100644 app/comicinfo/comicinfo.go create mode 100644 app/opds/opds.go create mode 100644 app/routes/routes.go create mode 100755 app/utils/utils.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec7d70d --- /dev/null +++ b/.gitignore @@ -0,0 +1,142 @@ +# Created by https://www.toptal.com/developers/gitignore/api/go,goland +# Edit at https://www.toptal.com/developers/gitignore?templates=go,goland + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +### GoLand ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### GoLand Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +# End of https://www.toptal.com/developers/gitignore/api/go,goland + +manga/* \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /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/go-opds-pse.iml b/.idea/go-opds-pse.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/go-opds-pse.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2ab4dbb --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4eec785 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Shane Clark + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a6ec698 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# go-opds-pse + +A tiny lightweight OPDS server with PSE support. Mainly written for those using Panels on iOS. + +### Usage +To use this, just download the [Latest Release](https://github.com/sclark-dev/go-opds-pse/releases/latest) and run it via terminal. + +```shell +go-opds-pse -m "./manga/" serve -p 8080 +``` + +### Building +To build this you'll want the latest version of Go. Doesn't use any external libraries. + +### Attributions +Thanks to [dir2opds](https://github.com/dubyte/dir2opds) for the inspiration for this. + +### FAQs +- Why Go? + +Well, Go is just very memory efficient. Yes, writing this in something like C would be even better, but that's a pain. My choice for Go was a cross-platform server that can be run on embedded hardware for local access to your comic/manga collection. + +- Is there a Web UI for this? + +No. This is designed to be a very lightweight server and a UI would add a lot of bulk. However since this server just serves a basic OPDS feed, there should be plenty of clients available. Alas, the PSE support is kinda lacking. It seems like there just isn't much of a market for that. + +- Does streaming use more bandwidth? + +Not really. While CBZ files are compressed, *generally* the bandwidth saved by compressing JPGs and non-lossless PNGs is pretty minimal. In fact, I don' +t compress my CBZ files at all. The few hundred kilobytes it saves just isn't enough to justify the extra battery usage to decompress it on a mobile device. \ No newline at end of file diff --git a/app/cmd/cmd.go b/app/cmd/cmd.go new file mode 100644 index 0000000..b9f6ef8 --- /dev/null +++ b/app/cmd/cmd.go @@ -0,0 +1,19 @@ +package cmd + +import "github.com/urfave/cli/v2" + +func IntFlag(name string, value int, usage string) *cli.IntFlag { + return &cli.IntFlag{ + Name: name, + Usage: usage, + Value: value, + } +} + +func StringFlag(name string, value string, usage string) *cli.StringFlag { + return &cli.StringFlag{ + Name: name, + Usage: usage, + Value: value, + } +} diff --git a/app/cmd/serve.go b/app/cmd/serve.go new file mode 100644 index 0000000..74d3ab2 --- /dev/null +++ b/app/cmd/serve.go @@ -0,0 +1,40 @@ +package cmd + +import ( + "fmt" + "github.com/gorilla/mux" + "github.com/patrickmn/go-cache" + "github.com/urfave/cli/v2" + "net/http" + "opds-pse-server/app/routes" + "time" +) + +var Serve = cli.Command{ + Name: "serve", + Usage: "start the http server", + Flags: []cli.Flag{ + IntFlag("port, p", 3000, "web server port"), + }, + Action: runServe, +} + +func runServe(ctx *cli.Context) error { + r := mux.NewRouter() + + fs := http.FileServer(http.Dir("manga/")) + r.PathPrefix("/manga/").Handler(http.StripPrefix("/manga", fs)) + + c := cache.New(5*time.Minute, 10*time.Minute) + + routes.Init(ctx, r, c) + + srv := &http.Server{ + Handler: r, + Addr: fmt.Sprintf(":%d", ctx.Int("port")), + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + return srv.ListenAndServe() +} diff --git a/app/comicinfo/comicinfo.go b/app/comicinfo/comicinfo.go new file mode 100644 index 0000000..d1449d0 --- /dev/null +++ b/app/comicinfo/comicinfo.go @@ -0,0 +1,18 @@ +package comicinfo + +import "encoding/xml" + +type ComicInfo struct { + XMLName xml.Name `xml:"ComicInfo"` + XmlnsXsd string `xml:"xmlns:xsd,attr"` + XmlnsXsi string `xml:"xmlns:xsi,attr"` + Title string `xml:"Title"` + Summary string `xml:"Summary"` + Writer string `xml:"Writer"` + Publisher string `xml:"Publisher"` + Imprint string `xml:"Imprint"` + Tags string `xml:"Tags"` + PageCount int `xml:"PageCount"` + Manga string `xml:"Manga"` + AgeRating string `xml:"AgeRating"` +} diff --git a/app/opds/opds.go b/app/opds/opds.go new file mode 100644 index 0000000..335b337 --- /dev/null +++ b/app/opds/opds.go @@ -0,0 +1,55 @@ +package opds + +import ( + "encoding/xml" + "golang.org/x/tools/blog/atom" +) + +type Feed struct { + XMLName xml.Name `xml:"http://www.w3.org/2005/Atom feed"` + XmlLang string `xml:"xml:lang,attr" default:"en"` + XmlnsDc string `xml:"xmlns:dc,attr" default:"http://purl.org/dc/terms/"` + XmlnsPse string `xml:"xmlns:pse,attr" default:"http://vaemendis.net/opds-pse/ns"` + XmlnsOpds string `xml:"xmlns:opds,attr" default:"http://opds-spec.org/2010/catalog"` + XmlnsOpenSearch string `xml:"xmlns:opensearch,attr" default:"http://a9.com/-/spec/opensearch/1.1/"` + ID string `xml:"id"` + Link []Link `xml:"link"` + Title string `xml:"title"` + Updated atom.TimeStr `xml:"updated"` + Author *Author `xml:"author,omitempty"` + Entry []*Entry `xml:"entry"` +} + +type Author struct { + Name string `xml:"name,omitempty"` + URI string `xml:"uri,omitempty"` + Email string `xml:"email,omitempty"` + InnerXML string `xml:",innerxml"` +} + +type Entry struct { + Title string `xml:"title"` + ID string `xml:"id"` + Author *Author `xml:"author,omitempty"` + Published atom.TimeStr `xml:"published,omitempty"` + Updated atom.TimeStr `xml:"updated,omitempty"` + DcPublisher string `xml:"dc:publisher,omitempty"` + Link []Link `xml:"link"` + Summary *Text `xml:"summary,omitempty"` + Content *Text `xml:"content,omitempty"` +} + +type Link struct { + Rel string `xml:"rel,attr,omitempty"` + Href string `xml:"href,attr"` + Type string `xml:"type,attr,omitempty"` + HrefLang string `xml:"hreflang,attr,omitempty"` + Title string `xml:"title,attr,omitempty"` + Length uint `xml:"length,attr,omitempty"` + PseCount uint `xml:"pse:count,attr,omitempty"` +} + +type Text struct { + Type string `xml:"type,attr,omitempty"` + Body string `xml:",chardata"` +} diff --git a/app/routes/routes.go b/app/routes/routes.go new file mode 100644 index 0000000..47c583c --- /dev/null +++ b/app/routes/routes.go @@ -0,0 +1,187 @@ +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" + "golang.org/x/tools/blog/atom" + "io" + "io/fs" + "net/http" + "opds-pse-server/app/comicinfo" + "opds-pse-server/app/opds" + "opds-pse-server/app/utils" + "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)) +} 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/go.mod b/go.mod new file mode 100644 index 0000000..f8653df --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module opds-pse-server + +go 1.22.0 + +toolchain go1.22.4 + +require ( + github.com/gorilla/mux v1.8.1 + github.com/urfave/cli/v2 v2.27.4 + golang.org/x/tools v0.25.0 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/creasty/defaults v1.8.0 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/samber/lo v1.47.0 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + golang.org/x/text v0.16.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..10da15c --- /dev/null +++ b/go.sum @@ -0,0 +1,24 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk= +github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +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/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +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/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= +github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= +github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= diff --git a/main.go b/main.go new file mode 100644 index 0000000..46c66bc --- /dev/null +++ b/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "github.com/urfave/cli/v2" + "log" + "mime" + "opds-pse-server/app/cmd" + "os" +) + +func main() { + _ = mime.AddExtensionType(".cbz", "application/x-cbz") + + app := cli.NewApp() + app.Name = "go-opds-pse" + app.Usage = "A lightweight OPDS server with PSE support." + app.Version = "0.0.1+dev" + app.Flags = []cli.Flag{ + cmd.StringFlag("manga, m", "manga", "default manga folder"), + } + app.Commands = []*cli.Command{ + &cmd.Serve, + } + + if err := app.Run(os.Args); err != nil { + log.Fatalf("Failed to start application: %v", err) + } +}