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