From fc1c1366dcfb18c6ac885fe271d04ec6fede8c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Thu, 20 Aug 2026 09:04:09 -0400 Subject: [PATCH] fix(cli): write `pls -p` playlist output to stdout (#5996) The export path used the `println` builtin, which writes to stderr, so `navidrome pls -p X > playlist.m3u8` produced an empty file while the M3U body was interleaved with the startup logs on stderr. `println` also appended a newline that `ToM3U8` already provides, so the piped output had a stray trailing blank line that `-o file` did not. Both destinations are now byte-identical. The stdout/file choice moved into a `writePlaylist` helper shared by `pls -p` and `pls export -p`, which both had the same bug. It takes the destination as an `io.Writer`, matching the existing convention in cmd/artwork.go. --- cmd/pls.go | 17 ++++++++++------- cmd/pls_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 cmd/pls_test.go diff --git a/cmd/pls.go b/cmd/pls.go index 184ca6fe7..93b411483 100644 --- a/cmd/pls.go +++ b/cmd/pls.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "os" "path/filepath" "strconv" @@ -141,14 +142,16 @@ func findPlaylist(ctx context.Context, ds model.DataStore, nameOrID string) *mod func runExporter(ctx context.Context) { ds, ctx := getAdminContext(ctx) playlist := findPlaylist(ctx, ds, playlistID) - pls := playlist.ToM3U8() - if outputFile == "-" || outputFile == "" { - println(pls) + writePlaylist(playlist.ToM3U8(), os.Stdout, outputFile) +} + +func writePlaylist(m3u string, out io.Writer, file string) { + if file == "" || file == "-" { + fmt.Fprint(out, m3u) return } - err := os.WriteFile(outputFile, []byte(pls), 0600) - if err != nil { - log.Fatal("Error writing to the output file", "file", outputFile, err) + if err := os.WriteFile(file, []byte(m3u), 0600); err != nil { + log.Fatal("Error writing to the output file", "file", file, err) } } @@ -157,7 +160,7 @@ func runExport(ctx context.Context) { if playlistID != "" && outputFile == "" { playlist := findPlaylist(ctx, ds, playlistID) - println(playlist.ToM3U8()) + writePlaylist(playlist.ToM3U8(), os.Stdout, outputFile) return } diff --git a/cmd/pls_test.go b/cmd/pls_test.go new file mode 100644 index 000000000..f3e8c7edd --- /dev/null +++ b/cmd/pls_test.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("writePlaylist", func() { + const m3u = "#EXTM3U\n#PLAYLIST:DJ Wave\n#EXTINF:364,Bel Canto - Dreaming Girl\n" + plsFile := filepath.Join(os.TempDir(), fmt.Sprintf("navidrome-pls-%d.m3u8", os.Getpid())) + + BeforeEach(func() { + DeferCleanup(func() { _ = os.Remove(plsFile) }) + }) + + DescribeTable("writes the playlist to exactly one destination", + func(file, wantStream, wantFile string) { + var out strings.Builder + + writePlaylist(m3u, &out, file) + + written, _ := os.ReadFile(plsFile) + Expect(out.String()).To(Equal(wantStream)) + Expect(string(written)).To(Equal(wantFile)) + }, + Entry("no file name writes to the stream", "", m3u, ""), + Entry("a dash writes to the stream", "-", m3u, ""), + Entry("a path writes to the file", plsFile, "", m3u), + ) +})