2022-07-27 20:43:24 -04:00
|
|
|
package criteria
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
|
|
|
"github.com/onsi/gomega"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var _ = Describe("fields", func() {
|
2026-04-24 23:18:20 -04:00
|
|
|
Describe("LookupField", func() {
|
|
|
|
|
It("finds built-in fields case-insensitively", func() {
|
|
|
|
|
field, ok := LookupField("Title")
|
|
|
|
|
|
|
|
|
|
gomega.Expect(ok).To(gomega.BeTrue())
|
2026-04-28 20:22:48 -04:00
|
|
|
gomega.Expect(field.Name()).To(gomega.Equal("title"))
|
2026-04-24 23:18:20 -04:00
|
|
|
})
|
|
|
|
|
|
2026-04-28 20:22:48 -04:00
|
|
|
It("resolves aliases to their canonical field name", func() {
|
2026-04-24 23:18:20 -04:00
|
|
|
field, ok := LookupField("albumtype")
|
|
|
|
|
|
|
|
|
|
gomega.Expect(ok).To(gomega.BeTrue())
|
2026-04-28 20:22:48 -04:00
|
|
|
gomega.Expect(field.Name()).To(gomega.Equal("releasetype"))
|
2026-04-24 23:18:20 -04:00
|
|
|
gomega.Expect(field.IsTag).To(gomega.BeTrue())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("finds registered tag names", func() {
|
|
|
|
|
AddTagNames([]string{"task3_mood"})
|
|
|
|
|
|
|
|
|
|
field, ok := LookupField("task3_mood")
|
|
|
|
|
|
|
|
|
|
gomega.Expect(ok).To(gomega.BeTrue())
|
2026-04-28 20:22:48 -04:00
|
|
|
gomega.Expect(field.Name()).To(gomega.Equal("task3_mood"))
|
2026-04-24 23:18:20 -04:00
|
|
|
gomega.Expect(field.IsTag).To(gomega.BeTrue())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("marks registered numeric tags", func() {
|
|
|
|
|
AddTagNames([]string{"task3_score"})
|
|
|
|
|
AddNumericTags([]string{"task3_score"})
|
|
|
|
|
|
|
|
|
|
field, ok := LookupField("task3_score")
|
|
|
|
|
|
|
|
|
|
gomega.Expect(ok).To(gomega.BeTrue())
|
|
|
|
|
gomega.Expect(field.IsTag).To(gomega.BeTrue())
|
|
|
|
|
gomega.Expect(field.Numeric).To(gomega.BeTrue())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("finds registered roles", func() {
|
|
|
|
|
AddRoles([]string{"task3_producer"})
|
|
|
|
|
|
|
|
|
|
field, ok := LookupField("task3_producer")
|
|
|
|
|
|
|
|
|
|
gomega.Expect(ok).To(gomega.BeTrue())
|
2026-04-28 20:22:48 -04:00
|
|
|
gomega.Expect(field.Name()).To(gomega.Equal("task3_producer"))
|
2026-04-24 23:18:20 -04:00
|
|
|
gomega.Expect(field.IsRole).To(gomega.BeTrue())
|
2022-07-27 20:43:24 -04:00
|
|
|
})
|
2026-05-01 19:21:48 -04:00
|
|
|
|
fix(smartplaylist): support isMissing/isPresent operators on ReplayGain fields (#5585)
* fix(smartplaylist): support isMissing/isPresent operators on ReplayGain fields
ReplayGain values are stored in dedicated nullable columns (rg_album_gain,
rg_album_peak, rg_track_gain, rg_track_peak) rather than in the media_file.tags
JSON blob. The isMissing/isPresent operators previously only supported tag and
role fields, causing two failure modes:
1. Using the documented alias names (replaygain_album_gain etc.) from PR #5256:
these got registered as JSON tags from mappings.yaml, so isMissing queried
json_tree(media_file.tags, '$.replaygain_album_gain') which is always empty
-> the playlist matched ALL songs.
2. Using the canonical field names (rgalbumgain etc.): not a tag/role, so SQL
generation returned an error. Because refreshSmartPlaylist deletes old tracks
before regenerating, the abort left the playlist empty.
Fix: add Nullable bool to FieldInfo and mark the four ReplayGain fields. Add
static alias entries (replaygain_album_gain -> rgalbumgain etc.) with
Numeric+Nullable set; because AddTagNames skips names already in the field map,
these static entries take precedence over the mappings.yaml tag registration.
missingExpr now emits IS NULL / IS NOT NULL for nullable column fields instead
of the json_tree lookup.
Fixes #5584
* chore(smartplaylist): address code review feedback
- Simplify the isMissing/isPresent unsupported-field error message,
removing the internal "nullable fields" jargon
- Standardize comments on mappings.yaml (the actual filename)
- Clarify the alias precedence comment in the LookupField test
2026-06-10 21:12:31 -04:00
|
|
|
It("marks ReplayGain column fields as nullable", func() {
|
|
|
|
|
field, ok := LookupField("rgAlbumGain")
|
|
|
|
|
|
|
|
|
|
gomega.Expect(ok).To(gomega.BeTrue())
|
|
|
|
|
gomega.Expect(field.Name()).To(gomega.Equal("rgalbumgain"))
|
|
|
|
|
gomega.Expect(field.Nullable).To(gomega.BeTrue())
|
|
|
|
|
gomega.Expect(field.IsTag).To(gomega.BeFalse())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("resolves replaygain_* tag names as aliases to nullable column fields", func() {
|
|
|
|
|
// AddTagNames skips names already in the field map, so the startup tag registration
|
|
|
|
|
// (from mappings.yaml) must not convert the pre-registered alias into a tag field.
|
|
|
|
|
AddTagNames([]string{"replaygain_album_gain"})
|
|
|
|
|
|
|
|
|
|
field, ok := LookupField("replaygain_album_gain")
|
|
|
|
|
|
|
|
|
|
gomega.Expect(ok).To(gomega.BeTrue())
|
|
|
|
|
gomega.Expect(field.Name()).To(gomega.Equal("rgalbumgain"))
|
|
|
|
|
gomega.Expect(field.Nullable).To(gomega.BeTrue())
|
|
|
|
|
gomega.Expect(field.IsTag).To(gomega.BeFalse())
|
|
|
|
|
})
|
|
|
|
|
|
feat(smartplaylist): extend isMissing/isPresent to bpm, bitDepth and many text fields (#5603)
* feat(smartplaylist): support isMissing/isPresent on mbz_* and lyrics fields
Mark the six mbz_* MusicBrainz ID columns and the lyrics column as Nullable
in the criteria field map, then extend missingExpr to handle string columns
where absence is encoded as NULL or empty string (plus '[]' for lyrics). The
Numeric/Boolean path (ReplayGain) is preserved via an explicit type check.
* refactor(model): make MediaFile BPM and BitDepth nullable pointers
Convert BPM and BitDepth fields in model.MediaFile from int to *int so that
'tag absent' is distinguishable from zero. The metadata mapper now uses
NullableFloat for BPM (nil when absent or zero/unparseable) and only sets
BitDepth when the audio property is non-zero (lossy codecs report 0).
All read sites use gg.V() for zero-fallback deref so Subsonic API output and
transcoding behaviour are byte-identical to before. The persistence layer
bridges the existing NOT NULL DB columns by coercing nil to 0 on write and 0
back to nil on read in PostMapArgs/PostScan; a later migration task will drop
those constraints.
Hash upgrade safety is verified by a new MediaFile.Hash describe block: nil
*int hashes identically to the old int(0) default via ZeroNil+IgnoreZeroValue,
so no files will be spuriously re-imported after this change.
Extra files touched beyond the plan's list: core/stream/legacy_client_test.go
(BitDepth in model.MediaFile literals), persistence/mediafile_repository.go
(NOT NULL bridge).
* test(model): pin pre-conversion golden hashes for BPM/BitDepth
* feat(smartplaylist): support isMissing/isPresent on bpm and bitDepth
* feat(db): make bpm and bit_depth columns nullable, backfill 0 to NULL
Drop the NOT NULL constraint on media_file.bpm and bit_depth via a
lossless migration that converts legacy 0-means-absent values to real
NULL. Remove the temporary shim in PostScan/PostMapArgs that was bridging
the old NOT NULL columns to the *int model fields. Add round-trip
persistence tests asserting NULL storage for nil pointers and correct
value round-trip for non-nil pointers.
* test(e2e): verify isMissing/isPresent partition for nullable fields
Add DescribeTable covering bpm, bitdepth, lyrics, and mbz_recording_id:
for each field, isMissing + isPresent song counts must equal the total
library count, proving the nullable-column SQL is exhaustive and correct.
* test(e2e): seed bpm tag so isMissing/isPresent partition is non-trivial
* fix(model): omit bitDepth from JSON when absent instead of emitting null
* feat(smartplaylist): support isMissing/isPresent on more string fields
Enable isMissing/isPresent operators for album, comment, catalognumber,
discsubtitle, albumcomment, sorttitle, sortalbum, sortartist,
sortalbumartist, and explicitstatus by marking them Nullable in fieldMap.
* refactor(smartplaylist): unify missingExpr column logic into one flow
Collapse the numeric/string fork in missingExpr into a single
empties-driven loop (numeric/boolean fields simply have no empties),
and replace the duplicated IsTag/IsRole guard with a three-way switch
that expresses the dispatch model once. No SQL semantics change for
string fields; numeric/boolean fields now emit a single-element Or/And
which squirrel parenthesizes (e.g. `(col IS NULL)` instead of bare
`col IS NULL`) — update the affected test expectations accordingly.
2026-06-13 13:15:20 -04:00
|
|
|
It("marks mbz_* and lyrics string fields as nullable (empty means missing)", func() {
|
|
|
|
|
for _, name := range []string{"mbz_album_id", "mbz_album_artist_id", "mbz_artist_id",
|
|
|
|
|
"mbz_recording_id", "mbz_release_track_id", "mbz_release_group_id", "lyrics",
|
|
|
|
|
"album", "comment", "catalognumber", "discsubtitle", "albumcomment",
|
|
|
|
|
"sorttitle", "sortalbum", "sortartist", "sortalbumartist", "explicitstatus"} {
|
|
|
|
|
field, ok := LookupField(name)
|
|
|
|
|
gomega.Expect(ok).To(gomega.BeTrue(), name)
|
|
|
|
|
gomega.Expect(field.Nullable).To(gomega.BeTrue(), name)
|
|
|
|
|
gomega.Expect(field.Numeric).To(gomega.BeFalse(), name)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2022-07-27 20:43:24 -04:00
|
|
|
})
|
|
|
|
|
})
|