mirror of
https://github.com/navidrome/navidrome.git
synced 2026-04-03 06:41:01 +00:00
Compare commits
16 Commits
8e5b8e70c0
...
dd6b4b541c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd6b4b541c | ||
|
|
0c3012bbbd | ||
|
|
353aff2c88 | ||
|
|
c873466e5b | ||
|
|
3d1946e31c | ||
|
|
6fb228bc10 | ||
|
|
ccc5bc5663 | ||
|
|
ec7f5c89b1 | ||
|
|
48ebeb78cd | ||
|
|
9cf63bea9c | ||
|
|
5888af6c4b | ||
|
|
cbdffc8e4c | ||
|
|
d65292b22f | ||
|
|
d619e695c0 | ||
|
|
b16ab5542d | ||
|
|
43433399ba |
@ -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
|
||||
|
||||
@ -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
20
go.mod
@ -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
40
go.sum
@ -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=
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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)
|
||||
|
||||
7
tests/fixtures/lastfm.artist.page.html
vendored
Normal file
7
tests/fixtures/lastfm.artist.page.html
vendored
Normal 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>
|
||||
7
tests/fixtures/lastfm.artist.page.ignored.html
vendored
Normal file
7
tests/fixtures/lastfm.artist.page.ignored.html
vendored
Normal 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>
|
||||
6
tests/fixtures/lastfm.artist.page.no_meta.html
vendored
Normal file
6
tests/fixtures/lastfm.artist.page.no_meta.html
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@ -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>
|
||||
|
||||
175
ui/src/themes/SquiddiesGlass.css.js
Normal file
175
ui/src/themes/SquiddiesGlass.css.js
Normal 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
|
||||
606
ui/src/themes/SquiddiesGlass.js
Normal file
606
ui/src/themes/SquiddiesGlass.js
Normal 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,
|
||||
},
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user