update
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled

This commit is contained in:
2025-12-08 16:16:23 +01:00
commit c251f174ed
1349 changed files with 194301 additions and 0 deletions

184
utils/slice/slice.go Normal file
View File

@@ -0,0 +1,184 @@
package slice
import (
"bufio"
"bytes"
"cmp"
"io"
"iter"
"slices"
"golang.org/x/exp/maps"
)
func Map[T any, R any](t []T, mapFunc func(T) R) []R {
r := make([]R, len(t))
for i, e := range t {
r[i] = mapFunc(e)
}
return r
}
func MapWithArg[I any, O any, A any](t []I, arg A, mapFunc func(A, I) O) []O {
return Map(t, func(e I) O {
return mapFunc(arg, e)
})
}
func Group[T any, K comparable](s []T, keyFunc func(T) K) map[K][]T {
m := map[K][]T{}
for _, item := range s {
k := keyFunc(item)
m[k] = append(m[k], item)
}
return m
}
func ToMap[T any, K comparable, V any](s []T, transformFunc func(T) (K, V)) map[K]V {
m := make(map[K]V, len(s))
for _, item := range s {
k, v := transformFunc(item)
m[k] = v
}
return m
}
func CompactByFrequency[T comparable](list []T) []T {
counters := make(map[T]int)
for _, item := range list {
counters[item]++
}
sorted := maps.Keys(counters)
slices.SortFunc(sorted, func(i, j T) int {
return cmp.Compare(counters[j], counters[i])
})
return sorted
}
func MostFrequent[T comparable](list []T) T {
var zero T
if len(list) == 0 {
return zero
}
counters := make(map[T]int)
var topItem T
var topCount int
for _, value := range list {
if value == zero {
continue
}
counters[value]++
if counters[value] > topCount {
topItem = value
topCount = counters[value]
}
}
return topItem
}
func Insert[T any](slice []T, value T, index int) []T {
return append(slice[:index], append([]T{value}, slice[index:]...)...)
}
func Remove[T any](slice []T, index int) []T {
return append(slice[:index], slice[index+1:]...)
}
func Move[T any](slice []T, srcIndex int, dstIndex int) []T {
value := slice[srcIndex]
return Insert(Remove(slice, srcIndex), value, dstIndex)
}
func Unique[T comparable](list []T) []T {
seen := make(map[T]struct{})
var result []T
for _, item := range list {
if _, ok := seen[item]; !ok {
seen[item] = struct{}{}
result = append(result, item)
}
}
return result
}
// LinesFrom returns a Seq that reads lines from the given reader
func LinesFrom(reader io.Reader) iter.Seq[string] {
return func(yield func(string) bool) {
scanner := bufio.NewScanner(reader)
scanner.Split(scanLines)
for scanner.Scan() {
if !yield(scanner.Text()) {
return
}
}
}
}
// From https://stackoverflow.com/a/41433698
func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexAny(data, "\r\n"); i >= 0 {
if data[i] == '\n' {
// We have a line terminated by single newline.
return i + 1, data[0:i], nil
}
advance = i + 1
if len(data) > i+1 && data[i+1] == '\n' {
advance += 1
}
return advance, data[0:i], nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), data, nil
}
// Request more data.
return 0, nil, nil
}
// CollectChunks collects chunks of n elements from the input sequence and return a Seq of chunks
func CollectChunks[T any](it iter.Seq[T], n int) iter.Seq[[]T] {
return func(yield func([]T) bool) {
s := make([]T, 0, n)
for x := range it {
s = append(s, x)
if len(s) >= n {
if !yield(s) {
return
}
s = make([]T, 0, n)
}
}
if len(s) > 0 {
yield(s)
}
}
}
// SeqFunc returns a Seq that iterates over the slice with the given mapping function
func SeqFunc[I, O any](s []I, f func(I) O) iter.Seq[O] {
return func(yield func(O) bool) {
for _, x := range s {
if !yield(f(x)) {
return
}
}
}
}
// Filter returns a new slice containing only the elements of s for which filterFunc returns true
func Filter[T any](s []T, filterFunc func(T) bool) []T {
var result []T
for _, item := range s {
if filterFunc(item) {
result = append(result, item)
}
}
return result
}

213
utils/slice/slice_test.go Normal file
View File

@@ -0,0 +1,213 @@
package slice_test
import (
"os"
"slices"
"strconv"
"testing"
"github.com/navidrome/navidrome/tests"
"github.com/navidrome/navidrome/utils/slice"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestSlice(t *testing.T) {
tests.Init(t, false)
RegisterFailHandler(Fail)
RunSpecs(t, "Slice Suite")
}
var _ = Describe("Slice Utils", func() {
Describe("Map", func() {
It("returns empty slice for an empty input", func() {
mapFunc := func(v int) string { return strconv.Itoa(v * 2) }
result := slice.Map([]int{}, mapFunc)
Expect(result).To(BeEmpty())
})
It("returns a new slice with elements mapped", func() {
mapFunc := func(v int) string { return strconv.Itoa(v * 2) }
result := slice.Map([]int{1, 2, 3, 4}, mapFunc)
Expect(result).To(ConsistOf("2", "4", "6", "8"))
})
})
Describe("MapWithArg", func() {
It("returns empty slice for an empty input", func() {
mapFunc := func(a int, v int) string { return strconv.Itoa(a + v) }
result := slice.MapWithArg([]int{}, 10, mapFunc)
Expect(result).To(BeEmpty())
})
It("returns a new slice with elements mapped", func() {
mapFunc := func(a int, v int) string { return strconv.Itoa(a + v) }
result := slice.MapWithArg([]int{1, 2, 3, 4}, 10, mapFunc)
Expect(result).To(ConsistOf("11", "12", "13", "14"))
})
})
Describe("Group", func() {
It("returns empty map for an empty input", func() {
keyFunc := func(v int) int { return v % 2 }
result := slice.Group([]int{}, keyFunc)
Expect(result).To(BeEmpty())
})
It("groups by the result of the key function", func() {
keyFunc := func(v int) int { return v % 2 }
result := slice.Group([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, keyFunc)
Expect(result).To(HaveLen(2))
Expect(result[0]).To(ConsistOf(2, 4, 6, 8, 10))
Expect(result[1]).To(ConsistOf(1, 3, 5, 7, 9, 11))
})
})
Describe("ToMap", func() {
It("returns empty map for an empty input", func() {
transformFunc := func(v int) (int, string) { return v, strconv.Itoa(v) }
result := slice.ToMap([]int{}, transformFunc)
Expect(result).To(BeEmpty())
})
It("returns a map with the result of the transform function", func() {
transformFunc := func(v int) (int, string) { return v * 2, strconv.Itoa(v * 2) }
result := slice.ToMap([]int{1, 2, 3, 4}, transformFunc)
Expect(result).To(HaveLen(4))
Expect(result).To(HaveKeyWithValue(2, "2"))
Expect(result).To(HaveKeyWithValue(4, "4"))
Expect(result).To(HaveKeyWithValue(6, "6"))
Expect(result).To(HaveKeyWithValue(8, "8"))
})
})
Describe("CompactByFrequency", func() {
It("returns empty slice for an empty input", func() {
Expect(slice.CompactByFrequency([]int{})).To(BeEmpty())
})
It("groups by frequency", func() {
Expect(slice.CompactByFrequency([]int{1, 2, 1, 2, 3, 2})).To(HaveExactElements(2, 1, 3))
})
})
Describe("MostFrequent", func() {
It("returns zero value if no arguments are passed", func() {
Expect(slice.MostFrequent([]int{})).To(BeZero())
})
It("returns the single item", func() {
Expect(slice.MostFrequent([]string{"123"})).To(Equal("123"))
})
It("returns the item that appeared more times", func() {
Expect(slice.MostFrequent([]string{"1", "2", "1", "2", "3", "2"})).To(Equal("2"))
})
It("ignores zero values", func() {
Expect(slice.MostFrequent([]int{0, 0, 0, 2, 2})).To(Equal(2))
})
})
Describe("Move", func() {
It("moves item to end of slice", func() {
Expect(slice.Move([]string{"1", "2", "3"}, 0, 2)).To(HaveExactElements("2", "3", "1"))
})
It("moves item to beginning of slice", func() {
Expect(slice.Move([]string{"1", "2", "3"}, 2, 0)).To(HaveExactElements("3", "1", "2"))
})
It("keeps item in same position if srcIndex == dstIndex", func() {
Expect(slice.Move([]string{"1", "2", "3"}, 1, 1)).To(HaveExactElements("1", "2", "3"))
})
})
Describe("Unique", func() {
It("returns empty slice for an empty input", func() {
Expect(slice.Unique([]int{})).To(BeEmpty())
})
It("returns the unique elements", func() {
Expect(slice.Unique([]int{1, 2, 1, 2, 3, 2})).To(HaveExactElements(1, 2, 3))
})
})
DescribeTable("LinesFrom",
func(path string, expected int) {
count := 0
file, _ := os.Open(path)
defer file.Close()
for _ = range slice.LinesFrom(file) {
count++
}
Expect(count).To(Equal(expected))
},
Entry("returns empty slice for an empty input", "tests/fixtures/empty.txt", 0),
Entry("returns the lines of a file", "tests/fixtures/playlists/pls1.m3u", 2),
Entry("returns empty if file does not exist", "tests/fixtures/NON-EXISTENT", 0),
)
DescribeTable("CollectChunks",
func(input []int, n int, expected [][]int) {
var result [][]int
for chunks := range slice.CollectChunks(slices.Values(input), n) {
result = append(result, chunks)
}
Expect(result).To(Equal(expected))
},
Entry("returns empty slice (nil) for an empty input", []int{}, 1, nil),
Entry("returns the slice in one chunk if len < chunkSize", []int{1, 2, 3}, 10, [][]int{{1, 2, 3}}),
Entry("breaks up the slice if len > chunkSize", []int{1, 2, 3, 4, 5}, 3, [][]int{{1, 2, 3}, {4, 5}}),
)
Describe("SeqFunc", func() {
It("returns empty slice for an empty input", func() {
it := slice.SeqFunc([]int{}, func(v int) int { return v })
result := slices.Collect(it)
Expect(result).To(BeEmpty())
})
It("returns a new slice with mapped elements", func() {
it := slice.SeqFunc([]int{1, 2, 3, 4}, func(v int) string { return strconv.Itoa(v * 2) })
result := slices.Collect(it)
Expect(result).To(ConsistOf("2", "4", "6", "8"))
})
})
Describe("Filter", func() {
It("returns empty slice for an empty input", func() {
filterFunc := func(v int) bool { return v > 0 }
result := slice.Filter([]int{}, filterFunc)
Expect(result).To(BeEmpty())
})
It("returns all elements when filter matches all", func() {
filterFunc := func(v int) bool { return v > 0 }
result := slice.Filter([]int{1, 2, 3, 4}, filterFunc)
Expect(result).To(HaveExactElements(1, 2, 3, 4))
})
It("returns empty slice when filter matches none", func() {
filterFunc := func(v int) bool { return v > 10 }
result := slice.Filter([]int{1, 2, 3, 4}, filterFunc)
Expect(result).To(BeEmpty())
})
It("returns only matching elements", func() {
filterFunc := func(v int) bool { return v%2 == 0 }
result := slice.Filter([]int{1, 2, 3, 4, 5, 6}, filterFunc)
Expect(result).To(HaveExactElements(2, 4, 6))
})
It("works with string slices", func() {
filterFunc := func(s string) bool { return len(s) > 3 }
result := slice.Filter([]string{"a", "abc", "abcd", "ab", "abcde"}, filterFunc)
Expect(result).To(HaveExactElements("abcd", "abcde"))
})
It("preserves order of elements", func() {
filterFunc := func(v int) bool { return v%2 == 1 }
result := slice.Filter([]int{9, 8, 7, 6, 5, 4, 3, 2, 1}, filterFunc)
Expect(result).To(HaveExactElements(9, 7, 5, 3, 1))
})
})
})