From 6ba9f55df0eed63b8178b6ce6da8dbd9f9ca0cbb Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Mon, 3 Oct 2022 13:01:35 +1100 Subject: [PATCH] Add default thumbnails for scenes and images (#2949) * Use default thumbnail for scene covers * Use defautl thumbnail for image thumbnails --- internal/api/routes_image.go | 44 +++++++++++++++++++++----- internal/manager/running_streams.go | 48 +++++++++++++++++++---------- internal/static/embed.go | 6 ++++ internal/static/image/image.svg | 7 +++++ internal/static/scene/scene.svg | 7 +++++ pkg/file/file.go | 12 +++----- 6 files changed, 92 insertions(+), 32 deletions(-) create mode 100644 internal/static/image/image.svg create mode 100644 internal/static/scene/scene.svg diff --git a/internal/api/routes_image.go b/internal/api/routes_image.go index ae0672662..b89821155 100644 --- a/internal/api/routes_image.go +++ b/internal/api/routes_image.go @@ -3,6 +3,8 @@ package api import ( "context" "errors" + "io" + "io/fs" "net/http" "os/exec" "strconv" @@ -10,6 +12,7 @@ import ( "github.com/go-chi/chi" "github.com/stashapp/stash/internal/manager" + "github.com/stashapp/stash/internal/static" "github.com/stashapp/stash/pkg/file" "github.com/stashapp/stash/pkg/fsutil" "github.com/stashapp/stash/pkg/image" @@ -55,11 +58,11 @@ func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) { if exists { http.ServeFile(w, r, filepath) } else { - // don't return anything if there is no file + const useDefault = true + f := img.Files.Primary() if f == nil { - // TODO - probably want to return a placeholder - http.Error(w, http.StatusText(404), 404) + rs.serveImage(w, r, img, useDefault) return } @@ -67,7 +70,8 @@ func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) { data, err := encoder.GetThumbnail(f, models.DefaultGthumbWidth) if err != nil { // don't log for unsupported image format - if !errors.Is(err, image.ErrNotSupportedForThumbnail) { + // don't log for file not found - can optionally be logged in serveImage + if !errors.Is(err, image.ErrNotSupportedForThumbnail) && !errors.Is(err, fs.ErrNotExist) { logger.Errorf("error generating thumbnail for %s: %v", f.Path, err) var exitErr *exec.ExitError @@ -77,7 +81,7 @@ func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) { } // backwards compatibility - fallback to original image instead - rs.Image(w, r) + rs.serveImage(w, r, img, useDefault) return } @@ -97,14 +101,38 @@ func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) { func (rs imageRoutes) Image(w http.ResponseWriter, r *http.Request) { i := r.Context().Value(imageKey).(*models.Image) - // if image is in a zip file, we need to serve it specifically + const useDefault = false + rs.serveImage(w, r, i, useDefault) +} - if i.Files.Primary() == nil { +func (rs imageRoutes) serveImage(w http.ResponseWriter, r *http.Request, i *models.Image, useDefault bool) { + const defaultImageImage = "image/image.svg" + + if i.Files.Primary() != nil { + err := i.Files.Primary().Serve(&file.OsFS{}, w, r) + if err == nil { + return + } + + if !useDefault { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // only log in debug since it can get noisy + logger.Debugf("Error serving %s: %v", i.DisplayName(), err) + } + + if !useDefault { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } - i.Files.Primary().Serve(&file.OsFS{}, w, r) + // fall back to static image + f, _ := static.Image.Open(defaultImageImage) + defer f.Close() + stat, _ := f.Stat() + http.ServeContent(w, r, "image.svg", stat.ModTime(), f.(io.ReadSeeker)) } // endregion diff --git a/internal/manager/running_streams.go b/internal/manager/running_streams.go index 9acd3deb3..5d010ef50 100644 --- a/internal/manager/running_streams.go +++ b/internal/manager/running_streams.go @@ -3,9 +3,11 @@ package manager import ( "context" "errors" + "io" "net/http" "github.com/stashapp/stash/internal/manager/config" + "github.com/stashapp/stash/internal/static" "github.com/stashapp/stash/pkg/fsutil" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" @@ -74,29 +76,41 @@ func (s *SceneServer) StreamSceneDirect(scene *models.Scene, w http.ResponseWrit } func (s *SceneServer) ServeScreenshot(scene *models.Scene, w http.ResponseWriter, r *http.Request) { + const defaultSceneImage = "scene/scene.svg" + filepath := GetInstance().Paths.Scene.GetScreenshotPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())) // fall back to the scene image blob if the file isn't present screenshotExists, _ := fsutil.FileExists(filepath) if screenshotExists { http.ServeFile(w, r, filepath) - } else { - var cover []byte - readTxnErr := txn.WithTxn(r.Context(), s.TxnManager, func(ctx context.Context) error { - cover, _ = s.SceneCoverGetter.GetCover(ctx, scene.ID) - return nil - }) - if errors.Is(readTxnErr, context.Canceled) { - return - } - if readTxnErr != nil { - logger.Warnf("read transaction error on fetch screenshot: %v", readTxnErr) - http.Error(w, readTxnErr.Error(), http.StatusInternalServerError) - return - } + return + } - if err := utils.ServeImage(cover, w, r); err != nil { - logger.Warnf("error serving screenshot image: %v", err) - } + var cover []byte + readTxnErr := txn.WithTxn(r.Context(), s.TxnManager, func(ctx context.Context) error { + cover, _ = s.SceneCoverGetter.GetCover(ctx, scene.ID) + return nil + }) + if errors.Is(readTxnErr, context.Canceled) { + return + } + if readTxnErr != nil { + logger.Warnf("read transaction error on fetch screenshot: %v", readTxnErr) + http.Error(w, readTxnErr.Error(), http.StatusInternalServerError) + return + } + + if cover == nil { + // fallback to default cover if none found + // should always be there + f, _ := static.Scene.Open(defaultSceneImage) + defer f.Close() + stat, _ := f.Stat() + http.ServeContent(w, r, "scene.svg", stat.ModTime(), f.(io.ReadSeeker)) + } + + if err := utils.ServeImage(cover, w, r); err != nil { + logger.Warnf("error serving screenshot image: %v", err) } } diff --git a/internal/static/embed.go b/internal/static/embed.go index a0563e6ac..bc49aec12 100644 --- a/internal/static/embed.go +++ b/internal/static/embed.go @@ -7,3 +7,9 @@ var Performer embed.FS //go:embed performer_male var PerformerMale embed.FS + +//go:embed scene +var Scene embed.FS + +//go:embed image +var Image embed.FS diff --git a/internal/static/image/image.svg b/internal/static/image/image.svg new file mode 100644 index 000000000..b43b03c7f --- /dev/null +++ b/internal/static/image/image.svg @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/internal/static/scene/scene.svg b/internal/static/scene/scene.svg new file mode 100644 index 000000000..940c5669e --- /dev/null +++ b/internal/static/scene/scene.svg @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/pkg/file/file.go b/pkg/file/file.go index 3323bbd8e..91c1cc448 100644 --- a/pkg/file/file.go +++ b/pkg/file/file.go @@ -118,14 +118,12 @@ func (f *BaseFile) Info(fs FS) (fs.FileInfo, error) { return f.info(fs, f.Path) } -func (f *BaseFile) Serve(fs FS, w http.ResponseWriter, r *http.Request) { +func (f *BaseFile) Serve(fs FS, w http.ResponseWriter, r *http.Request) error { w.Header().Add("Cache-Control", "max-age=604800000") // 1 Week reader, err := f.Open(fs) if err != nil { - // assume not found - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) - return + return err } defer reader.Close() @@ -135,8 +133,7 @@ func (f *BaseFile) Serve(fs FS, w http.ResponseWriter, r *http.Request) { // fallback to direct copy data, err := io.ReadAll(reader) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return err } k, err := w.Write(data) @@ -144,10 +141,11 @@ func (f *BaseFile) Serve(fs FS, w http.ResponseWriter, r *http.Request) { logger.Warnf("error serving file (wrote %v bytes out of %v): %v", k, len(data), err) } - return + return nil } http.ServeContent(w, r, f.Basename, f.ModTime, rsc) + return nil } type Finder interface {