diff --git a/cmd/missing.go b/cmd/missing.go index 0ff4db8d0..9e5f96f40 100644 --- a/cmd/missing.go +++ b/cmd/missing.go @@ -1,6 +1,7 @@ package cmd import ( + "bufio" "context" "encoding/csv" "encoding/json" @@ -84,18 +85,19 @@ func runMissingList(ctx context.Context) { // writeMissingList streams the cursor so a library with many missing files doesn't get loaded into memory func writeMissingList(w io.Writer, format string, mfs model.MediaFileCursor) error { if format == "json" { - _, _ = io.WriteString(w, "[") + bw := bufio.NewWriter(w) + _, _ = io.WriteString(bw, "[") sep := "" for mf, err := range mfs { if err != nil { return err } j, _ := json.Marshal(displayMissingFile{ID: mf.ID, LibraryID: mf.LibraryID, Title: mf.Title, Album: mf.Album, Artist: mf.Artist, Path: mf.Path}) - _, _ = fmt.Fprintf(w, "%s%s", sep, j) + _, _ = fmt.Fprintf(bw, "%s%s", sep, j) sep = "," } - _, err := io.WriteString(w, "]\n") - return err + _, _ = io.WriteString(bw, "]\n") + return bw.Flush() } cw := csv.NewWriter(w) diff --git a/persistence/mediafile_repository.go b/persistence/mediafile_repository.go index e34e8601f..2fcbaee90 100644 --- a/persistence/mediafile_repository.go +++ b/persistence/mediafile_repository.go @@ -362,11 +362,11 @@ func (r *mediaFileRepository) FindByPaths(paths []string) (model.MediaFiles, err var unqualified []string for _, path := range paths { - // A colon only qualifies the path when what precedes it is a library id ("Bach: Goldberg/01.mp3" is a plain path) + // A numeric prefix is ambiguous: "1:foo.mp3" qualifies a library, but "1999: A Life/01.mp3" + // is a plain path. Search both ways rather than guessing. if id, rest, ok := strings.Cut(path, ":"); ok { if libraryID, err := strconv.Atoi(id); err == nil { byLibrary[libraryID] = append(byLibrary[libraryID], rest) - continue } } unqualified = append(unqualified, path) diff --git a/persistence/mediafile_repository_test.go b/persistence/mediafile_repository_test.go index 020488bc8..29346c732 100644 --- a/persistence/mediafile_repository_test.go +++ b/persistence/mediafile_repository_test.go @@ -993,6 +993,7 @@ var _ = Describe("MediaRepository", func() { // French diacritic: è (U+00E8, can decompose to e + combining grave) {ID: "findpath-4", LibraryID: 1, Path: "artist/Michèle/song.mp3", Title: "French"}, {ID: "findpath-5", LibraryID: 1, Path: "Bach: Goldberg Variations/01.mp3", Title: "Colon"}, + {ID: "findpath-6", LibraryID: 1, Path: "1999: A Different Life/01.mp3", Title: "Numeric colon"}, } for _, mf := range testFiles { Expect(mr.Put(&mf)).To(Succeed()) @@ -1012,6 +1013,13 @@ var _ = Describe("MediaRepository", func() { Expect(results[0].ID).To(Equal("findpath-5")) }) + It("finds a plain path whose colon prefix looks like a library id", func() { + results, err := mr.FindByPaths([]string{"1999: A Different Life/01.mp3"}) + Expect(err).ToNot(HaveOccurred()) + Expect(results).To(HaveLen(1)) + Expect(results[0].ID).To(Equal("findpath-6")) + }) + It("splits only the first colon of a library-qualified path", func() { results, err := mr.FindByPaths([]string{"1:Bach: Goldberg Variations/01.mp3"}) Expect(err).ToNot(HaveOccurred())