diff --git a/conf/configuration.go b/conf/configuration.go index 6f295ebeb..0173f5628 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -514,6 +514,11 @@ func Load(noConfigDump bool) { Server.UICoverArtSize = newValue } + if Server.Scanner.Extractor != consts.DefaultScannerExtractor { + log.Warn("Invalid Scanner.Extractor, using default", "value", Server.Scanner.Extractor, "default", consts.DefaultScannerExtractor) + Server.Scanner.Extractor = consts.DefaultScannerExtractor + } + // Floor MaxImageSize at MaxImageUploadSize so accepted uploads can always be read back. imgSize, _ := humanize.ParseBytes(Server.MaxImageSize) uploadSize, _ := humanize.ParseBytes(Server.MaxImageUploadSize) diff --git a/conf/configuration_test.go b/conf/configuration_test.go index 9404f01bc..8c4c8ab86 100644 --- a/conf/configuration_test.go +++ b/conf/configuration_test.go @@ -402,6 +402,27 @@ var _ = Describe("Configuration", func() { }) }) + Describe("Scanner.Extractor", func() { + BeforeEach(func() { + viper.Reset() + conf.SetViperDefaults() + viper.SetDefault("datafolder", GinkgoT().TempDir()) + viper.SetDefault("loglevel", "error") + conf.ResetConf() + }) + + It("falls back to taglib for an unknown extractor", func() { + viper.SetDefault("scanner.extractor", "ffmpeg") + conf.Load(true) + Expect(conf.Server.Scanner.Extractor).To(Equal("taglib")) + }) + + It("keeps taglib", func() { + conf.Load(true) + Expect(conf.Server.Scanner.Extractor).To(Equal("taglib")) + }) + }) + Describe("EnforceNonRootUser", func() { It("defaults to false", func() { conf.Load(true) diff --git a/core/ffmpeg/ffmpeg.go b/core/ffmpeg/ffmpeg.go index cc38dd9de..af59178af 100644 --- a/core/ffmpeg/ffmpeg.go +++ b/core/ffmpeg/ffmpeg.go @@ -49,7 +49,6 @@ type FFmpeg interface { Transcode(ctx context.Context, opts TranscodeOptions) (io.ReadCloser, error) ExtractImage(ctx context.Context, path string) (io.ReadCloser, error) ConvertAnimatedImage(ctx context.Context, reader io.Reader, maxSize int, quality int) (io.ReadCloser, error) - Probe(ctx context.Context, files []string) (string, error) ProbeAudioStream(ctx context.Context, filePath string) (*AudioProbeResult, error) CmdPath() (string, error) IsAvailable() bool @@ -68,7 +67,6 @@ var ErrAnimatedWebPUnsupported = errors.New("ffmpeg lacks libwebp_anim encoder const ( extractImageCmd = "ffmpeg -i %s -map 0:v -map -0:V -vcodec copy -f image2pipe -" - probeCmd = "ffmpeg %s -f ffmetadata" probeAudioStreamCmd = "ffprobe -v error -select_streams a:0 -print_format json -show_streams -show_format %s" ) @@ -149,17 +147,6 @@ func fileExists(path string) error { return nil } -func (e *ffmpeg) Probe(ctx context.Context, files []string) (string, error) { - if _, err := ffmpegCmd(); err != nil { - return "", err - } - args := createProbeCommand(probeCmd, files) - log.Trace(ctx, "Executing ffmpeg command", "args", args) - cmd := exec.CommandContext(ctx, args[0], args[1:]...) // #nosec - output, _ := cmd.CombinedOutput() - return string(output), nil -} - func (e *ffmpeg) ProbeAudioStream(ctx context.Context, filePath string) (*AudioProbeResult, error) { if _, err := ffmpegCmd(); err != nil { return nil, err @@ -593,20 +580,6 @@ func createFFmpegCommand(cmd, path string, maxBitRate, offset int) []string { return args } -func createProbeCommand(cmd string, inputs []string) []string { - var args []string - for _, s := range fixCmd(cmd) { - if s == "%s" { - for _, inp := range inputs { - args = append(args, "-i", inp) - } - } else { - args = append(args, s) - } - } - return args -} - func fixCmd(cmd string) []string { split := strings.Fields(cmd) cmdPath, _ := ffmpegCmd() diff --git a/core/ffmpeg/ffmpeg_test.go b/core/ffmpeg/ffmpeg_test.go index dbc8fa3c8..46684fe14 100644 --- a/core/ffmpeg/ffmpeg_test.go +++ b/core/ffmpeg/ffmpeg_test.go @@ -62,23 +62,16 @@ var _ = Describe("ffmpeg", func() { }) }) - Describe("createProbeCommand", func() { - It("creates a valid command line", func() { - args := createProbeCommand(probeCmd, []string{"/music library/one.mp3", "/music library/two.mp3"}) - Expect(args).To(Equal([]string{"ffmpeg", "-i", "/music library/one.mp3", "-i", "/music library/two.mp3", "-f", "ffmetadata"})) - }) - }) - When("ffmpegPath is set", func() { It("returns the correct ffmpeg path", func() { ffmpegPath = "/usr/bin/ffmpeg" - args := createProbeCommand(probeCmd, []string{"one.mp3"}) - Expect(args).To(Equal([]string{"/usr/bin/ffmpeg", "-i", "one.mp3", "-f", "ffmetadata"})) + args := createFFmpegCommand("ffmpeg -i %s -f mp3 -", "one.mp3", 0, 0) + Expect(args).To(Equal([]string{"/usr/bin/ffmpeg", "-i", "one.mp3", "-f", "mp3", "-"})) }) It("returns the correct ffmpeg path with spaces", func() { ffmpegPath = "/usr/bin/with spaces/ffmpeg.exe" - args := createProbeCommand(probeCmd, []string{"one.mp3"}) - Expect(args).To(Equal([]string{"/usr/bin/with spaces/ffmpeg.exe", "-i", "one.mp3", "-f", "ffmetadata"})) + args := createFFmpegCommand("ffmpeg -i %s -f mp3 -", "one.mp3", 0, 0) + Expect(args).To(Equal([]string{"/usr/bin/with spaces/ffmpeg.exe", "-i", "one.mp3", "-f", "mp3", "-"})) }) }) diff --git a/scanner/metadata_old/ffmpeg/ffmpeg.go b/scanner/metadata_old/ffmpeg/ffmpeg.go deleted file mode 100644 index 8fc496c02..000000000 --- a/scanner/metadata_old/ffmpeg/ffmpeg.go +++ /dev/null @@ -1,211 +0,0 @@ -package ffmpeg - -import ( - "bufio" - "context" - "errors" - "regexp" - "strconv" - "strings" - "time" - - "github.com/navidrome/navidrome/core/ffmpeg" - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/scanner/metadata_old" -) - -const ExtractorID = "ffmpeg" - -type Extractor struct { - ffmpeg ffmpeg.FFmpeg -} - -func (e *Extractor) Parse(files ...string) (map[string]metadata_old.ParsedTags, error) { - output, err := e.ffmpeg.Probe(context.TODO(), files) - if err != nil { - log.Error("Cannot use ffmpeg to extract tags. Aborting", err) - return nil, err - } - fileTags := map[string]metadata_old.ParsedTags{} - if len(output) == 0 { - return fileTags, errors.New("error extracting metadata files") - } - infos := e.parseOutput(output) - for file, info := range infos { - tags, err := e.extractMetadata(file, info) - // Skip files with errors - if err == nil { - fileTags[file] = tags - } - } - return fileTags, nil -} - -func (e *Extractor) CustomMappings() metadata_old.ParsedTags { - return metadata_old.ParsedTags{ - "disc": {"tpa"}, - "has_picture": {"metadata_block_picture"}, - "originaldate": {"tdor"}, - } -} - -func (e *Extractor) Version() string { - return e.ffmpeg.Version() -} - -func (e *Extractor) extractMetadata(filePath, info string) (metadata_old.ParsedTags, error) { - tags := e.parseInfo(info) - if len(tags) == 0 { - log.Trace("Not a media file. Skipping", "filePath", filePath) - return nil, errors.New("not a media file") - } - - return tags, nil -} - -var ( - // Input #0, mp3, from 'groovin.mp3': - inputRegex = regexp.MustCompile(`(?m)^Input #\d+,.*,\sfrom\s'(.*)'`) - - // TITLE : Back In Black - tagsRx = regexp.MustCompile(`(?i)^\s{4,6}([\w\s-]+)\s*:(.*)`) - - // : Second comment line - continuationRx = regexp.MustCompile(`(?i)^\s+:(.*)`) - - // Duration: 00:04:16.00, start: 0.000000, bitrate: 995 kb/s` - durationRx = regexp.MustCompile(`^\s\sDuration: ([\d.:]+).*bitrate: (\d+)`) - - // Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 192 kb/s - bitRateRx = regexp.MustCompile(`^\s{2,4}Stream #\d+:\d+: Audio:.*, (\d+) kb/s`) - - // Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 192 kb/s - // Stream #0:0: Audio: flac, 44100 Hz, stereo, s16 - // Stream #0:0: Audio: dsd_lsbf_planar, 352800 Hz, stereo, fltp, 5644 kb/s - audioStreamRx = regexp.MustCompile(`^\s{2,4}Stream #\d+:\d+.*: Audio: (.*), (.*) Hz, ([\w.]+),*(.*.,)*`) - - // Stream #0:1: Video: mjpeg, yuvj444p(pc, bt470bg/unknown/unknown), 600x600 [SAR 1:1 DAR 1:1], 90k tbr, 90k tbn, 90k tbc` - coverRx = regexp.MustCompile(`^\s{2,4}Stream #\d+:.+: (Video):.*`) -) - -func (e *Extractor) parseOutput(output string) map[string]string { - outputs := map[string]string{} - all := inputRegex.FindAllStringSubmatchIndex(output, -1) - for i, loc := range all { - // Filename is the first captured group - file := output[loc[2]:loc[3]] - - // File info is everything from the match, up until the beginning of the next match - info := "" - initial := loc[1] - if i < len(all)-1 { - end := all[i+1][0] - 1 - info = output[initial:end] - } else { - // if this is the last match - info = output[initial:] - } - outputs[file] = info - } - return outputs -} - -func (e *Extractor) parseInfo(info string) map[string][]string { - tags := map[string][]string{} - - reader := strings.NewReader(info) - scanner := bufio.NewScanner(reader) - lastTag := "" - for scanner.Scan() { - line := scanner.Text() - if len(line) == 0 { - continue - } - match := tagsRx.FindStringSubmatch(line) - if len(match) > 0 { - tagName := strings.TrimSpace(strings.ToLower(match[1])) - if tagName != "" { - tagValue := strings.TrimSpace(match[2]) - tags[tagName] = append(tags[tagName], tagValue) - lastTag = tagName - continue - } - } - - if lastTag != "" { - match = continuationRx.FindStringSubmatch(line) - if len(match) > 0 { - if tags[lastTag] == nil { - tags[lastTag] = []string{""} - } - tagValue := tags[lastTag][0] - tags[lastTag][0] = tagValue + "\n" + strings.TrimSpace(match[1]) - continue - } - } - - lastTag = "" - match = coverRx.FindStringSubmatch(line) - if len(match) > 0 { - tags["has_picture"] = []string{"true"} - continue - } - - match = durationRx.FindStringSubmatch(line) - if len(match) > 0 { - tags["duration"] = []string{e.parseDuration(match[1])} - if len(match) > 1 { - tags["bitrate"] = []string{match[2]} - } - continue - } - - match = bitRateRx.FindStringSubmatch(line) - if len(match) > 0 { - tags["bitrate"] = []string{match[1]} - } - - match = audioStreamRx.FindStringSubmatch(line) - if len(match) > 0 { - tags["samplerate"] = []string{match[2]} - tags["channels"] = []string{e.parseChannels(match[3])} - } - } - - comment := tags["comment"] - if len(comment) > 0 && comment[0] == "Cover (front)" { - delete(tags, "comment") - } - - return tags -} - -var zeroTime = time.Date(0000, time.January, 1, 0, 0, 0, 0, time.UTC) - -func (e *Extractor) parseDuration(tag string) string { - d, err := time.Parse("15:04:05", tag) - if err != nil { - return "0" - } - return strconv.FormatFloat(d.Sub(zeroTime).Seconds(), 'f', 2, 32) -} - -func (e *Extractor) parseChannels(tag string) string { - switch tag { - case "mono": - return "1" - case "stereo": - return "2" - case "5.1": - return "6" - case "7.1": - return "8" - default: - return "0" - } -} - -// Inputs will always be absolute paths -func init() { - metadata_old.RegisterExtractor(ExtractorID, &Extractor{ffmpeg: ffmpeg.New()}) -} diff --git a/scanner/metadata_old/ffmpeg/ffmpeg_suite_test.go b/scanner/metadata_old/ffmpeg/ffmpeg_suite_test.go deleted file mode 100644 index 815940381..000000000 --- a/scanner/metadata_old/ffmpeg/ffmpeg_suite_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package ffmpeg - -import ( - "testing" - - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/tests" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestFFMpeg(t *testing.T) { - tests.Init(t, true) - log.SetLevel(log.LevelFatal) - RegisterFailHandler(Fail) - RunSpecs(t, "FFMpeg Suite") -} diff --git a/scanner/metadata_old/ffmpeg/ffmpeg_test.go b/scanner/metadata_old/ffmpeg/ffmpeg_test.go deleted file mode 100644 index 6c7f43a5d..000000000 --- a/scanner/metadata_old/ffmpeg/ffmpeg_test.go +++ /dev/null @@ -1,375 +0,0 @@ -package ffmpeg - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("Extractor", func() { - var e *Extractor - BeforeEach(func() { - e = &Extractor{} - }) - - Context("extractMetadata", func() { - It("extracts MusicBrainz custom tags", func() { - const output = ` -Input #0, ape, from './Capture/02 01 - Symphony No. 5 in C minor, Op. 67 I. Allegro con brio - Ludwig van Beethoven.ape': - Metadata: - ALBUM : Forever Classics - ARTIST : Ludwig van Beethoven - TITLE : Symphony No. 5 in C minor, Op. 67: I. Allegro con brio - MUSICBRAINZ_ALBUMSTATUS: official - MUSICBRAINZ_ALBUMTYPE: album - MusicBrainz_AlbumComment: MP3 - Musicbrainz_Albumid: 71eb5e4a-90e2-4a31-a2d1-a96485fcb667 - musicbrainz_trackid: ffe06940-727a-415a-b608-b7e45737f9d8 - Musicbrainz_Artistid: 1f9df192-a621-4f54-8850-2c5373b7eac9 - Musicbrainz_Albumartistid: 89ad4ac3-39f7-470e-963a-56509c546377 - Musicbrainz_Releasegroupid: 708b1ae1-2d3d-34c7-b764-2732b154f5b6 - musicbrainz_releasetrackid: 6fee2e35-3049-358f-83be-43b36141028b - CatalogNumber : PLD 1201 -` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(SatisfyAll( - HaveKeyWithValue("catalognumber", []string{"PLD 1201"}), - HaveKeyWithValue("musicbrainz_trackid", []string{"ffe06940-727a-415a-b608-b7e45737f9d8"}), - HaveKeyWithValue("musicbrainz_albumid", []string{"71eb5e4a-90e2-4a31-a2d1-a96485fcb667"}), - HaveKeyWithValue("musicbrainz_artistid", []string{"1f9df192-a621-4f54-8850-2c5373b7eac9"}), - HaveKeyWithValue("musicbrainz_albumartistid", []string{"89ad4ac3-39f7-470e-963a-56509c546377"}), - HaveKeyWithValue("musicbrainz_albumtype", []string{"album"}), - HaveKeyWithValue("musicbrainz_albumcomment", []string{"MP3"}), - )) - }) - - It("detects embedded cover art correctly", func() { - const output = ` -Input #0, mp3, from '/Users/deluan/Music/iTunes/iTunes Media/Music/Compilations/Putumayo Presents Blues Lounge/09 Pablo's Blues.mp3': - Metadata: - compilation : 1 - Duration: 00:00:01.02, start: 0.000000, bitrate: 477 kb/s - Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 192 kb/s - Stream #0:1: Video: mjpeg, yuvj444p(pc, bt470bg/unknown/unknown), 600x600 [SAR 1:1 DAR 1:1], 90k tbr, 90k tbn, 90k tbc` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(HaveKeyWithValue("has_picture", []string{"true"})) - }) - - It("detects embedded cover art in ffmpeg 4.4 output", func() { - const output = ` -Input #0, flac, from '/run/media/naomi/Archivio/Musica/Katy Perry/Chained to the Rhythm/01 Katy Perry featuring Skip Marley - Chained to the Rhythm.flac': - Metadata: - ARTIST : Katy Perry featuring Skip Marley - Duration: 00:03:57.91, start: 0.000000, bitrate: 983 kb/s - Stream #0:0: Audio: flac, 44100 Hz, stereo, s16 - Stream #0:1: Video: mjpeg (Baseline), yuvj444p(pc, bt470bg/unknown/unknown), 599x518, 90k tbr, 90k tbn, 90k tbc (attached pic) - Metadata: - comment : Cover (front)` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(HaveKeyWithValue("has_picture", []string{"true"})) - }) - - It("detects embedded cover art in ogg containers", func() { - const output = ` -Input #0, ogg, from '/Users/deluan/Music/iTunes/iTunes Media/Music/_Testes/Jamaican In New York/01-02 Jamaican In New York (Album Version).opus': - Duration: 00:04:28.69, start: 0.007500, bitrate: 139 kb/s - Stream #0:0(eng): Audio: opus, 48000 Hz, stereo, fltp - Metadata: - ALBUM : Jamaican In New York - metadata_block_picture: AAAAAwAAAAppbWFnZS9qcGVnAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4Id/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQ - TITLE : Jamaican In New York (Album Version)` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(HaveKey("metadata_block_picture")) - md = md.Map(e.CustomMappings()) - Expect(md).To(HaveKey("has_picture")) - }) - - It("detects embedded cover art in m4a containers", func() { - const output = ` -Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'Putumayo Presents_ Euro Groove/01 Destins et Désirs.m4a': - Metadata: - album : Putumayo Presents: Euro Groove - Duration: 00:05:15.81, start: 0.047889, bitrate: 133 kb/s - Stream #0:0[0x1](und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 125 kb/s (default) - Metadata: - creation_time : 2008-03-11T21:03:23.000000Z - vendor_id : [0][0][0][0] - Stream #0:1[0x0]: Video: png, rgb24(pc), 350x350, 90k tbr, 90k tbn (attached pic) -` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(HaveKeyWithValue("has_picture", []string{"true"})) - }) - - It("gets bitrate from the stream, if available", func() { - const output = ` -Input #0, mp3, from '/Users/deluan/Music/iTunes/iTunes Media/Music/Compilations/Putumayo Presents Blues Lounge/09 Pablo's Blues.mp3': - Duration: 00:00:01.02, start: 0.000000, bitrate: 477 kb/s - Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 192 kb/s` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(HaveKeyWithValue("bitrate", []string{"192"})) - }) - - It("parses duration with milliseconds", func() { - const output = ` -Input #0, mp3, from '/Users/deluan/Music/iTunes/iTunes Media/Music/Compilations/Putumayo Presents Blues Lounge/09 Pablo's Blues.mp3': - Duration: 00:05:02.63, start: 0.000000, bitrate: 140 kb/s` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(HaveKeyWithValue("duration", []string{"302.63"})) - }) - - It("parse flac bitrates", func() { - const output = ` -Input #0, mp3, from '/Users/deluan/Music/iTunes/iTunes Media/Music/Compilations/Putumayo Presents Blues Lounge/09 Pablo's Blues.mp3': - Duration: 00:00:01.02, start: 0.000000, bitrate: 477 kb/s - Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 192 kb/s` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(HaveKeyWithValue("channels", []string{"2"})) - }) - - It("parse channels from the stream with bitrate", func() { - const output = ` -Input #0, flac, from '/Users/deluan/Music/Music/Media/__/Crazy For You/01-01 Crazy For You.flac': - Metadata: - TITLE : Crazy For You - Duration: 00:04:13.00, start: 0.000000, bitrate: 852 kb/s - Stream #0:0: Audio: flac, 44100 Hz, stereo, s16 - Stream #0:1: Video: mjpeg (Progressive), yuvj444p(pc, bt470bg/unknown/unknown), 600x600, 90k tbr, 90k tbn, 90k tbc (attached pic) -` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(HaveKeyWithValue("bitrate", []string{"852"})) - }) - - It("parse 7.1 channels from the stream", func() { - const output = ` -Input #0, wav, from '/Users/deluan/Music/Music/Media/_/multichannel/Nums_7dot1_24_48000.wav': - Duration: 00:00:09.05, bitrate: 9216 kb/s - Stream #0:0: Audio: pcm_s24le ([1][0][0][0] / 0x0001), 48000 Hz, 7.1, s32 (24 bit), 9216 kb/s` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(HaveKeyWithValue("channels", []string{"8"})) - }) - - It("parse channels from the stream without bitrate", func() { - const output = ` -Input #0, flac, from '/Users/deluan/Music/iTunes/iTunes Media/Music/Compilations/Putumayo Presents Blues Lounge/09 Pablo's Blues.flac': - Duration: 00:00:01.02, start: 0.000000, bitrate: 1371 kb/s - Stream #0:0: Audio: flac, 44100 Hz, stereo, fltp, s32 (24 bit)` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(HaveKeyWithValue("channels", []string{"2"})) - }) - - It("parse channels from the stream with lang", func() { - const output = ` -Input #0, flac, from '/Users/deluan/Music/iTunes/iTunes Media/Music/Compilations/Putumayo Presents Blues Lounge/09 Pablo's Blues.m4a': - Duration: 00:00:01.02, start: 0.000000, bitrate: 1371 kb/s - Stream #0:0(eng): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 262 kb/s (default)` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(HaveKeyWithValue("channels", []string{"2"})) - }) - - It("parse channels from the stream with lang 2", func() { - const output = ` -Input #0, flac, from '/Users/deluan/Music/iTunes/iTunes Media/Music/Compilations/Putumayo Presents Blues Lounge/09 Pablo's Blues.m4a': - Duration: 00:00:01.02, start: 0.000000, bitrate: 1371 kb/s - Stream #0:0(eng): Audio: vorbis, 44100 Hz, stereo, fltp, 192 kb/s` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(HaveKeyWithValue("channels", []string{"2"})) - }) - - It("parse sampleRate from the stream", func() { - const output = ` -Input #0, dsf, from '/Users/deluan/Downloads/06-04 Perpetual Change.dsf': - Duration: 00:14:19.46, start: 0.000000, bitrate: 5644 kb/s - Stream #0:0: Audio: dsd_lsbf_planar, 352800 Hz, stereo, fltp, 5644 kb/s` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(HaveKeyWithValue("samplerate", []string{"352800"})) - }) - - It("parse sampleRate from the stream", func() { - const output = ` -Input #0, wav, from '/Users/deluan/Music/Music/Media/_/multichannel/Nums_7dot1_24_48000.wav': - Duration: 00:00:09.05, bitrate: 9216 kb/s - Stream #0:0: Audio: pcm_s24le ([1][0][0][0] / 0x0001), 48000 Hz, 7.1, s32 (24 bit), 9216 kb/s` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(HaveKeyWithValue("samplerate", []string{"48000"})) - }) - - It("parses stream level tags", func() { - const output = ` -Input #0, ogg, from './01-02 Drive (Teku).opus': - Metadata: - ALBUM : Hot Wheels Acceleracers Soundtrack - Duration: 00:03:37.37, start: 0.007500, bitrate: 135 kb/s - Stream #0:0(eng): Audio: opus, 48000 Hz, stereo, fltp - Metadata: - TITLE : Drive (Teku)` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(HaveKeyWithValue("title", []string{"Drive (Teku)"})) - }) - - It("does not overlap top level tags with the stream level tags", func() { - const output = ` -Input #0, mp3, from 'groovin.mp3': - Metadata: - title : Groovin' (feat. Daniel Sneijers, Susanne Alt) - Duration: 00:03:34.28, start: 0.025056, bitrate: 323 kb/s - Metadata: - title : garbage` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(HaveKeyWithValue("title", []string{"Groovin' (feat. Daniel Sneijers, Susanne Alt)", "garbage"})) - }) - - It("parses multiline tags", func() { - const outputWithMultilineComment = ` -Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'modulo.m4a': - Metadata: - comment : https://www.mixcloud.com/codigorock/30-minutos-com-saara-saara/ - : - : Tracklist: - : - : 01. Saara Saara - : 02. Carta Corrente - : 03. X - : 04. Eclipse Lunar - : 05. Vírus de Sírius - : 06. Doktor Fritz - : 07. Wunderbar - : 08. Quarta Dimensão - Duration: 00:26:46.96, start: 0.052971, bitrate: 69 kb/s` - const expectedComment = `https://www.mixcloud.com/codigorock/30-minutos-com-saara-saara/ - -Tracklist: - -01. Saara Saara -02. Carta Corrente -03. X -04. Eclipse Lunar -05. Vírus de Sírius -06. Doktor Fritz -07. Wunderbar -08. Quarta Dimensão` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", outputWithMultilineComment) - Expect(md).To(HaveKeyWithValue("comment", []string{expectedComment})) - }) - - It("parses sort tags correctly", func() { - const output = ` -Input #0, mp3, from '/Users/deluan/Downloads/椎名林檎 - 加爾基 精液 栗ノ花 - 2003/02 - ドツペルゲンガー.mp3': - Metadata: - title-sort : Dopperugengā - album : 加爾基 精液 栗ノ花 - artist : 椎名林檎 - album_artist : 椎名林檎 - title : ドツペルゲンガー - albumsort : Kalk Samen Kuri No Hana - artist_sort : Shiina, Ringo - ALBUMARTISTSORT : Shiina, Ringo -` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(SatisfyAll( - HaveKeyWithValue("title", []string{"ドツペルゲンガー"}), - HaveKeyWithValue("album", []string{"加爾基 精液 栗ノ花"}), - HaveKeyWithValue("artist", []string{"椎名林檎"}), - HaveKeyWithValue("album_artist", []string{"椎名林檎"}), - HaveKeyWithValue("title-sort", []string{"Dopperugengā"}), - HaveKeyWithValue("albumsort", []string{"Kalk Samen Kuri No Hana"}), - HaveKeyWithValue("artist_sort", []string{"Shiina, Ringo"}), - HaveKeyWithValue("albumartistsort", []string{"Shiina, Ringo"}), - )) - }) - - It("ignores cover comment", func() { - const output = ` -Input #0, mp3, from './Edie Brickell/Picture Perfect Morning/01-01 Tomorrow Comes.mp3': - Metadata: - title : Tomorrow Comes - artist : Edie Brickell - Duration: 00:03:56.12, start: 0.000000, bitrate: 332 kb/s - Stream #0:0: Audio: mp3, 44100 Hz, stereo, s16p, 320 kb/s - Stream #0:1: Video: mjpeg, yuvj420p(pc, bt470bg/unknown/unknown), 1200x1200 [SAR 72:72 DAR 1:1], 90k tbr, 90k tbn, 90k tbc - Metadata: - comment : Cover (front)` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).ToNot(HaveKey("comment")) - }) - - It("parses tags with spaces in the name", func() { - const output = ` -Input #0, mp3, from '/Users/deluan/Music/Music/Media/_/Wyclef Jean - From the Hut, to the Projects, to the Mansion/10 - The Struggle (interlude).mp3': - Metadata: - ALBUM ARTIST : Wyclef Jean -` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(HaveKeyWithValue("album artist", []string{"Wyclef Jean"})) - }) - }) - - It("parses an integer TBPM tag", func() { - const output = ` - Input #0, mp3, from 'tests/fixtures/test.mp3': - Metadata: - TBPM : 123` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(HaveKeyWithValue("tbpm", []string{"123"})) - }) - - It("parses and rounds a floating point fBPM tag", func() { - const output = ` - Input #0, ogg, from 'tests/fixtures/test.ogg': - Metadata: - FBPM : 141.7` - md, _ := e.extractMetadata("tests/fixtures/test.ogg", output) - Expect(md).To(HaveKeyWithValue("fbpm", []string{"141.7"})) - }) - - It("parses replaygain data correctly", func() { - const output = ` - Input #0, mp3, from 'test.mp3': - Metadata: - REPLAYGAIN_ALBUM_PEAK: 0.9125 - REPLAYGAIN_TRACK_PEAK: 0.4512 - REPLAYGAIN_TRACK_GAIN: -1.48 dB - REPLAYGAIN_ALBUM_GAIN: +3.21518 dB - Side data: - replaygain: track gain - -1.480000, track peak - 0.000011, album gain - 3.215180, album peak - 0.000021, - ` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(SatisfyAll( - HaveKeyWithValue("replaygain_track_gain", []string{"-1.48 dB"}), - HaveKeyWithValue("replaygain_track_peak", []string{"0.4512"}), - HaveKeyWithValue("replaygain_album_gain", []string{"+3.21518 dB"}), - HaveKeyWithValue("replaygain_album_peak", []string{"0.9125"}), - )) - }) - - It("parses lyrics with language code", func() { - const output = ` - Input #0, mp3, from 'test.mp3': - Metadata: - lyrics-eng : [00:00.00]This is - : [00:02.50]English - lyrics-xxx : [00:00.00]This is - : [00:02.50]unspecified - ` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(SatisfyAll( - HaveKeyWithValue("lyrics-eng", []string{ - "[00:00.00]This is\n[00:02.50]English", - }), - HaveKeyWithValue("lyrics-xxx", []string{ - "[00:00.00]This is\n[00:02.50]unspecified", - }), - )) - }) - - It("parses normal LYRICS tag", func() { - const output = ` - Input #0, mp3, from 'test.mp3': - Metadata: - LYRICS : [00:00.00]This is - : [00:02.50]English - ` - md, _ := e.extractMetadata("tests/fixtures/test.mp3", output) - Expect(md).To(HaveKeyWithValue("lyrics", []string{ - "[00:00.00]This is\n[00:02.50]English", - })) - }) -}) diff --git a/scanner/metadata_old/metadata.go b/scanner/metadata_old/metadata.go deleted file mode 100644 index 2906a2c09..000000000 --- a/scanner/metadata_old/metadata.go +++ /dev/null @@ -1,411 +0,0 @@ -package metadata_old - -import ( - "context" - "encoding/json" - "fmt" - "math" - "os" - "path" - "regexp" - "strconv" - "strings" - "time" - - "github.com/djherbis/times" - "github.com/google/uuid" - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/consts" - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/model" -) - -type Extractor interface { - Parse(files ...string) (map[string]ParsedTags, error) - CustomMappings() ParsedTags - Version() string -} - -var extractors = map[string]Extractor{} - -func RegisterExtractor(id string, parser Extractor) { - extractors[id] = parser -} - -func LogExtractors() { - for id, p := range extractors { - log.Debug("Registered metadata extractor", "id", id, "version", p.Version()) - } -} - -func Extract(files ...string) (map[string]Tags, error) { - p, ok := extractors[conf.Server.Scanner.Extractor] - if !ok { - log.Warn("Invalid 'Scanner.Extractor' option. Using default", "requested", conf.Server.Scanner.Extractor, - "validOptions", "ffmpeg,taglib", "default", consts.DefaultScannerExtractor) - p = extractors[consts.DefaultScannerExtractor] - } - - extractedTags, err := p.Parse(files...) - if err != nil { - return nil, err - } - - result := map[string]Tags{} - for filePath, tags := range extractedTags { - fileInfo, err := os.Stat(filePath) - if err != nil { - log.Warn("Error stating file. Skipping", "filePath", filePath, err) - continue - } - - tags = tags.Map(p.CustomMappings()) - result[filePath] = NewTag(filePath, fileInfo, tags) - } - - return result, nil -} - -func NewTag(filePath string, fileInfo os.FileInfo, tags ParsedTags) Tags { - for t, values := range tags { - values = removeDuplicatesAndEmpty(values) - if len(values) == 0 { - delete(tags, t) - continue - } - tags[t] = values - } - return Tags{ - filePath: filePath, - fileInfo: fileInfo, - Tags: tags, - } -} - -func removeDuplicatesAndEmpty(values []string) []string { - encountered := map[string]struct{}{} - empty := true - result := make([]string, 0, len(values)) - for _, v := range values { - if _, ok := encountered[v]; ok { - continue - } - encountered[v] = struct{}{} - empty = empty && v == "" - result = append(result, v) - } - if empty { - return nil - } - return result -} - -type ParsedTags map[string][]string - -func (p ParsedTags) Map(customMappings ParsedTags) ParsedTags { - if customMappings == nil { - return p - } - for tagName, alternatives := range customMappings { - for _, altName := range alternatives { - if altValue, ok := p[altName]; ok { - p[tagName] = append(p[tagName], altValue...) - delete(p, altName) - } - } - } - return p -} - -type Tags struct { - filePath string - fileInfo os.FileInfo - Tags ParsedTags -} - -// Common tags - -func (t Tags) Title() string { return t.getFirstTagValue("title", "sort_name", "titlesort") } -func (t Tags) Album() string { return t.getFirstTagValue("album", "sort_album", "albumsort") } -func (t Tags) Artist() string { return t.getFirstTagValue("artist", "sort_artist", "artistsort") } -func (t Tags) AlbumArtist() string { - return t.getFirstTagValue("album_artist", "album artist", "albumartist") -} -func (t Tags) SortTitle() string { return t.getSortTag("tsot", "title", "name") } -func (t Tags) SortAlbum() string { return t.getSortTag("tsoa", "album") } -func (t Tags) SortArtist() string { return t.getSortTag("tsop", "artist") } -func (t Tags) SortAlbumArtist() string { return t.getSortTag("tso2", "albumartist", "album_artist") } -func (t Tags) Genres() []string { return t.getAllTagValues("genre") } -func (t Tags) Date() (int, string) { return t.getDate("date") } -func (t Tags) OriginalDate() (int, string) { return t.getDate("originaldate") } -func (t Tags) ReleaseDate() (int, string) { return t.getDate("releasedate") } -func (t Tags) Comment() string { return t.getFirstTagValue("comment") } -func (t Tags) Compilation() bool { return t.getBool("tcmp", "compilation", "wm/iscompilation") } -func (t Tags) TrackNumber() (int, int) { return t.getTuple("track", "tracknumber") } -func (t Tags) DiscNumber() (int, int) { return t.getTuple("disc", "discnumber") } -func (t Tags) DiscSubtitle() string { - return t.getFirstTagValue("tsst", "discsubtitle", "setsubtitle") -} -func (t Tags) CatalogNum() string { return t.getFirstTagValue("catalognumber") } -func (t Tags) Bpm() int { return (int)(math.Round(t.getFloat("tbpm", "bpm", "fbpm"))) } -func (t Tags) HasPicture() bool { return t.getFirstTagValue("has_picture") != "" } - -// MusicBrainz Identifiers - -func (t Tags) MbzReleaseTrackID() string { - return t.getMbzID("musicbrainz_releasetrackid", "musicbrainz release track id") -} - -func (t Tags) MbzRecordingID() string { - return t.getMbzID("musicbrainz_trackid", "musicbrainz track id") -} -func (t Tags) MbzAlbumID() string { return t.getMbzID("musicbrainz_albumid", "musicbrainz album id") } -func (t Tags) MbzArtistID() string { - return t.getMbzID("musicbrainz_artistid", "musicbrainz artist id") -} -func (t Tags) MbzAlbumArtistID() string { - return t.getMbzID("musicbrainz_albumartistid", "musicbrainz album artist id") -} -func (t Tags) MbzAlbumType() string { - return t.getFirstTagValue("musicbrainz_albumtype", "musicbrainz album type") -} -func (t Tags) MbzAlbumComment() string { - return t.getFirstTagValue("musicbrainz_albumcomment", "musicbrainz album comment") -} - -// Gain Properties - -func (t Tags) RGAlbumGain() float64 { - return t.getGainValue("replaygain_album_gain", "r128_album_gain") -} -func (t Tags) RGAlbumPeak() float64 { return t.getPeakValue("replaygain_album_peak") } -func (t Tags) RGTrackGain() float64 { - return t.getGainValue("replaygain_track_gain", "r128_track_gain") -} -func (t Tags) RGTrackPeak() float64 { return t.getPeakValue("replaygain_track_peak") } - -// File properties - -func (t Tags) Duration() float32 { return float32(t.getFloat("duration")) } -func (t Tags) SampleRate() int { return t.getInt("samplerate") } -func (t Tags) BitRate() int { return t.getInt("bitrate") } -func (t Tags) Channels() int { return t.getInt("channels") } -func (t Tags) ModificationTime() time.Time { return t.fileInfo.ModTime() } -func (t Tags) Size() int64 { return t.fileInfo.Size() } -func (t Tags) FilePath() string { return t.filePath } -func (t Tags) Suffix() string { return strings.ToLower(strings.TrimPrefix(path.Ext(t.filePath), ".")) } -func (t Tags) BirthTime() time.Time { - if ts := times.Get(t.fileInfo); ts.HasBirthTime() { - return ts.BirthTime() - } - return time.Now() -} - -func (t Tags) Lyrics() string { - lyricList := model.LyricList{} - basicLyrics := t.getAllTagValues("lyrics", "unsynced_lyrics", "unsynced lyrics", "unsyncedlyrics") - - for _, value := range basicLyrics { - parsed, err := model.ParseLyrics(context.Background(), ".lrc", "xxx", []byte(value)) - if err != nil { - log.Warn("Unexpected failure occurred when parsing lyrics", "file", t.filePath, "error", err) - continue - } - if main, ok := parsed.Main(); ok { - lyricList = append(lyricList, main) - } - } - - for tag, value := range t.Tags { - if after, ok := strings.CutPrefix(tag, "lyrics-"); ok { - language := strings.TrimSpace(after) - - if language == "" { - language = "xxx" - } - - for _, text := range value { - parsed, err := model.ParseLyrics(context.Background(), ".lrc", language, []byte(text)) - if err != nil { - log.Warn("Unexpected failure occurred when parsing lyrics", "file", t.filePath, "error", err) - continue - } - if main, ok := parsed.Main(); ok { - lyricList = append(lyricList, main) - } - } - } - } - - res, err := json.Marshal(lyricList) - if err != nil { - log.Warn("Unexpected error occurred when serializing lyrics", "file", t.filePath, "error", err) - return "" - } - return string(res) -} - -func (t Tags) getGainValue(rgTagName, r128TagName string) float64 { - // Check for ReplayGain first - // ReplayGain is in the form [-]a.bb dB and normalized to -18dB - var tag = t.getFirstTagValue(rgTagName) - if tag != "" { - tag = strings.TrimSpace(strings.Replace(tag, "dB", "", 1)) - var value, err = strconv.ParseFloat(tag, 64) - if err != nil || value == math.Inf(-1) || value == math.Inf(1) { - return 0 - } - return value - } - - // If ReplayGain is not found, check for R128 gain - // R128 gain is a Q7.8 fixed point number normalized to -23dB - tag = t.getFirstTagValue(r128TagName) - if tag != "" { - var iValue, err = strconv.Atoi(tag) - if err != nil { - return 0 - } - // Convert Q7.8 to float - var value = float64(iValue) / 256.0 - // Adding 5 dB to normalize with ReplayGain level - return value + 5 - } - - return 0 -} - -func (t Tags) getPeakValue(tagName string) float64 { - var tag = t.getFirstTagValue(tagName) - var value, err = strconv.ParseFloat(tag, 64) - if err != nil || value == math.Inf(-1) || value == math.Inf(1) { - // A default of 1 for peak value results in no changes - return 1 - } - return value -} - -func (t Tags) getTags(tagNames ...string) []string { - for _, tag := range tagNames { - if v, ok := t.Tags[tag]; ok { - return v - } - } - return nil -} - -func (t Tags) getFirstTagValue(tagNames ...string) string { - ts := t.getTags(tagNames...) - if len(ts) > 0 { - return ts[0] - } - return "" -} - -func (t Tags) getAllTagValues(tagNames ...string) []string { - values := make([]string, 0, len(tagNames)*2) - for _, tag := range tagNames { - if v, ok := t.Tags[tag]; ok { - values = append(values, v...) - } - } - return values -} - -func (t Tags) getSortTag(originalTag string, tagNames ...string) string { - formats := []string{"sort%s", "sort_%s", "sort-%s", "%ssort", "%s_sort", "%s-sort"} - all := make([]string, 1, len(tagNames)*len(formats)+1) - all[0] = originalTag - for _, tag := range tagNames { - for _, format := range formats { - name := fmt.Sprintf(format, tag) - all = append(all, name) - } - } - return t.getFirstTagValue(all...) -} - -var dateRegex = regexp.MustCompile(`([12]\d\d\d)`) - -func (t Tags) getDate(tagNames ...string) (int, string) { - tag := t.getFirstTagValue(tagNames...) - if len(tag) < 4 { - return 0, "" - } - // first get just the year - match := dateRegex.FindStringSubmatch(tag) - if len(match) == 0 { - log.Warn("Error parsing "+tagNames[0]+" field for year", "file", t.filePath, "date", tag) - return 0, "" - } - year, _ := strconv.Atoi(match[1]) - - if len(tag) < 5 { - return year, match[1] - } - - //then try YYYY-MM-DD - if len(tag) > 10 { - tag = tag[:10] - } - layout := "2006-01-02" - _, err := time.Parse(layout, tag) - if err != nil { - layout = "2006-01" - _, err = time.Parse(layout, tag) - if err != nil { - log.Warn("Error parsing "+tagNames[0]+" field for month + day", "file", t.filePath, "date", tag) - return year, match[1] - } - } - return year, tag -} - -func (t Tags) getBool(tagNames ...string) bool { - tag := t.getFirstTagValue(tagNames...) - if tag == "" { - return false - } - i, _ := strconv.Atoi(strings.TrimSpace(tag)) - return i == 1 -} - -func (t Tags) getTuple(tagNames ...string) (int, int) { - tag := t.getFirstTagValue(tagNames...) - if tag == "" { - return 0, 0 - } - tuple := strings.Split(tag, "/") - t1, t2 := 0, 0 - t1, _ = strconv.Atoi(tuple[0]) - if len(tuple) > 1 { - t2, _ = strconv.Atoi(tuple[1]) - } else { - t2tag := t.getFirstTagValue(tagNames[0] + "total") - t2, _ = strconv.Atoi(t2tag) - } - return t1, t2 -} - -func (t Tags) getMbzID(tagNames ...string) string { - tag := t.getFirstTagValue(tagNames...) - if _, err := uuid.Parse(tag); err != nil { - return "" - } - return tag -} - -func (t Tags) getInt(tagNames ...string) int { - tag := t.getFirstTagValue(tagNames...) - i, _ := strconv.Atoi(tag) - return i -} - -func (t Tags) getFloat(tagNames ...string) float64 { - var tag = t.getFirstTagValue(tagNames...) - var value, err = strconv.ParseFloat(tag, 64) - if err != nil { - return 0 - } - return value -} diff --git a/scanner/metadata_old/metadata_internal_test.go b/scanner/metadata_old/metadata_internal_test.go deleted file mode 100644 index aff1ede9c..000000000 --- a/scanner/metadata_old/metadata_internal_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package metadata_old - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("Tags", func() { - DescribeTable("getDate", - func(tag string, expectedYear int, expectedDate string) { - md := &Tags{} - md.Tags = map[string][]string{"date": {tag}} - testYear, testDate := md.Date() - Expect(testYear).To(Equal(expectedYear)) - Expect(testDate).To(Equal(expectedDate)) - }, - Entry(nil, "1985", 1985, "1985"), - Entry(nil, "2002-01", 2002, "2002-01"), - Entry(nil, "1969.06", 1969, "1969"), - Entry(nil, "1980.07.25", 1980, "1980"), - Entry(nil, "2004-00-00", 2004, "2004"), - Entry(nil, "2016-12-31", 2016, "2016-12-31"), - Entry(nil, "2013-May-12", 2013, "2013"), - Entry(nil, "May 12, 2016", 2016, "2016"), - Entry(nil, "01/10/1990", 1990, "1990"), - Entry(nil, "invalid", 0, ""), - ) - - Describe("getMbzID", func() { - It("return a valid MBID", func() { - md := &Tags{} - md.Tags = map[string][]string{ - "musicbrainz_trackid": {"8f84da07-09a0-477b-b216-cc982dabcde1"}, - "musicbrainz_releasetrackid": {"6caf16d3-0b20-3fe6-8020-52e31831bc11"}, - "musicbrainz_albumid": {"f68c985d-f18b-4f4a-b7f0-87837cf3fbf9"}, - "musicbrainz_artistid": {"89ad4ac3-39f7-470e-963a-56509c546377"}, - "musicbrainz_albumartistid": {"ada7a83c-e3e1-40f1-93f9-3e73dbc9298a"}, - } - Expect(md.MbzRecordingID()).To(Equal("8f84da07-09a0-477b-b216-cc982dabcde1")) - Expect(md.MbzReleaseTrackID()).To(Equal("6caf16d3-0b20-3fe6-8020-52e31831bc11")) - Expect(md.MbzAlbumID()).To(Equal("f68c985d-f18b-4f4a-b7f0-87837cf3fbf9")) - Expect(md.MbzArtistID()).To(Equal("89ad4ac3-39f7-470e-963a-56509c546377")) - Expect(md.MbzAlbumArtistID()).To(Equal("ada7a83c-e3e1-40f1-93f9-3e73dbc9298a")) - }) - It("return empty string for invalid MBID", func() { - md := &Tags{} - md.Tags = map[string][]string{ - "musicbrainz_trackid": {"11406732-6"}, - "musicbrainz_albumid": {"11406732"}, - "musicbrainz_artistid": {"200455"}, - "musicbrainz_albumartistid": {"194"}, - } - Expect(md.MbzRecordingID()).To(Equal("")) - Expect(md.MbzAlbumID()).To(Equal("")) - Expect(md.MbzArtistID()).To(Equal("")) - Expect(md.MbzAlbumArtistID()).To(Equal("")) - }) - }) - - Describe("getAllTagValues", func() { - It("returns values from all tag names", func() { - md := &Tags{} - md.Tags = map[string][]string{ - "genre": {"Rock", "Pop", "New Wave"}, - } - - Expect(md.Genres()).To(ConsistOf("Rock", "Pop", "New Wave")) - }) - }) - - Describe("removeDuplicatesAndEmpty", func() { - It("removes duplicates", func() { - md := NewTag("/music/artist/album01/Song.mp3", nil, ParsedTags{ - "genre": []string{"pop", "rock", "pop"}, - "date": []string{"2023-03-01", "2023-03-01"}, - "mood": []string{"happy", "sad"}, - }) - Expect(md.Tags).To(HaveKeyWithValue("genre", []string{"pop", "rock"})) - Expect(md.Tags).To(HaveKeyWithValue("date", []string{"2023-03-01"})) - Expect(md.Tags).To(HaveKeyWithValue("mood", []string{"happy", "sad"})) - }) - It("removes empty tags", func() { - md := NewTag("/music/artist/album01/Song.mp3", nil, ParsedTags{ - "genre": []string{"pop", "rock", "pop"}, - "mood": []string{"", ""}, - }) - Expect(md.Tags).To(HaveKeyWithValue("genre", []string{"pop", "rock"})) - Expect(md.Tags).ToNot(HaveKey("mood")) - }) - }) - - Describe("BPM", func() { - var t *Tags - BeforeEach(func() { - t = &Tags{Tags: map[string][]string{ - "fbpm": {"141.7"}, - }} - }) - - It("rounds a floating point fBPM tag", func() { - Expect(t.Bpm()).To(Equal(142)) - }) - }) - - Describe("ReplayGain", func() { - DescribeTable("getGainValue", - func(tag string, expected float64) { - md := &Tags{} - md.Tags = map[string][]string{"replaygain_track_gain": {tag}} - Expect(md.RGTrackGain()).To(Equal(expected)) - - }, - Entry("0", "0", 0.0), - Entry("1.2dB", "1.2dB", 1.2), - Entry("Infinity", "Infinity", 0.0), - Entry("Invalid value", "INVALID VALUE", 0.0), - ) - DescribeTable("getPeakValue", - func(tag string, expected float64) { - md := &Tags{} - md.Tags = map[string][]string{"replaygain_track_peak": {tag}} - Expect(md.RGTrackPeak()).To(Equal(expected)) - - }, - Entry("0", "0", 0.0), - Entry("0.5", "0.5", 0.5), - Entry("Invalid dB suffix", "0.7dB", 1.0), - Entry("Infinity", "Infinity", 1.0), - Entry("Invalid value", "INVALID VALUE", 1.0), - ) - DescribeTable("getR128GainValue", - func(tag string, expected float64) { - md := &Tags{} - md.Tags = map[string][]string{"r128_track_gain": {tag}} - Expect(md.RGTrackGain()).To(Equal(expected)) - - }, - Entry("0", "0", 5.0), - Entry("-3776", "-3776", -9.75), - Entry("Infinity", "Infinity", 0.0), - Entry("Invalid value", "INVALID VALUE", 0.0), - ) - }) -}) diff --git a/scanner/metadata_old/metadata_suite_test.go b/scanner/metadata_old/metadata_suite_test.go deleted file mode 100644 index 03ec3c847..000000000 --- a/scanner/metadata_old/metadata_suite_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package metadata_old - -import ( - "testing" - - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/tests" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestMetadata(t *testing.T) { - tests.Init(t, true) - log.SetLevel(log.LevelFatal) - RegisterFailHandler(Fail) - RunSpecs(t, "Metadata Suite") -} diff --git a/scanner/metadata_old/metadata_test.go b/scanner/metadata_old/metadata_test.go deleted file mode 100644 index 444bb7fc4..000000000 --- a/scanner/metadata_old/metadata_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package metadata_old_test - -import ( - "cmp" - "encoding/json" - "slices" - - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/conf/configtest" - "github.com/navidrome/navidrome/core/ffmpeg" - "github.com/navidrome/navidrome/model" - "github.com/navidrome/navidrome/scanner/metadata_old" - _ "github.com/navidrome/navidrome/scanner/metadata_old/ffmpeg" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("Tags", func() { - var zero int64 = 0 - var secondTs int64 = 2500 - - makeLyrics := func(synced bool, lang, secondLine string) model.Lyrics { - lines := []model.Line{ - {Value: "This is"}, - {Value: secondLine}, - } - - if synced { - lines[0].Start = &zero - lines[1].Start = &secondTs - } - - lyrics := model.Lyrics{ - Lang: lang, - Line: lines, - Synced: synced, - } - - return lyrics - } - - sortLyrics := func(lines model.LyricList) model.LyricList { - slices.SortFunc(lines, func(a, b model.Lyrics) int { - langDiff := cmp.Compare(a.Lang, b.Lang) - if langDiff != 0 { - return langDiff - } - return cmp.Compare(a.Line[1].Value, b.Line[1].Value) - }) - - return lines - } - - compareLyrics := func(m metadata_old.Tags, expected model.LyricList) { - lyrics := model.LyricList{} - Expect(json.Unmarshal([]byte(m.Lyrics()), &lyrics)).To(BeNil()) - Expect(sortLyrics(lyrics)).To(Equal(sortLyrics(expected))) - } - - // Only run these tests if FFmpeg is available - FFmpegContext := XContext - if ffmpeg.New().IsAvailable() { - FFmpegContext = Context - } - FFmpegContext("Extract with FFmpeg", func() { - BeforeEach(func() { - DeferCleanup(configtest.SetupConfig()) - conf.Server.Scanner.Extractor = "ffmpeg" - }) - - DescribeTable("Lyrics test", - func(file string) { - path := "tests/fixtures/" + file - mds, err := metadata_old.Extract(path) - Expect(err).ToNot(HaveOccurred()) - Expect(mds).To(HaveLen(1)) - - m := mds[path] - compareLyrics(m, model.LyricList{ - makeLyrics(true, "eng", "English"), - makeLyrics(true, "xxx", "unspecified"), - }) - }, - - Entry("Parses AIFF file", "test.aiff"), - Entry("Parses MP3 files", "test.mp3"), - // Disabled, because it fails in pipeline - // Entry("Parses WAV files", "test.wav"), - - // FFMPEG behaves very weirdly for multivalued tags for non-ID3 - // Specifically, they are separated by ";, which is indistinguishable - // from other fields - ) - }) -}) diff --git a/server/initial_setup.go b/server/initial_setup.go index be9e14ae9..462e22e54 100644 --- a/server/initial_setup.go +++ b/server/initial_setup.go @@ -72,10 +72,6 @@ func checkFFmpegInstallation() { _, err := f.CmdPath() 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 } if !f.IsProbeAvailable() { diff --git a/tests/harness/harness.go b/tests/harness/harness.go index 92196ef26..67f3150a2 100644 --- a/tests/harness/harness.go +++ b/tests/harness/harness.go @@ -176,8 +176,6 @@ func (NoopFFmpeg) ExtractImage(context.Context, string) (io.ReadCloser, error) { return nil, errors.New("noop ffmpeg: extract image not supported") } -func (NoopFFmpeg) Probe(context.Context, []string) (string, error) { return "", nil } - func (NoopFFmpeg) ProbeAudioStream(context.Context, string) (*ffmpeg.AudioProbeResult, error) { return nil, errors.New("noop ffmpeg: probe not supported") } diff --git a/tests/mock_ffmpeg.go b/tests/mock_ffmpeg.go index f9862767e..8e4d12f0b 100644 --- a/tests/mock_ffmpeg.go +++ b/tests/mock_ffmpeg.go @@ -57,12 +57,6 @@ func (ff *MockFFmpeg) ConvertAnimatedImage(_ context.Context, reader io.Reader, return io.NopCloser(bytes.NewReader(data)), nil } -func (ff *MockFFmpeg) Probe(context.Context, []string) (string, error) { - if ff.Error != nil { - return "", ff.Error - } - return "", nil -} func (ff *MockFFmpeg) ProbeAudioStream(context.Context, string) (*ffmpeg.AudioProbeResult, error) { if ff.Error != nil { return nil, ff.Error