Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change ffmpeg handling #4688

Merged
merged 7 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading