diff --git a/core/ffmpeg/ffmpeg.go b/core/ffmpeg/ffmpeg.go index 33d6733c8..c034ca7d0 100644 --- a/core/ffmpeg/ffmpeg.go +++ b/core/ffmpeg/ffmpeg.go @@ -49,6 +49,7 @@ type FFmpeg interface { ProbeAudioStream(ctx context.Context, filePath string) (*AudioProbeResult, error) CmdPath() (string, error) IsAvailable() bool + IsProbeAvailable() bool Version() string } @@ -224,6 +225,19 @@ func (e *ffmpeg) IsAvailable() bool { return err == nil } +func (e *ffmpeg) IsProbeAvailable() bool { + if _, err := ffmpegCmd(); err != nil { + return false + } + probeOnce.Do(func() { + probePath := ffprobePath(ffmpegPath) + if _, err := exec.LookPath(probePath); err == nil { + probeAvail = true + } + }) + return probeAvail +} + // Version executes ffmpeg -version and extracts the version from the output. // Sample output: ffmpeg version 6.0 Copyright (c) 2000-2023 the FFmpeg developers func (e *ffmpeg) Version() string { @@ -533,4 +547,6 @@ var ( ffOnce sync.Once ffmpegPath string ffmpegErr error + probeOnce sync.Once + probeAvail bool ) diff --git a/core/stream/decider.go b/core/stream/decider.go index 6c1f06a06..713c779fe 100644 --- a/core/stream/decider.go +++ b/core/stream/decider.go @@ -44,10 +44,14 @@ func (s *deciderService) MakeDecision(ctx context.Context, mf *model.MediaFile, var probe *ffmpeg.AudioProbeResult if !opts.SkipProbe { - var err error - probe, err = s.ensureProbed(ctx, mf) - if err != nil { - return nil, err + if !s.ff.IsProbeAvailable() { + log.Debug(ctx, "ffprobe not available, using tag metadata for transcode decision", "mediaID", mf.ID) + } else { + var err error + probe, err = s.ensureProbed(ctx, mf) + if err != nil { + return nil, err + } } } diff --git a/core/stream/decider_test.go b/core/stream/decider_test.go index 9eaa00990..c776cbdc3 100644 --- a/core/stream/decider_test.go +++ b/core/stream/decider_test.go @@ -1164,6 +1164,7 @@ var _ = Describe("Decider", func() { Expect(bitrate).To(Equal(fallbackBitrate)) }) }) + }) Describe("ensureProbed", func() { diff --git a/release/wix/build_msi.sh b/release/wix/build_msi.sh index 7e595311e..a8781a965 100755 --- a/release/wix/build_msi.sh +++ b/release/wix/build_msi.sh @@ -43,8 +43,9 @@ FFMPEG_FILE="ffmpeg-n${FFMPEG_VERSION}-latest-${WIN_ARCH}-gpl-${FFMPEG_VERSION}" wget --quiet --output-document="${DOWNLOAD_FOLDER}/ffmpeg.zip" \ "https://github.com/${FFMPEG_REPOSITORY}/releases/download/latest/${FFMPEG_FILE}.zip" rm -rf "${DOWNLOAD_FOLDER}/extracted_ffmpeg" -unzip -d "${DOWNLOAD_FOLDER}/extracted_ffmpeg" "${DOWNLOAD_FOLDER}/ffmpeg.zip" "*/ffmpeg.exe" +unzip -d "${DOWNLOAD_FOLDER}/extracted_ffmpeg" "${DOWNLOAD_FOLDER}/ffmpeg.zip" "*/ffmpeg.exe" "*/ffprobe.exe" cp "${DOWNLOAD_FOLDER}"/extracted_ffmpeg/${FFMPEG_FILE}/bin/ffmpeg.exe "$MSI_OUTPUT_DIR" +cp "${DOWNLOAD_FOLDER}"/extracted_ffmpeg/${FFMPEG_FILE}/bin/ffprobe.exe "$MSI_OUTPUT_DIR" cp "$WORKSPACE"/LICENSE "$WORKSPACE"/README.md "$MSI_OUTPUT_DIR" cp "$BINARY" "$MSI_OUTPUT_DIR" diff --git a/release/wix/navidrome.wxs b/release/wix/navidrome.wxs index 8ebba4632..6d94bab9d 100644 --- a/release/wix/navidrome.wxs +++ b/release/wix/navidrome.wxs @@ -67,6 +67,10 @@ + + + + @@ -87,6 +91,7 @@ + diff --git a/server/e2e/e2e_suite_test.go b/server/e2e/e2e_suite_test.go index 262a5ed36..03fa9bbef 100644 --- a/server/e2e/e2e_suite_test.go +++ b/server/e2e/e2e_suite_test.go @@ -337,6 +337,7 @@ func (n noopFFmpeg) ConvertAnimatedImage(context.Context, io.Reader, int, int) ( func (n noopFFmpeg) CmdPath() (string, error) { return "", nil } func (n noopFFmpeg) IsAvailable() bool { return false } +func (n noopFFmpeg) IsProbeAvailable() bool { return true } func (n noopFFmpeg) Version() string { return "noop" } // noopArchiver implements core.Archiver diff --git a/server/initial_setup.go b/server/initial_setup.go index d50f25958..7e974dc21 100644 --- a/server/initial_setup.go +++ b/server/initial_setup.go @@ -68,13 +68,16 @@ func createInitialAdminUser(ds model.DataStore, initialPassword string) error { func checkFFmpegInstallation() { f := ffmpeg.New() _, err := f.CmdPath() - if err == nil { + if err != nil { + log.Warn("Unable to find ffmpeg. Transcoding will fail if used", err) + if conf.Server.Scanner.Extractor == "ffmpeg" { + log.Warn("ffmpeg cannot be used for metadata extraction. Falling back to taglib") + conf.Server.Scanner.Extractor = "taglib" + } return } - log.Warn("Unable to find ffmpeg. Transcoding will fail if used", err) - if conf.Server.Scanner.Extractor == "ffmpeg" { - log.Warn("ffmpeg cannot be used for metadata extraction. Falling back to taglib") - conf.Server.Scanner.Extractor = "taglib" + if !f.IsProbeAvailable() { + log.Warn("Unable to find ffprobe. Transcoding decisions will be limited") } } diff --git a/tests/mock_ffmpeg.go b/tests/mock_ffmpeg.go index 346209b71..f9862767e 100644 --- a/tests/mock_ffmpeg.go +++ b/tests/mock_ffmpeg.go @@ -12,7 +12,7 @@ import ( ) func NewMockFFmpeg(data string) *MockFFmpeg { - return &MockFFmpeg{Reader: strings.NewReader(data)} + return &MockFFmpeg{Reader: strings.NewReader(data), ProbeAvailable: true} } type MockFFmpeg struct { @@ -21,12 +21,17 @@ type MockFFmpeg struct { closed atomic.Bool Error error ProbeAudioResult *ffmpeg.AudioProbeResult + ProbeAvailable bool } func (ff *MockFFmpeg) IsAvailable() bool { return true } +func (ff *MockFFmpeg) IsProbeAvailable() bool { + return ff.ProbeAvailable +} + func (ff *MockFFmpeg) Transcode(_ context.Context, _ ffmpeg.TranscodeOptions) (io.ReadCloser, error) { if ff.Error != nil { return nil, ff.Error