feat(plugins): add similar songs retrieval functions and improve duration consistency (#4933)

* feat: add duration filtering for similar songs matching

Signed-off-by: Deluan <deluan@navidrome.org>

* test: refactor expectations for similar songs in provider matching tests

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(plugins): add functions to retrieve similar songs by track, album, and artist

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(plugins): support uint32 in ndpgen

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(plugins): update duration field to use seconds as float instead of milliseconds as uint32

Signed-off-by: Deluan <deluan@navidrome.org>

* fix: add helper functions for Rust's skip_serializing_if with numeric types

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(provider): enhance track matching logic to fallback to title match when duration-filtered tracks fail

---------

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan Quintão
2026-01-26 18:28:41 -05:00
committed by GitHub
parent 4d4740b83b
commit fda35dd8ce
20 changed files with 1147 additions and 70 deletions
+12
View File
@@ -568,6 +568,18 @@ func skipSerializingFunc(goType string) string {
return "String::is_empty"
case "bool":
return "std::ops::Not::not"
case "int32":
return "is_zero_i32"
case "uint32":
return "is_zero_u32"
case "int64":
return "is_zero_i64"
case "uint64":
return "is_zero_u64"
case "float32":
return "is_zero_f32"
case "float64":
return "is_zero_f64"
default:
return "Option::is_none"
}
@@ -1234,6 +1234,37 @@ type OnInitOutput struct {
})
var _ = Describe("Rust Generation", func() {
Describe("skipSerializingFunc", func() {
It("should return Option::is_none for pointer, slice, and map types", func() {
Expect(skipSerializingFunc("*string")).To(Equal("Option::is_none"))
Expect(skipSerializingFunc("*MyStruct")).To(Equal("Option::is_none"))
Expect(skipSerializingFunc("[]string")).To(Equal("Option::is_none"))
Expect(skipSerializingFunc("[]int32")).To(Equal("Option::is_none"))
Expect(skipSerializingFunc("map[string]int")).To(Equal("Option::is_none"))
})
It("should return String::is_empty for string type", func() {
Expect(skipSerializingFunc("string")).To(Equal("String::is_empty"))
})
It("should return std::ops::Not::not for bool type", func() {
Expect(skipSerializingFunc("bool")).To(Equal("std::ops::Not::not"))
})
It("should return is_zero_* functions for numeric types", func() {
Expect(skipSerializingFunc("int32")).To(Equal("is_zero_i32"))
Expect(skipSerializingFunc("uint32")).To(Equal("is_zero_u32"))
Expect(skipSerializingFunc("int64")).To(Equal("is_zero_i64"))
Expect(skipSerializingFunc("uint64")).To(Equal("is_zero_u64"))
Expect(skipSerializingFunc("float32")).To(Equal("is_zero_f32"))
Expect(skipSerializingFunc("float64")).To(Equal("is_zero_f64"))
})
It("should return Option::is_none for unknown types", func() {
Expect(skipSerializingFunc("CustomType")).To(Equal("Option::is_none"))
})
})
Describe("rustOutputType", func() {
It("should convert Go primitives to Rust primitives", func() {
Expect(rustOutputType("bool")).To(Equal("bool"))
@@ -7,6 +7,20 @@ use serde::{Deserialize, Serialize};
{{- if hasHashMap .Capability}}
use std::collections::HashMap;
{{- end}}
// Helper functions for skip_serializing_if with numeric types
#[allow(dead_code)]
fn is_zero_i32(value: &i32) -> bool { *value == 0 }
#[allow(dead_code)]
fn is_zero_u32(value: &u32) -> bool { *value == 0 }
#[allow(dead_code)]
fn is_zero_i64(value: &i64) -> bool { *value == 0 }
#[allow(dead_code)]
fn is_zero_u64(value: &u64) -> bool { *value == 0 }
#[allow(dead_code)]
fn is_zero_f32(value: &f32) -> bool { *value == 0.0 }
#[allow(dead_code)]
fn is_zero_f64(value: &f64) -> bool { *value == 0.0 }
{{- end}}
{{- /* Generate type alias definitions */ -}}
+5 -3
View File
@@ -466,9 +466,7 @@ func RustDefaultValue(goType string) string {
switch goType {
case "string":
return `String::new()`
case "int", "int32":
return "0"
case "int64":
case "int", "int32", "int64", "uint", "uint32", "uint64":
return "0"
case "float32", "float64":
return "0.0"
@@ -602,6 +600,10 @@ func ToRustTypeWithStructs(goType string, knownStructs map[string]bool) string {
return "i32"
case "int64":
return "i64"
case "uint", "uint32":
return "u32"
case "uint64":
return "u64"
case "float32":
return "f32"
case "float64":
+7 -1
View File
@@ -106,7 +106,7 @@ func buildExport(export Export) xtpExport {
// isPrimitiveGoType returns true if the Go type is a primitive type.
func isPrimitiveGoType(goType string) bool {
switch goType {
case "bool", "string", "int", "int32", "int64", "float32", "float64", "[]byte":
case "bool", "string", "int", "int32", "int64", "uint", "uint32", "uint64", "float32", "float64", "[]byte":
return true
}
return false
@@ -302,6 +302,12 @@ func goTypeToXTPTypeAndFormat(goType string) (typ, format string) {
return "integer", "int32"
case "int64":
return "integer", "int64"
case "uint", "uint32":
// XTP schema doesn't support unsigned formats; use int64 to hold full uint32 range
return "integer", "int64"
case "uint64":
// XTP schema doesn't support unsigned formats; use int64 (may lose precision for large values)
return "integer", "int64"
case "float32":
return "number", "float"
case "float64":