* feat(smartplaylist): add IsMissing and IsPresent operator types Add two new Expression types for detecting absent/present tags and roles in smart playlist criteria. Includes JSON marshal/unmarshal support and Walk visitor registration. * test(smartplaylist): add JSON marshal/unmarshal tests for isMissing/isPresent * feat(smartplaylist): add SQL generation for isMissing/isPresent operators Tags check json_tree(media_file.tags) for key existence. Roles check json_tree(media_file.participants) for key existence. Regular DB column fields are rejected with an error. * test(smartplaylist): add e2e tests for isMissing/isPresent operators Tests cover tag presence/absence with selective matching (grouping), universal absence (lyricist role), universal presence (composer role), and combined operator usage. * refactor(smartplaylist): use strconv.ParseBool in IsTruthy Replace hand-rolled string truthiness check with strconv.ParseBool, which correctly handles standard boolean strings and rejects unrecognized values as false. * refactor(smartplaylist): clarify missingExpr parameter naming Rename defaultNegate to checkAbsence and extract truthy local for readability. The XNOR logic (checkAbsence == truthy) is now easier to follow: isMissing passes true, isPresent passes false. * refactor(smartplaylist): reuse jsonExpr in missingExpr, improve errors - tagCond/roleCond now handle nil cond (existence-only check) - missingExpr delegates to jsonExpr(info, nil, negate) instead of building SQL manually - Better error messages: unknown fields now report the field name
222 lines
4.6 KiB
Go
222 lines
4.6 KiB
Go
package criteria
|
|
|
|
import (
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
// Conjunctions need to implement this interface, to allow Criteria to extract child playlist IDs recursively
|
|
type conjunction interface {
|
|
ChildPlaylistIds() []string
|
|
}
|
|
|
|
type (
|
|
All []Expression
|
|
And = All
|
|
)
|
|
|
|
func (All) fields() map[string]any { return nil }
|
|
|
|
func (all All) MarshalJSON() ([]byte, error) {
|
|
return marshalConjunction("all", all)
|
|
}
|
|
|
|
func (all All) ChildPlaylistIds() (ids []string) {
|
|
return extractPlaylistIds(all)
|
|
}
|
|
|
|
type (
|
|
Any []Expression
|
|
Or = Any
|
|
)
|
|
|
|
func (Any) fields() map[string]any { return nil }
|
|
|
|
func (any Any) MarshalJSON() ([]byte, error) {
|
|
return marshalConjunction("any", any)
|
|
}
|
|
|
|
func (any Any) ChildPlaylistIds() (ids []string) {
|
|
return extractPlaylistIds(any)
|
|
}
|
|
|
|
type Is map[string]any
|
|
type Eq = Is
|
|
|
|
func (is Is) MarshalJSON() ([]byte, error) {
|
|
return marshalExpression("is", is)
|
|
}
|
|
|
|
func (is Is) fields() map[string]any { return is }
|
|
|
|
type IsNot map[string]any
|
|
|
|
func (isn IsNot) MarshalJSON() ([]byte, error) {
|
|
return marshalExpression("isNot", isn)
|
|
}
|
|
|
|
func (isn IsNot) fields() map[string]any { return isn }
|
|
|
|
type Gt map[string]any
|
|
|
|
func (gt Gt) MarshalJSON() ([]byte, error) {
|
|
return marshalExpression("gt", gt)
|
|
}
|
|
|
|
func (gt Gt) fields() map[string]any { return gt }
|
|
|
|
type Lt map[string]any
|
|
|
|
func (lt Lt) MarshalJSON() ([]byte, error) {
|
|
return marshalExpression("lt", lt)
|
|
}
|
|
|
|
func (lt Lt) fields() map[string]any { return lt }
|
|
|
|
type Before map[string]any
|
|
|
|
func (bf Before) MarshalJSON() ([]byte, error) {
|
|
return marshalExpression("before", bf)
|
|
}
|
|
|
|
func (bf Before) fields() map[string]any { return bf }
|
|
|
|
type After Gt
|
|
|
|
func (af After) MarshalJSON() ([]byte, error) {
|
|
return marshalExpression("after", af)
|
|
}
|
|
|
|
func (af After) fields() map[string]any { return af }
|
|
|
|
type Contains map[string]any
|
|
|
|
func (ct Contains) MarshalJSON() ([]byte, error) {
|
|
return marshalExpression("contains", ct)
|
|
}
|
|
|
|
func (ct Contains) fields() map[string]any { return ct }
|
|
|
|
type NotContains map[string]any
|
|
|
|
func (nct NotContains) MarshalJSON() ([]byte, error) {
|
|
return marshalExpression("notContains", nct)
|
|
}
|
|
|
|
func (nct NotContains) fields() map[string]any { return nct }
|
|
|
|
type StartsWith map[string]any
|
|
|
|
func (sw StartsWith) MarshalJSON() ([]byte, error) {
|
|
return marshalExpression("startsWith", sw)
|
|
}
|
|
|
|
func (sw StartsWith) fields() map[string]any { return sw }
|
|
|
|
type EndsWith map[string]any
|
|
|
|
func (ew EndsWith) MarshalJSON() ([]byte, error) {
|
|
return marshalExpression("endsWith", ew)
|
|
}
|
|
|
|
func (ew EndsWith) fields() map[string]any { return ew }
|
|
|
|
type InTheRange map[string]any
|
|
|
|
func (itr InTheRange) MarshalJSON() ([]byte, error) {
|
|
return marshalExpression("inTheRange", itr)
|
|
}
|
|
|
|
func (itr InTheRange) fields() map[string]any { return itr }
|
|
|
|
type InTheLast map[string]any
|
|
|
|
func (itl InTheLast) MarshalJSON() ([]byte, error) {
|
|
return marshalExpression("inTheLast", itl)
|
|
}
|
|
|
|
func (itl InTheLast) fields() map[string]any { return itl }
|
|
|
|
type NotInTheLast map[string]any
|
|
|
|
func (nitl NotInTheLast) MarshalJSON() ([]byte, error) {
|
|
return marshalExpression("notInTheLast", nitl)
|
|
}
|
|
|
|
func (nitl NotInTheLast) fields() map[string]any { return nitl }
|
|
|
|
func startOfPeriod(numDays int64, from time.Time) string {
|
|
return from.Add(time.Duration(-24*numDays) * time.Hour).Format("2006-01-02")
|
|
}
|
|
|
|
type InPlaylist map[string]any
|
|
|
|
func (ipl InPlaylist) MarshalJSON() ([]byte, error) {
|
|
return marshalExpression("inPlaylist", ipl)
|
|
}
|
|
|
|
func (ipl InPlaylist) fields() map[string]any { return ipl }
|
|
|
|
type NotInPlaylist map[string]any
|
|
|
|
func (nipl NotInPlaylist) MarshalJSON() ([]byte, error) {
|
|
return marshalExpression("notInPlaylist", nipl)
|
|
}
|
|
|
|
func (nipl NotInPlaylist) fields() map[string]any { return nipl }
|
|
|
|
type IsMissing map[string]any
|
|
|
|
func (im IsMissing) MarshalJSON() ([]byte, error) {
|
|
return marshalExpression("isMissing", im)
|
|
}
|
|
|
|
func (im IsMissing) fields() map[string]any { return im }
|
|
|
|
type IsPresent map[string]any
|
|
|
|
func (ip IsPresent) MarshalJSON() ([]byte, error) {
|
|
return marshalExpression("isPresent", ip)
|
|
}
|
|
|
|
func (ip IsPresent) fields() map[string]any { return ip }
|
|
|
|
func IsTruthy(v any) bool {
|
|
switch val := v.(type) {
|
|
case bool:
|
|
return val
|
|
case float64:
|
|
return val != 0
|
|
case string:
|
|
b, err := strconv.ParseBool(val)
|
|
return err == nil && b
|
|
default:
|
|
return v != nil
|
|
}
|
|
}
|
|
|
|
func extractPlaylistIds(inputRule any) (ids []string) {
|
|
var id string
|
|
var ok bool
|
|
|
|
switch rule := inputRule.(type) {
|
|
case Any:
|
|
for _, rules := range rule {
|
|
ids = append(ids, extractPlaylistIds(rules)...)
|
|
}
|
|
case All:
|
|
for _, rules := range rule {
|
|
ids = append(ids, extractPlaylistIds(rules)...)
|
|
}
|
|
case InPlaylist:
|
|
if id, ok = rule["id"].(string); ok {
|
|
ids = append(ids, id)
|
|
}
|
|
case NotInPlaylist:
|
|
if id, ok = rule["id"].(string); ok {
|
|
ids = append(ids, id)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|