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 {