feat(model): add uploaded_image column and avatar helpers to user

This commit is contained in:
Deluan 2026-09-07 18:59:22 -04:00
parent 962910f0de
commit 42008a1b88
5 changed files with 81 additions and 0 deletions

View File

@ -0,0 +1,5 @@
-- +goose Up
ALTER TABLE user ADD COLUMN uploaded_image VARCHAR(255) DEFAULT '';
-- +goose Down
ALTER TABLE user DROP COLUMN uploaded_image;

View File

@ -13,5 +13,8 @@ func UploadedImagePath(entityType, filename string) string {
if filename == "" {
return ""
}
if entityType == consts.EntityUser {
return filepath.Join(conf.Server.DataFolder.String(), consts.AvatarFolder, filename)
}
return filepath.Join(conf.Server.DataFolder.String(), consts.ArtworkFolder, entityType, filename)
}

29
model/image_test.go Normal file
View File

@ -0,0 +1,29 @@
package model_test
import (
"path/filepath"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/model"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("UploadedImagePath", func() {
BeforeEach(func() {
DeferCleanup(configtest.SetupConfig())
conf.Server.DataFolder = conf.NewDir("/data")
})
It("puts user avatars in the avatar folder, not under artwork", func() {
Expect(model.UploadedImagePath(consts.EntityUser, "abc_deluan.png")).
To(Equal(filepath.Join("/data", "avatar", "abc_deluan.png")))
})
It("keeps other entities under the artwork folder", func() {
Expect(model.UploadedImagePath(consts.EntityArtist, "abc_bowie.png")).
To(Equal(filepath.Join("/data", "artwork", "artist", "abc_bowie.png")))
})
})

View File

@ -1,7 +1,12 @@
package model
import (
"crypto/md5"
"encoding/hex"
"fmt"
"time"
"github.com/navidrome/navidrome/consts"
)
type User struct {
@ -17,6 +22,10 @@ type User struct {
// Smart-playlist criteria JSON; matching songs are not sent to external scrobblers
ScrobbleFilter string `structs:"scrobble_filter" json:"scrobbleFilter"`
// structs:"-" because userRepository.Put writes every mapped column with no column list, so a
// profile save from the UI would blank this. UpdateImage is the only writer.
UploadedImage string `structs:"-" json:"uploadedImage,omitempty"`
// Library associations (many-to-many relationship)
Libraries Libraries `structs:"-" json:"libraries,omitempty"`
@ -43,6 +52,18 @@ func (u User) HasLibraryAccess(libraryID int) bool {
return false
}
func (u User) UploadedImagePath() string {
return UploadedImagePath(consts.EntityUser, u.UploadedImage)
}
func (u User) AvatarTag() string {
if u.UploadedImage == "" {
return ""
}
sum := md5.Sum(fmt.Appendf(nil, "%s|%d", u.UploadedImage, u.UpdatedAt.UnixNano()))
return hex.EncodeToString(sum[:])[:16]
}
type Users []User
type UserRepository interface {

View File

@ -1,6 +1,8 @@
package model_test
import (
"time"
"github.com/navidrome/navidrome/model"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
@ -80,4 +82,25 @@ var _ = Describe("User", func() {
})
})
})
Describe("AvatarTag", func() {
It("returns an empty tag when there is no avatar", func() {
u := model.User{ID: "1", UpdatedAt: time.Now()}
Expect(u.AvatarTag()).To(BeEmpty())
})
It("changes the tag when the image changes", func() {
at := time.Unix(1000, 0)
a := model.User{ID: "1", UploadedImage: "1_deluan.png", UpdatedAt: at}
b := model.User{ID: "1", UploadedImage: "1_deluan.jpg", UpdatedAt: at}
Expect(a.AvatarTag()).NotTo(BeEmpty())
Expect(a.AvatarTag()).NotTo(Equal(b.AvatarTag()))
})
It("changes the tag when the same file is replaced", func() {
a := model.User{ID: "1", UploadedImage: "1_deluan.png", UpdatedAt: time.Unix(1000, 0)}
b := model.User{ID: "1", UploadedImage: "1_deluan.png", UpdatedAt: time.Unix(2000, 0)}
Expect(a.AvatarTag()).NotTo(Equal(b.AvatarTag()))
})
})
})