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:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user