2019-10-17 20:42:12 +00:00
|
|
|
package manager
|
|
|
|
|
|
|
|
import (
|
2022-05-03 23:27:22 +00:00
|
|
|
"context"
|
2022-09-19 05:01:40 +00:00
|
|
|
"errors"
|
2022-10-03 02:01:35 +00:00
|
|
|
"io"
|
2019-10-17 20:42:12 +00:00
|
|
|
"net/http"
|
2022-11-21 23:21:27 +00:00
|
|
|
"time"
|
2019-10-17 20:42:12 +00:00
|
|
|
|
2022-03-17 00:33:59 +00:00
|
|
|
"github.com/stashapp/stash/internal/manager/config"
|
2022-10-03 02:01:35 +00:00
|
|
|
"github.com/stashapp/stash/internal/static"
|
2022-03-17 00:33:59 +00:00
|
|
|
"github.com/stashapp/stash/pkg/fsutil"
|
2019-10-17 20:42:12 +00:00
|
|
|
"github.com/stashapp/stash/pkg/logger"
|
2021-05-20 06:58:43 +00:00
|
|
|
"github.com/stashapp/stash/pkg/models"
|
2022-05-19 07:49:32 +00:00
|
|
|
"github.com/stashapp/stash/pkg/txn"
|
2021-05-20 06:58:43 +00:00
|
|
|
"github.com/stashapp/stash/pkg/utils"
|
2019-10-17 20:42:12 +00:00
|
|
|
)
|
|
|
|
|
2022-05-03 23:27:22 +00:00
|
|
|
type StreamRequestContext struct {
|
|
|
|
context.Context
|
|
|
|
ResponseWriter http.ResponseWriter
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewStreamRequestContext(w http.ResponseWriter, r *http.Request) *StreamRequestContext {
|
|
|
|
return &StreamRequestContext{
|
|
|
|
Context: r.Context(),
|
|
|
|
ResponseWriter: w,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *StreamRequestContext) Cancel() {
|
|
|
|
hj, ok := (c.ResponseWriter).(http.Hijacker)
|
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// hijack and close the connection
|
2022-10-11 03:24:09 +00:00
|
|
|
conn, bw, _ := hj.Hijack()
|
2022-05-03 23:27:22 +00:00
|
|
|
if conn != nil {
|
2022-10-11 03:24:09 +00:00
|
|
|
if bw != nil {
|
|
|
|
// notify end of stream
|
|
|
|
_, err := bw.WriteString("0\r\n")
|
|
|
|
if err != nil {
|
|
|
|
logger.Warnf("unable to write end of stream: %v", err)
|
|
|
|
}
|
|
|
|
_, err = bw.WriteString("\r\n")
|
|
|
|
if err != nil {
|
|
|
|
logger.Warnf("unable to write end of stream: %v", err)
|
|
|
|
}
|
2022-11-21 23:21:27 +00:00
|
|
|
|
|
|
|
// flush the buffer, but don't wait indefinitely
|
|
|
|
timeout := make(chan struct{}, 1)
|
|
|
|
go func() {
|
|
|
|
_ = bw.Flush()
|
|
|
|
close(timeout)
|
|
|
|
}()
|
|
|
|
|
|
|
|
const waitTime = time.Second
|
|
|
|
|
|
|
|
select {
|
|
|
|
case <-timeout:
|
|
|
|
case <-time.After(waitTime):
|
|
|
|
logger.Warnf("unable to flush buffer - closing connection")
|
|
|
|
}
|
2022-10-11 03:24:09 +00:00
|
|
|
}
|
|
|
|
|
2022-05-03 23:27:22 +00:00
|
|
|
conn.Close()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-29 03:08:32 +00:00
|
|
|
func KillRunningStreams(scene *models.Scene, fileNamingAlgo models.HashAlgorithm) {
|
2022-09-01 07:54:34 +00:00
|
|
|
instance.ReadLockManager.Cancel(scene.Path)
|
2021-11-29 03:08:32 +00:00
|
|
|
|
|
|
|
sceneHash := scene.GetHash(fileNamingAlgo)
|
|
|
|
|
|
|
|
if sceneHash == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
transcodePath := GetInstance().Paths.Scene.GetTranscodePath(sceneHash)
|
2022-04-18 00:50:10 +00:00
|
|
|
instance.ReadLockManager.Cancel(transcodePath)
|
2019-10-17 20:42:12 +00:00
|
|
|
}
|
2021-05-20 06:58:43 +00:00
|
|
|
|
2022-05-19 07:49:32 +00:00
|
|
|
type SceneCoverGetter interface {
|
|
|
|
GetCover(ctx context.Context, sceneID int) ([]byte, error)
|
|
|
|
}
|
|
|
|
|
2021-05-20 06:58:43 +00:00
|
|
|
type SceneServer struct {
|
2022-05-19 07:49:32 +00:00
|
|
|
TxnManager txn.Manager
|
|
|
|
SceneCoverGetter SceneCoverGetter
|
2021-05-20 06:58:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *SceneServer) StreamSceneDirect(scene *models.Scene, w http.ResponseWriter, r *http.Request) {
|
|
|
|
fileNamingAlgo := config.GetInstance().GetVideoFileNamingAlgorithm()
|
|
|
|
|
2022-09-01 07:54:34 +00:00
|
|
|
filepath := GetInstance().Paths.Scene.GetStreamPath(scene.Path, scene.GetHash(fileNamingAlgo))
|
2022-05-03 23:27:22 +00:00
|
|
|
streamRequestCtx := NewStreamRequestContext(w, r)
|
2022-05-12 02:10:46 +00:00
|
|
|
|
|
|
|
// #2579 - hijacking and closing the connection here causes video playback to fail in Safari
|
|
|
|
// We trust that the request context will be closed, so we don't need to call Cancel on the
|
|
|
|
// returned context here.
|
|
|
|
_ = GetInstance().ReadLockManager.ReadLock(streamRequestCtx, filepath)
|
2021-05-20 06:58:43 +00:00
|
|
|
http.ServeFile(w, r, filepath)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *SceneServer) ServeScreenshot(scene *models.Scene, w http.ResponseWriter, r *http.Request) {
|
2022-10-03 02:01:35 +00:00
|
|
|
const defaultSceneImage = "scene/scene.svg"
|
|
|
|
|
2022-11-14 05:35:09 +00:00
|
|
|
if scene.Path != "" {
|
|
|
|
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)
|
|
|
|
return
|
|
|
|
}
|
2022-10-03 02:01:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var cover []byte
|
2022-11-20 19:49:10 +00:00
|
|
|
readTxnErr := txn.WithReadTxn(r.Context(), s.TxnManager, func(ctx context.Context) error {
|
2022-10-03 02:01:35 +00:00
|
|
|
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)
|
2021-05-20 06:58:43 +00:00
|
|
|
}
|
|
|
|
}
|