summaryrefslogtreecommitdiffstats
path: root/roseh.moe.go
diff options
context:
space:
mode:
Diffstat (limited to 'roseh.moe.go')
-rw-r--r--roseh.moe.go364
1 files changed, 364 insertions, 0 deletions
diff --git a/roseh.moe.go b/roseh.moe.go
index 206679b..f3c7c78 100644
--- a/roseh.moe.go
+++ b/roseh.moe.go
@@ -2,6 +2,7 @@ package main
import (
"archive/tar"
+ "archive/zip"
"cmp"
"compress/gzip"
"crypto/hkdf"
@@ -10,6 +11,7 @@ import (
"crypto/sha1"
"crypto/sha256"
"crypto/subtle"
+ "database/sql"
"embed"
"encoding/base64"
"encoding/binary"
@@ -18,6 +20,8 @@ import (
"flag"
"fmt"
"html/template"
+ "image"
+ "image/jpeg"
"io"
"io/fs"
"log"
@@ -26,13 +30,16 @@ import (
"net/http/httputil"
"net/url"
"os"
+ "path/filepath"
"slices"
"strconv"
"strings"
"sync"
"time"
+ _ "github.com/mattn/go-sqlite3"
"github.com/skip2/go-qrcode"
+ "golang.org/x/image/draw"
"roseh.moe/pkg/oae2"
"roseh.moe/pkg/roseh.moe/internal/pwhash"
"roseh.moe/pkg/wordlist"
@@ -48,6 +55,8 @@ var (
holeTempDir = flag.String("hole-temp-dir", "hole", "directory to store temporary files for the wormhole")
codeDir = flag.String("code-dir", "code", "directory to serve git repos")
nezuko = flag.String("nezuko", "nezuko", "nezuko partition")
+ martDir = flag.String("mart-dir", "/mnt/nezuko/downloads/[metart.com] Photosets - 1999 to 2023", "met art data directory")
+ martDB = flag.String("mart-db", "/mnt/nezuko/files/met-art.sqlite", "met art index")
serverStartTime = time.Now()
)
@@ -905,6 +914,303 @@ func (h *nezukoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.srv.ServeHTTP(w, r)
}
+var (
+ //go:embed templates/mart/index.html.template
+ metArtIndexString string
+ metArtIndexTemplate = template.Must(outlinedTemplate(metArtIndexString))
+)
+
+type metArtIndexTemplateArgsGirl struct {
+ ID int64
+ Name string
+}
+
+type metArtIndexTemplateArgs struct {
+ Girls []metArtIndexTemplateArgsGirl
+}
+
+type metArtIndexHandler struct {
+ s *stmts
+}
+
+func (h *metArtIndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if !stupidCache(w, r) {
+ return
+ }
+ rows, err := h.s.listGirls.Query()
+ if err != nil {
+ log.Printf("Warning: list girls: %s", err)
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+ var girls []metArtIndexTemplateArgsGirl
+ for rows.Next() {
+ var girl metArtIndexTemplateArgsGirl
+ if err := rows.Scan(&girl.ID, &girl.Name); err != nil {
+ log.Printf("Warning: list girls: %s", err)
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+ girls = append(girls, girl)
+ }
+ if err := rows.Err(); err != nil {
+ log.Printf("Warning: list girls: %s", err)
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+ if err := metArtIndexTemplate.Execute(w, metArtIndexTemplateArgs{girls}); err != nil {
+ log.Printf("Warning: mart index: %s", err)
+ }
+}
+
+type metArtPhotoHandler struct {
+ s *stmts
+}
+
+func (h *metArtPhotoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if !stupidCache(w, r) {
+ return
+ }
+ id, err := strconv.ParseInt(r.PathValue("photo"), 10, 64)
+ if err != nil {
+ notFound(w, r)
+ return
+ }
+ var path, filename string
+ if err := h.s.findPhoto.QueryRow(id).Scan(&path, &filename); err != nil {
+ if err == sql.ErrNoRows {
+ notFound(w, r)
+ return
+ }
+ log.Printf("Warning: find photo: %s", err)
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+ archive, err := zip.OpenReader(filepath.Join(*martDir, path))
+ if err != nil {
+ log.Printf("Warning: open archive: %s", err)
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+ defer archive.Close()
+ f, err := archive.Open(filename)
+ if err != nil {
+ log.Printf("Warning: open archive file: %s", err)
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+ defer f.Close()
+ w.Header().Set("Content-Type", "image/jpeg")
+ if stat, err := f.Stat(); err == nil {
+ w.Header().Set("Content-Length", strconv.FormatInt(stat.Size(), 10))
+ }
+ if _, err := io.Copy(w, f); err != nil {
+ log.Printf("Warning: met art image: %s", err)
+ }
+}
+
+var (
+ //go:embed templates/mart/girl.html.template
+ metArtGirlString string
+ metArtGirlTemplate = template.Must(outlinedTemplate(metArtGirlString))
+)
+
+type metArtGirlTemplateArgsSet struct {
+ ID int64
+ CoverID int64
+ Name string
+}
+
+type metArtGirlTemplateArgs struct {
+ Name string
+ Sets []metArtGirlTemplateArgsSet
+}
+
+type metArtGirlHandler struct {
+ s *stmts
+}
+
+func (h *metArtGirlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if !stupidCache(w, r) {
+ return
+ }
+ id, err := strconv.ParseInt(r.PathValue("girl"), 10, 64)
+ if err != nil {
+ notFound(w, r)
+ return
+ }
+ wg := new(sync.WaitGroup)
+ var name string
+ var nameErr error
+ wg.Go(func() {
+ nameErr = h.s.girlName.QueryRow(id).Scan(&name)
+ })
+ var sets []metArtGirlTemplateArgsSet
+ var setsErr error
+ wg.Go(func() {
+ rows, err := h.s.listSetsForGirl.Query(id)
+ if err != nil {
+ setsErr = err
+ return
+ }
+ for rows.Next() {
+ var set metArtGirlTemplateArgsSet
+ if err := rows.Scan(&set.ID, &set.CoverID, &set.Name); err != nil {
+ setsErr = err
+ return
+ }
+ sets = append(sets, set)
+ }
+ setsErr = rows.Err()
+ })
+ wg.Wait()
+ if nameErr != nil {
+ if nameErr == sql.ErrNoRows {
+ notFound(w, r)
+ return
+ }
+ log.Printf("Warning: find girl name: %s", nameErr)
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+ if setsErr != nil {
+ log.Printf("Warning: list sets: %s", setsErr)
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+ if err := metArtGirlTemplate.Execute(w, metArtGirlTemplateArgs{Name: name, Sets: sets}); err != nil {
+ log.Printf("Warning: mart girl: %s", err)
+ }
+}
+
+type metArtGirlPreviewHandler struct {
+ s *stmts
+}
+
+func (h *metArtGirlPreviewHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if !stupidCache(w, r) {
+ return
+ }
+ id, err := strconv.ParseInt(r.PathValue("girl"), 10, 64)
+ if err != nil {
+ notFound(w, r)
+ return
+ }
+ var path, filename string
+ if err := h.s.findPreview.QueryRow(id).Scan(&path, &filename); err != nil {
+ if err == sql.ErrNoRows {
+ notFound(w, r)
+ return
+ }
+ log.Printf("Warning: find preview photo: %s", err)
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+ archive, err := zip.OpenReader(filepath.Join(*martDir, path))
+ if err != nil {
+ log.Printf("Warning: open archive: %s", err)
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+ defer archive.Close()
+ f, err := archive.Open(filename)
+ if err != nil {
+ log.Printf("Warning: open archive file: %s", err)
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+ defer f.Close()
+ fullSize, _, err := image.Decode(f)
+ if err != nil {
+ log.Printf("Warning: open preview photo: %s", err)
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+ resized := image.NewRGBA(image.Rect(0, 0, 250, fullSize.Bounds().Dy()*250/fullSize.Bounds().Dx()))
+ draw.ApproxBiLinear.Scale(resized, resized.Bounds(), fullSize, fullSize.Bounds(), draw.Src, nil)
+ w.Header().Set("Content-Type", "image/jpeg")
+ if err := jpeg.Encode(w, resized, &jpeg.Options{Quality: 50}); err != nil {
+ log.Printf("Warning: met art preview image: %s", err)
+ }
+}
+
+var (
+ //go:embed templates/mart/set.html.template
+ metArtSetString string
+ metArtSetTemplate = template.Must(outlinedTemplate(metArtSetString))
+)
+
+type metArtSetTemplateArgs struct {
+ Name string
+ Photos []int64
+}
+
+type metArtSetHandler struct {
+ s *stmts
+}
+
+func (h *metArtSetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if !stupidCache(w, r) {
+ return
+ }
+ id, err := strconv.ParseInt(r.PathValue("set"), 10, 64)
+ if err != nil {
+ notFound(w, r)
+ return
+ }
+ wg := new(sync.WaitGroup)
+ var name string
+ var nameErr error
+ wg.Go(func() {
+ nameErr = h.s.setName.QueryRow(id).Scan(&name)
+ })
+ var photos []int64
+ var photosErr error
+ wg.Go(func() {
+ rows, err := h.s.listPhotosForSet.Query(id)
+ if err != nil {
+ photosErr = err
+ return
+ }
+ for rows.Next() {
+ var photoID int64
+ if err := rows.Scan(&photoID); err != nil {
+ photosErr = err
+ return
+ }
+ photos = append(photos, photoID)
+ }
+ photosErr = rows.Err()
+ })
+ wg.Wait()
+ if nameErr != nil {
+ if nameErr == sql.ErrNoRows {
+ notFound(w, r)
+ return
+ }
+ log.Printf("Warning: find set name: %s", nameErr)
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+ if photosErr != nil {
+ log.Printf("Warning: list set photos: %s", photosErr)
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+ if err := metArtSetTemplate.Execute(w, metArtSetTemplateArgs{Name: name, Photos: photos}); err != nil {
+ log.Printf("Warning: mart set: %s", err)
+ }
+}
+
+func stupidCache(w http.ResponseWriter, r *http.Request) bool {
+ w.Header().Set("Last-Modified", "Mon, 01 Jan 1990 00:00:00 GMT")
+ if _, ok := r.Header["If-Modified-Since"]; ok {
+ w.WriteHeader(http.StatusNotModified)
+ return false
+ }
+ return true
+}
+
type jellyfinReverseProxy struct {
proxy httputil.ReverseProxy
}
@@ -917,6 +1223,50 @@ func (p *jellyfinReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request)
p.proxy.ServeHTTP(w, r)
}
+type stmts struct {
+ listGirls *sql.Stmt
+ findPhoto *sql.Stmt
+ findPreview *sql.Stmt
+ girlName *sql.Stmt
+ listSetsForGirl *sql.Stmt
+ setName *sql.Stmt
+ listPhotosForSet *sql.Stmt
+}
+
+func prepareStatements(db *sql.DB) (*stmts, error) {
+ s := new(stmts)
+ var err error
+ s.listGirls, err = db.Prepare("SELECT girl_id, name FROM girls ORDER BY substr(name, instr(name, ' ')+1), name")
+ if err != nil {
+ return nil, err
+ }
+ s.findPhoto, err = db.Prepare("SELECT path, filename FROM photos JOIN sets USING (set_id) WHERE photo_id = ?")
+ if err != nil {
+ return nil, err
+ }
+ s.findPreview, err = db.Prepare("SELECT path, filename FROM girls JOIN sets USING (girl_id) JOIN photos ON photo_id = cover_photo_id WHERE girl_id = ? ORDER BY date ASC, sets.name ASC LIMIT 1")
+ if err != nil {
+ return nil, err
+ }
+ s.girlName, err = db.Prepare("SELECT name FROM girls WHERE girl_id = ?")
+ if err != nil {
+ return nil, err
+ }
+ s.listSetsForGirl, err = db.Prepare("SELECT set_id, cover_photo_id, sets.name FROM girls JOIN sets USING (girl_id) WHERE girl_id = ? ORDER BY date ASC, sets.name ASC")
+ if err != nil {
+ return nil, err
+ }
+ s.setName, err = db.Prepare("SELECT name FROM sets WHERE set_id = ?")
+ if err != nil {
+ return nil, err
+ }
+ s.listPhotosForSet, err = db.Prepare("SELECT photo_id FROM sets JOIN photos USING (set_id) WHERE set_id = ? AND idx IS NOT NULL ORDER BY idx ASC")
+ if err != nil {
+ return nil, err
+ }
+ return s, nil
+}
+
func main() {
flag.Parse()
@@ -924,6 +1274,15 @@ func main() {
log.Fatal(err)
}
+ db, err := sql.Open("sqlite3", *martDB)
+ if err != nil {
+ log.Fatal(err)
+ }
+ stmts, err := prepareStatements(db)
+ if err != nil {
+ log.Fatal(err)
+ }
+
go func() {
for range time.NewTicker(2 * time.Hour).C {
if err := cleanHole(); err != nil {
@@ -951,6 +1310,11 @@ func main() {
mux.HandleFunc("GET /pkg/{pkg}", pkg)
mux.Handle("GET /code/", http.StripPrefix("/code/", http.FileServer(http.Dir(*codeDir))))
mux.Handle("GET /nezuko/", &nezukoHandler{http.StripPrefix("/nezuko/", http.FileServer(http.Dir(*nezuko)))})
+ mux.Handle("GET /mart", &metArtIndexHandler{stmts})
+ mux.Handle("GET /mart/photos/{photo}", &metArtPhotoHandler{stmts})
+ mux.Handle("GET /mart/girls/{girl}", &metArtGirlHandler{stmts})
+ mux.Handle("GET /mart/girls/{girl}/preview.jpeg", &metArtGirlPreviewHandler{stmts})
+ mux.Handle("GET /mart/sets/{set}", &metArtSetHandler{stmts})
mux.HandleFunc("GET /static/", static)
mux.HandleFunc("GET /favicon.ico", favicon)
apiMux := http.NewServeMux()