feat: extract embedded album artwork during scans

Add a dedicated artwork cache directory and save embedded album art from audio tags during library scans. Prefer embedded artwork for album cover resolution while keeping sidecar image files as the fallback path.
This commit is contained in:
2026-04-02 22:43:11 +03:00
parent 83b7addb88
commit 2f7034fae2
4 changed files with 52 additions and 4 deletions

View File

@@ -2,6 +2,7 @@ APP_ENV=development
SERVER_HOST=0.0.0.0 SERVER_HOST=0.0.0.0
SERVER_PORT=4040 SERVER_PORT=4040
DATABASE_PATH=./data/app.db DATABASE_PATH=./data/app.db
ARTWORK_CACHE_DIR=./data/artwork
MEDIA_ROOT=./media MEDIA_ROOT=./media
CORS_ORIGINS=http://localhost:5173 CORS_ORIGINS=http://localhost:5173
DEFAULT_ADMIN_USERNAME=demo DEFAULT_ADMIN_USERNAME=demo

View File

@@ -33,7 +33,7 @@ func main() {
log.Fatalf("database seed failed: %v", err) log.Fatalf("database seed failed: %v", err)
} }
scanService := scanner.NewService(database, cfg.MediaRoot) scanService := scanner.NewService(database, cfg.MediaRoot, cfg.ArtworkCacheDir)
if scanService.HasMediaFiles() { if scanService.HasMediaFiles() {
if result, err := scanService.Scan(ctx); err != nil { if result, err := scanService.Scan(ctx); err != nil {
log.Printf("startup scan failed: %v", err) log.Printf("startup scan failed: %v", err)

View File

@@ -7,6 +7,7 @@ type Config struct {
ServerHost string ServerHost string
ServerPort string ServerPort string
DatabasePath string DatabasePath string
ArtworkCacheDir string
MediaRoot string MediaRoot string
CORSOrigins string CORSOrigins string
DefaultAdminUsername string DefaultAdminUsername string
@@ -19,6 +20,7 @@ func Load() Config {
ServerHost: getenv("SERVER_HOST", "0.0.0.0"), ServerHost: getenv("SERVER_HOST", "0.0.0.0"),
ServerPort: getenv("SERVER_PORT", "4040"), ServerPort: getenv("SERVER_PORT", "4040"),
DatabasePath: getenv("DATABASE_PATH", "./data/app.db"), DatabasePath: getenv("DATABASE_PATH", "./data/app.db"),
ArtworkCacheDir: getenv("ARTWORK_CACHE_DIR", "./data/artwork"),
MediaRoot: getenv("MEDIA_ROOT", "./media"), MediaRoot: getenv("MEDIA_ROOT", "./media"),
CORSOrigins: getenv("CORS_ORIGINS", "http://localhost:5173"), CORSOrigins: getenv("CORS_ORIGINS", "http://localhost:5173"),
DefaultAdminUsername: getenv("DEFAULT_ADMIN_USERNAME", "demo"), DefaultAdminUsername: getenv("DEFAULT_ADMIN_USERNAME", "demo"),

View File

@@ -38,6 +38,7 @@ type Result struct {
type Service struct { type Service struct {
db *sql.DB db *sql.DB
mediaRoot string mediaRoot string
artworkRoot string
mu sync.RWMutex mu sync.RWMutex
status Status status Status
} }
@@ -78,8 +79,8 @@ type scannedTrack struct {
ContentType string ContentType string
} }
func NewService(db *sql.DB, mediaRoot string) *Service { func NewService(db *sql.DB, mediaRoot, artworkRoot string) *Service {
return &Service{db: db, mediaRoot: mediaRoot} return &Service{db: db, mediaRoot: mediaRoot, artworkRoot: artworkRoot}
} }
func (s *Service) HasMediaFiles() bool { func (s *Service) HasMediaFiles() bool {
@@ -327,7 +328,13 @@ func (s *Service) scanFile(path string) (scannedItem, error) {
artistID := hashID("artist", artistName) artistID := hashID("artist", artistName)
albumID := hashID("album", artistName, albumTitle, fmt.Sprintf("%d", year)) albumID := hashID("album", artistName, albumTitle, fmt.Sprintf("%d", year))
trackID := hashID("track", path) trackID := hashID("track", path)
coverArt := findCoverArt(filepath.Dir(path)) coverArt := ""
if err == nil {
coverArt = s.extractEmbeddedCoverArt(metadata, albumID)
}
if coverArt == "" {
coverArt = findCoverArt(filepath.Dir(path))
}
return scannedItem{ return scannedItem{
artist: scannedArtist{ artist: scannedArtist{
@@ -417,6 +424,44 @@ func findCoverArt(dir string) string {
return "" return ""
} }
func (s *Service) extractEmbeddedCoverArt(metadata tag.Metadata, albumID string) string {
if metadata == nil {
return ""
}
picture := metadata.Picture()
if picture == nil || len(picture.Data) == 0 {
return ""
}
if s.artworkRoot == "" {
return ""
}
if err := os.MkdirAll(s.artworkRoot, 0o755); err != nil {
return ""
}
filename := albumID + coverExtension(picture.MIMEType)
path := filepath.Join(s.artworkRoot, filename)
if err := os.WriteFile(path, picture.Data, 0o644); err != nil {
return ""
}
return path
}
func coverExtension(mimeType string) string {
switch strings.ToLower(mimeType) {
case "image/png":
return ".png"
case "image/webp":
return ".webp"
default:
return ".jpg"
}
}
func (s *Service) tryMarkStarted() bool { func (s *Service) tryMarkStarted() bool {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()