311 lines
6.5 KiB
Go
311 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"embed"
|
|
"flag"
|
|
"fmt"
|
|
"html/template"
|
|
"log"
|
|
"math/rand"
|
|
"net/http"
|
|
"net/url"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
var imageExtensions = map[string]bool{
|
|
".png": true,
|
|
".jpg": true,
|
|
".jpeg": true,
|
|
".gif": true,
|
|
".webp": true,
|
|
".bmp": true,
|
|
".avif": true,
|
|
}
|
|
|
|
func isImageFile(path string) bool {
|
|
ext := strings.ToLower(filepath.Ext(path))
|
|
return imageExtensions[ext]
|
|
}
|
|
|
|
//go:embed templates/*
|
|
var templatesFS embed.FS
|
|
|
|
//go:embed assets/*
|
|
var assetsFS embed.FS
|
|
|
|
type CoverData struct {
|
|
Comic int
|
|
Path string
|
|
}
|
|
|
|
var (
|
|
db *sql.DB
|
|
tmpl *template.Template
|
|
imaginaryURL string
|
|
galleryPath string
|
|
databasePath string
|
|
listenAddr string
|
|
listenPort int
|
|
randomCovers []CoverData
|
|
)
|
|
|
|
func main() {
|
|
flag.StringVar(&imaginaryURL, "imaginary", "http://192.168.88.54:10001", "imaginary root URL")
|
|
flag.StringVar(&galleryPath, "gallery", "/home/user/mnt/panda/galleries/", "gallery path")
|
|
flag.StringVar(&databasePath, "database", "./db.sqlite", "database path")
|
|
flag.StringVar(&listenAddr, "address", "localhost", "listen address")
|
|
flag.IntVar(&listenPort, "port", 10000, "listen port")
|
|
flag.Parse()
|
|
|
|
var err error
|
|
|
|
db, err = sql.Open("sqlite3", databasePath)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
|
|
if err = db.Ping(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
log.Println("SQLite connected")
|
|
|
|
tmpl, err = template.ParseFS(templatesFS, "templates/*.html")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
http.HandleFunc("/gallery/", handleGallery)
|
|
http.HandleFunc("/random/", handleRandom)
|
|
http.HandleFunc("/random", handleRandom)
|
|
http.HandleFunc("/", handleIndex)
|
|
http.Handle("/assets/", http.FileServer(http.FS(assetsFS)))
|
|
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(galleryPath))))
|
|
|
|
addr := fmt.Sprintf("%s:%d", listenAddr, listenPort)
|
|
log.Printf("Server listening on %s", addr)
|
|
log.Fatal(http.ListenAndServe(addr, nil))
|
|
}
|
|
|
|
func handleRandom(w http.ResponseWriter, r *http.Request) {
|
|
path := strings.TrimPrefix(r.URL.Path, "/random")
|
|
path = strings.TrimPrefix(path, "/")
|
|
|
|
// If accessing /random without chapter, shuffle and redirect to /random/0
|
|
if path == "" {
|
|
query := `
|
|
select page.id_comic, comic.directory || '/' || page.filename path
|
|
from page
|
|
join comic on comic.id_comic = page.id_comic
|
|
where page.number = 0
|
|
`
|
|
rows, err := db.Query(query)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
randomCovers = nil
|
|
for rows.Next() {
|
|
var c CoverData
|
|
if err := rows.Scan(&c.Comic, &c.Path); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
randomCovers = append(randomCovers, c)
|
|
}
|
|
|
|
rand.Shuffle(len(randomCovers), func(i, j int) {
|
|
randomCovers[i], randomCovers[j] = randomCovers[j], randomCovers[i]
|
|
})
|
|
|
|
http.Redirect(w, r, "/random/0", http.StatusFound)
|
|
return
|
|
}
|
|
|
|
chapter, err := strconv.Atoi(path)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
const limit = 18
|
|
offset := chapter * limit
|
|
|
|
type Cover struct {
|
|
Comic int
|
|
Url string
|
|
}
|
|
|
|
end := offset + limit*2
|
|
if end > len(randomCovers) {
|
|
end = len(randomCovers)
|
|
}
|
|
start := offset
|
|
if start > len(randomCovers) {
|
|
start = len(randomCovers)
|
|
}
|
|
|
|
all := make([]Cover, 0, limit*2)
|
|
for _, c := range randomCovers[start:end] {
|
|
var coverUrl string
|
|
if isImageFile(c.Path) {
|
|
coverUrl = fmt.Sprintf("%s/smartcrop?width=300&height=370&file=%s", imaginaryURL, url.QueryEscape(c.Path))
|
|
} else {
|
|
coverUrl = "/static/" + c.Path
|
|
}
|
|
all = append(all, Cover{Comic: c.Comic, Url: coverUrl})
|
|
}
|
|
|
|
covers := all
|
|
var preload []Cover
|
|
if len(all) > limit {
|
|
covers = all[:limit]
|
|
preload = all[limit:]
|
|
}
|
|
|
|
var nextChapter int
|
|
if len(preload) > 0 {
|
|
nextChapter = chapter + 1
|
|
} else {
|
|
nextChapter = chapter
|
|
}
|
|
|
|
data := struct {
|
|
Covers []Cover
|
|
Preload []Cover
|
|
Chapter int
|
|
Prefix string
|
|
}{covers, preload, nextChapter, "/random"}
|
|
|
|
tmpl.ExecuteTemplate(w, "index.html", data)
|
|
}
|
|
|
|
func handleGallery(w http.ResponseWriter, r *http.Request) {
|
|
comic := strings.TrimPrefix(r.URL.Path, "/gallery/")
|
|
if comic == "" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
query := `
|
|
select comic.title, comic.directory || '/' || page.filename
|
|
from page
|
|
join comic on page.id_comic = comic.id_comic
|
|
where comic.id_comic = ?
|
|
order by page.number
|
|
`
|
|
rows, err := db.Query(query, comic)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var title string
|
|
var pages []string
|
|
for rows.Next() {
|
|
var path string
|
|
if err := rows.Scan(&title, &path); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
pages = append(pages, path)
|
|
}
|
|
|
|
if len(pages) == 0 {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
data := struct {
|
|
Title string
|
|
Pages []string
|
|
ImaginaryURL string
|
|
}{title, pages, imaginaryURL}
|
|
|
|
tmpl.ExecuteTemplate(w, "gallery.html", data)
|
|
}
|
|
|
|
func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|
chapter := 0
|
|
path := strings.TrimPrefix(r.URL.Path, "/")
|
|
if path != "" {
|
|
var err error
|
|
chapter, err = strconv.Atoi(path)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
}
|
|
|
|
const limit = 18
|
|
offset := chapter * limit
|
|
|
|
query := `
|
|
select page.id_comic, comic.directory || '/' || page.filename path
|
|
from page
|
|
join comic on comic.id_comic = page.id_comic
|
|
where page.number = 0
|
|
order by comic.id_comic desc
|
|
limit ? offset ?
|
|
`
|
|
|
|
rows, err := db.Query(query, limit*2, offset)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
type Cover struct {
|
|
Comic int
|
|
Url string
|
|
}
|
|
|
|
all := make([]Cover, 0, limit*2)
|
|
for rows.Next() {
|
|
var comic int
|
|
var path string
|
|
if err := rows.Scan(&comic, &path); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
var coverUrl string
|
|
if isImageFile(path) {
|
|
coverUrl = fmt.Sprintf("%s/smartcrop?width=300&height=370&file=%s", imaginaryURL, url.QueryEscape(path))
|
|
} else {
|
|
coverUrl = "/static/" + path
|
|
}
|
|
all = append(all, Cover{Comic: comic, Url: coverUrl})
|
|
}
|
|
|
|
covers := all
|
|
var preload []Cover
|
|
if len(all) > limit {
|
|
covers = all[:limit]
|
|
preload = all[limit:]
|
|
}
|
|
|
|
var nextChapter int
|
|
if len(preload) > 0 {
|
|
nextChapter = chapter + 1
|
|
} else {
|
|
nextChapter = chapter
|
|
}
|
|
data := struct {
|
|
Covers []Cover
|
|
Preload []Cover
|
|
Chapter int
|
|
Prefix string
|
|
}{covers, preload, nextChapter, ""}
|
|
|
|
tmpl.ExecuteTemplate(w, "index.html", data)
|
|
}
|
|
|