package main import ( "crypto/rand" _ "embed" "errors" "fmt" "io" "io/fs" "log" "math/big" "net" "net/http" "os" "path/filepath" "slices" "sort" "strconv" "goreader/gallery" "goreader/state" view "goreader/views" "image" _ "image/gif" _ "image/jpeg" "image/png" "golang.org/x/image/draw" "github.com/lithammer/fuzzysearch/fuzzy" ) var searchGalleriesError = fmt.Errorf("failure reading archives") func searchGalleryDl(state *state.State) error { defer func() { log.Println("done searching galleries") state.Done = true }() sourceDirectories, err := os.ReadDir(state.Root) if err != nil { return errors.Join(searchGalleriesError, err) } for _, source := range sourceDirectories { log.Println("attemping", source) if !source.IsDir() { continue } sourcePath := filepath.Join(state.Root, source.Name()) sourceDir, err := os.ReadDir(sourcePath) if err != nil { return errors.Join(searchGalleriesError, err) } for _, galleryDir := range sourceDir { galleryPath := filepath.Join(sourcePath, galleryDir.Name()) galleryFiles, err := os.ReadDir(galleryPath) if err != nil { log.Println(err) continue } infoIndex := slices.IndexFunc(galleryFiles, func(a fs.DirEntry) bool { return a.Name() == "info.json" }) if infoIndex == -1 { log.Println(fmt.Errorf("no index file: %s", galleryPath)) continue } infoPath := filepath.Join(galleryPath, "info.json") infoBinary, err := os.ReadFile(infoPath) if err != nil { log.Println(err) continue } var from gallery.Source switch source.Name() { case "nhentai": from = gallery.Nhentai case "exhentai": from = gallery.Exhentai case "hitomi": from = gallery.Hitomi default: log.Printf("unrecognized source: \"%s\"\n", source.Name()) continue } info, err := gallery.NewGallery(from, infoBinary, galleryPath, galleryFiles) if err != nil { log.Println(err) continue } err = state.AddGallery(info) if os.IsNotExist(err) { log.Println(err) } else if err != nil { panic(err) } } } return nil } func main() { state := &state.State{ Port: ":2323", Root: "/home/xi/seed/gallery-dl/", Galleries: make([]gallery.Gallery, 0, 50), UniqueTags: make(map[string]int), UniqueArtists: make(map[string]int), UniqueGroups: make(map[string]int), UniqueParodies: make(map[string]int), Filtered: make([]gallery.Tag, 0), } fmt.Printf("live at http://localhost%s\n", state.Port) server := &http.Server{ Addr: state.Port, Handler: Logging(http.DefaultServeMux), } ln, err := net.Listen("tcp", server.Addr) if err != nil { panic(err) } go func() { // defer func() { // if err := recover(); err != nil { // e, ok := err.(error) // if !ok { // log.Println(err) // } else { // log.Println(e.Error()) // } // } // }() err := searchGalleryDl(state) if err != nil { log.Println(err) log.Println() } }() cacheDir, err := os.UserCacheDir() if err != nil { panic(err) } state.CacheDir = filepath.Join(cacheDir, "omakase") err = os.MkdirAll(state.CacheDir, 0o755) if err != nil { panic(err) } log.Println("cache directory path:", state.CacheDir) http.DefaultServeMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" && r.URL.Path != "index.html" { log.Println("not found:", r.URL.Path) http.NotFound(w, r) return } page, err := strconv.Atoi(r.URL.Query().Get("page")) if err != nil { page = 1 } view.Index(state, page).Render(w) }) // http.DefaultServeMux.HandleFunc("/static/styles.css", func(w http.ResponseWriter, r *http.Request) { // file, err := os.Open("./views/static/styles.css") // if err != nil { // panic(err) // } // io.Copy(w, file) // }) http.DefaultServeMux.HandleFunc("/read/{uuid}", func(w http.ResponseWriter, r *http.Request) { uuid := r.PathValue("uuid") if uuid == "" { panic(fmt.Errorf("could not get gallery UUID. empty string.")) } page := "1" pageString := r.URL.Query().Get("page") if pageString != "" { page = pageString } found, err := state.FindTitle(uuid) if err != nil { panic(err) } _ = view.Reader(found, page).Render(w) }) http.DefaultServeMux.HandleFunc("/random", func(w http.ResponseWriter, r *http.Request) { f := state.FilterGalleries() n, err := rand.Int(rand.Reader, big.NewInt(int64(len(f)))) if err != nil { panic(err) } http.Redirect(w, r, fmt.Sprintf("/read/%s", f[n.Int64()].Uuid()), http.StatusFound) }) http.DefaultServeMux.HandleFunc("/page/{uuid}/{page}", func(w http.ResponseWriter, r *http.Request) { uuid := r.PathValue("uuid") pageString := r.PathValue("page") if uuid == "" || pageString == "" { panic(fmt.Errorf("uuid or page could not be found: %s", r.URL.Path)) } page, err := strconv.Atoi(pageString) if err != nil { panic(err) } found, err := state.FindTitle(uuid) if err != nil { panic(err) } image, err := os.Open(found.Images()[page-1]) if err != nil { panic(err) } _, _ = io.Copy(w, image) }) http.DefaultServeMux.HandleFunc("/thumb/{uuid}", func(w http.ResponseWriter, r *http.Request) { uuid := r.PathValue("uuid") if uuid == "" { panic(fmt.Errorf("uuid could not be found: %s", r.URL.Path)) } found, err := state.FindTitle(uuid) if err != nil { panic(err) } if len(found.Images()) == 0 { panic(fmt.Errorf("why does gallery contain no images? \"%s\"", uuid)) } imageCacheFilepath := filepath.Join(state.CacheDir, uuid) imageCache, err := os.Open(imageCacheFilepath) if err == nil { _, _ = io.Copy(w, imageCache) imageCache.Close() return } if len(found.Images()) == 0 { panic(fmt.Errorf("why does gallery contain no images? \"%s\"", uuid)) } coverImagePath := found.Images()[0] imageStream, err := os.Open(coverImagePath) if err != nil { panic(err) } defer imageStream.Close() sourceImage, _, err := image.Decode(imageStream) if err != nil { panic(err) } sourceSize := sourceImage.Bounds().Size() aspectRatio := float64(sourceSize.X) / float64(sourceSize.Y) destWidth := 148 destHeight := int(float64(destWidth) / aspectRatio) scaler := draw.BiLinear.NewScaler(destWidth, destHeight, sourceSize.X, sourceSize.Y) destRect := image.Rect(0, 0, destWidth, destHeight) destImage := image.NewRGBA(destRect) scaler.Scale(destImage, destRect, sourceImage, sourceImage.Bounds(), draw.Src, nil) imageCache, err = os.Create(imageCacheFilepath) if err != nil { panic(err) } defer imageCache.Close() _ = png.Encode(io.MultiWriter(imageCache, w), destImage) }) // http.DefaultServeMux.HandleFunc("GET /filter/{tag}/{sex}", func(w http.ResponseWriter, r *http.Request) { // tagLookup := r.PathValue("tag") // if tagLookup == "" { // panic(fmt.Errorf("tag could not be found: %s", r.URL.Path)) // } // if tagLookup == "clear" { // state.Filtered = []gallery.Tag{} // view.SearchResults(state.Galleries).Render(w) // return // } // tagSexLookup := r.PathValue("sex") // if tagSexLookup == "" { // panic(fmt.Errorf("tag sex could not be found: %s", r.URL.Path)) // } // sex := gallery.Gender(tagSexLookup) // i := slices.IndexFunc(state.TagKeys, func(a gallery.Tag) bool { // return a.Name == tagLookup && a.Sex == sex // }) // if i < 0 { // panic(fmt.Errorf("tag could not be found: %s", r.URL.Path)) // } // tag := state.TagKeys[i] // i = slices.Index(state.Filtered, tag) // if i < 0 { // state.Filtered = append(state.Filtered, tag) // } else { // state.Filtered = slices.Delete(state.Filtered, i, i+1) // } // view.SearchResults(state.FilterGalleries()).Render(w) // }) http.DefaultServeMux.HandleFunc("GET /stats", func(w http.ResponseWriter, r *http.Request) { view.Stats(state).Render(w) }) http.DefaultServeMux.HandleFunc("GET /details/{uuid}", func(w http.ResponseWriter, r *http.Request) { uuid := r.PathValue("uuid") if uuid == "" { panic(fmt.Errorf("uuid could not be found: %s", r.URL.Path)) } g, err := state.FindTitle(uuid) if err != nil { panic(err) } view.InspectorInfo(g, state).Render(w) }) // FIXME http.DefaultServeMux.HandleFunc("POST /search", func(w http.ResponseWriter, r *http.Request) { query := r.FormValue("search") log.Println("search form result:", query) if query == "" { view.SearchResults(state.Galleries).Render(w) return } // ranks := fuzzy.RankFindFold(search, state.GalleryNames) var ranks fuzzy.Ranks for i, g := range state.Galleries { en := fuzzy.RankMatchFold(query, g.Name()) rank := fuzzy.Rank{ Source: query, Target: g.JpName(), Distance: en, OriginalIndex: i, } if jp := fuzzy.RankMatchFold(query, g.JpName()); jp >= 0 && jp < en { rank = fuzzy.Rank{ Source: query, Target: g.JpName(), Distance: jp, OriginalIndex: i, } } if rank.Distance < 0 { continue } ranks = append(ranks, rank) } sort.Sort(ranks) var galleries []gallery.Gallery for i, r := range ranks { if i == 30 { break } galleries = append(galleries, state.Galleries[r.OriginalIndex]) } view.SearchResults(galleries).Render(w) }) log.Fatal(server.Serve(ln)) }