Merge branch 'master' into album-rating-in-smart-playlists

This commit is contained in:
Deluan Quintão 2026-02-22 21:45:04 -05:00 committed by GitHub
commit 266e328731
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 121 additions and 10 deletions

View File

@ -134,7 +134,7 @@ func isDottedAbbreviation(w string, subTokens []string) bool {
// special characters to prevent query injection.
func buildFTS5Query(userInput string) string {
q := strings.TrimSpace(userInput)
if q == "" {
if q == "" || q == `""` {
return ""
}
@ -239,7 +239,7 @@ var ftsSearchColumns = map[string]string{
func ftsSearchExpr(tableName string, s string) Sqlizer {
q := buildFTS5Query(s)
if q == "" {
s = strings.TrimSpace(s)
s = strings.TrimSpace(strings.ReplaceAll(s, `"`, ""))
if s != "" {
log.Trace("Search using LIKE fallback for non-tokenizable query", "table", tableName, "query", s)
return likeSearchExpr(tableName, s)

View File

@ -49,6 +49,7 @@ var _ = DescribeTable("buildFTS5Query",
Entry("preserves quoted abbreviation verbatim", `"R.E.M."`, `"R.E.M."`),
Entry("returns empty string for punctuation-only input", "!!!!!!!", ""),
Entry("returns empty string for mixed punctuation", "!@#$%^&", ""),
Entry("returns empty string for empty quoted phrase", `""`, ""),
)
var _ = DescribeTable("normalizeForFTS",
@ -204,6 +205,10 @@ var _ = Describe("ftsSearchExpr", func() {
Expect(ftsSearchExpr("media_file", "")).To(BeNil())
Expect(ftsSearchExpr("media_file", " ")).To(BeNil())
})
It("returns nil for empty quoted phrase", func() {
Expect(ftsSearchExpr("media_file", `""`)).To(BeNil())
})
})
var _ = Describe("FTS5 Integration Search", func() {

View File

@ -52,6 +52,17 @@ type _t = map[string]any
var template = storagetest.Template
var track = storagetest.Track
// MusicBrainz ID constants for test data (valid UUID v4 values)
const (
mbidBeatlesArtist = "b10bbbfc-cf9e-42e0-be17-e2c3e1d2600d"
mbidAbbeyRoadAlbum = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
mbidAbbeyRoadRelGroup = "d4c3b2a1-f6e5-4b7a-9d8c-1f0e3a2b5c4d"
mbidComeTogether = "11111111-1111-4111-a111-111111111111" // mbz_release_track_id
mbidComeTogetherRec = "22222222-2222-4222-a222-222222222222" // mbz_recording_id
mbidSomething = "33333333-3333-4333-a333-333333333333" // mbz_release_track_id
mbidSomethingRec = "44444444-4444-4444-a444-444444444444" // mbz_recording_id
)
// Shared test state
var (
ctx context.Context
@ -89,23 +100,37 @@ func createFS(files fstest.MapFS) storagetest.FakeFS {
// buildTestFS creates the full test filesystem matching the plan
func buildTestFS() storagetest.FakeFS {
abbeyRoad := template(_t{"albumartist": "The Beatles", "artist": "The Beatles", "album": "Abbey Road", "year": 1969, "genre": "Rock"})
abbeyRoad := template(_t{
"albumartist": "The Beatles",
"artist": "The Beatles",
"album": "Abbey Road",
"year": 1969,
"genre": "Rock",
"musicbrainz_artistid": mbidBeatlesArtist,
"musicbrainz_albumartistid": mbidBeatlesArtist,
"musicbrainz_albumid": mbidAbbeyRoadAlbum,
"musicbrainz_releasegroupid": mbidAbbeyRoadRelGroup,
})
help := template(_t{"albumartist": "The Beatles", "artist": "The Beatles", "album": "Help!", "year": 1965, "genre": "Rock"})
ledZepIV := template(_t{"albumartist": "Led Zeppelin", "artist": "Led Zeppelin", "album": "IV", "year": 1971, "genre": "Rock"})
kindOfBlue := template(_t{"albumartist": "Miles Davis", "artist": "Miles Davis", "album": "Kind of Blue", "year": 1959, "genre": "Jazz"})
popTrack := template(_t{"albumartist": "Various", "artist": "Various", "album": "Pop", "year": 2020, "genre": "Pop"})
return createFS(fstest.MapFS{
// Rock / The Beatles / Abbey Road
"Rock/The Beatles/Abbey Road/01 - Come Together.mp3": abbeyRoad(track(1, "Come Together")),
"Rock/The Beatles/Abbey Road/02 - Something.mp3": abbeyRoad(track(2, "Something")),
// Rock / The Beatles / Help!
// Rock / The Beatles / Abbey Road (with MBIDs)
// Note: "musicbrainz_trackid" is an alias for the musicbrainz_recordingid tag (populates MbzRecordingID),
// "musicbrainz_releasetrackid" is an alias for the musicbrainz_trackid tag (populates MbzReleaseTrackID).
"Rock/The Beatles/Abbey Road/01 - Come Together.mp3": abbeyRoad(track(1, "Come Together",
_t{"musicbrainz_releasetrackid": mbidComeTogether, "musicbrainz_trackid": mbidComeTogetherRec})),
"Rock/The Beatles/Abbey Road/02 - Something.mp3": abbeyRoad(track(2, "Something",
_t{"musicbrainz_releasetrackid": mbidSomething, "musicbrainz_trackid": mbidSomethingRec})),
// Rock / The Beatles / Help! (no MBIDs)
"Rock/The Beatles/Help!/01 - Help.mp3": help(track(1, "Help!")),
// Rock / Led Zeppelin / IV
// Rock / Led Zeppelin / IV (no MBIDs)
"Rock/Led Zeppelin/IV/01 - Stairway To Heaven.mp3": ledZepIV(track(1, "Stairway To Heaven")),
// Jazz / Miles Davis / Kind of Blue
// Jazz / Miles Davis / Kind of Blue (no MBIDs)
"Jazz/Miles Davis/Kind of Blue/01 - So What.mp3": kindOfBlue(track(1, "So What")),
// Pop (standalone track)
// Pop (standalone track, no MBIDs)
"Pop/01 - Standalone Track.mp3": popTrack(track(1, "Standalone Track")),
// _empty folder (directory with no audio)
"_empty/.keep": &fstest.MapFile{Data: []byte{}, ModTime: time.Now()},

View File

@ -1,6 +1,7 @@
package e2e
import (
"github.com/google/uuid"
"github.com/navidrome/navidrome/server/subsonic/responses"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
@ -107,6 +108,16 @@ var _ = Describe("Search Endpoints", func() {
Expect(resp.SearchResult3.Artist[0].Id).ToNot(BeEmpty())
})
It("returns all results when query is empty (OpenSubsonic)", func() {
resp := doReq("search3", "query", "")
Expect(resp.Status).To(Equal(responses.StatusOK))
Expect(resp.SearchResult3).ToNot(BeNil())
Expect(resp.SearchResult3.Artist).To(HaveLen(4))
Expect(resp.SearchResult3.Album).To(HaveLen(5))
Expect(resp.SearchResult3.Song).To(HaveLen(6))
})
It("finds across all entity types simultaneously", func() {
// "Beatles" should match artist, albums, and songs by The Beatles
resp := doReq("search3", "query", "Beatles")
@ -136,5 +147,75 @@ var _ = Describe("Search Endpoints", func() {
Expect(s.Title).ToNot(BeEmpty())
}
})
Describe("MBID search", func() {
It("finds songs by mbz_recording_id", func() {
resp := doReq("search3", "query", mbidComeTogetherRec)
Expect(resp.Status).To(Equal(responses.StatusOK))
Expect(resp.SearchResult3).ToNot(BeNil())
Expect(resp.SearchResult3.Song).To(HaveLen(1))
Expect(resp.SearchResult3.Song[0].Title).To(Equal("Come Together"))
})
It("finds songs by mbz_release_track_id", func() {
resp := doReq("search3", "query", mbidSomething)
Expect(resp.Status).To(Equal(responses.StatusOK))
Expect(resp.SearchResult3).ToNot(BeNil())
Expect(resp.SearchResult3.Song).To(HaveLen(1))
Expect(resp.SearchResult3.Song[0].Title).To(Equal("Something"))
})
It("finds albums by mbz_album_id", func() {
resp := doReq("search3", "query", mbidAbbeyRoadAlbum)
Expect(resp.Status).To(Equal(responses.StatusOK))
Expect(resp.SearchResult3).ToNot(BeNil())
Expect(resp.SearchResult3.Album).To(HaveLen(1))
Expect(resp.SearchResult3.Album[0].Name).To(Equal("Abbey Road"))
})
It("finds albums by mbz_release_group_id", func() {
resp := doReq("search3", "query", mbidAbbeyRoadRelGroup)
Expect(resp.Status).To(Equal(responses.StatusOK))
Expect(resp.SearchResult3).ToNot(BeNil())
Expect(resp.SearchResult3.Album).To(HaveLen(1))
Expect(resp.SearchResult3.Album[0].Name).To(Equal("Abbey Road"))
})
It("finds artists by mbz_artist_id", func() {
resp := doReq("search3", "query", mbidBeatlesArtist)
Expect(resp.Status).To(Equal(responses.StatusOK))
Expect(resp.SearchResult3).ToNot(BeNil())
Expect(resp.SearchResult3.Artist).To(HaveLen(1))
Expect(resp.SearchResult3.Artist[0].Name).To(Equal("The Beatles"))
})
It("returns empty results for non-matching UUID", func() {
nonMatchingUUID := uuid.NewString()
resp := doReq("search3", "query", nonMatchingUUID)
Expect(resp.Status).To(Equal(responses.StatusOK))
Expect(resp.SearchResult3).ToNot(BeNil())
Expect(resp.SearchResult3.Artist).To(BeEmpty())
Expect(resp.SearchResult3.Album).To(BeEmpty())
Expect(resp.SearchResult3.Song).To(BeEmpty())
})
It("does not return songs for artist MBID", func() {
// media_file MBID search only checks mbz_recording_id and mbz_release_track_id,
// so an artist MBID should return only the artist, not songs
resp := doReq("search3", "query", mbidBeatlesArtist)
Expect(resp.Status).To(Equal(responses.StatusOK))
Expect(resp.SearchResult3).ToNot(BeNil())
Expect(resp.SearchResult3.Artist).To(HaveLen(1))
Expect(resp.SearchResult3.Artist[0].Name).To(Equal("The Beatles"))
Expect(resp.SearchResult3.Song).To(BeEmpty())
})
})
})
})