diff --git a/internal/api/routes_image.go b/internal/api/routes_image.go index 2a3098bb5..4d37e862f 100644 --- a/internal/api/routes_image.go +++ b/internal/api/routes_image.go @@ -6,6 +6,7 @@ import ( "net/http" "os/exec" "strconv" + "syscall" "github.com/go-chi/chi" "github.com/stashapp/stash/internal/manager" @@ -84,11 +85,11 @@ func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) { if manager.GetInstance().Config.IsWriteImageThumbnails() { logger.Debugf("writing thumbnail to disk: %s", img.Path) if err := fsutil.WriteFile(filepath, data); err != nil { - logger.Errorf("error writing thumbnail for image %s: %s", img.Path, err) + logger.Errorf("error writing thumbnail for image %s: %v", img.Path, err) } } - if n, err := w.Write(data); err != nil { - logger.Errorf("error writing thumbnail response. Wrote %v bytes: %v", n, err) + if n, err := w.Write(data); err != nil && !errors.Is(err, syscall.EPIPE) { + logger.Errorf("error serving thumbnail (wrote %v bytes out of %v): %v", n, len(data), err) } } } @@ -114,7 +115,7 @@ func (rs imageRoutes) ImageCtx(next http.Handler) http.Handler { imageID, _ := strconv.Atoi(imageIdentifierQueryParam) var image *models.Image - readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + _ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { qb := rs.imageFinder if imageID == 0 { images, _ := qb.FindByChecksum(ctx, imageIdentifierQueryParam) @@ -131,10 +132,6 @@ func (rs imageRoutes) ImageCtx(next http.Handler) http.Handler { return nil }) - if readTxnErr != nil { - logger.Warnf("read transaction failure while trying to read image by id: %v", readTxnErr) - } - if image == nil { http.Error(w, http.StatusText(404), 404) return diff --git a/internal/api/routes_movie.go b/internal/api/routes_movie.go index 8fbccdb53..032fefca1 100644 --- a/internal/api/routes_movie.go +++ b/internal/api/routes_movie.go @@ -2,6 +2,7 @@ package api import ( "context" + "errors" "net/http" "strconv" @@ -40,12 +41,15 @@ func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) { defaultParam := r.URL.Query().Get("default") var image []byte if defaultParam != "true" { - err := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { image, _ = rs.movieFinder.GetFrontImage(ctx, movie.ID) return nil }) - if err != nil { - logger.Warnf("read transaction error while getting front image: %v", err) + if errors.Is(readTxnErr, context.Canceled) { + return + } + if readTxnErr != nil { + logger.Warnf("read transaction error on fetch movie front image: %v", readTxnErr) } } @@ -54,7 +58,7 @@ func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) { } if err := utils.ServeImage(image, w, r); err != nil { - logger.Warnf("error serving front image: %v", err) + logger.Warnf("error serving movie front image: %v", err) } } @@ -63,12 +67,15 @@ func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) { defaultParam := r.URL.Query().Get("default") var image []byte if defaultParam != "true" { - err := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { image, _ = rs.movieFinder.GetBackImage(ctx, movie.ID) return nil }) - if err != nil { - logger.Warnf("read transaction error on fetch back image: %v", err) + if errors.Is(readTxnErr, context.Canceled) { + return + } + if readTxnErr != nil { + logger.Warnf("read transaction error on fetch movie back image: %v", readTxnErr) } } @@ -77,7 +84,7 @@ func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) { } if err := utils.ServeImage(image, w, r); err != nil { - logger.Warnf("error while serving image: %v", err) + logger.Warnf("error serving movie back image: %v", err) } } @@ -90,11 +97,11 @@ func (rs movieRoutes) MovieCtx(next http.Handler) http.Handler { } var movie *models.Movie - if err := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { - var err error - movie, err = rs.movieFinder.Find(ctx, movieID) - return err - }); err != nil { + _ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + movie, _ = rs.movieFinder.Find(ctx, movieID) + return nil + }) + if movie == nil { http.Error(w, http.StatusText(404), 404) return } diff --git a/internal/api/routes_performer.go b/internal/api/routes_performer.go index 15ad3c743..40e41833c 100644 --- a/internal/api/routes_performer.go +++ b/internal/api/routes_performer.go @@ -2,6 +2,7 @@ package api import ( "context" + "errors" "net/http" "strconv" @@ -44,8 +45,11 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) { image, _ = rs.performerFinder.GetImage(ctx, performer.ID) return nil }) + if errors.Is(readTxnErr, context.Canceled) { + return + } if readTxnErr != nil { - logger.Warnf("couldn't execute getting a performer image from read transaction: %v", readTxnErr) + logger.Warnf("read transaction error on fetch performer image: %v", readTxnErr) } } @@ -54,7 +58,7 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) { } if err := utils.ServeImage(image, w, r); err != nil { - logger.Warnf("error serving image: %v", err) + logger.Warnf("error serving performer image: %v", err) } } @@ -67,11 +71,12 @@ func (rs performerRoutes) PerformerCtx(next http.Handler) http.Handler { } var performer *models.Performer - if err := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + _ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { var err error performer, err = rs.performerFinder.Find(ctx, performerID) return err - }); err != nil { + }) + if performer == nil { http.Error(w, http.StatusText(404), 404) return } diff --git a/internal/api/routes_scene.go b/internal/api/routes_scene.go index d6fb0847f..586325872 100644 --- a/internal/api/routes_scene.go +++ b/internal/api/routes_scene.go @@ -3,6 +3,7 @@ package api import ( "bytes" "context" + "errors" "net/http" "strconv" "strings" @@ -253,12 +254,12 @@ func (rs sceneRoutes) Webp(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, filepath) } -func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.SceneMarker) string { +func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.SceneMarker) (*string, error) { if marker.Title != "" { - return marker.Title + return &marker.Title, nil } - var ret string + var title string if err := txn.WithTxn(ctx, rs.txnManager, func(ctx context.Context) error { qb := rs.tagFinder primaryTag, err := qb.Find(ctx, marker.PrimaryTagID) @@ -266,7 +267,7 @@ func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.Sce return err } - ret = primaryTag.Name + title = primaryTag.Name tags, err := qb.FindBySceneMarkerID(ctx, marker.ID) if err != nil { @@ -274,26 +275,31 @@ func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.Sce } for _, t := range tags { - ret += ", " + t.Name + title += ", " + t.Name } return nil }); err != nil { - panic(err) + return nil, err } - return ret + return &title, nil } func (rs sceneRoutes) ChapterVtt(w http.ResponseWriter, r *http.Request) { scene := r.Context().Value(sceneKey).(*models.Scene) var sceneMarkers []*models.SceneMarker - if err := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { var err error sceneMarkers, err = rs.sceneMarkerFinder.FindBySceneID(ctx, scene.ID) return err - }); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + }) + if errors.Is(readTxnErr, context.Canceled) { + return + } + if readTxnErr != nil { + logger.Warnf("read transaction error on fetch scene markers: %v", readTxnErr) + http.Error(w, readTxnErr.Error(), http.StatusInternalServerError) return } @@ -302,7 +308,18 @@ func (rs sceneRoutes) ChapterVtt(w http.ResponseWriter, r *http.Request) { vttLines = append(vttLines, strconv.Itoa(i+1)) time := utils.GetVTTTime(marker.Seconds) vttLines = append(vttLines, time+" --> "+time) - vttLines = append(vttLines, rs.getChapterVttTitle(r.Context(), marker)) + + vttTitle, err := rs.getChapterVttTitle(r.Context(), marker) + if errors.Is(err, context.Canceled) { + return + } + if err != nil { + logger.Warnf("read transaction error on fetch scene marker title: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + vttLines = append(vttLines, *vttTitle) vttLines = append(vttLines, "") } vtt := strings.Join(vttLines, "\n") @@ -327,35 +344,50 @@ func (rs sceneRoutes) InteractiveHeatmap(w http.ResponseWriter, r *http.Request) func (rs sceneRoutes) Caption(w http.ResponseWriter, r *http.Request, lang string, ext string) { s := r.Context().Value(sceneKey).(*models.Scene) - if err := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + var captions []*models.VideoCaption + readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { var err error primaryFile := s.Files.Primary() if primaryFile == nil { return nil } - captions, err := rs.captionFinder.GetCaptions(ctx, primaryFile.Base().ID) - for _, caption := range captions { - if lang == caption.LanguageCode && ext == caption.CaptionType { - sub, err := video.ReadSubs(caption.Path(s.Path)) - if err == nil { - var b bytes.Buffer - err = sub.WriteToWebVTT(&b) - if err == nil { - w.Header().Set("Content-Type", "text/vtt") - w.Header().Add("Cache-Control", "no-cache") - _, _ = b.WriteTo(w) - } - return err - } - logger.Debugf("Error while reading subs: %v", err) - } - } + captions, err = rs.captionFinder.GetCaptions(ctx, primaryFile.Base().ID) + return err - }); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + }) + if errors.Is(readTxnErr, context.Canceled) { return } + if readTxnErr != nil { + logger.Warnf("read transaction error on fetch scene captions: %v", readTxnErr) + http.Error(w, readTxnErr.Error(), http.StatusInternalServerError) + return + } + + for _, caption := range captions { + if lang != caption.LanguageCode || ext != caption.CaptionType { + return + } + + sub, err := video.ReadSubs(caption.Path(s.Path)) + if err != nil { + logger.Warnf("error while reading subs: %v", err) + http.Error(w, readTxnErr.Error(), http.StatusInternalServerError) + return + } + + var b bytes.Buffer + err = sub.WriteToWebVTT(&b) + if err != nil { + http.Error(w, readTxnErr.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/vtt") + w.Header().Add("Cache-Control", "no-cache") + _, _ = b.WriteTo(w) + } } func (rs sceneRoutes) CaptionLang(w http.ResponseWriter, r *http.Request) { @@ -387,13 +419,17 @@ func (rs sceneRoutes) SceneMarkerStream(w http.ResponseWriter, r *http.Request) scene := r.Context().Value(sceneKey).(*models.Scene) sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId")) var sceneMarker *models.SceneMarker - if err := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { var err error sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID) return err - }); err != nil { - logger.Warnf("Error when getting scene marker for stream: %s", err.Error()) - http.Error(w, http.StatusText(500), 500) + }) + if errors.Is(readTxnErr, context.Canceled) { + return + } + if readTxnErr != nil { + logger.Warnf("read transaction error on fetch scene marker: %v", readTxnErr) + http.Error(w, readTxnErr.Error(), http.StatusInternalServerError) return } @@ -410,13 +446,17 @@ func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request) scene := r.Context().Value(sceneKey).(*models.Scene) sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId")) var sceneMarker *models.SceneMarker - if err := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { var err error sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID) return err - }); err != nil { - logger.Warnf("Error when getting scene marker for stream: %s", err.Error()) - http.Error(w, http.StatusText(500), 500) + }) + if errors.Is(readTxnErr, context.Canceled) { + return + } + if readTxnErr != nil { + logger.Warnf("read transaction error on fetch scene marker preview: %v", readTxnErr) + http.Error(w, readTxnErr.Error(), http.StatusInternalServerError) return } @@ -443,13 +483,17 @@ func (rs sceneRoutes) SceneMarkerScreenshot(w http.ResponseWriter, r *http.Reque scene := r.Context().Value(sceneKey).(*models.Scene) sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId")) var sceneMarker *models.SceneMarker - if err := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { var err error sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID) return err - }); err != nil { - logger.Warnf("Error when getting scene marker for stream: %s", err.Error()) - http.Error(w, http.StatusText(500), 500) + }) + if errors.Is(readTxnErr, context.Canceled) { + return + } + if readTxnErr != nil { + logger.Warnf("read transaction error on fetch scene marker screenshot: %v", readTxnErr) + http.Error(w, readTxnErr.Error(), http.StatusInternalServerError) return } @@ -480,7 +524,7 @@ func (rs sceneRoutes) SceneCtx(next http.Handler) http.Handler { sceneID, _ := strconv.Atoi(sceneIdentifierQueryParam) var scene *models.Scene - readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + _ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { qb := rs.sceneFinder if sceneID == 0 { var scenes []*models.Scene @@ -505,10 +549,6 @@ func (rs sceneRoutes) SceneCtx(next http.Handler) http.Handler { return nil }) - if readTxnErr != nil { - logger.Warnf("error executing SceneCtx transaction: %v", readTxnErr) - } - if scene == nil { http.Error(w, http.StatusText(404), 404) return diff --git a/internal/api/routes_studio.go b/internal/api/routes_studio.go index e26499f04..c0b51b715 100644 --- a/internal/api/routes_studio.go +++ b/internal/api/routes_studio.go @@ -5,7 +5,6 @@ import ( "errors" "net/http" "strconv" - "syscall" "github.com/go-chi/chi" "github.com/stashapp/stash/pkg/logger" @@ -42,12 +41,15 @@ func (rs studioRoutes) Image(w http.ResponseWriter, r *http.Request) { var image []byte if defaultParam != "true" { - err := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { image, _ = rs.studioFinder.GetImage(ctx, studio.ID) return nil }) - if err != nil { - logger.Warnf("read transaction error while fetching studio image: %v", err) + if errors.Is(readTxnErr, context.Canceled) { + return + } + if readTxnErr != nil { + logger.Warnf("read transaction error on fetch studio image: %v", readTxnErr) } } @@ -56,12 +58,7 @@ func (rs studioRoutes) Image(w http.ResponseWriter, r *http.Request) { } if err := utils.ServeImage(image, w, r); err != nil { - // Broken pipe errors are common when serving images and the remote - // connection closes the connection. Filter them out of the error - // messages, as they are benign. - if !errors.Is(err, syscall.EPIPE) { - logger.Warnf("cannot serve studio image: %v", err) - } + logger.Warnf("error serving studio image: %v", err) } } @@ -74,11 +71,12 @@ func (rs studioRoutes) StudioCtx(next http.Handler) http.Handler { } var studio *models.Studio - if err := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + _ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { var err error studio, err = rs.studioFinder.Find(ctx, studioID) return err - }); err != nil { + }) + if studio == nil { http.Error(w, http.StatusText(404), 404) return } diff --git a/internal/api/routes_tag.go b/internal/api/routes_tag.go index 69c573cb2..1773e0daa 100644 --- a/internal/api/routes_tag.go +++ b/internal/api/routes_tag.go @@ -2,6 +2,7 @@ package api import ( "context" + "errors" "net/http" "strconv" @@ -40,12 +41,15 @@ func (rs tagRoutes) Image(w http.ResponseWriter, r *http.Request) { var image []byte if defaultParam != "true" { - err := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { image, _ = rs.tagFinder.GetImage(ctx, tag.ID) return nil }) - if err != nil { - logger.Warnf("read transaction error while getting tag image: %v", err) + if errors.Is(readTxnErr, context.Canceled) { + return + } + if readTxnErr != nil { + logger.Warnf("read transaction error on fetch tag image: %v", readTxnErr) } } @@ -67,11 +71,12 @@ func (rs tagRoutes) TagCtx(next http.Handler) http.Handler { } var tag *models.Tag - if err := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { + _ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error { var err error tag, err = rs.tagFinder.Find(ctx, tagID) return err - }); err != nil { + }) + if tag == nil { http.Error(w, http.StatusText(404), 404) return } diff --git a/internal/manager/running_streams.go b/internal/manager/running_streams.go index 9d43d26d2..9acd3deb3 100644 --- a/internal/manager/running_streams.go +++ b/internal/manager/running_streams.go @@ -2,6 +2,7 @@ package manager import ( "context" + "errors" "net/http" "github.com/stashapp/stash/internal/manager/config" @@ -81,16 +82,21 @@ func (s *SceneServer) ServeScreenshot(scene *models.Scene, w http.ResponseWriter http.ServeFile(w, r, filepath) } else { var cover []byte - err := txn.WithTxn(r.Context(), s.TxnManager, func(ctx context.Context) error { + readTxnErr := txn.WithTxn(r.Context(), s.TxnManager, func(ctx context.Context) error { cover, _ = s.SceneCoverGetter.GetCover(ctx, scene.ID) return nil }) - if err != nil { - logger.Warnf("read transaction failed while serving screenshot: %v", err) + 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 err = utils.ServeImage(cover, w, r); err != nil { - logger.Warnf("unable to serve screenshot image: %v", err) + if err := utils.ServeImage(cover, w, r); err != nil { + logger.Warnf("error serving screenshot image: %v", err) } } } diff --git a/pkg/ffmpeg/stream.go b/pkg/ffmpeg/stream.go index a024f8aa6..1088778d1 100644 --- a/pkg/ffmpeg/stream.go +++ b/pkg/ffmpeg/stream.go @@ -2,10 +2,12 @@ package ffmpeg import ( "context" + "errors" "io" "net/http" "os/exec" "strings" + "syscall" "github.com/stashapp/stash/pkg/logger" ) @@ -35,8 +37,8 @@ func (s *Stream) Serve(w http.ResponseWriter, r *http.Request) { // process killing should be handled by command context _, err := io.Copy(w, s.Stdout) - if err != nil { - logger.Errorf("[stream] error serving transcoded video file: %s", err.Error()) + if err != nil && !errors.Is(err, syscall.EPIPE) { + logger.Errorf("[stream] error serving transcoded video file: %v", err) } } diff --git a/pkg/file/file.go b/pkg/file/file.go index 3b83e4946..611ceee50 100644 --- a/pkg/file/file.go +++ b/pkg/file/file.go @@ -2,10 +2,12 @@ package file import ( "context" + "errors" "io" "io/fs" "net/http" "strconv" + "syscall" "time" "github.com/stashapp/stash/pkg/logger" @@ -137,8 +139,9 @@ func (f *BaseFile) Serve(fs FS, w http.ResponseWriter, r *http.Request) { return } - if k, err := w.Write(data); err != nil { - logger.Warnf("failure while serving image (wrote %v bytes out of %v): %v", k, len(data), err) + k, err := w.Write(data) + if err != nil && !errors.Is(err, syscall.EPIPE) { + logger.Warnf("error serving file (wrote %v bytes out of %v): %v", k, len(data), err) } return diff --git a/pkg/fsutil/file.go b/pkg/fsutil/file.go index a3ccfc2e6..60a50d74f 100644 --- a/pkg/fsutil/file.go +++ b/pkg/fsutil/file.go @@ -84,14 +84,10 @@ func FileExists(path string) (bool, error) { func WriteFile(path string, file []byte) error { pathErr := EnsureDirAll(filepath.Dir(path)) if pathErr != nil { - return fmt.Errorf("cannot ensure path %s", pathErr) + return fmt.Errorf("cannot ensure path exists: %w", pathErr) } - err := os.WriteFile(path, file, 0755) - if err != nil { - return fmt.Errorf("write error for thumbnail %s: %s ", path, err) - } - return nil + return os.WriteFile(path, file, 0755) } // GetNameFromPath returns the name of a file from its path diff --git a/pkg/utils/image.go b/pkg/utils/image.go index 061a01460..df73bec82 100644 --- a/pkg/utils/image.go +++ b/pkg/utils/image.go @@ -5,11 +5,13 @@ import ( "crypto/md5" "crypto/tls" "encoding/base64" + "errors" "fmt" "io" "net/http" "regexp" "strings" + "syscall" "time" ) @@ -127,5 +129,11 @@ func ServeImage(image []byte, w http.ResponseWriter, r *http.Request) error { w.Header().Add("Etag", etag) w.Header().Set("Cache-Control", "public, max-age=604800, immutable") _, err := w.Write(image) + // Broken pipe errors are common when serving images and the remote + // connection closes the connection. Filter them out of the error + // messages, as they are benign. + if errors.Is(err, syscall.EPIPE) { + return nil + } return err }