Add default thumbnails for scenes and images (#2949)

* Use default thumbnail for scene covers
* Use defautl thumbnail for image thumbnails
This commit is contained in:
WithoutPants 2022-10-03 13:01:35 +11:00 committed by GitHub
parent 88bfda1980
commit 6ba9f55df0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 92 additions and 32 deletions

View File

@ -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

View File

@ -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)
}
}

View File

@ -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

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-384 -104 1280 720">
<!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc.
Modified from https://github.com/FortAwesome/Font-Awesome/blob/6.x/svgs/solid/image.svg
Changed view box and fill style.
-->
<path d="M0 96C0 60.7 28.7 32 64 32H448c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6h96 32H424c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192c26.5 0 48-21.5 48-48s-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48z" style="fill:#ffffff;fill-opacity:1"/>
</svg>

After

Width:  |  Height:  |  Size: 880 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-384 -104 1280 720">
<!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc.
Modified from https://github.com/FortAwesome/Font-Awesome/blob/6.x/svgs/solid/circle-play.svg
Changed view box and added fill style.
-->
<path d="M512 256c0 141.4-114.6 256-256 256S0 397.4 0 256S114.6 0 256 0S512 114.6 512 256zM188.3 147.1c-7.6 4.2-12.3 12.3-12.3 20.9V344c0 8.7 4.7 16.7 12.3 20.9s16.8 4.1 24.3-.5l144-88c7.1-4.4 11.5-12.1 11.5-20.5s-4.4-16.1-11.5-20.5l-144-88c-7.4-4.5-16.7-4.7-24.3-.5z" style="fill:#ffffff;fill-opacity:1"/>
</svg>

After

Width:  |  Height:  |  Size: 733 B

View File

@ -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 {