diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index c7ccbf9f..b2aa7645 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -13,17 +13,5 @@ RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/shar RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get -y install --no-install-recommends ffmpeg -# Install TagLib from cross-taglib releases -ARG CROSS_TAGLIB_VERSION="2.2.0-1" -ARG TARGETARCH -RUN DOWNLOAD_ARCH="linux-${TARGETARCH}" \ - && wget -q "https://github.com/navidrome/cross-taglib/releases/download/v${CROSS_TAGLIB_VERSION}/taglib-${DOWNLOAD_ARCH}.tar.gz" -O /tmp/cross-taglib.tar.gz \ - && tar -xzf /tmp/cross-taglib.tar.gz -C /usr --strip-components=1 \ - && mv /usr/include/taglib/* /usr/include/ \ - && rmdir /usr/include/taglib \ - && rm /tmp/cross-taglib.tar.gz /usr/provenance.json - -ENV CGO_CFLAGS_ALLOW="--define-prefix" - # [Optional] Uncomment this line to install global node packages. # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 81398a3c..311090b9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,8 +7,7 @@ "VARIANT": "1.25", // Options "INSTALL_NODE": "true", - "NODE_VERSION": "v24", - "CROSS_TAGLIB_VERSION": "2.2.0-1" + "NODE_VERSION": "v24" } }, "workspaceMount": "", diff --git a/.github/actions/download-taglib/action.yml b/.github/actions/download-taglib/action.yml deleted file mode 100644 index ea6de878..00000000 --- a/.github/actions/download-taglib/action.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: 'Download TagLib' -description: 'Downloads and extracts the TagLib library, adding it to PKG_CONFIG_PATH' -inputs: - version: - description: 'Version of TagLib to download' - required: true - platform: - description: 'Platform to download TagLib for' - default: 'linux-amd64' -runs: - using: 'composite' - steps: - - name: Download TagLib - shell: bash - run: | - mkdir -p /tmp/taglib - cd /tmp - FILE=taglib-${{ inputs.platform }}.tar.gz - wget https://github.com/navidrome/cross-taglib/releases/download/v${{ inputs.version }}/${FILE} - tar -xzf ${FILE} -C taglib - PKG_CONFIG_PREFIX=/tmp/taglib - echo "PKG_CONFIG_PREFIX=${PKG_CONFIG_PREFIX}" >> $GITHUB_ENV - echo "PKG_CONFIG_PATH=${PKG_CONFIG_PATH}:${PKG_CONFIG_PREFIX}/lib/pkgconfig" >> $GITHUB_ENV diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 2529aaf3..e939f1d1 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -14,8 +14,6 @@ concurrency: cancel-in-progress: true env: - CROSS_TAGLIB_VERSION: "2.2.0-1" - CGO_CFLAGS_ALLOW: "--define-prefix" IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') && 'true' || 'false' }} jobs: @@ -66,11 +64,6 @@ jobs: steps: - uses: actions/checkout@v6 - - name: Download TagLib - uses: ./.github/actions/download-taglib - with: - version: ${{ env.CROSS_TAGLIB_VERSION }} - - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: @@ -106,18 +99,11 @@ jobs: - name: Check out code into the Go module directory uses: actions/checkout@v6 - - name: Download TagLib - uses: ./.github/actions/download-taglib - with: - version: ${{ env.CROSS_TAGLIB_VERSION }} - - name: Download dependencies run: go mod download - name: Test - run: | - pkg-config --define-prefix --cflags --libs taglib # for debugging - go test -shuffle=on -tags netgo,sqlite_fts5 -race ./... -v + run: go test -shuffle=on -tags netgo,sqlite_fts5 -race ./... -v - name: Test ndpgen run: | @@ -232,7 +218,6 @@ jobs: build-args: | GIT_SHA=${{ env.GIT_SHA }} GIT_TAG=${{ env.GIT_TAG }} - CROSS_TAGLIB_VERSION=${{ env.CROSS_TAGLIB_VERSION }} - name: Upload Binaries uses: actions/upload-artifact@v7 @@ -253,7 +238,6 @@ jobs: build-args: | GIT_SHA=${{ env.GIT_SHA }} GIT_TAG=${{ env.GIT_TAG }} - CROSS_TAGLIB_VERSION=${{ env.CROSS_TAGLIB_VERSION }} outputs: | type=image,name=${{ steps.docker.outputs.hub_repository }},push-by-digest=true,name-canonical=true,push=${{ steps.docker.outputs.hub_enabled }} type=image,name=ghcr.io/${{ github.repository }},push-by-digest=true,name-canonical=true,push=true diff --git a/Dockerfile b/Dockerfile index b32c1df5..f6ea14ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,26 +24,6 @@ RUN cd /out && \ FROM scratch AS xx COPY --from=xx-build /out/ /usr/bin/ -######################################################################################################################## -### Get TagLib -FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/alpine:3.20 AS taglib-build -ARG TARGETPLATFORM -ARG CROSS_TAGLIB_VERSION=2.2.0-1 -ENV CROSS_TAGLIB_RELEASES_URL=https://github.com/navidrome/cross-taglib/releases/download/v${CROSS_TAGLIB_VERSION}/ - -# wget in busybox can't follow redirects -RUN < - -const size_t SIZEOF_WCHAR_T = sizeof(wchar_t); - -void gowchar_set (wchar_t *arr, int pos, wchar_t val) -{ - arr[pos] = val; -} - -wchar_t gowchar_get (wchar_t *arr, int pos) -{ - return arr[pos]; -} -*/ -import "C" - -import ( - "fmt" - "unicode/utf16" - "unicode/utf8" -) - -var SIZEOF_WCHAR_T C.size_t = C.size_t(C.SIZEOF_WCHAR_T) - -func getFilename(s string) *C.wchar_t { - wstr, _ := StringToWcharT(s) - return wstr -} - -func StringToWcharT(s string) (*C.wchar_t, C.size_t) { - switch SIZEOF_WCHAR_T { - case 2: - return stringToWchar2(s) // Windows - case 4: - return stringToWchar4(s) // Unix - default: - panic(fmt.Sprintf("Invalid sizeof(wchar_t) = %v", SIZEOF_WCHAR_T)) - } - panic("?!!") -} - -// Windows -func stringToWchar2(s string) (*C.wchar_t, C.size_t) { - var slen int - s1 := s - for len(s1) > 0 { - r, size := utf8.DecodeRuneInString(s1) - if er, _ := utf16.EncodeRune(r); er == '\uFFFD' { - slen += 1 - } else { - slen += 2 - } - s1 = s1[size:] - } - slen++ // \0 - res := C.malloc(C.size_t(slen) * SIZEOF_WCHAR_T) - var i int - for len(s) > 0 { - r, size := utf8.DecodeRuneInString(s) - if r1, r2 := utf16.EncodeRune(r); r1 != '\uFFFD' { - C.gowchar_set((*C.wchar_t)(res), C.int(i), C.wchar_t(r1)) - i++ - C.gowchar_set((*C.wchar_t)(res), C.int(i), C.wchar_t(r2)) - i++ - } else { - C.gowchar_set((*C.wchar_t)(res), C.int(i), C.wchar_t(r)) - i++ - } - s = s[size:] - } - C.gowchar_set((*C.wchar_t)(res), C.int(slen-1), C.wchar_t(0)) // \0 - return (*C.wchar_t)(res), C.size_t(slen) -} - -// Unix -func stringToWchar4(s string) (*C.wchar_t, C.size_t) { - slen := utf8.RuneCountInString(s) - slen++ // \0 - res := C.malloc(C.size_t(slen) * SIZEOF_WCHAR_T) - var i int - for len(s) > 0 { - r, size := utf8.DecodeRuneInString(s) - C.gowchar_set((*C.wchar_t)(res), C.int(i), C.wchar_t(r)) - s = s[size:] - i++ - } - C.gowchar_set((*C.wchar_t)(res), C.int(slen-1), C.wchar_t(0)) // \0 - return (*C.wchar_t)(res), C.size_t(slen) -} diff --git a/adapters/taglib/taglib.go b/adapters/taglib/taglib.go deleted file mode 100644 index ac299ea2..00000000 --- a/adapters/taglib/taglib.go +++ /dev/null @@ -1,178 +0,0 @@ -package taglib - -import ( - "io/fs" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/core/storage/local" - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/model/metadata" -) - -type extractor struct { - baseDir string -} - -func (e extractor) Parse(files ...string) (map[string]metadata.Info, error) { - results := make(map[string]metadata.Info) - for _, path := range files { - props, err := e.extractMetadata(path) - if err != nil { - continue - } - results[path] = *props - } - return results, nil -} - -func (e extractor) Version() string { - return Version() -} - -func (e extractor) extractMetadata(filePath string) (*metadata.Info, error) { - fullPath := filepath.Join(e.baseDir, filePath) - tags, err := Read(fullPath) - if err != nil { - log.Warn("extractor: Error reading metadata from file. Skipping", "filePath", fullPath, err) - return nil, err - } - - // Parse audio properties - ap := metadata.AudioProperties{} - ap.BitRate = parseProp(tags, "__bitrate") - ap.Channels = parseProp(tags, "__channels") - ap.SampleRate = parseProp(tags, "__samplerate") - ap.BitDepth = parseProp(tags, "__bitspersample") - length := parseProp(tags, "__lengthinmilliseconds") - ap.Duration = (time.Millisecond * time.Duration(length)).Round(time.Millisecond * 10) - - // Extract basic tags - parseBasicTag(tags, "__title", "title") - parseBasicTag(tags, "__artist", "artist") - parseBasicTag(tags, "__album", "album") - parseBasicTag(tags, "__comment", "comment") - parseBasicTag(tags, "__genre", "genre") - parseBasicTag(tags, "__year", "year") - parseBasicTag(tags, "__track", "tracknumber") - - // Parse track/disc totals - parseTuple := func(prop string) { - tagName := prop + "number" - tagTotal := prop + "total" - if value, ok := tags[tagName]; ok && len(value) > 0 { - parts := strings.Split(value[0], "/") - tags[tagName] = []string{parts[0]} - if len(parts) == 2 { - tags[tagTotal] = []string{parts[1]} - } - } - } - parseTuple("track") - parseTuple("disc") - - // Adjust some ID3 tags - parseLyrics(tags) - parseTIPL(tags) - delete(tags, "tmcl") // TMCL is already parsed by TagLib - - return &metadata.Info{ - Tags: tags, - AudioProperties: ap, - HasPicture: tags["has_picture"] != nil && len(tags["has_picture"]) > 0 && tags["has_picture"][0] == "true", - }, nil -} - -// parseLyrics make sure lyrics tags have language -func parseLyrics(tags map[string][]string) { - lyrics := tags["lyrics"] - if len(lyrics) > 0 { - tags["lyrics:xxx"] = lyrics - delete(tags, "lyrics") - } -} - -// These are the only roles we support, based on Picard's tag map: -// https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html -var tiplMapping = map[string]string{ - "arranger": "arranger", - "engineer": "engineer", - "producer": "producer", - "mix": "mixer", - "DJ-mix": "djmixer", -} - -// parseProp parses a property from the tags map and sets it to the target integer. -// It also deletes the property from the tags map after parsing. -func parseProp(tags map[string][]string, prop string) int { - if value, ok := tags[prop]; ok && len(value) > 0 { - v, _ := strconv.Atoi(value[0]) - delete(tags, prop) - return v - } - return 0 -} - -// parseBasicTag checks if a basic tag (like __title, __artist, etc.) exists in the tags map. -// If it does, it moves the value to a more appropriate tag name (like title, artist, etc.), -// and deletes the basic tag from the map. If the target tag already exists, it ignores the basic tag. -func parseBasicTag(tags map[string][]string, basicName string, tagName string) { - basicValue := tags[basicName] - if len(basicValue) == 0 { - return - } - delete(tags, basicName) - if len(tags[tagName]) == 0 { - tags[tagName] = basicValue - } -} - -// parseTIPL parses the ID3v2.4 TIPL frame string, which is received from TagLib in the format: -// -// "arranger Andrew Powell engineer Chris Blair engineer Pat Stapley producer Eric Woolfson". -// -// and breaks it down into a map of roles and names, e.g.: -// -// {"arranger": ["Andrew Powell"], "engineer": ["Chris Blair", "Pat Stapley"], "producer": ["Eric Woolfson"]}. -func parseTIPL(tags map[string][]string) { - tipl := tags["tipl"] - if len(tipl) == 0 { - return - } - - addRole := func(currentRole string, currentValue []string) { - if currentRole != "" && len(currentValue) > 0 { - role := tiplMapping[currentRole] - tags[role] = append(tags[role], strings.Join(currentValue, " ")) - } - } - - var currentRole string - var currentValue []string - for _, part := range strings.Split(tipl[0], " ") { - if _, ok := tiplMapping[part]; ok { - addRole(currentRole, currentValue) - currentRole = part - currentValue = nil - continue - } - currentValue = append(currentValue, part) - } - addRole(currentRole, currentValue) - delete(tags, "tipl") -} - -var _ local.Extractor = (*extractor)(nil) - -func init() { - local.RegisterExtractor("legacy-taglib", func(_ fs.FS, baseDir string) local.Extractor { - // ignores fs, as taglib extractor only works with local files - return &extractor{baseDir} - }) - conf.AddHook(func() { - log.Debug("TagLib version", "version", Version()) - }) -} diff --git a/adapters/taglib/taglib_suite_test.go b/adapters/taglib/taglib_suite_test.go deleted file mode 100644 index 2b26612c..00000000 --- a/adapters/taglib/taglib_suite_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package taglib - -import ( - "testing" - - "github.com/navidrome/navidrome/log" - "github.com/navidrome/navidrome/tests" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestTagLib(t *testing.T) { - tests.Init(t, true) - log.SetLevel(log.LevelFatal) - RegisterFailHandler(Fail) - RunSpecs(t, "TagLib Suite") -} diff --git a/adapters/taglib/taglib_test.go b/adapters/taglib/taglib_test.go deleted file mode 100644 index f524f77e..00000000 --- a/adapters/taglib/taglib_test.go +++ /dev/null @@ -1,295 +0,0 @@ -package taglib - -import ( - "io/fs" - "os" - "strings" - - "github.com/navidrome/navidrome/utils" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("Extractor", func() { - var e *extractor - - BeforeEach(func() { - e = &extractor{} - }) - - Describe("Parse", func() { - It("correctly parses metadata from all files in folder", func() { - mds, err := e.Parse( - "tests/fixtures/test.mp3", - "tests/fixtures/test.ogg", - ) - Expect(err).NotTo(HaveOccurred()) - Expect(mds).To(HaveLen(2)) - - // Test MP3 - m := mds["tests/fixtures/test.mp3"] - Expect(m.Tags).To(HaveKeyWithValue("title", []string{"Song"})) - Expect(m.Tags).To(HaveKeyWithValue("album", []string{"Album"})) - Expect(m.Tags).To(HaveKeyWithValue("artist", []string{"Artist"})) - Expect(m.Tags).To(HaveKeyWithValue("albumartist", []string{"Album Artist"})) - - Expect(m.HasPicture).To(BeTrue()) - Expect(m.AudioProperties.Duration.String()).To(Equal("1.02s")) - Expect(m.AudioProperties.BitRate).To(Equal(192)) - Expect(m.AudioProperties.Channels).To(Equal(2)) - Expect(m.AudioProperties.SampleRate).To(Equal(44100)) - - Expect(m.Tags).To(Or( - HaveKeyWithValue("compilation", []string{"1"}), - HaveKeyWithValue("tcmp", []string{"1"})), - ) - Expect(m.Tags).To(HaveKeyWithValue("genre", []string{"Rock"})) - Expect(m.Tags).To(HaveKeyWithValue("date", []string{"2014-05-21"})) - Expect(m.Tags).To(HaveKeyWithValue("originaldate", []string{"1996-11-21"})) - Expect(m.Tags).To(HaveKeyWithValue("releasedate", []string{"2020-12-31"})) - Expect(m.Tags).To(HaveKeyWithValue("discnumber", []string{"1"})) - Expect(m.Tags).To(HaveKeyWithValue("disctotal", []string{"2"})) - Expect(m.Tags).To(HaveKeyWithValue("comment", []string{"Comment1\nComment2"})) - Expect(m.Tags).To(HaveKeyWithValue("bpm", []string{"123"})) - Expect(m.Tags).To(HaveKeyWithValue("replaygain_album_gain", []string{"+3.21518 dB"})) - Expect(m.Tags).To(HaveKeyWithValue("replaygain_album_peak", []string{"0.9125"})) - Expect(m.Tags).To(HaveKeyWithValue("replaygain_track_gain", []string{"-1.48 dB"})) - Expect(m.Tags).To(HaveKeyWithValue("replaygain_track_peak", []string{"0.4512"})) - - Expect(m.Tags).To(HaveKeyWithValue("tracknumber", []string{"2"})) - Expect(m.Tags).To(HaveKeyWithValue("tracktotal", []string{"10"})) - - Expect(m.Tags).ToNot(HaveKey("lyrics")) - Expect(m.Tags).To(Or(HaveKeyWithValue("lyrics:eng", []string{ - "[00:00.00]This is\n[00:02.50]English SYLT\n", - "[00:00.00]This is\n[00:02.50]English", - }), HaveKeyWithValue("lyrics:eng", []string{ - "[00:00.00]This is\n[00:02.50]English", - "[00:00.00]This is\n[00:02.50]English SYLT\n", - }))) - Expect(m.Tags).To(Or(HaveKeyWithValue("lyrics:xxx", []string{ - "[00:00.00]This is\n[00:02.50]unspecified SYLT\n", - "[00:00.00]This is\n[00:02.50]unspecified", - }), HaveKeyWithValue("lyrics:xxx", []string{ - "[00:00.00]This is\n[00:02.50]unspecified", - "[00:00.00]This is\n[00:02.50]unspecified SYLT\n", - }))) - - // Test OGG - m = mds["tests/fixtures/test.ogg"] - Expect(err).To(BeNil()) - Expect(m.Tags).To(HaveKeyWithValue("fbpm", []string{"141.7"})) - - // TagLib 1.12 returns 18, previous versions return 39. - // See https://github.com/taglib/taglib/commit/2f238921824741b2cfe6fbfbfc9701d9827ab06b - Expect(m.AudioProperties.BitRate).To(BeElementOf(18, 19, 39, 40, 43, 49)) - Expect(m.AudioProperties.Channels).To(BeElementOf(2)) - Expect(m.AudioProperties.SampleRate).To(BeElementOf(8000)) - Expect(m.HasPicture).To(BeTrue()) - }) - - DescribeTable("Format-Specific tests", - func(file, duration string, channels, samplerate, bitdepth int, albumGain, albumPeak, trackGain, trackPeak string, id3Lyrics bool, image bool) { - file = "tests/fixtures/" + file - mds, err := e.Parse(file) - Expect(err).NotTo(HaveOccurred()) - Expect(mds).To(HaveLen(1)) - - m := mds[file] - - Expect(m.HasPicture).To(Equal(image)) - Expect(m.AudioProperties.Duration.String()).To(Equal(duration)) - Expect(m.AudioProperties.Channels).To(Equal(channels)) - Expect(m.AudioProperties.SampleRate).To(Equal(samplerate)) - Expect(m.AudioProperties.BitDepth).To(Equal(bitdepth)) - - Expect(m.Tags).To(Or( - HaveKeyWithValue("replaygain_album_gain", []string{albumGain}), - HaveKeyWithValue("----:com.apple.itunes:replaygain_album_gain", []string{albumGain}), - )) - - Expect(m.Tags).To(Or( - HaveKeyWithValue("replaygain_album_peak", []string{albumPeak}), - HaveKeyWithValue("----:com.apple.itunes:replaygain_album_peak", []string{albumPeak}), - )) - Expect(m.Tags).To(Or( - HaveKeyWithValue("replaygain_track_gain", []string{trackGain}), - HaveKeyWithValue("----:com.apple.itunes:replaygain_track_gain", []string{trackGain}), - )) - Expect(m.Tags).To(Or( - HaveKeyWithValue("replaygain_track_peak", []string{trackPeak}), - HaveKeyWithValue("----:com.apple.itunes:replaygain_track_peak", []string{trackPeak}), - )) - - Expect(m.Tags).To(HaveKeyWithValue("title", []string{"Title"})) - Expect(m.Tags).To(HaveKeyWithValue("album", []string{"Album"})) - Expect(m.Tags).To(HaveKeyWithValue("artist", []string{"Artist"})) - Expect(m.Tags).To(HaveKeyWithValue("albumartist", []string{"Album Artist"})) - Expect(m.Tags).To(HaveKeyWithValue("genre", []string{"Rock"})) - Expect(m.Tags).To(HaveKeyWithValue("date", []string{"2014"})) - - Expect(m.Tags).To(HaveKeyWithValue("bpm", []string{"123"})) - Expect(m.Tags).To(Or( - HaveKeyWithValue("tracknumber", []string{"3"}), - HaveKeyWithValue("tracknumber", []string{"3/10"}), - )) - if !strings.HasSuffix(file, "test.wma") { - // TODO Not sure why this is not working for WMA - Expect(m.Tags).To(HaveKeyWithValue("tracktotal", []string{"10"})) - } - Expect(m.Tags).To(Or( - HaveKeyWithValue("discnumber", []string{"1"}), - HaveKeyWithValue("discnumber", []string{"1/2"}), - )) - Expect(m.Tags).To(HaveKeyWithValue("disctotal", []string{"2"})) - - // WMA does not have a "compilation" tag, but "wm/iscompilation" - Expect(m.Tags).To(Or( - HaveKeyWithValue("compilation", []string{"1"}), - HaveKeyWithValue("wm/iscompilation", []string{"1"})), - ) - - if id3Lyrics { - Expect(m.Tags).To(HaveKeyWithValue("lyrics:eng", []string{ - "[00:00.00]This is\n[00:02.50]English", - })) - Expect(m.Tags).To(HaveKeyWithValue("lyrics:xxx", []string{ - "[00:00.00]This is\n[00:02.50]unspecified", - })) - } else { - Expect(m.Tags).To(HaveKeyWithValue("lyrics:xxx", []string{ - "[00:00.00]This is\n[00:02.50]unspecified", - "[00:00.00]This is\n[00:02.50]English", - })) - } - - Expect(m.Tags).To(HaveKeyWithValue("comment", []string{"Comment1\nComment2"})) - }, - - // ffmpeg -f lavfi -i "sine=frequency=1200:duration=1" test.flac - Entry("correctly parses flac tags", "test.flac", "1s", 1, 44100, 16, "+4.06 dB", "0.12496948", "+4.06 dB", "0.12496948", false, true), - - Entry("correctly parses m4a (aac) gain tags", "01 Invisible (RED) Edit Version.m4a", "1.04s", 2, 44100, 16, "0.37", "0.48", "0.37", "0.48", false, true), - Entry("correctly parses m4a (aac) gain tags (uppercase)", "test.m4a", "1.04s", 2, 44100, 16, "0.37", "0.48", "0.37", "0.48", false, true), - Entry("correctly parses ogg (vorbis) tags", "test.ogg", "1.04s", 2, 8000, 0, "+7.64 dB", "0.11772506", "+7.64 dB", "0.11772506", false, true), - - // ffmpeg -f lavfi -i "sine=frequency=900:duration=1" test.wma - // Weird note: for the tag parsing to work, the lyrics are actually stored in the reverse order - Entry("correctly parses wma/asf tags", "test.wma", "1.02s", 1, 44100, 16, "3.27 dB", "0.132914", "3.27 dB", "0.132914", false, true), - - // ffmpeg -f lavfi -i "sine=frequency=800:duration=1" test.wv - Entry("correctly parses wv (wavpak) tags", "test.wv", "1s", 1, 44100, 16, "3.43 dB", "0.125061", "3.43 dB", "0.125061", false, true), - - // ffmpeg -f lavfi -i "sine=frequency=1000:duration=1" test.wav - Entry("correctly parses wav tags", "test.wav", "1s", 1, 44100, 16, "3.06 dB", "0.125056", "3.06 dB", "0.125056", true, true), - - // ffmpeg -f lavfi -i "sine=frequency=1400:duration=1" test.aiff - Entry("correctly parses aiff tags", "test.aiff", "1s", 1, 44100, 16, "2.00 dB", "0.124972", "2.00 dB", "0.124972", true, true), - ) - - // Skip these tests when running as root - Context("Access Forbidden", func() { - var accessForbiddenFile string - var RegularUserContext = XContext - var isRegularUser = os.Getuid() != 0 - if isRegularUser { - RegularUserContext = Context - } - - // Only run permission tests if we are not root - RegularUserContext("when run without root privileges", func() { - BeforeEach(func() { - accessForbiddenFile = utils.TempFileName("access_forbidden-", ".mp3") - - f, err := os.OpenFile(accessForbiddenFile, os.O_WRONLY|os.O_CREATE, 0222) - Expect(err).ToNot(HaveOccurred()) - - DeferCleanup(func() { - Expect(f.Close()).To(Succeed()) - Expect(os.Remove(accessForbiddenFile)).To(Succeed()) - }) - }) - - It("correctly handle unreadable file due to insufficient read permission", func() { - _, err := e.extractMetadata(accessForbiddenFile) - Expect(err).To(MatchError(os.ErrPermission)) - }) - - It("skips the file if it cannot be read", func() { - files := []string{ - "tests/fixtures/test.mp3", - "tests/fixtures/test.ogg", - accessForbiddenFile, - } - mds, err := e.Parse(files...) - Expect(err).NotTo(HaveOccurred()) - Expect(mds).To(HaveLen(2)) - Expect(mds).ToNot(HaveKey(accessForbiddenFile)) - }) - }) - }) - - }) - - Describe("Error Checking", func() { - It("returns a generic ErrPath if file does not exist", func() { - testFilePath := "tests/fixtures/NON_EXISTENT.ogg" - _, err := e.extractMetadata(testFilePath) - Expect(err).To(MatchError(fs.ErrNotExist)) - }) - It("does not throw a SIGSEGV error when reading a file with an invalid frame", func() { - // File has an empty TDAT frame - md, err := e.extractMetadata("tests/fixtures/invalid-files/test-invalid-frame.mp3") - Expect(err).ToNot(HaveOccurred()) - Expect(md.Tags).To(HaveKeyWithValue("albumartist", []string{"Elvis Presley"})) - }) - }) - - Describe("parseTIPL", func() { - var tags map[string][]string - - BeforeEach(func() { - tags = make(map[string][]string) - }) - - Context("when the TIPL string is populated", func() { - It("correctly parses roles and names", func() { - tags["tipl"] = []string{"arranger Andrew Powell DJ-mix François Kevorkian DJ-mix Jane Doe engineer Chris Blair"} - parseTIPL(tags) - Expect(tags["arranger"]).To(ConsistOf("Andrew Powell")) - Expect(tags["engineer"]).To(ConsistOf("Chris Blair")) - Expect(tags["djmixer"]).To(ConsistOf("François Kevorkian", "Jane Doe")) - }) - - It("handles multiple names for a single role", func() { - tags["tipl"] = []string{"engineer Pat Stapley producer Eric Woolfson engineer Chris Blair"} - parseTIPL(tags) - Expect(tags["producer"]).To(ConsistOf("Eric Woolfson")) - Expect(tags["engineer"]).To(ConsistOf("Pat Stapley", "Chris Blair")) - }) - - It("discards roles without names", func() { - tags["tipl"] = []string{"engineer Pat Stapley producer engineer Chris Blair"} - parseTIPL(tags) - Expect(tags).ToNot(HaveKey("producer")) - Expect(tags["engineer"]).To(ConsistOf("Pat Stapley", "Chris Blair")) - }) - }) - - Context("when the TIPL string is empty", func() { - It("does nothing", func() { - tags["tipl"] = []string{""} - parseTIPL(tags) - Expect(tags).To(BeEmpty()) - }) - }) - - Context("when the TIPL is not present", func() { - It("does nothing", func() { - parseTIPL(tags) - Expect(tags).To(BeEmpty()) - }) - }) - }) - -}) diff --git a/adapters/taglib/taglib_wrapper.cpp b/adapters/taglib/taglib_wrapper.cpp deleted file mode 100644 index 2985e8f1..00000000 --- a/adapters/taglib/taglib_wrapper.cpp +++ /dev/null @@ -1,299 +0,0 @@ -#include -#include - -#define TAGLIB_STATIC -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "taglib_wrapper.h" - -char has_cover(const TagLib::FileRef f); - -static char TAGLIB_VERSION[16]; - -char* taglib_version() { - snprintf((char *)TAGLIB_VERSION, 16, "%d.%d.%d", TAGLIB_MAJOR_VERSION, TAGLIB_MINOR_VERSION, TAGLIB_PATCH_VERSION); - return (char *)TAGLIB_VERSION; -} - -int taglib_read(const FILENAME_CHAR_T *filename, unsigned long id) { - TagLib::FileRef f(filename, true, TagLib::AudioProperties::Fast); - - if (f.isNull()) { - return TAGLIB_ERR_PARSE; - } - - if (!f.audioProperties()) { - return TAGLIB_ERR_AUDIO_PROPS; - } - - // Add audio properties to the tags - const TagLib::AudioProperties *props(f.audioProperties()); - goPutInt(id, (char *)"__lengthinmilliseconds", props->lengthInMilliseconds()); - goPutInt(id, (char *)"__bitrate", props->bitrate()); - goPutInt(id, (char *)"__channels", props->channels()); - goPutInt(id, (char *)"__samplerate", props->sampleRate()); - - // Extract bits per sample for supported formats - int bitsPerSample = 0; - if (const auto* apeProperties{ dynamic_cast(props) }) - bitsPerSample = apeProperties->bitsPerSample(); - else if (const auto* asfProperties{ dynamic_cast(props) }) - bitsPerSample = asfProperties->bitsPerSample(); - else if (const auto* flacProperties{ dynamic_cast(props) }) - bitsPerSample = flacProperties->bitsPerSample(); - else if (const auto* mp4Properties{ dynamic_cast(props) }) - bitsPerSample = mp4Properties->bitsPerSample(); - else if (const auto* wavePackProperties{ dynamic_cast(props) }) - bitsPerSample = wavePackProperties->bitsPerSample(); - else if (const auto* aiffProperties{ dynamic_cast(props) }) - bitsPerSample = aiffProperties->bitsPerSample(); - else if (const auto* wavProperties{ dynamic_cast(props) }) - bitsPerSample = wavProperties->bitsPerSample(); - else if (const auto* dsfProperties{ dynamic_cast(props) }) - bitsPerSample = dsfProperties->bitsPerSample(); - - if (bitsPerSample > 0) { - goPutInt(id, (char *)"__bitspersample", bitsPerSample); - } - - // Send all properties to the Go map - TagLib::PropertyMap tags = f.file()->properties(); - - // Make sure at least the basic properties are extracted - TagLib::Tag *basic = f.file()->tag(); - if (!basic->isEmpty()) { - if (!basic->title().isEmpty()) { - tags.insert("__title", basic->title()); - } - if (!basic->artist().isEmpty()) { - tags.insert("__artist", basic->artist()); - } - if (!basic->album().isEmpty()) { - tags.insert("__album", basic->album()); - } - if (!basic->comment().isEmpty()) { - tags.insert("__comment", basic->comment()); - } - if (!basic->genre().isEmpty()) { - tags.insert("__genre", basic->genre()); - } - if (basic->year() > 0) { - tags.insert("__year", TagLib::String::number(basic->year())); - } - if (basic->track() > 0) { - tags.insert("__track", TagLib::String::number(basic->track())); - } - } - - TagLib::ID3v2::Tag *id3Tags = NULL; - - // Get some extended/non-standard ID3-only tags (ex: iTunes extended frames) - TagLib::MPEG::File *mp3File(dynamic_cast(f.file())); - if (mp3File != NULL) { - id3Tags = mp3File->ID3v2Tag(); - } - - if (id3Tags == NULL) { - TagLib::RIFF::WAV::File *wavFile(dynamic_cast(f.file())); - if (wavFile != NULL && wavFile->hasID3v2Tag()) { - id3Tags = wavFile->ID3v2Tag(); - } - } - - if (id3Tags == NULL) { - TagLib::RIFF::AIFF::File *aiffFile(dynamic_cast(f.file())); - if (aiffFile && aiffFile->hasID3v2Tag()) { - id3Tags = aiffFile->tag(); - } - } - - // Yes, it is possible to have ID3v2 tags in FLAC. However, that can cause problems - // with many players, so they will not be parsed - - if (id3Tags != NULL) { - const auto &frames = id3Tags->frameListMap(); - - for (const auto &kv: frames) { - if (kv.first == "USLT") { - for (const auto &tag: kv.second) { - TagLib::ID3v2::UnsynchronizedLyricsFrame *frame = dynamic_cast(tag); - if (frame == NULL) continue; - - tags.erase("LYRICS"); - - const auto bv = frame->language(); - char language[4] = {'x', 'x', 'x', '\0'}; - if (bv.size() == 3) { - strncpy(language, bv.data(), 3); - } - - char *val = const_cast(frame->text().toCString(true)); - - goPutLyrics(id, language, val); - } - } else if (kv.first == "SYLT") { - for (const auto &tag: kv.second) { - TagLib::ID3v2::SynchronizedLyricsFrame *frame = dynamic_cast(tag); - if (frame == NULL) continue; - - const auto bv = frame->language(); - char language[4] = {'x', 'x', 'x', '\0'}; - if (bv.size() == 3) { - strncpy(language, bv.data(), 3); - } - - const auto format = frame->timestampFormat(); - if (format == TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds) { - - for (const auto &line: frame->synchedText()) { - char *text = const_cast(line.text.toCString(true)); - goPutLyricLine(id, language, text, line.time); - } - } else if (format == TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMpegFrames) { - const int sampleRate = props->sampleRate(); - - if (sampleRate != 0) { - for (const auto &line: frame->synchedText()) { - const int timeInMs = (line.time * 1000) / sampleRate; - char *text = const_cast(line.text.toCString(true)); - goPutLyricLine(id, language, text, timeInMs); - } - } - } - } - } else if (kv.first == "TIPL"){ - if (!kv.second.isEmpty()) { - tags.insert(kv.first, kv.second.front()->toString()); - } - } - } - } - - // M4A may have some iTunes specific tags not captured by the PropertyMap interface - TagLib::MP4::File *m4afile(dynamic_cast(f.file())); - if (m4afile != NULL) { - const auto itemListMap = m4afile->tag()->itemMap(); - for (const auto item: itemListMap) { - char *key = const_cast(item.first.toCString(true)); - for (const auto value: item.second.toStringList()) { - char *val = const_cast(value.toCString(true)); - goPutM4AStr(id, key, val); - } - } - } - - // WMA/ASF files may have additional tags not captured by the PropertyMap interface - TagLib::ASF::File *asfFile(dynamic_cast(f.file())); - if (asfFile != NULL) { - const TagLib::ASF::Tag *asfTags{asfFile->tag()}; - const auto itemListMap = asfTags->attributeListMap(); - for (const auto item : itemListMap) { - char *key = const_cast(item.first.toCString(true)); - - for (auto j = item.second.begin(); - j != item.second.end(); ++j) { - - char *val = const_cast(j->toString().toCString(true)); - goPutStr(id, key, val); - } - } - } - - // Send all collected tags to the Go map - for (TagLib::PropertyMap::ConstIterator i = tags.begin(); i != tags.end(); - ++i) { - char *key = const_cast(i->first.toCString(true)); - for (TagLib::StringList::ConstIterator j = i->second.begin(); - j != i->second.end(); ++j) { - char *val = const_cast((*j).toCString(true)); - goPutStr(id, key, val); - } - } - - // Cover art has to be handled separately - if (has_cover(f)) { - goPutStr(id, (char *)"has_picture", (char *)"true"); - } - - return 0; -} - -// Detect if the file has cover art. Returns 1 if the file has cover art, 0 otherwise. -char has_cover(const TagLib::FileRef f) { - char hasCover = 0; - // ----- MP3 - if (TagLib::MPEG::File * mp3File{dynamic_cast(f.file())}) { - if (mp3File->ID3v2Tag()) { - const auto &frameListMap{mp3File->ID3v2Tag()->frameListMap()}; - hasCover = !frameListMap["APIC"].isEmpty(); - } - } - // ----- FLAC - else if (TagLib::FLAC::File * flacFile{dynamic_cast(f.file())}) { - hasCover = !flacFile->pictureList().isEmpty(); - } - // ----- MP4 - else if (TagLib::MP4::File * mp4File{dynamic_cast(f.file())}) { - auto &coverItem{mp4File->tag()->itemMap()["covr"]}; - TagLib::MP4::CoverArtList coverArtList{coverItem.toCoverArtList()}; - hasCover = !coverArtList.isEmpty(); - } - // ----- Ogg - else if (TagLib::Ogg::Vorbis::File * vorbisFile{dynamic_cast(f.file())}) { - hasCover = !vorbisFile->tag()->pictureList().isEmpty(); - } - // ----- Opus - else if (TagLib::Ogg::Opus::File * opusFile{dynamic_cast(f.file())}) { - hasCover = !opusFile->tag()->pictureList().isEmpty(); - } - // ----- WAV - else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast(f.file()) }) { - if (wavFile->hasID3v2Tag()) { - const auto& frameListMap{ wavFile->ID3v2Tag()->frameListMap() }; - hasCover = !frameListMap["APIC"].isEmpty(); - } - } - // ----- AIFF - else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast(f.file())}) { - if (aiffFile->hasID3v2Tag()) { - const auto& frameListMap{ aiffFile->tag()->frameListMap() }; - hasCover = !frameListMap["APIC"].isEmpty(); - } - } - // ----- WMA - else if (TagLib::ASF::File * asfFile{dynamic_cast(f.file())}) { - const TagLib::ASF::Tag *tag{ asfFile->tag() }; - hasCover = tag && tag->attributeListMap().contains("WM/Picture"); - } - // ----- DSF - else if (TagLib::DSF::File * dsffile{ dynamic_cast(f.file())}) { - const TagLib::ID3v2::Tag *tag { dsffile->tag() }; - hasCover = tag && !tag->frameListMap()["APIC"].isEmpty(); - } - // ----- WAVPAK (APE tag) - else if (TagLib::WavPack::File * wvFile{dynamic_cast(f.file())}) { - if (wvFile->hasAPETag()) { - // This is the particular string that Picard uses - hasCover = !wvFile->APETag()->itemListMap()["COVER ART (FRONT)"].isEmpty(); - } - } - - return hasCover; -} diff --git a/adapters/taglib/taglib_wrapper.go b/adapters/taglib/taglib_wrapper.go deleted file mode 100644 index 4a979920..00000000 --- a/adapters/taglib/taglib_wrapper.go +++ /dev/null @@ -1,157 +0,0 @@ -package taglib - -/* -#cgo !windows pkg-config: --define-prefix taglib -#cgo windows pkg-config: taglib -#cgo illumos LDFLAGS: -lstdc++ -lsendfile -#cgo linux darwin CXXFLAGS: -std=c++11 -#cgo darwin LDFLAGS: -L/opt/homebrew/opt/taglib/lib -#include -#include -#include -#include "taglib_wrapper.h" -*/ -import "C" -import ( - "encoding/json" - "fmt" - "os" - "runtime/debug" - "strconv" - "strings" - "sync" - "sync/atomic" - "unsafe" - - "github.com/navidrome/navidrome/log" -) - -const iTunesKeyPrefix = "----:com.apple.itunes:" - -func Version() string { - return C.GoString(C.taglib_version()) -} - -func Read(filename string) (tags map[string][]string, err error) { - // Do not crash on failures in the C code/library - debug.SetPanicOnFault(true) - defer func() { - if r := recover(); r != nil { - log.Error("extractor: recovered from panic when reading tags", "file", filename, "error", r) - err = fmt.Errorf("extractor: recovered from panic: %s", r) - } - }() - - fp := getFilename(filename) - defer C.free(unsafe.Pointer(fp)) - id, m, release := newMap() - defer release() - - log.Trace("extractor: reading tags", "filename", filename, "map_id", id) - res := C.taglib_read(fp, C.ulong(id)) - switch res { - case C.TAGLIB_ERR_PARSE: - // Check additional case whether the file is unreadable due to permission - file, fileErr := os.OpenFile(filename, os.O_RDONLY, 0600) - defer file.Close() - - if os.IsPermission(fileErr) { - return nil, fmt.Errorf("navidrome does not have permission: %w", fileErr) - } else if fileErr != nil { - return nil, fmt.Errorf("cannot parse file media file: %w", fileErr) - } else { - return nil, fmt.Errorf("cannot parse file media file") - } - case C.TAGLIB_ERR_AUDIO_PROPS: - return nil, fmt.Errorf("can't get audio properties from file") - } - if log.IsGreaterOrEqualTo(log.LevelDebug) { - j, _ := json.Marshal(m) - log.Trace("extractor: read tags", "tags", string(j), "filename", filename, "id", id) - } else { - log.Trace("extractor: read tags", "tags", m, "filename", filename, "id", id) - } - - return m, nil -} - -type tagMap map[string][]string - -var allMaps sync.Map -var mapsNextID atomic.Uint32 - -func newMap() (uint32, tagMap, func()) { - id := mapsNextID.Add(1) - - m := tagMap{} - allMaps.Store(id, m) - - return id, m, func() { - allMaps.Delete(id) - } -} - -func doPutTag(id C.ulong, key string, val *C.char) { - if key == "" { - return - } - - r, _ := allMaps.Load(uint32(id)) - m := r.(tagMap) - k := strings.ToLower(key) - v := strings.TrimSpace(C.GoString(val)) - m[k] = append(m[k], v) -} - -//export goPutM4AStr -func goPutM4AStr(id C.ulong, key *C.char, val *C.char) { - k := C.GoString(key) - - // Special for M4A, do not catch keys that have no actual name - k = strings.TrimPrefix(k, iTunesKeyPrefix) - doPutTag(id, k, val) -} - -//export goPutStr -func goPutStr(id C.ulong, key *C.char, val *C.char) { - doPutTag(id, C.GoString(key), val) -} - -//export goPutInt -func goPutInt(id C.ulong, key *C.char, val C.int) { - valStr := strconv.Itoa(int(val)) - vp := C.CString(valStr) - defer C.free(unsafe.Pointer(vp)) - goPutStr(id, key, vp) -} - -//export goPutLyrics -func goPutLyrics(id C.ulong, lang *C.char, val *C.char) { - doPutTag(id, "lyrics:"+C.GoString(lang), val) -} - -//export goPutLyricLine -func goPutLyricLine(id C.ulong, lang *C.char, text *C.char, time C.int) { - language := C.GoString(lang) - line := C.GoString(text) - timeGo := int64(time) - - ms := timeGo % 1000 - timeGo /= 1000 - sec := timeGo % 60 - timeGo /= 60 - minimum := timeGo % 60 - formattedLine := fmt.Sprintf("[%02d:%02d.%02d]%s\n", minimum, sec, ms/10, line) - - key := "lyrics:" + language - - r, _ := allMaps.Load(uint32(id)) - m := r.(tagMap) - k := strings.ToLower(key) - existing, ok := m[k] - if ok { - existing[0] += formattedLine - } else { - m[k] = []string{formattedLine} - } -} diff --git a/adapters/taglib/taglib_wrapper.h b/adapters/taglib/taglib_wrapper.h deleted file mode 100644 index c93f4c14..00000000 --- a/adapters/taglib/taglib_wrapper.h +++ /dev/null @@ -1,24 +0,0 @@ -#define TAGLIB_ERR_PARSE -1 -#define TAGLIB_ERR_AUDIO_PROPS -2 - -#ifdef __cplusplus -extern "C" { -#endif - -#ifdef WIN32 -#define FILENAME_CHAR_T wchar_t -#else -#define FILENAME_CHAR_T char -#endif - -extern void goPutM4AStr(unsigned long id, char *key, char *val); -extern void goPutStr(unsigned long id, char *key, char *val); -extern void goPutInt(unsigned long id, char *key, int val); -extern void goPutLyrics(unsigned long id, char *lang, char *val); -extern void goPutLyricLine(unsigned long id, char *lang, char *text, int time); -int taglib_read(const FILENAME_CHAR_T *filename, unsigned long id); -char* taglib_version(); - -#ifdef __cplusplus -} -#endif diff --git a/cmd/root.go b/cmd/root.go index 5fdb591f..08773176 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -27,7 +27,6 @@ import ( _ "github.com/navidrome/navidrome/adapters/gotaglib" _ "github.com/navidrome/navidrome/adapters/lastfm" _ "github.com/navidrome/navidrome/adapters/listenbrainz" - _ "github.com/navidrome/navidrome/adapters/taglib" ) var ( diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index b25b4c10..f66df2e7 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -40,7 +40,6 @@ import ( _ "github.com/navidrome/navidrome/adapters/gotaglib" _ "github.com/navidrome/navidrome/adapters/lastfm" _ "github.com/navidrome/navidrome/adapters/listenbrainz" - _ "github.com/navidrome/navidrome/adapters/taglib" ) // Injectors from wire_injectors.go: diff --git a/core/storage/local/local.go b/core/storage/local/local.go index cd60c9ef..5384581e 100644 --- a/core/storage/local/local.go +++ b/core/storage/local/local.go @@ -11,6 +11,7 @@ import ( "github.com/djherbis/times" "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/core/storage" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model/metadata" @@ -28,7 +29,13 @@ type localStorage struct { func newLocalStorage(u url.URL) storage.Storage { newExtractor, ok := extractors[conf.Server.Scanner.Extractor] if !ok || newExtractor == nil { - log.Fatal("Extractor not found", "path", conf.Server.Scanner.Extractor) + if conf.Server.Scanner.Extractor != consts.DefaultScannerExtractor { + log.Warn("Extractor not found, using default", "extractor", conf.Server.Scanner.Extractor, "default", consts.DefaultScannerExtractor) + } + newExtractor = extractors[consts.DefaultScannerExtractor] + if newExtractor == nil { + log.Fatal("Default extractor not registered", "extractor", consts.DefaultScannerExtractor) + } } isWindowsPath := filepath.VolumeName(u.Host) != "" if u.Scheme == storage.LocalSchemaID && isWindowsPath { diff --git a/core/storage/local/local_test.go b/core/storage/local/local_test.go index 3ed01bbc..b977ef4a 100644 --- a/core/storage/local/local_test.go +++ b/core/storage/local/local_test.go @@ -10,6 +10,7 @@ import ( "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf/configtest" + "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/core/storage" "github.com/navidrome/navidrome/model/metadata" . "github.com/onsi/ginkgo/v2" @@ -135,16 +136,31 @@ var _ = Describe("LocalStorage", func() { }) }) - Context("with invalid extractor", func() { - It("should handle extractor validation correctly", func() { - // Note: The actual implementation uses log.Fatal which exits the process, - // so we test the normal path where extractors exist + Context("when the configured extractor is not registered", func() { + var defaultExtractor *mockTestExtractor + + BeforeEach(func() { + defaultExtractor = &mockTestExtractor{results: make(map[string]metadata.Info)} + RegisterExtractor(consts.DefaultScannerExtractor, func(fs.FS, string) Extractor { + return defaultExtractor + }) + DeferCleanup(func() { + lock.Lock() + delete(extractors, consts.DefaultScannerExtractor) + lock.Unlock() + }) + }) + + It("falls back to the default extractor instead of crashing", func() { + conf.Server.Scanner.Extractor = "nonexistent-extractor" u, err := url.Parse("file://" + tempDir) Expect(err).ToNot(HaveOccurred()) storage := newLocalStorage(*u) - Expect(storage).ToNot(BeNil()) + ls, ok := storage.(*localStorage) + Expect(ok).To(BeTrue()) + Expect(ls.extractor).To(BeIdenticalTo(defaultExtractor)) }) }) })