Compare commits

..

1 Commits

17 changed files with 45 additions and 232 deletions

View File

@ -217,7 +217,7 @@ jobs:
CROSS_TAGLIB_VERSION=${{ env.CROSS_TAGLIB_VERSION }} CROSS_TAGLIB_VERSION=${{ env.CROSS_TAGLIB_VERSION }}
- name: Upload Binaries - name: Upload Binaries
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v4
with: with:
name: navidrome-${{ env.PLATFORM }} name: navidrome-${{ env.PLATFORM }}
path: ./output path: ./output
@ -248,7 +248,7 @@ jobs:
touch "/tmp/digests/${digest#sha256:}" touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest - name: Upload digest
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v4
if: env.IS_LINUX == 'true' && env.IS_DOCKER_PUSH_CONFIGURED == 'true' && env.IS_ARMV5 == 'false' if: env.IS_LINUX == 'true' && env.IS_DOCKER_PUSH_CONFIGURED == 'true' && env.IS_ARMV5 == 'false'
with: with:
name: digests-${{ env.PLATFORM }} name: digests-${{ env.PLATFORM }}
@ -267,7 +267,7 @@ jobs:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Download digests - name: Download digests
uses: actions/download-artifact@v6 uses: actions/download-artifact@v5
with: with:
path: /tmp/digests path: /tmp/digests
pattern: digests-* pattern: digests-*
@ -320,7 +320,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: actions/download-artifact@v6 - uses: actions/download-artifact@v5
with: with:
path: ./binaries path: ./binaries
pattern: navidrome-windows* pattern: navidrome-windows*
@ -339,7 +339,7 @@ jobs:
du -h binaries/msi/*.msi du -h binaries/msi/*.msi
- name: Upload MSI files - name: Upload MSI files
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v4
with: with:
name: navidrome-windows-installers name: navidrome-windows-installers
path: binaries/msi/*.msi path: binaries/msi/*.msi
@ -357,7 +357,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
fetch-tags: true fetch-tags: true
- uses: actions/download-artifact@v6 - uses: actions/download-artifact@v5
with: with:
path: ./binaries path: ./binaries
pattern: navidrome-* pattern: navidrome-*
@ -383,7 +383,7 @@ jobs:
rm ./dist/*.tar.gz ./dist/*.zip rm ./dist/*.tar.gz ./dist/*.zip
- name: Upload all-packages artifact - name: Upload all-packages artifact
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v4
with: with:
name: packages name: packages
path: dist/navidrome_0* path: dist/navidrome_0*
@ -406,13 +406,13 @@ jobs:
item: ${{ fromJson(needs.release.outputs.package_list) }} item: ${{ fromJson(needs.release.outputs.package_list) }}
steps: steps:
- name: Download all-packages artifact - name: Download all-packages artifact
uses: actions/download-artifact@v6 uses: actions/download-artifact@v5
with: with:
name: packages name: packages
path: ./dist path: ./dist
- name: Upload all-packages artifact - name: Upload all-packages artifact
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v4
with: with:
name: navidrome_linux_${{ matrix.item }} name: navidrome_linux_${{ matrix.item }}
path: dist/navidrome_0*_linux_${{ matrix.item }} path: dist/navidrome_0*_linux_${{ matrix.item }}

View File

@ -13,7 +13,6 @@ import (
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
. "github.com/navidrome/navidrome/utils/gg" . "github.com/navidrome/navidrome/utils/gg"
"github.com/navidrome/navidrome/utils/slice" "github.com/navidrome/navidrome/utils/slice"
"github.com/navidrome/navidrome/utils/str"
) )
type Share interface { type Share interface {
@ -120,8 +119,9 @@ func (r *shareRepositoryWrapper) Save(entity interface{}) (string, error) {
log.Error(r.ctx, "Invalid Resource ID", "id", firstId) log.Error(r.ctx, "Invalid Resource ID", "id", firstId)
return "", model.ErrNotFound return "", model.ErrNotFound
} }
if len(s.Contents) > 30 {
s.Contents = str.TruncateRunes(s.Contents, 30, "...") s.Contents = s.Contents[:26] + "..."
}
id, err = r.Persistable.Save(s) id, err = r.Persistable.Save(s)
return id, err return id, err

View File

@ -38,38 +38,6 @@ var _ = Describe("Share", func() {
Expect(id).ToNot(BeEmpty()) Expect(id).ToNot(BeEmpty())
Expect(entity.ID).To(Equal(id)) Expect(entity.ID).To(Equal(id))
}) })
It("does not truncate ASCII labels shorter than 30 characters", func() {
_ = ds.MediaFile(ctx).Put(&model.MediaFile{ID: "456", Title: "Example Media File"})
entity := &model.Share{Description: "test", ResourceIDs: "456"}
_, err := repo.Save(entity)
Expect(err).ToNot(HaveOccurred())
Expect(entity.Contents).To(Equal("Example Media File"))
})
It("truncates ASCII labels longer than 30 characters", func() {
_ = ds.MediaFile(ctx).Put(&model.MediaFile{ID: "789", Title: "Example Media File But The Title Is Really Long For Testing Purposes"})
entity := &model.Share{Description: "test", ResourceIDs: "789"}
_, err := repo.Save(entity)
Expect(err).ToNot(HaveOccurred())
Expect(entity.Contents).To(Equal("Example Media File But The ..."))
})
It("does not truncate CJK labels shorter than 30 runes", func() {
_ = ds.MediaFile(ctx).Put(&model.MediaFile{ID: "456", Title: "青春コンプレックス"})
entity := &model.Share{Description: "test", ResourceIDs: "456"}
_, err := repo.Save(entity)
Expect(err).ToNot(HaveOccurred())
Expect(entity.Contents).To(Equal("青春コンプレックス"))
})
It("truncates CJK labels longer than 30 runes", func() {
_ = ds.MediaFile(ctx).Put(&model.MediaFile{ID: "789", Title: "私の中の幻想的世界観及びその顕現を想起させたある現実での出来事に関する一考察"})
entity := &model.Share{Description: "test", ResourceIDs: "789"}
_, err := repo.Save(entity)
Expect(err).ToNot(HaveOccurred())
Expect(entity.Contents).To(Equal("私の中の幻想的世界観及びその顕現を想起させたある現実で..."))
})
}) })
Describe("Update", func() { Describe("Update", func() {

View File

@ -1,9 +0,0 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE playqueue ADD COLUMN position_int integer;
UPDATE playqueue SET position_int = CAST(position as INTEGER) ;
ALTER TABLE playqueue DROP COLUMN position;
ALTER TABLE playqueue RENAME COLUMN position_int TO position;
-- +goose StatementEnd
-- +goose Down

13
go.mod
View File

@ -1,13 +1,9 @@
module github.com/navidrome/navidrome module github.com/navidrome/navidrome
go 1.25.4 go 1.25.3
replace ( // Fork to fix https://github.com/navidrome/navidrome/pull/3254
// Fork to fix https://github.com/navidrome/navidrome/issues/3254 replace github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 => github.com/deluan/tag v0.0.0-20241002021117-dfe5e6ea396d
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 => github.com/deluan/tag v0.0.0-20241002021117-dfe5e6ea396d
// Using version from main that fixes https://github.com/navidrome/navidrome/issues/4396
github.com/tetratelabs/wazero v1.9.0 => github.com/tetratelabs/wazero v0.0.0-20251106165119-514cdb337684
)
require ( require (
github.com/Masterminds/squirrel v1.5.4 github.com/Masterminds/squirrel v1.5.4
@ -47,7 +43,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.32 github.com/mattn/go-sqlite3 v1.14.32
github.com/microcosm-cc/bluemonday v1.0.27 github.com/microcosm-cc/bluemonday v1.0.27
github.com/mileusna/useragent v1.3.5 github.com/mileusna/useragent v1.3.5
github.com/onsi/ginkgo/v2 v2.27.2 github.com/onsi/ginkgo/v2 v2.27.1
github.com/onsi/gomega v1.38.2 github.com/onsi/gomega v1.38.2
github.com/pelletier/go-toml/v2 v2.2.4 github.com/pelletier/go-toml/v2 v2.2.4
github.com/pocketbase/dbx v1.11.0 github.com/pocketbase/dbx v1.11.0
@ -128,6 +124,7 @@ require (
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect

12
go.sum
View File

@ -186,8 +186,8 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750= github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750=
github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g=
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= github.com/onsi/ginkgo/v2 v2.27.1 h1:0LJC8MpUSQnfnp4n/3W3GdlmJP3ENGF0ZPzjQGLPP7s=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/ginkgo/v2 v2.27.1/go.mod h1:wmy3vCqiBjirARfVhAqFpYt8uvX0yaFe+GudAqqcCqA=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
@ -201,6 +201,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU= github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs= github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM= github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM=
github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
@ -265,8 +267,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tetratelabs/wazero v0.0.0-20251106165119-514cdb337684 h1:ugT1JTRsK1Jhn95BWilCugyZ1Svsyxm9xSiflOa2e7E= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v0.0.0-20251106165119-514cdb337684/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
@ -284,6 +286,8 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=

View File

@ -55,7 +55,6 @@ var _ = Describe("AlbumRepository", func() {
It("returns all records sorted", func() { It("returns all records sorted", func() {
Expect(GetAll(model.QueryOptions{Sort: "name"})).To(Equal(model.Albums{ Expect(GetAll(model.QueryOptions{Sort: "name"})).To(Equal(model.Albums{
albumAbbeyRoad, albumAbbeyRoad,
albumMultiDisc,
albumRadioactivity, albumRadioactivity,
albumSgtPeppers, albumSgtPeppers,
})) }))
@ -65,7 +64,6 @@ var _ = Describe("AlbumRepository", func() {
Expect(GetAll(model.QueryOptions{Sort: "name", Order: "desc"})).To(Equal(model.Albums{ Expect(GetAll(model.QueryOptions{Sort: "name", Order: "desc"})).To(Equal(model.Albums{
albumSgtPeppers, albumSgtPeppers,
albumRadioactivity, albumRadioactivity,
albumMultiDisc,
albumAbbeyRoad, albumAbbeyRoad,
})) }))
}) })

View File

@ -38,7 +38,7 @@ var _ = Describe("MediaRepository", func() {
}) })
It("counts the number of mediafiles in the DB", func() { It("counts the number of mediafiles in the DB", func() {
Expect(mr.CountAll()).To(Equal(int64(10))) Expect(mr.CountAll()).To(Equal(int64(6)))
}) })
It("returns songs ordered by lyrics with a specific title/artist", func() { It("returns songs ordered by lyrics with a specific title/artist", func() {

View File

@ -69,12 +69,10 @@ var (
albumSgtPeppers = al(model.Album{ID: "101", Name: "Sgt Peppers", AlbumArtist: "The Beatles", OrderAlbumName: "sgt peppers", AlbumArtistID: "3", EmbedArtPath: p("/beatles/1/sgt/a day.mp3"), SongCount: 1, MaxYear: 1967}) albumSgtPeppers = al(model.Album{ID: "101", Name: "Sgt Peppers", AlbumArtist: "The Beatles", OrderAlbumName: "sgt peppers", AlbumArtistID: "3", EmbedArtPath: p("/beatles/1/sgt/a day.mp3"), SongCount: 1, MaxYear: 1967})
albumAbbeyRoad = al(model.Album{ID: "102", Name: "Abbey Road", AlbumArtist: "The Beatles", OrderAlbumName: "abbey road", AlbumArtistID: "3", EmbedArtPath: p("/beatles/1/come together.mp3"), SongCount: 1, MaxYear: 1969}) albumAbbeyRoad = al(model.Album{ID: "102", Name: "Abbey Road", AlbumArtist: "The Beatles", OrderAlbumName: "abbey road", AlbumArtistID: "3", EmbedArtPath: p("/beatles/1/come together.mp3"), SongCount: 1, MaxYear: 1969})
albumRadioactivity = al(model.Album{ID: "103", Name: "Radioactivity", AlbumArtist: "Kraftwerk", OrderAlbumName: "radioactivity", AlbumArtistID: "2", EmbedArtPath: p("/kraft/radio/radio.mp3"), SongCount: 2}) albumRadioactivity = al(model.Album{ID: "103", Name: "Radioactivity", AlbumArtist: "Kraftwerk", OrderAlbumName: "radioactivity", AlbumArtistID: "2", EmbedArtPath: p("/kraft/radio/radio.mp3"), SongCount: 2})
albumMultiDisc = al(model.Album{ID: "104", Name: "Multi Disc Album", AlbumArtist: "Test Artist", OrderAlbumName: "multi disc album", AlbumArtistID: "1", EmbedArtPath: p("/test/multi/disc1/track1.mp3"), SongCount: 4})
testAlbums = model.Albums{ testAlbums = model.Albums{
albumSgtPeppers, albumSgtPeppers,
albumAbbeyRoad, albumAbbeyRoad,
albumRadioactivity, albumRadioactivity,
albumMultiDisc,
} }
) )
@ -96,22 +94,13 @@ var (
Lyrics: `[{"lang":"xxx","line":[{"value":"This is a set of lyrics"}],"synced":false}]`, Lyrics: `[{"lang":"xxx","line":[{"value":"This is a set of lyrics"}],"synced":false}]`,
}) })
songAntenna2 = mf(model.MediaFile{ID: "1006", Title: "Antenna", ArtistID: "2", Artist: "Kraftwerk", AlbumID: "103"}) songAntenna2 = mf(model.MediaFile{ID: "1006", Title: "Antenna", ArtistID: "2", Artist: "Kraftwerk", AlbumID: "103"})
// Multi-disc album tracks (intentionally out of order to test sorting) testSongs = model.MediaFiles{
songDisc2Track11 = mf(model.MediaFile{ID: "2001", Title: "Disc 2 Track 11", ArtistID: "1", Artist: "Test Artist", AlbumID: "104", Album: "Multi Disc Album", DiscNumber: 2, TrackNumber: 11, Path: p("/test/multi/disc2/track11.mp3"), OrderAlbumName: "multi disc album", OrderArtistName: "test artist"})
songDisc1Track01 = mf(model.MediaFile{ID: "2002", Title: "Disc 1 Track 1", ArtistID: "1", Artist: "Test Artist", AlbumID: "104", Album: "Multi Disc Album", DiscNumber: 1, TrackNumber: 1, Path: p("/test/multi/disc1/track1.mp3"), OrderAlbumName: "multi disc album", OrderArtistName: "test artist"})
songDisc2Track01 = mf(model.MediaFile{ID: "2003", Title: "Disc 2 Track 1", ArtistID: "1", Artist: "Test Artist", AlbumID: "104", Album: "Multi Disc Album", DiscNumber: 2, TrackNumber: 1, Path: p("/test/multi/disc2/track1.mp3"), OrderAlbumName: "multi disc album", OrderArtistName: "test artist"})
songDisc1Track02 = mf(model.MediaFile{ID: "2004", Title: "Disc 1 Track 2", ArtistID: "1", Artist: "Test Artist", AlbumID: "104", Album: "Multi Disc Album", DiscNumber: 1, TrackNumber: 2, Path: p("/test/multi/disc1/track2.mp3"), OrderAlbumName: "multi disc album", OrderArtistName: "test artist"})
testSongs = model.MediaFiles{
songDayInALife, songDayInALife,
songComeTogether, songComeTogether,
songRadioactivity, songRadioactivity,
songAntenna, songAntenna,
songAntennaWithLyrics, songAntennaWithLyrics,
songAntenna2, songAntenna2,
songDisc2Track11,
songDisc1Track01,
songDisc2Track01,
songDisc1Track02,
} }
) )

View File

@ -259,37 +259,4 @@ var _ = Describe("PlaylistRepository", func() {
}) })
}) })
}) })
Describe("Playlist Track Sorting", func() {
var testPlaylistID string
AfterEach(func() {
if testPlaylistID != "" {
Expect(repo.Delete(testPlaylistID)).To(BeNil())
testPlaylistID = ""
}
})
It("sorts tracks correctly by album (disc and track number)", func() {
By("creating a playlist with multi-disc album tracks in arbitrary order")
newPls := model.Playlist{Name: "Multi-Disc Test", OwnerID: "userid"}
// Add tracks in intentionally scrambled order
newPls.AddMediaFilesByID([]string{"2001", "2002", "2003", "2004"})
Expect(repo.Put(&newPls)).To(Succeed())
testPlaylistID = newPls.ID
By("retrieving tracks sorted by album")
tracksRepo := repo.Tracks(newPls.ID, false)
tracks, err := tracksRepo.GetAll(model.QueryOptions{Sort: "album", Order: "asc"})
Expect(err).ToNot(HaveOccurred())
By("verifying tracks are sorted by disc number then track number")
Expect(tracks).To(HaveLen(4))
// Expected order: Disc 1 Track 1, Disc 1 Track 2, Disc 2 Track 1, Disc 2 Track 11
Expect(tracks[0].MediaFileID).To(Equal("2002")) // Disc 1, Track 1
Expect(tracks[1].MediaFileID).To(Equal("2004")) // Disc 1, Track 2
Expect(tracks[2].MediaFileID).To(Equal("2003")) // Disc 2, Track 1
Expect(tracks[3].MediaFileID).To(Equal("2001")) // Disc 2, Track 11
})
})
}) })

View File

@ -55,7 +55,7 @@ func (r *playlistRepository) Tracks(playlistId string, refreshSmartPlaylist bool
"id": "playlist_tracks.id", "id": "playlist_tracks.id",
"artist": "order_artist_name", "artist": "order_artist_name",
"album_artist": "order_album_artist_name", "album_artist": "order_album_artist_name",
"album": "order_album_name, album_id, disc_number, track_number, order_artist_name, title", "album": "order_album_name, order_album_artist_name",
"title": "order_title", "title": "order_title",
// To make sure these fields will be whitelisted // To make sure these fields will be whitelisted
"duration": "duration", "duration": "duration",

View File

@ -57,7 +57,6 @@ func NewUserRepository(ctx context.Context, db dbx.Builder) model.UserRepository
r.db = db r.db = db
r.tableName = "user" r.tableName = "user"
r.registerModel(&model.User{}, map[string]filterFunc{ r.registerModel(&model.User{}, map[string]filterFunc{
"id": idFilter(r.tableName),
"password": invalidFilter(ctx), "password": invalidFilter(ctx),
"name": r.withTableName(startsWithFilter), "name": r.withTableName(startsWithFilter),
}) })

View File

@ -559,15 +559,4 @@ var _ = Describe("UserRepository", func() {
Expect(user.Libraries[0].ID).To(Equal(1)) Expect(user.Libraries[0].ID).To(Equal(1))
}) })
}) })
Describe("filters", func() {
It("qualifies id filter with table name", func() {
r := repo.(*userRepository)
qo := r.parseRestOptions(r.ctx, rest.QueryOptions{Filters: map[string]any{"id": "123"}})
sel := r.selectUserWithLibraries(qo)
query, _, err := r.toSQL(sel)
Expect(err).NotTo(HaveOccurred())
Expect(query).To(ContainSubstring("user.id = {:p0}"))
})
})
}) })

View File

@ -95,7 +95,7 @@ export const formatFullDate = (date, locale) => {
return new Date(date).toLocaleDateString(locale, options) return new Date(date).toLocaleDateString(locale, options)
} }
export const formatNumber = (value, locale) => { export const formatNumber = (value) => {
if (value === null || value === undefined) return '0' if (value === null || value === undefined) return '0'
return value.toLocaleString(locale) return value.toLocaleString()
} }

View File

@ -121,35 +121,35 @@ describe('formatDuration2', () => {
describe('formatNumber', () => { describe('formatNumber', () => {
it('handles null and undefined values', () => { it('handles null and undefined values', () => {
expect(formatNumber(null, 'en-CA')).toEqual('0') expect(formatNumber(null)).toEqual('0')
expect(formatNumber(undefined, 'en-CA')).toEqual('0') expect(formatNumber(undefined)).toEqual('0')
}) })
it('formats integers', () => { it('formats integers', () => {
expect(formatNumber(0, 'en-CA')).toEqual('0') expect(formatNumber(0)).toEqual('0')
expect(formatNumber(1, 'en-CA')).toEqual('1') expect(formatNumber(1)).toEqual('1')
expect(formatNumber(123, 'en-CA')).toEqual('123') expect(formatNumber(123)).toEqual('123')
expect(formatNumber(1000, 'en-CA')).toEqual('1,000') expect(formatNumber(1000)).toEqual('1,000')
expect(formatNumber(1234567, 'en-CA')).toEqual('1,234,567') expect(formatNumber(1234567)).toEqual('1,234,567')
}) })
it('formats decimal numbers', () => { it('formats decimal numbers', () => {
expect(formatNumber(123.45, 'en-CA')).toEqual('123.45') expect(formatNumber(123.45)).toEqual('123.45')
expect(formatNumber(1234.567, 'en-CA')).toEqual('1,234.567') expect(formatNumber(1234.567)).toEqual('1,234.567')
}) })
it('formats negative numbers', () => { it('formats negative numbers', () => {
expect(formatNumber(-123, 'en-CA')).toEqual('-123') expect(formatNumber(-123)).toEqual('-123')
expect(formatNumber(-1234, 'en-CA')).toEqual('-1,234') expect(formatNumber(-1234)).toEqual('-1,234')
expect(formatNumber(-123.45, 'en-CA')).toEqual('-123.45') expect(formatNumber(-123.45)).toEqual('-123.45')
}) })
}) })
describe('formatFullDate', () => { describe('formatFullDate', () => {
it('format dates', () => { it('format dates', () => {
expect(formatFullDate('2011', 'en-CA')).toEqual('2011') expect(formatFullDate('2011', 'en-US')).toEqual('2011')
expect(formatFullDate('2011-06', 'en-CA')).toEqual('Jun 2011') expect(formatFullDate('2011-06', 'en-US')).toEqual('Jun 2011')
expect(formatFullDate('1985-01-01', 'en-CA')).toEqual('Jan 1, 1985') expect(formatFullDate('1985-01-01', 'en-US')).toEqual('Jan 1, 1985')
expect(formatFullDate('199704')).toEqual('') expect(formatFullDate('199704')).toEqual('')
}) })
}) })

View File

@ -2,7 +2,6 @@ package str
import ( import (
"strings" "strings"
"unicode/utf8"
) )
var utf8ToAscii = func() *strings.Replacer { var utf8ToAscii = func() *strings.Replacer {
@ -40,25 +39,3 @@ func LongestCommonPrefix(list []string) string {
} }
return list[0] return list[0]
} }
// TruncateRunes truncates a string to a maximum number of runes, adding a suffix if truncated.
// The suffix is included in the rune count, so if maxRunes is 30 and suffix is "...", the actual
// string content will be truncated to fit within the maxRunes limit including the suffix.
func TruncateRunes(s string, maxRunes int, suffix string) string {
if utf8.RuneCountInString(s) <= maxRunes {
return s
}
suffixRunes := utf8.RuneCountInString(suffix)
truncateAt := maxRunes - suffixRunes
if truncateAt < 0 {
truncateAt = 0
}
runes := []rune(s)
if truncateAt >= len(runes) {
return s + suffix
}
return string(runes[:truncateAt]) + suffix
}

View File

@ -31,72 +31,6 @@ var _ = Describe("String Utils", func() {
Expect(str.LongestCommonPrefix(albums)).To(Equal("/artist/album")) Expect(str.LongestCommonPrefix(albums)).To(Equal("/artist/album"))
}) })
}) })
Describe("TruncateRunes", func() {
It("returns string unchanged if under max runes", func() {
Expect(str.TruncateRunes("hello", 10, "...")).To(Equal("hello"))
})
It("returns string unchanged if exactly at max runes", func() {
Expect(str.TruncateRunes("hello", 5, "...")).To(Equal("hello"))
})
It("truncates and adds suffix when over max runes", func() {
Expect(str.TruncateRunes("hello world", 8, "...")).To(Equal("hello..."))
})
It("handles unicode characters correctly", func() {
// 6 emoji characters, maxRunes=5, suffix="..." (3 runes)
// So content gets 5-3=2 runes
Expect(str.TruncateRunes("😀😁😂😃😄😅", 5, "...")).To(Equal("😀😁..."))
})
It("handles multi-byte UTF-8 characters", func() {
// Characters like é are single runes
Expect(str.TruncateRunes("Café au Lait", 5, "...")).To(Equal("Ca..."))
})
It("works with empty suffix", func() {
Expect(str.TruncateRunes("hello world", 5, "")).To(Equal("hello"))
})
It("accounts for suffix length in truncation", func() {
// maxRunes=10, suffix="..." (3 runes) -> leaves 7 runes for content
result := str.TruncateRunes("hello world this is long", 10, "...")
Expect(result).To(Equal("hello w..."))
// Verify total rune count is <= maxRunes
runeCount := len([]rune(result))
Expect(runeCount).To(BeNumerically("<=", 10))
})
It("handles very long suffix gracefully", func() {
// If suffix is longer than maxRunes, we still add it
// but the content will be truncated to 0
result := str.TruncateRunes("hello world", 5, "... (truncated)")
// Result will be just the suffix (since truncateAt=0)
Expect(result).To(Equal("... (truncated)"))
})
It("handles empty string", func() {
Expect(str.TruncateRunes("", 10, "...")).To(Equal(""))
})
It("uses custom suffix", func() {
// maxRunes=11, suffix=" [...]" (6 runes) -> content gets 5 runes
// "hello world" is 11 runes exactly, so we need a longer string
Expect(str.TruncateRunes("hello world extra", 11, " [...]")).To(Equal("hello [...]"))
})
DescribeTable("truncates at rune boundaries (not byte boundaries)",
func(input string, maxRunes int, suffix string, expected string) {
Expect(str.TruncateRunes(input, maxRunes, suffix)).To(Equal(expected))
},
Entry("ASCII", "abcdefghij", 5, "...", "ab..."),
Entry("Mixed ASCII and Unicode", "ab😀cd", 4, ".", "ab😀."),
Entry("All emoji", "😀😁😂😃😄", 3, "…", "😀😁…"),
Entry("Japanese", "こんにちは世界", 3, "…", "こん…"),
)
})
}) })
var testPaths = []string{ var testPaths = []string{