mirror of
https://github.com/stashapp/stash.git
synced 2025-12-09 09:53:40 +01:00
* Remove stuff which isn't being used Some fields, functions and structs aren't in use by the project. Remove them for janitorial reasons. * Remove more unused code All of these functions are currently not in use. Clean up the code by removal, since the version control has the code if need be. * Remove unused functions There's a large set of unused functions and variables in the code base. Remove these, so it clearer what code to support going forward. Dead code has been eliminated. Where applicable, comment const-sections in tests, so reserved identifiers are still known. * Fix use-def of tsURL The first def of tsURL doesn't matter because there's no use before we hit the 2nd def. * Remove dead code assignment Setting logFile = "" is effectively dead code, because there's no use of it later. * Comment out found The variable 'found' is dead in the function (because no post-process action is following it). Comment it for now. * Comment dead code in tests These might provide hints as to what isn't covered at the moment. * Dead code removal In the case of constants where iota is involved, move the iota so it matches the current key values. This avoids problems with persistently stored key IDs.
354 lines
11 KiB
Go
354 lines
11 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/go-chi/chi"
|
|
"github.com/stashapp/stash/pkg/ffmpeg"
|
|
"github.com/stashapp/stash/pkg/logger"
|
|
"github.com/stashapp/stash/pkg/manager"
|
|
"github.com/stashapp/stash/pkg/manager/config"
|
|
"github.com/stashapp/stash/pkg/models"
|
|
"github.com/stashapp/stash/pkg/utils"
|
|
)
|
|
|
|
type sceneRoutes struct {
|
|
txnManager models.TransactionManager
|
|
}
|
|
|
|
func (rs sceneRoutes) Routes() chi.Router {
|
|
r := chi.NewRouter()
|
|
|
|
r.Route("/{sceneId}", func(r chi.Router) {
|
|
r.Use(SceneCtx)
|
|
|
|
// streaming endpoints
|
|
r.Get("/stream", rs.StreamDirect)
|
|
r.Get("/stream.mkv", rs.StreamMKV)
|
|
r.Get("/stream.webm", rs.StreamWebM)
|
|
r.Get("/stream.m3u8", rs.StreamHLS)
|
|
r.Get("/stream.ts", rs.StreamTS)
|
|
r.Get("/stream.mp4", rs.StreamMp4)
|
|
|
|
r.Get("/screenshot", rs.Screenshot)
|
|
r.Get("/preview", rs.Preview)
|
|
r.Get("/webp", rs.Webp)
|
|
r.Get("/vtt/chapter", rs.ChapterVtt)
|
|
r.Get("/funscript", rs.Funscript)
|
|
|
|
r.Get("/scene_marker/{sceneMarkerId}/stream", rs.SceneMarkerStream)
|
|
r.Get("/scene_marker/{sceneMarkerId}/preview", rs.SceneMarkerPreview)
|
|
})
|
|
r.With(SceneCtx).Get("/{sceneId}_thumbs.vtt", rs.VttThumbs)
|
|
r.With(SceneCtx).Get("/{sceneId}_sprite.jpg", rs.VttSprite)
|
|
|
|
return r
|
|
}
|
|
|
|
// region Handlers
|
|
|
|
func getSceneFileContainer(scene *models.Scene) ffmpeg.Container {
|
|
var container ffmpeg.Container
|
|
if scene.Format.Valid {
|
|
container = ffmpeg.Container(scene.Format.String)
|
|
} else { // container isn't in the DB
|
|
// shouldn't happen, fallback to ffprobe
|
|
tmpVideoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path, false)
|
|
if err != nil {
|
|
logger.Errorf("[transcode] error reading video file: %s", err.Error())
|
|
return ffmpeg.Container("")
|
|
}
|
|
|
|
container = ffmpeg.MatchContainer(tmpVideoFile.Container, scene.Path)
|
|
}
|
|
|
|
return container
|
|
}
|
|
|
|
func (rs sceneRoutes) StreamDirect(w http.ResponseWriter, r *http.Request) {
|
|
scene := r.Context().Value(sceneKey).(*models.Scene)
|
|
|
|
ss := manager.SceneServer{
|
|
TXNManager: rs.txnManager,
|
|
}
|
|
ss.StreamSceneDirect(scene, w, r)
|
|
}
|
|
|
|
func (rs sceneRoutes) StreamMKV(w http.ResponseWriter, r *http.Request) {
|
|
// only allow mkv streaming if the scene container is an mkv already
|
|
scene := r.Context().Value(sceneKey).(*models.Scene)
|
|
|
|
container := getSceneFileContainer(scene)
|
|
if container != ffmpeg.Matroska {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte("not an mkv file"))
|
|
return
|
|
}
|
|
|
|
rs.streamTranscode(w, r, ffmpeg.CodecMKVAudio)
|
|
}
|
|
|
|
func (rs sceneRoutes) StreamWebM(w http.ResponseWriter, r *http.Request) {
|
|
rs.streamTranscode(w, r, ffmpeg.CodecVP9)
|
|
}
|
|
|
|
func (rs sceneRoutes) StreamMp4(w http.ResponseWriter, r *http.Request) {
|
|
rs.streamTranscode(w, r, ffmpeg.CodecH264)
|
|
}
|
|
|
|
func (rs sceneRoutes) StreamHLS(w http.ResponseWriter, r *http.Request) {
|
|
scene := r.Context().Value(sceneKey).(*models.Scene)
|
|
|
|
videoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path, false)
|
|
if err != nil {
|
|
logger.Errorf("[stream] error reading video file: %s", err.Error())
|
|
return
|
|
}
|
|
|
|
logger.Debug("Returning HLS playlist")
|
|
|
|
// getting the playlist manifest only
|
|
w.Header().Set("Content-Type", ffmpeg.MimeHLS)
|
|
var str strings.Builder
|
|
|
|
ffmpeg.WriteHLSPlaylist(*videoFile, r.URL.String(), &str)
|
|
|
|
requestByteRange := utils.CreateByteRange(r.Header.Get("Range"))
|
|
if requestByteRange.RawString != "" {
|
|
logger.Debugf("Requested range: %s", requestByteRange.RawString)
|
|
}
|
|
|
|
ret := requestByteRange.Apply([]byte(str.String()))
|
|
rangeStr := requestByteRange.ToHeaderValue(int64(str.Len()))
|
|
w.Header().Set("Content-Range", rangeStr)
|
|
|
|
w.Write(ret)
|
|
}
|
|
|
|
func (rs sceneRoutes) StreamTS(w http.ResponseWriter, r *http.Request) {
|
|
rs.streamTranscode(w, r, ffmpeg.CodecHLS)
|
|
}
|
|
|
|
func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, videoCodec ffmpeg.Codec) {
|
|
logger.Debugf("Streaming as %s", videoCodec.MimeType)
|
|
scene := r.Context().Value(sceneKey).(*models.Scene)
|
|
|
|
// needs to be transcoded
|
|
|
|
videoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path, false)
|
|
if err != nil {
|
|
logger.Errorf("[stream] error reading video file: %s", err.Error())
|
|
return
|
|
}
|
|
|
|
// start stream based on query param, if provided
|
|
r.ParseForm()
|
|
startTime := r.Form.Get("start")
|
|
requestedSize := r.Form.Get("resolution")
|
|
|
|
var stream *ffmpeg.Stream
|
|
|
|
audioCodec := ffmpeg.MissingUnsupported
|
|
if scene.AudioCodec.Valid {
|
|
audioCodec = ffmpeg.AudioCodec(scene.AudioCodec.String)
|
|
}
|
|
|
|
options := ffmpeg.GetTranscodeStreamOptions(*videoFile, videoCodec, audioCodec)
|
|
options.StartTime = startTime
|
|
options.MaxTranscodeSize = config.GetInstance().GetMaxStreamingTranscodeSize()
|
|
if requestedSize != "" {
|
|
options.MaxTranscodeSize = models.StreamingResolutionEnum(requestedSize)
|
|
}
|
|
|
|
encoder := ffmpeg.NewEncoder(manager.GetInstance().FFMPEGPath)
|
|
stream, err = encoder.GetTranscodeStream(options)
|
|
|
|
if err != nil {
|
|
logger.Errorf("[stream] error transcoding video file: %s", err.Error())
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(err.Error()))
|
|
return
|
|
}
|
|
|
|
stream.Serve(w, r)
|
|
}
|
|
|
|
func (rs sceneRoutes) Screenshot(w http.ResponseWriter, r *http.Request) {
|
|
scene := r.Context().Value(sceneKey).(*models.Scene)
|
|
|
|
ss := manager.SceneServer{
|
|
TXNManager: rs.txnManager,
|
|
}
|
|
ss.ServeScreenshot(scene, w, r)
|
|
}
|
|
|
|
func (rs sceneRoutes) Preview(w http.ResponseWriter, r *http.Request) {
|
|
scene := r.Context().Value(sceneKey).(*models.Scene)
|
|
filepath := manager.GetInstance().Paths.Scene.GetStreamPreviewPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
|
|
utils.ServeFileNoCache(w, r, filepath)
|
|
}
|
|
|
|
func (rs sceneRoutes) Webp(w http.ResponseWriter, r *http.Request) {
|
|
scene := r.Context().Value(sceneKey).(*models.Scene)
|
|
filepath := manager.GetInstance().Paths.Scene.GetStreamPreviewImagePath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
|
|
http.ServeFile(w, r, filepath)
|
|
}
|
|
|
|
func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.SceneMarker) string {
|
|
if marker.Title != "" {
|
|
return marker.Title
|
|
}
|
|
|
|
var ret string
|
|
if err := rs.txnManager.WithReadTxn(ctx, func(repo models.ReaderRepository) error {
|
|
qb := repo.Tag()
|
|
primaryTag, err := qb.Find(marker.PrimaryTagID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ret = primaryTag.Name
|
|
|
|
tags, err := qb.FindBySceneMarkerID(marker.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, t := range tags {
|
|
ret += ", " + t.Name
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func (rs sceneRoutes) ChapterVtt(w http.ResponseWriter, r *http.Request) {
|
|
scene := r.Context().Value(sceneKey).(*models.Scene)
|
|
var sceneMarkers []*models.SceneMarker
|
|
if err := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
|
var err error
|
|
sceneMarkers, err = repo.SceneMarker().FindBySceneID(scene.ID)
|
|
return err
|
|
}); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
vttLines := []string{"WEBVTT", ""}
|
|
for i, marker := range sceneMarkers {
|
|
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))
|
|
vttLines = append(vttLines, "")
|
|
}
|
|
vtt := strings.Join(vttLines, "\n")
|
|
|
|
w.Header().Set("Content-Type", "text/vtt")
|
|
_, _ = w.Write([]byte(vtt))
|
|
}
|
|
|
|
func (rs sceneRoutes) Funscript(w http.ResponseWriter, r *http.Request) {
|
|
scene := r.Context().Value(sceneKey).(*models.Scene)
|
|
funscript := utils.GetFunscriptPath(scene.Path)
|
|
utils.ServeFileNoCache(w, r, funscript)
|
|
}
|
|
|
|
func (rs sceneRoutes) VttThumbs(w http.ResponseWriter, r *http.Request) {
|
|
scene := r.Context().Value(sceneKey).(*models.Scene)
|
|
w.Header().Set("Content-Type", "text/vtt")
|
|
filepath := manager.GetInstance().Paths.Scene.GetSpriteVttFilePath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
|
|
http.ServeFile(w, r, filepath)
|
|
}
|
|
|
|
func (rs sceneRoutes) VttSprite(w http.ResponseWriter, r *http.Request) {
|
|
scene := r.Context().Value(sceneKey).(*models.Scene)
|
|
w.Header().Set("Content-Type", "image/jpeg")
|
|
filepath := manager.GetInstance().Paths.Scene.GetSpriteImageFilePath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
|
|
http.ServeFile(w, r, filepath)
|
|
}
|
|
|
|
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 := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
|
var err error
|
|
sceneMarker, err = repo.SceneMarker().Find(sceneMarkerID)
|
|
return err
|
|
}); err != nil {
|
|
logger.Warnf("Error when getting scene marker for stream: %s", err.Error())
|
|
http.Error(w, http.StatusText(500), 500)
|
|
return
|
|
}
|
|
filepath := manager.GetInstance().Paths.SceneMarkers.GetStreamPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds))
|
|
http.ServeFile(w, r, filepath)
|
|
}
|
|
|
|
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 := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
|
var err error
|
|
sceneMarker, err = repo.SceneMarker().Find(sceneMarkerID)
|
|
return err
|
|
}); err != nil {
|
|
logger.Warnf("Error when getting scene marker for stream: %s", err.Error())
|
|
http.Error(w, http.StatusText(500), 500)
|
|
return
|
|
}
|
|
filepath := manager.GetInstance().Paths.SceneMarkers.GetStreamPreviewImagePath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds))
|
|
|
|
// If the image doesn't exist, send the placeholder
|
|
exists, _ := utils.FileExists(filepath)
|
|
if !exists {
|
|
w.Header().Set("Content-Type", "image/png")
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
_, _ = w.Write(utils.PendingGenerateResource)
|
|
return
|
|
}
|
|
|
|
http.ServeFile(w, r, filepath)
|
|
}
|
|
|
|
// endregion
|
|
|
|
func SceneCtx(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
sceneIdentifierQueryParam := chi.URLParam(r, "sceneId")
|
|
sceneID, _ := strconv.Atoi(sceneIdentifierQueryParam)
|
|
|
|
var scene *models.Scene
|
|
manager.GetInstance().TxnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
|
qb := repo.Scene()
|
|
if sceneID == 0 {
|
|
// determine checksum/os by the length of the query param
|
|
if len(sceneIdentifierQueryParam) == 32 {
|
|
scene, _ = qb.FindByChecksum(sceneIdentifierQueryParam)
|
|
} else {
|
|
scene, _ = qb.FindByOSHash(sceneIdentifierQueryParam)
|
|
}
|
|
} else {
|
|
scene, _ = qb.Find(sceneID)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if scene == nil {
|
|
http.Error(w, http.StatusText(404), 404)
|
|
return
|
|
}
|
|
|
|
ctx := context.WithValue(r.Context(), sceneKey, scene)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|