Compare commits

...

16 Commits

Author SHA1 Message Date
Xavier Araque
dd6b4b541c
Merge ccc5bc566312e71d48d75c2dfa97f7d2e8359483 into 0c3012bbbdf232e3aeffd461ee05422e6f83829d 2025-11-20 02:42:08 -05:00
Deluan
0c3012bbbd chore(deps): update Go dependencies to latest versions
Signed-off-by: Deluan <deluan@navidrome.org>
2025-11-19 22:05:46 -05:00
Deluan
353aff2c88 fix(lastfm): ignore artist placeholder image.
Fix #4702

Signed-off-by: Deluan <deluan@navidrome.org>
2025-11-19 20:49:29 -05:00
Deluan
c873466e5b fix(scanner): reset watcher trigger timer for debounce on notification receipt
Signed-off-by: Deluan <deluan@navidrome.org>
2025-11-19 20:24:13 -05:00
Deluan Quintão
3d1946e31c
fix(plugins): avoid Chi RouteContext pollution by using http.NewRequest (#4713)
Signed-off-by: Deluan <deluan@navidrome.org>
2025-11-19 20:17:01 -05:00
Dongeun
6fb228bc10
fix(ui): fix translation display for library list terms (#4712) 2025-11-19 13:42:33 -05:00
Xavier Araque
ccc5bc5663
Merge branch 'master' into feat/add-squiddies-glass-theme 2025-11-04 20:09:33 +01:00
Xavier Araque
ec7f5c89b1 fix: loading albbun, play button color 2025-11-04 19:58:49 +01:00
Xavier Araque
48ebeb78cd Merge branch 'feat/add-squiddies-glass-theme' of github.com:rendergraf/navidrome into feat/add-squiddies-glass-theme 2025-10-29 17:42:38 +01:00
Xavier Araque
9cf63bea9c feat: fix chip, title artist 2025-10-29 17:39:43 +01:00
Xavier Araque
5888af6c4b
Merge branch 'master' into feat/add-squiddies-glass-theme 2025-10-29 16:05:09 +01:00
Xavier Araque
cbdffc8e4c feat: fix play button, and text mobile, prettier 2025-10-29 15:40:17 +01:00
Xavier Araque
d65292b22f feat: fix play button, and text mobile 2025-10-29 15:38:41 +01:00
Xavier Araque
d619e695c0 feat: fix Prettier format 2025-10-29 13:55:05 +01:00
Xavier Araque
b16ab5542d feat: fix commnets by gemini-code-assist in PR 2025-10-29 12:24:35 +01:00
Xavier Araque
43433399ba feat: Add SquiddiesGlass Theme 2025-10-29 11:57:08 +01:00
13 changed files with 932 additions and 51 deletions

View File

@ -38,6 +38,7 @@ type lastfmAgent struct {
secret string
lang string
client *client
httpClient httpDoer
getInfoMutex sync.Mutex
}
@ -56,6 +57,7 @@ func lastFMConstructor(ds model.DataStore) *lastfmAgent {
Timeout: consts.DefaultHttpClientTimeOut,
}
chc := cache.NewHTTPClient(hc, consts.DefaultHttpClientTimeOut)
l.httpClient = chc
l.client = newClient(l.apiKey, l.secret, l.lang, chc)
return l
}
@ -190,13 +192,13 @@ func (l *lastfmAgent) GetArtistTopSongs(ctx context.Context, id, artistName, mbi
return res, nil
}
var artistOpenGraphQuery = cascadia.MustCompile(`html > head > meta[property="og:image"]`)
var (
artistOpenGraphQuery = cascadia.MustCompile(`html > head > meta[property="og:image"]`)
artistIgnoredImage = "2a96cbd8b46e442fc41c2b86b821562f" // Last.fm artist placeholder image name
)
func (l *lastfmAgent) GetArtistImages(ctx context.Context, _, name, mbid string) ([]agents.ExternalImage, error) {
log.Debug(ctx, "Getting artist images from Last.fm", "name", name)
hc := http.Client{
Timeout: consts.DefaultHttpClientTimeOut,
}
a, err := l.callArtistGetInfo(ctx, name)
if err != nil {
return nil, fmt.Errorf("get artist info: %w", err)
@ -205,7 +207,7 @@ func (l *lastfmAgent) GetArtistImages(ctx context.Context, _, name, mbid string)
if err != nil {
return nil, fmt.Errorf("create artist image request: %w", err)
}
resp, err := hc.Do(req)
resp, err := l.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("get artist url: %w", err)
}
@ -222,11 +224,16 @@ func (l *lastfmAgent) GetArtistImages(ctx context.Context, _, name, mbid string)
return res, nil
}
for _, attr := range n.Attr {
if attr.Key == "content" {
res = []agents.ExternalImage{
{URL: attr.Val},
}
break
if attr.Key != "content" {
continue
}
if strings.Contains(attr.Val, artistIgnoredImage) {
log.Debug(ctx, "Artist image is ignored default image", "name", name, "url", attr.Val)
return res, nil
}
res = []agents.ExternalImage{
{URL: attr.Val},
}
}
return res, nil

View File

@ -393,4 +393,73 @@ var _ = Describe("lastfmAgent", func() {
})
})
})
Describe("GetArtistImages", func() {
var agent *lastfmAgent
var apiClient *tests.FakeHttpClient
var httpClient *tests.FakeHttpClient
BeforeEach(func() {
apiClient = &tests.FakeHttpClient{}
httpClient = &tests.FakeHttpClient{}
client := newClient("API_KEY", "SECRET", "pt", apiClient)
agent = lastFMConstructor(ds)
agent.client = client
agent.httpClient = httpClient
})
It("returns the artist image from the page", func() {
fApi, _ := os.Open("tests/fixtures/lastfm.artist.getinfo.json")
apiClient.Res = http.Response{Body: fApi, StatusCode: 200}
fScraper, _ := os.Open("tests/fixtures/lastfm.artist.page.html")
httpClient.Res = http.Response{Body: fScraper, StatusCode: 200}
images, err := agent.GetArtistImages(ctx, "123", "U2", "")
Expect(err).ToNot(HaveOccurred())
Expect(images).To(HaveLen(1))
Expect(images[0].URL).To(Equal("https://lastfm.freetls.fastly.net/i/u/ar0/818148bf682d429dc21b59a73ef6f68e.png"))
})
It("returns empty list if image is the ignored default image", func() {
fApi, _ := os.Open("tests/fixtures/lastfm.artist.getinfo.json")
apiClient.Res = http.Response{Body: fApi, StatusCode: 200}
fScraper, _ := os.Open("tests/fixtures/lastfm.artist.page.ignored.html")
httpClient.Res = http.Response{Body: fScraper, StatusCode: 200}
images, err := agent.GetArtistImages(ctx, "123", "U2", "")
Expect(err).ToNot(HaveOccurred())
Expect(images).To(BeEmpty())
})
It("returns empty list if page has no meta tags", func() {
fApi, _ := os.Open("tests/fixtures/lastfm.artist.getinfo.json")
apiClient.Res = http.Response{Body: fApi, StatusCode: 200}
fScraper, _ := os.Open("tests/fixtures/lastfm.artist.page.no_meta.html")
httpClient.Res = http.Response{Body: fScraper, StatusCode: 200}
images, err := agent.GetArtistImages(ctx, "123", "U2", "")
Expect(err).ToNot(HaveOccurred())
Expect(images).To(BeEmpty())
})
It("returns error if API call fails", func() {
apiClient.Err = errors.New("api error")
_, err := agent.GetArtistImages(ctx, "123", "U2", "")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("get artist info"))
})
It("returns error if scraper call fails", func() {
fApi, _ := os.Open("tests/fixtures/lastfm.artist.getinfo.json")
apiClient.Res = http.Response{Body: fApi, StatusCode: 200}
httpClient.Err = errors.New("scraper error")
_, err := agent.GetArtistImages(ctx, "123", "U2", "")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("get artist url"))
})
})
})

20
go.mod
View File

@ -57,16 +57,16 @@ require (
github.com/spf13/cobra v1.10.1
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/tetratelabs/wazero v1.10.0
github.com/tetratelabs/wazero v1.10.1
github.com/unrolled/secure v1.17.0
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342
go.uber.org/goleak v1.3.0
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
golang.org/x/image v0.32.0
golang.org/x/net v0.46.0
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6
golang.org/x/image v0.33.0
golang.org/x/net v0.47.0
golang.org/x/sync v0.18.0
golang.org/x/sys v0.38.0
golang.org/x/text v0.30.0
golang.org/x/text v0.31.0
golang.org/x/time v0.14.0
google.golang.org/protobuf v1.36.10
gopkg.in/yaml.v3 v3.0.1
@ -90,7 +90,7 @@ require (
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 // indirect
github.com/google/subcommands v1.2.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
@ -128,10 +128,10 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect
golang.org/x/tools v0.39.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
)

40
go.sum
View File

@ -99,8 +99,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc h1:hd+uUVsB1vdxohPneMrhGH2YfQuH5hRIK9u4/XCeUtw=
github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc/go.mod h1:SL66SJVysrh7YbDCP9tH30b8a9o/N2HeiQNUm85EKhc=
github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE5ed8Aol7JsKiI5X7yWKAtzhM0=
github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 h1:3DsUAV+VNEQa2CUVLxCY3f87278uWfIDhJnbdvDjvmE=
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@ -265,8 +265,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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tetratelabs/wazero v1.10.0 h1:CXP3zneLDl6J4Zy8N/J+d5JsWKfrjE6GtvVK1fpnDlk=
github.com/tetratelabs/wazero v1.10.0/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8=
github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
@ -298,20 +298,20 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -323,8 +323,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -353,8 +353,8 @@ golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo=
golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -373,8 +373,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -384,8 +384,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=

View File

@ -93,8 +93,12 @@ func (s *subsonicAPIServiceImpl) Call(ctx context.Context, req *subsonicapi.Call
RawQuery: query.Encode(),
}
// Create HTTP request with internal authentication
httpReq, err := http.NewRequestWithContext(ctx, "GET", finalURL.String(), nil)
// Create HTTP request with a fresh context to avoid Chi RouteContext pollution.
// Using http.NewRequest (instead of http.NewRequestWithContext) ensures the internal
// SubsonicAPI call doesn't inherit routing information from the parent handler,
// which would cause Chi to invoke the wrong handler. Authentication context is
// explicitly added in the next step via request.WithInternalAuth.
httpReq, err := http.NewRequest("GET", finalURL.String(), nil)
if err != nil {
return &subsonicapi.CallResponse{
Error: fmt.Sprintf("failed to create HTTP request: %v", err),

View File

@ -122,6 +122,9 @@ func (w *watcher) Run(ctx context.Context) error {
w.mu.Unlock()
return nil
case notification := <-w.watcherNotify:
// Reset the trigger timer for debounce
trigger.Reset(w.triggerWait)
lib := notification.Library
folderPath := notification.FolderPath
@ -131,7 +134,6 @@ func (w *watcher) Run(ctx context.Context) error {
continue
}
targets[target] = struct{}{}
trigger.Reset(w.triggerWait)
log.Debug(ctx, "Watcher: Detected changes. Waiting for more changes before triggering scan",
"libraryID", lib.ID, "name", lib.Name, "path", lib.Path, "folderPath", folderPath)

View File

@ -0,0 +1,7 @@
<html>
<head>
<meta property="og:image" content="https://lastfm.freetls.fastly.net/i/u/ar0/818148bf682d429dc21b59a73ef6f68e.png" />
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,7 @@
<html>
<head>
<meta property="og:image" content="https://lastfm.freetls.fastly.net/i/u/ar0/2a96cbd8b46e442fc41c2b86b821562f.png" />
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,6 @@
<html>
<head>
</head>
<body>
</body>
</html>

View File

@ -42,15 +42,11 @@ const LibraryList = (props) => {
<TextField source="name" />
<TextField source="path" />
<BooleanField source="defaultNewUsers" />
<NumberField source="totalSongs" label="Songs" />
<NumberField source="totalAlbums" label="Albums" />
<NumberField source="totalMissingFiles" label="Missing Files" />
<NumberField source="totalSongs" />
<NumberField source="totalAlbums" />
<NumberField source="totalMissingFiles" />
<SizeField source="totalSize" />
<DateField
source="lastScanAt"
label="Last Scan"
sortByOrder={'DESC'}
/>
<DateField source="lastScanAt" sortByOrder={'DESC'} />
</Datagrid>
)}
</List>

View File

@ -0,0 +1,175 @@
const stylesheet = `
.react-jinke-music-player-main .music-player-panel .panel-content .rc-slider-handle {
background: #c231ab
}
.react-jinke-music-player-main .music-player-panel .panel-content .rc-slider-track,
.react-jinke-music-player-mobile-progress .rc-slider-track {
background: linear-gradient(to left, #c231ab, #380eff)
}
.react-jinke-music-player-mobile {
background-color: #171717 !important;
}
.react-jinke-music-player-mobile-progress .rc-slider-handle {
background: #c231ab;
height: 20px;
width: 20px;
margin-top: -9px;
}
.react-jinke-music-player-main ::-webkit-scrollbar-thumb {
background-color: #c231ab;
}
.react-jinke-music-player-pause-icon {
background-color: #c231ab;
border-radius: 50%;
outline: auto;
color: white;
}
.react-jinke-music-player-main .music-player-panel .panel-content .player-content {
z-index: 99999;
}
.react-jinke-music-player-main .music-player-panel .panel-content .player-content .play-btn svg {
border-radius: 50%;
outline: auto;
color: white;
}
.react-jinke-music-player-main .music-player-panel .panel-content .player-content .play-btn svg:hover {
background-color: #c231ab;
border-radius: 50%;
outline: auto;
color: white;
}
.react-jinke-music-player-main svg:hover {
color: #c231ab;
}
.react-jinke-music-player .music-player-controller {
color: #c231ab;
border: 1px solid #e14ac2;
}
.react-jinke-music-player .music-player-controller.music-player-playing:before {
border: 1px solid rgba(194, 49, 171, 0.3);
}
.react-jinke-music-player .music-player .destroy-btn {
background-color: #c2c1c2;
top: -7px;
border-radius: 50%;
display: flex;
}
.react-jinke-music-player .music-player .destroy-btn svg {
font-size: 20px;
}
@media screen and (max-width: 767px) {
.react-jinke-music-player .music-player .destroy-btn {
right: -12px;
}
}
.react-jinke-music-player-mobile-header-right {
right: 0;
top: 0;
}
@media screen and (max-width: 767px) {
.react-jinke-music-player-main svg {
font-size: 32px;
}
}
@keyframes gradientFlow {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.RaBulkActionsToolbar .MuiButton-label {
color: white;
}
a[aria-current="page"] {
color: #c231ab !important;
font-weight: bold;
}
a[aria-current="page"] .MuiListItemIcon-root {
color: #c231ab !important;
}
.panel-content {
position: relative;
overflow: hidden;
background: linear-gradient(90deg, #311f2f, #0a0912, #2f0c28);
background-size: 300% 300%;
animation: gradientFlow 10s ease-in-out infinite;
}
/* Equalizer bars */
.panel-content::before {
content: "";
position: absolute;
inset: 0;
background: repeating-linear-gradient(
90deg,
rgba(255, 255, 255, 0.05) 0px,
rgba(255, 255, 255, 0.05) 2px,
transparent 1px,
transparent 3px
);
animation: equalizer 1.8s infinite ease-in-out;
filter: blur(1px);
opacity: 0.5;
}
@keyframes backgroundFlow {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
/* Vertical movement, equalizer type */
@keyframes equalizer {
0%, 100% {
transform: scaleY(1);
opacity: 0.2;
}
25% {
transform: scaleY(1.4);
opacity: 0.9;
}
50% {
transform: scaleY(0.7);
opacity: 0.2;
}
75% {
transform: scaleY(1.2);
opacity: 0.8;
}
}
@keyframes pulse {
0% { opacity: 0.5; }
100% { opacity: 1; }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`
export default stylesheet

View File

@ -0,0 +1,606 @@
import stylesheet from './SquiddiesGlass.css.js'
/**
* Color constants used throughout the Squiddies Glass theme.
* Provides a consistent color palette with pink, gray, purple, and basic colors.
* @type {Object}
*/
const colors = {
pink: {
100: '#fbe3f4',
200: '#f5b9e3',
300: '#ec7cd6',
400: '#e14ac2',
500: '#c231ab', // base
600: '#a31a92',
700: '#8b0f7e',
800: '#7a006d',
900: '#670066',
},
gray: {
50: '#c2c1c2',
100: '#b3b3b3', // light gray
200: '#282828', // medium dark
300: '#1d1d1d', // darker
400: '#181818', // even darker
500: '#171717', // darkest
},
purple: {
400: '#524590',
500: '#4d3249',
600: '#6d1c5e',
},
black: '#000',
white: '#fff',
dark: '#121212',
}
/**
* Shared style object for music list action buttons.
* Defines common styling for buttons in music lists, including hover effects and responsive scaling.
* @type {Object}
*/
const musicListActions = {
padding: '1rem 0',
alignItems: 'center',
'@global': {
button: {
border: '1px solid transparent',
backgroundColor: 'inherit',
color: colors.gray[100],
'&:hover': {
border: `1px solid ${colors.gray[100]}`,
backgroundColor: 'inherit !important',
},
},
'button:first-child:not(:only-child)': {
'@media screen and (max-width: 720px)': {
transform: 'scale(1.3)',
margin: '1em',
'&:hover': {
transform: 'scale(1.2) !important',
},
},
transform: 'scale(1.3)',
margin: '1em',
minWidth: 0,
padding: 5,
transition: 'transform .3s ease',
background: colors.pink[500],
color: `${colors.black} !important`,
borderRadius: 500,
border: 0,
'&:hover': {
transform: 'scale(1.2)',
backgroundColor: `${colors.pink[500]} !important`,
border: 0,
},
},
'button:only-child': {
marginTop: '0.3em',
},
'button:first-child>span:first-child': {
padding: 0,
color: `${colors.black} !important`,
},
'button:first-child>span:first-child>span': {
display: 'none',
},
'button>span:first-child>span, button:not(:first-child)>span:first-child>svg':
{
color: colors.gray[100],
},
},
}
/**
* Squiddies Glass theme configuration object.
* Defines the complete theme structure including typography, palette, component overrides, and player settings.
* @type {Object}
*/
export default {
/**
* The name of the theme.
* @type {string}
*/
themeName: 'Squiddies Glass',
/**
* Typography settings for the theme.
* Specifies font family and heading sizes.
* @type {Object}
*/
typography: {
fontFamily: "system-ui, 'Helvetica Neue', Helvetica, Arial, sans-serif",
h6: {
fontSize: '1rem', // AppBar title
},
},
/**
* Color palette configuration.
* Defines primary, secondary, and background colors for the theme.
* @type {Object}
*/
palette: {
primary: {
light: colors.pink[300],
main: colors.pink[500],
},
secondary: {
main: colors.white,
contrastText: colors.white,
},
background: {
default: colors.dark,
paper: colors.dark,
},
type: 'dark',
},
/**
* Component overrides for Material-UI and custom Navidrome components.
* Customizes the appearance and behavior of various UI components.
* @type {Object}
*/
overrides: {
// Material-UI Components
MuiAppBar: {
positionFixed: {
backgroundColor: `${colors.black} !important`,
boxShadow: 'none',
},
},
MuiButton: {
root: {
background: colors.pink[500],
color: colors.white,
border: '1px solid transparent',
borderRadius: 500,
'&:hover': {
background: `${colors.pink[900]} !important`,
},
},
textSecondary: {
border: `1px solid ${colors.gray[100]}`,
background: colors.black,
'&:hover': {
border: `1px solid ${colors.white} !important`,
background: `${colors.black} !important`,
},
},
label: {
color: colors.white,
paddingRight: '1rem',
paddingLeft: '0.7rem',
},
},
MuiCardMedia: {
root: {
position: 'relative',
overflow: 'hidden',
boxShadow: `0 2px 32px rgba(0,0,0,0.5), 0px 1px 5px rgba(0,0,0,0.1)`,
},
},
MuiDivider: {
root: {
margin: '.75rem 0',
},
},
MuiDrawer: {
root: {
background: colors.gray[500],
paddingTop: '10px',
},
},
MuiFormGroup: {
root: {
color: colors.pink[500],
},
},
MuiMenuItem: {
root: {
fontSize: '0.875rem',
},
},
MuiTableCell: {
root: {
borderBottom: `1px solid ${colors.gray[300]}`,
padding: '10px !important',
color: `${colors.gray[100]} !important`,
'& img': {
filter:
'brightness(0) saturate(100%) invert(36%) sepia(93%) saturate(7463%) hue-rotate(289deg) brightness(95%) contrast(102%);',
},
'& img + span': {
color: colors.pink[500],
},
},
head: {
borderBottom: `1px solid ${colors.gray[200]}`,
fontSize: '0.75rem',
textTransform: 'uppercase',
letterSpacing: 1.2,
},
},
MuiTableRow: {
root: {
padding: '10px 0',
transition: 'background-color .3s ease',
'&:hover': {
backgroundColor: `${colors.gray[300]} !important`,
},
'@global': {
'td:nth-child(4)': {
color: `${colors.white} !important`,
},
},
},
},
// React Admin Components
RaBulkActionsToolbar: {
topToolbar: {
gap: '8px',
},
},
RaFilter: {
form: {
'& .MuiOutlinedInput-input:-webkit-autofill': {
'-webkit-box-shadow': `0 0 0 100px ${colors.gray[50]} inset`,
'-webkit-text-fill-color': colors.white,
},
},
},
RaFilterButton: {
root: {
marginRight: '1rem',
},
},
RaLayout: {
content: {
padding: '0 !important',
background: `linear-gradient(${colors.dark}, ${colors.gray[500]})`,
borderTopRightRadius: '8px',
borderTopLeftRadius: '8px',
},
contentWithSidebar: {
gap: '2px',
},
},
RaList: {
content: {
backgroundColor: 'inherit',
},
bulkActionsDisplayed: {
marginTop: '-20px',
},
},
RaListToolbar: {
toolbar: {
padding: '0 .55rem !important',
},
},
RaPaginationActions: {
currentPageButton: {
border: `1px solid ${colors.gray[100]}`,
},
button: {
backgroundColor: 'inherit',
minWidth: 48,
margin: '0 4px',
border: `1px solid ${colors.gray[200]}`,
'@global': {
'> .MuiButton-label': {
padding: 0,
},
},
},
actions: {
'@global': {
'.next-page': {
marginLeft: 8,
marginRight: 8,
},
'.previous-page': {
marginRight: 8,
},
},
},
},
RaSearchInput: {
input: {
paddingLeft: '.9rem',
border: 0,
'& .MuiInputBase-root': {
backgroundColor: `${colors.white} !important`,
borderRadius: '20px !important',
color: colors.black,
border: '0px',
'& fieldset': {
borderColor: colors.white,
},
'&:hover fieldset': {
borderColor: colors.white,
},
'&.Mui-focused fieldset': {
borderColor: colors.white,
},
'& svg': {
color: `${colors.black} !important`,
},
'& .MuiOutlinedInput-input:-webkit-autofill': {
borderRadius: '20px 0px 0px 20px',
'-webkit-box-shadow': `0 0 0 100px ${colors.gray[50]} inset`,
'-webkit-text-fill-color': colors.black,
},
},
},
},
RaSidebar: {
root: {
height: 'initial',
borderTopRightRadius: '8px',
borderTopLeftRadius: '8px',
},
},
// Navidrome Custom Components
NDAlbumDetails: {
root: {
boxShadow: 'none',
background: `linear-gradient(45deg, ${colors.purple[500]}, ${colors.purple[400]}, ${colors.purple[600]})`,
backgroundSize: '200% 200%',
animation: 'gradientFlow 8s ease-in-out infinite',
position: 'relative',
'&:before': {
content: '""',
position: 'absolute',
top: '0',
left: '0',
width: '100%',
height: '100%',
background: `linear-gradient(to bottom, transparent, ${colors.dark})`,
},
},
cardContents: {
alignItems: 'flex-start',
},
coverParent: {
zIndex: '99999',
position: 'relative',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
'&::before': {
content: '""',
position: 'absolute',
inset: '0',
width: '100%',
height: '100%',
borderRadius: '50%',
animation: 'pulse 1.5s ease-in-out infinite alternate',
zIndex: -1,
},
'&::after': {
content: '""',
position: 'absolute',
inset: '0',
zIndex: '-1',
borderRadius: '50%',
background: 'repeating-conic-gradient(from 0deg, rgba(255,255,255,0.08) 0deg, rgba(255,255,255,0.08) 0.5deg, rgba(0,0,0,1) 1deg)',
filter: 'contrast(999) sepia(1)',
boxShadow: 'inset 0 0 25px rgba(255,255,255,0.05), inset 0 0 95px rgba(0,0,0,0.9)',
animation: 'spin 6s linear infinite',
}
},
details: {
zIndex: '99999',
},
recordName: {
fontSize: 'calc(1rem + 1.5vw)',
fontWeight: 900,
},
recordArtist: {
fontSize: '1.5rem',
fontWeight: 700,
textShadow: '0 2px 16px rgba(0, 0, 0, 0.3)',
},
recordMeta: {
fontSize: '.875rem',
color: `rgba(${colors.white}, 0.8)`,
},
content: {
paddingBottom: '0px !important',
paddingTop: '0px',
},
},
RaSingleFieldList: {
root: {
'& a:first-of-type > .MuiChip-root': {
marginLeft: '0px',
},
'& a > .MuiChip-root': {
backgroundColor: colors.pink[500],
fontSize: '0.6rem',
height: '20px',
'& .MuiChip-label': {
color: colors.white,
paddingLeft: '5px',
paddingRight: '5px',
},
},
},
},
MuiGridListTile: {
tile: {
'&:hover': {
boxShadow: '0 2px 32px rgba(0,0,0,0.5), 0px 1px 5px rgba(0,0,0,0.1)',
},
},
},
NDAlbumGridView: {
tileBar: {
background:
'linear-gradient(to top, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.4) 50%, rgba(0, 0, 0, 0) 100%)',
marginBottom: '2px',
},
albumName: {
marginTop: '0.5rem',
fontWeight: 700,
textTransform: 'none',
color: colors.white,
},
albumSubtitle: {
color: colors.gray[100],
},
albumContainer: {
backgroundColor: colors.gray[400],
borderRadius: '.5rem',
padding: '.75rem',
transition: 'background-color .3s ease',
'&:hover': {
backgroundColor: colors.gray[200],
},
},
albumPlayButton: {
color: colors.black,
backgroundColor: colors.pink[500],
borderRadius: '50%',
boxShadow: '0 8px 8px rgb(0 0 0 / 30%)',
padding: '0.35rem',
transition: 'padding .3s ease',
'&:hover': {
background: `${colors.pink[500]} !important`,
padding: '0.45rem',
},
},
},
NDAlbumShow: {
albumActions: musicListActions,
},
NDArtistShow: {
actions: {
padding: '2rem 0',
alignItems: 'center',
overflow: 'visible',
minHeight: '120px',
'@global': {
button: {
border: '1px solid transparent',
backgroundColor: 'inherit',
color: colors.gray[100],
margin: '0 0.5rem',
'&:hover': {
border: `1px solid ${colors.gray[100]}`,
backgroundColor: 'inherit !important',
},
},
// Hide shuffle button label (first button)
'button:first-child>span:first-child>span': {
display: 'none',
},
// Style shuffle button (first button)
'button:first-child': {
'@media screen and (max-width: 720px)': {
transform: 'scale(1.5)',
margin: '1rem',
'&:hover': {
transform: 'scale(1.6) !important',
},
},
transform: 'scale(2)',
margin: '1.5rem',
minWidth: 0,
padding: 5,
transition: 'transform .3s ease',
background: colors.pink[500],
color: colors.white,
borderRadius: 500,
border: 0,
'&:hover': {
transform: 'scale(2.1)',
backgroundColor: `${colors.pink[500]} !important`,
border: 0,
},
},
'button:first-child>span:first-child': {
padding: 0,
color: `${colors.black} !important`,
},
'button>span:first-child>span, button:not(:first-child)>span:first-child>svg':
{
color: colors.gray[100],
},
},
},
actionsContainer: {
overflow: 'visible',
},
},
NDAudioPlayer: {
audioTitle: {
color: colors.white,
fontSize: '1.5rem',
'& span:nth-child(3)': {
fontSize: '0.8rem',
},
},
songTitle: {
fontWeight: 900,
},
songInfo: {
fontSize: '0.9rem',
color: colors.gray[100],
},
},
NDCollapsibleComment: {
commentBlock: {
fontSize: '.875rem',
color: `rgba(${colors.white}, 0.8)`,
},
},
NDLogin: {
main: {
boxShadow: `inset 0 0 0 2000px rgba(${colors.black}, .75)`,
},
systemNameLink: {
color: colors.white,
},
card: {
border: `1px solid ${colors.gray[200]}`,
},
avatar: {
marginBottom: 0,
},
},
NDPlaylistDetails: {
container: {
background: `linear-gradient(${colors.gray[300]}, transparent)`,
borderRadius: 0,
paddingTop: '2.5rem !important',
boxShadow: 'none',
},
title: {
fontSize: 'calc(1.5rem + 1.5vw)',
fontWeight: 700,
color: colors.white,
},
details: {
fontSize: '.875rem',
color: `rgba(${colors.white}, 0.8)`,
},
},
NDPlaylistShow: {
playlistActions: musicListActions,
},
},
/**
* Player configuration settings.
* Specifies the player theme and associated stylesheet.
* @type {Object}
*/
player: {
theme: 'dark',
stylesheet,
},
}

View File

@ -10,6 +10,7 @@ import NordTheme from './nord'
import GruvboxDarkTheme from './gruvboxDark'
import CatppuccinMacchiatoTheme from './catppuccinMacchiato'
import NuclearTheme from './nuclear'
import SquiddiesGlassTheme from './SquiddiesGlass'
export default {
// Classic default themes
@ -27,4 +28,5 @@ export default {
NordTheme,
NuclearTheme,
SpotifyTheme,
SquiddiesGlassTheme,
}