Skip to content

Commit

Permalink
Change ffmpeg handling (#4688)
Browse files Browse the repository at this point in the history
* Make ffmpeg/ffprobe settable and remove auto download
* Detect when ffmpeg not present in setup
* Add download ffmpeg task
* Add download ffmpeg button in system settings
* Download ffmpeg during setup
  • Loading branch information
WithoutPants authored Mar 21, 2024
1 parent a369613 commit 7086109
Show file tree
Hide file tree
Showing 22 changed files with 694 additions and 297 deletions.
10 changes: 9 additions & 1 deletion cmd/phasher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package main
import (
"fmt"
"os"
"os/exec"

flag "github.com/spf13/pflag"
"github.com/stashapp/stash/pkg/ffmpeg"
Expand Down Expand Up @@ -45,6 +46,13 @@ func printPhash(ff *ffmpeg.FFMpeg, ffp ffmpeg.FFProbe, inputfile string, quiet *
return nil
}

func getPaths() (string, string) {
ffmpegPath, _ := exec.LookPath("ffmpeg")
ffprobePath, _ := exec.LookPath("ffprobe")

return ffmpegPath, ffprobePath
}

func main() {
flag.Usage = customUsage
quiet := flag.BoolP("quiet", "q", false, "print only the phash")
Expand All @@ -69,7 +77,7 @@ func main() {
fmt.Fprintf(os.Stderr, "Example: parallel %v ::: *.mp4\n", os.Args[0])
}

ffmpegPath, ffprobePath := ffmpeg.GetPaths(nil)
ffmpegPath, ffprobePath := getPaths()
encoder := ffmpeg.NewEncoder(ffmpegPath)
// don't need to InitHWSupport, phashing doesn't use hw acceleration
ffprobe := ffmpeg.FFProbe(ffprobePath)
Expand Down
3 changes: 3 additions & 0 deletions graphql/schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,9 @@ type Mutation {
"Migrates the schema to the required version. Returns the job ID"
migrate(input: MigrateInput!): ID!

"Downloads and installs ffmpeg and ffprobe binaries into the configuration directory. Returns the job ID."
downloadFFMpeg: ID!

sceneCreate(input: SceneCreateInput!): Scene
sceneUpdate(input: SceneUpdateInput!): Scene
sceneMerge(input: SceneMergeInput!): Scene
Expand Down
8 changes: 8 additions & 0 deletions graphql/schema/types/config.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ input ConfigGeneralInput {
blobsPath: String
"Where to store blobs"
blobsStorage: BlobsStorageType
"Path to the ffmpeg binary. If empty, stash will attempt to find it in the path or config directory"
ffmpegPath: String
"Path to the ffprobe binary. If empty, stash will attempt to find it in the path or config directory"
ffprobePath: String
"Whether to calculate MD5 checksums for scene video files"
calculateMD5: Boolean
"Hash algorithm to use for generated file naming"
Expand Down Expand Up @@ -199,6 +203,10 @@ type ConfigGeneralResult {
blobsPath: String!
"Where to store blobs"
blobsStorage: BlobsStorageType!
"Path to the ffmpeg binary. If empty, stash will attempt to find it in the path or config directory"
ffmpegPath: String!
"Path to the ffprobe binary. If empty, stash will attempt to find it in the path or config directory"
ffprobePath: String!
"Whether to calculate MD5 checksums for scene video files"
calculateMD5: Boolean!
"Hash algorithm to use for generated file naming"
Expand Down
2 changes: 2 additions & 0 deletions graphql/schema/types/metadata.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,8 @@ type SystemStatus {
os: String!
workingDir: String!
homeDir: String!
ffmpegPath: String
ffprobePath: String
}

input MigrateInput {
Expand Down
61 changes: 60 additions & 1 deletion internal/api/resolver_mutation_configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import (
"fmt"
"path/filepath"
"regexp"
"strconv"

"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/internal/manager/task"
"github.com/stashapp/stash/pkg/ffmpeg"
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
Expand All @@ -22,6 +25,34 @@ func (r *mutationResolver) Setup(ctx context.Context, input manager.SetupInput)
return err == nil, err
}

func (r *mutationResolver) DownloadFFMpeg(ctx context.Context) (string, error) {
mgr := manager.GetInstance()
configDir := mgr.Config.GetConfigPath()

// don't run if ffmpeg is already installed
ffmpegPath := ffmpeg.FindFFMpeg(configDir)
ffprobePath := ffmpeg.FindFFProbe(configDir)
if ffmpegPath != "" && ffprobePath != "" {
return "", fmt.Errorf("ffmpeg and ffprobe already installed at %s and %s", ffmpegPath, ffprobePath)
}

t := &task.DownloadFFmpegJob{
ConfigDirectory: configDir,
OnComplete: func(ctx context.Context) {
// clear the ffmpeg and ffprobe paths
logger.Infof("Clearing ffmpeg and ffprobe config paths so they are resolved from the config directory")
mgr.Config.Set(config.FFMpegPath, "")
mgr.Config.Set(config.FFProbePath, "")
mgr.RefreshFFMpeg(ctx)
mgr.RefreshStreamManager()
},
}

jobID := mgr.JobManager.Add(ctx, "Downloading ffmpeg...", t)

return strconv.Itoa(jobID), nil
}

func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGeneralInput) (*ConfigGeneralResult, error) {
c := config.GetInstance()

Expand Down Expand Up @@ -161,12 +192,34 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
return makeConfigGeneralResult(), fmt.Errorf("blobs path must be set when using filesystem storage")
}

// TODO - migrate between systems
c.Set(config.BlobsStorage, input.BlobsStorage)

refreshBlobStorage = true
}

refreshFfmpeg := false
if input.FfmpegPath != nil && *input.FfmpegPath != c.GetFFMpegPath() {
if *input.FfmpegPath != "" {
if err := ffmpeg.ValidateFFMpeg(*input.FfmpegPath); err != nil {
return makeConfigGeneralResult(), fmt.Errorf("invalid ffmpeg path: %w", err)
}
}

c.Set(config.FFMpegPath, input.FfmpegPath)
refreshFfmpeg = true
}

if input.FfprobePath != nil && *input.FfprobePath != c.GetFFProbePath() {
if *input.FfprobePath != "" {
if err := ffmpeg.ValidateFFProbe(*input.FfprobePath); err != nil {
return makeConfigGeneralResult(), fmt.Errorf("invalid ffprobe path: %w", err)
}
}

c.Set(config.FFProbePath, input.FfprobePath)
refreshFfmpeg = true
}

if input.VideoFileNamingAlgorithm != nil && *input.VideoFileNamingAlgorithm != c.GetVideoFileNamingAlgorithm() {
calculateMD5 := c.IsCalculateMD5()
if input.CalculateMd5 != nil {
Expand Down Expand Up @@ -379,6 +432,12 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
if refreshPluginCache {
manager.GetInstance().RefreshPluginCache()
}
if refreshFfmpeg {
manager.GetInstance().RefreshFFMpeg(ctx)

// refresh stream manager is required since ffmpeg changed
refreshStreamManager = true
}
if refreshStreamManager {
manager.GetInstance().RefreshStreamManager()
}
Expand Down
2 changes: 2 additions & 0 deletions internal/api/resolver_query_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
CachePath: config.GetCachePath(),
BlobsPath: config.GetBlobsPath(),
BlobsStorage: config.GetBlobsStorage(),
FfmpegPath: config.GetFFMpegPath(),
FfprobePath: config.GetFFProbePath(),
CalculateMd5: config.IsCalculateMD5(),
VideoFileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
ParallelTasks: config.GetParallelTasks(),
Expand Down
15 changes: 15 additions & 0 deletions internal/manager/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ const (
Password = "password"
MaxSessionAge = "max_session_age"

FFMpegPath = "ffmpeg_path"
FFProbePath = "ffprobe_path"

BlobsStorage = "blobs_storage"

DefaultMaxSessionAge = 60 * 60 * 1 // 1 hours
Expand Down Expand Up @@ -603,6 +606,18 @@ func (i *Config) GetBackupDirectoryPathOrDefault() string {
return ret
}

// GetFFMpegPath returns the path to the FFMpeg executable.
// If empty, stash will attempt to resolve it from the path.
func (i *Config) GetFFMpegPath() string {
return i.getString(FFMpegPath)
}

// GetFFProbePath returns the path to the FFProbe executable.
// If empty, stash will attempt to resolve it from the path.
func (i *Config) GetFFProbePath() string {
return i.getString(FFProbePath)
}

func (i *Config) GetJWTSignKey() []byte {
return []byte(i.getString(JWTSignKey))
}
Expand Down
69 changes: 37 additions & 32 deletions internal/manager/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,6 @@ func (s *Manager) postInit(ctx context.Context) error {
s.RefreshScraperCache()
s.RefreshScraperSourceManager()

s.RefreshStreamManager()
s.RefreshDLNA()

s.SetBlobStoreOptions()
Expand Down Expand Up @@ -239,9 +238,8 @@ func (s *Manager) postInit(ctx context.Context) error {
logger.Info("Using HTTP proxy")
}

if err := s.initFFmpeg(ctx); err != nil {
return fmt.Errorf("error initializing FFmpeg subsystem: %v", err)
}
s.RefreshFFMpeg(ctx)
s.RefreshStreamManager()

return nil
}
Expand All @@ -260,41 +258,48 @@ func (s *Manager) writeStashIcon() {
}
}

func (s *Manager) initFFmpeg(ctx context.Context) error {
func (s *Manager) RefreshFFMpeg(ctx context.Context) {
// use same directory as config path
configDirectory := s.Config.GetConfigPath()
paths := []string{
configDirectory,
paths.GetStashHomeDirectory(),
}
ffmpegPath, ffprobePath := ffmpeg.GetPaths(paths)

if ffmpegPath == "" || ffprobePath == "" {
logger.Infof("couldn't find FFmpeg, attempting to download it")
if err := ffmpeg.Download(ctx, configDirectory); err != nil {
path, absErr := filepath.Abs(configDirectory)
if absErr != nil {
path = configDirectory
}
msg := `Unable to automatically download FFmpeg
stashHomeDir := paths.GetStashHomeDirectory()

Check the readme for download links.
The ffmpeg and ffprobe binaries should be placed in %s.
// prefer the configured paths
ffmpegPath := s.Config.GetFFMpegPath()
ffprobePath := s.Config.GetFFProbePath()

`
logger.Errorf(msg, path)
return err
} else {
// After download get new paths for ffmpeg and ffprobe
ffmpegPath, ffprobePath = ffmpeg.GetPaths(paths)
// ensure the paths are valid
if ffmpegPath != "" {
if err := ffmpeg.ValidateFFMpeg(ffmpegPath); err != nil {
logger.Errorf("invalid ffmpeg path: %v", err)
return
}
} else {
ffmpegPath = ffmpeg.ResolveFFMpeg(configDirectory, stashHomeDir)
}

if ffprobePath != "" {
if err := ffmpeg.ValidateFFProbe(ffmpegPath); err != nil {
logger.Errorf("invalid ffprobe path: %v", err)
return
}
} else {
ffprobePath = ffmpeg.ResolveFFProbe(configDirectory, stashHomeDir)
}

s.FFMpeg = ffmpeg.NewEncoder(ffmpegPath)
s.FFProbe = ffmpeg.FFProbe(ffprobePath)
if ffmpegPath == "" {
logger.Warn("Couldn't find FFmpeg")
}
if ffprobePath == "" {
logger.Warn("Couldn't find FFProbe")
}

s.FFMpeg.InitHWSupport(ctx)
s.RefreshStreamManager()
if ffmpegPath != "" && ffprobePath != "" {
logger.Debugf("using ffmpeg: %s", ffmpegPath)
logger.Debugf("using ffprobe: %s", ffprobePath)

return nil
s.FFMpeg = ffmpeg.NewEncoder(ffmpegPath)
s.FFProbe = ffmpeg.FFProbe(ffprobePath)

s.FFMpeg.InitHWSupport(ctx)
}
}
12 changes: 12 additions & 0 deletions internal/manager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,16 @@ func (s *Manager) GetSystemStatus() *SystemStatus {

configFile := s.Config.GetConfigFile()

ffmpegPath := ""
if s.FFMpeg != nil {
ffmpegPath = s.FFMpeg.Path()
}

ffprobePath := ""
if s.FFProbe != "" {
ffprobePath = s.FFProbe.Path()
}

return &SystemStatus{
Os: runtime.GOOS,
WorkingDir: workingDir,
Expand All @@ -400,6 +410,8 @@ func (s *Manager) GetSystemStatus() *SystemStatus {
AppSchema: appSchema,
Status: status,
ConfigPath: &configFile,
FfmpegPath: &ffmpegPath,
FfprobePath: &ffprobePath,
}
}

Expand Down
2 changes: 2 additions & 0 deletions internal/manager/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ type SystemStatus struct {
Os string `json:"os"`
WorkingDir string `json:"working_dir"`
HomeDir string `json:"home_dir"`
FfmpegPath *string `json:"ffmpegPath"`
FfprobePath *string `json:"ffprobePath"`
}

type SetupInput struct {
Expand Down
Loading

0 comments on commit 7086109

Please sign in to comment.