Commit all code. Should be mostly functional.
This commit is contained in:
+142
@@ -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/*
|
||||||
Generated
+8
@@ -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
|
||||||
Generated
+9
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/opds-pse-server.iml" filepath="$PROJECT_DIR$/.idea/opds-pse-server.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FileBasename(path string) string {
|
||||||
|
return strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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=
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user