diff --git a/internal/controller/library.go b/internal/controller/library.go index e5a0dde..b3de030 100644 --- a/internal/controller/library.go +++ b/internal/controller/library.go @@ -122,13 +122,13 @@ func (c *LibraryController) Scan(w http.ResponseWriter, r *http.Request) { c.scanMutex.Unlock() }() - err := c.service.Scan(id) + report, err := c.service.Scan(id) if err != nil { - log.Printf("Scan failed: %v", err) + log.Printf("Scan failed for library %d: %v", id, err) return } - log.Printf("Scan completed successfully for library %d", id) + log.Printf("Scan completed for library %d: %+v", id, report) }() jsonMsg(w, "Library scan started.") diff --git a/internal/service/library.go b/internal/service/library.go index d1dcc8a..332c561 100644 --- a/internal/service/library.go +++ b/internal/service/library.go @@ -3,9 +3,21 @@ package service import ( "butterfliu/internal/repository" "butterfliu/internal/scanner" + "errors" + "fmt" "log" + "path/filepath" + "strings" ) +// ScanReport holds the result of a library scan. +type ScanReport struct { + TotalFiles int + Added int + Skipped int + FailedFiles []string +} + type LibraryService struct { repo *repository.LibraryRepository } @@ -23,14 +35,26 @@ func (s *LibraryService) GetByID(id int) (repository.Library, error) { } func (s *LibraryService) Create(name, path string) (repository.Library, error) { + if name = strings.TrimSpace(name); name == "" { + return repository.Library{}, errors.New("library name cannot be empty") + } + if path = strings.TrimSpace(path); path == "" { + return repository.Library{}, errors.New("library path cannot be empty") + } return s.repo.Create(name, path) } func (s *LibraryService) UpdateName(id int, name string) error { + if name = strings.TrimSpace(name); name == "" { + return errors.New("library name cannot be empty") + } return s.repo.UpdateName(id, name) } func (s *LibraryService) UpdatePath(id int, path string) error { + if path = strings.TrimSpace(path); path == "" { + return errors.New("library path cannot be empty") + } return s.repo.UpdatePath(id, path) } @@ -50,53 +74,75 @@ func (s *LibraryService) GetAlbums() ([]repository.Album, error) { return s.repo.GetAlbums() } -func (s *LibraryService) Scan(id int) error { +func (s *LibraryService) Scan(id int) (*ScanReport, error) { lib, err := s.repo.GetByID(id) if err != nil { - return err + return nil, fmt.Errorf("library %d not found: %w", id, err) } scannedSongs, err := scanner.ScanDirectory(lib.Path) if err != nil { - return err + return nil, fmt.Errorf("scan directory %s: %w", lib.Path, err) } - log.Println("Scanned songs:", len(scannedSongs)) + report := &ScanReport{TotalFiles: len(scannedSongs)} for _, song := range scannedSongs { - mediaFile, err := s.repo.GetMediaFileByPath(song.Path) - if err != nil { - mediaFile, err = s.repo.CreateMediaFile(song.Path, id) - if err != nil { - log.Println("Error adding media file:", song.Path, err) - continue - } - } - - artist, err := s.repo.GetArtistByName(song.Artist) - if err != nil { - artist, err = s.repo.CreateArtist(song.Artist) - if err != nil { - log.Println("Error adding artist:", song.Artist, err) - continue - } - } - - album, err := s.repo.GetAlbumByTitleAndArtist(song.Album, artist.ID) - if err != nil { - album, err = s.repo.CreateAlbum(song.Album, artist.ID) - if err != nil { - log.Println("Error adding album:", err) - continue - } - } - - _, err = s.repo.CreateSong(song.Title, artist.ID, album.ID, song.Duration, mediaFile.ID) - if err != nil { - log.Printf("Song already exists or error adding: %s - %v", song.Title, err) - continue + if err := s.addScannedSong(song, lib.ID, report); err != nil { + log.Printf("Failed to add %s: %v", filepath.Base(song.Path), err) + report.FailedFiles = append(report.FailedFiles, song.Path) } } + log.Printf("Scan complete for library %d: %+v", id, report) + return report, nil +} + +// addScannedSong upserts a scanned song and its related artist, album, and media file. +func (s *LibraryService) addScannedSong(song scanner.ScannedSong, libraryID int, report *ScanReport) error { + mediaFile, err := s.getOrCreateMediaFile(song.Path, libraryID) + if err != nil { + return fmt.Errorf("media file: %w", err) + } + + artist, err := s.getOrCreateArtist(song.Artist) + if err != nil { + return fmt.Errorf("artist: %w", err) + } + + album, err := s.getOrCreateAlbum(song.Album, artist.ID) + if err != nil { + return fmt.Errorf("album: %w", err) + } + + if _, err := s.repo.CreateSong(song.Title, artist.ID, album.ID, song.Duration, mediaFile.ID); err != nil { + return fmt.Errorf("song: %w", err) + } + + report.Added++ return nil } + +func (s *LibraryService) getOrCreateMediaFile(path string, libraryID int) (repository.MediaFile, error) { + mf, err := s.repo.GetMediaFileByPath(path) + if err == nil { + return mf, nil + } + return s.repo.CreateMediaFile(path, libraryID) +} + +func (s *LibraryService) getOrCreateArtist(name string) (repository.Artist, error) { + a, err := s.repo.GetArtistByName(name) + if err == nil { + return a, nil + } + return s.repo.CreateArtist(name) +} + +func (s *LibraryService) getOrCreateAlbum(title string, artistID int) (repository.Album, error) { + al, err := s.repo.GetAlbumByTitleAndArtist(title, artistID) + if err == nil { + return al, nil + } + return s.repo.CreateAlbum(title, artistID) +} diff --git a/internal/service/song_svc.go b/internal/service/song_svc.go index dee18fd..bc0eda0 100644 --- a/internal/service/song_svc.go +++ b/internal/service/song_svc.go @@ -1,6 +1,8 @@ package service -import "butterfliu/internal/repository" +import ( + "butterfliu/internal/repository" +) type SongService struct { songRepo *repository.SongRepository @@ -8,25 +10,21 @@ type SongService struct { } func NewSongService(songRepo *repository.SongRepository, mediaRepo *repository.MediaRepository) *SongService { - return &SongService{songRepo, mediaRepo} -} - -func (s *SongService) GetAll() ([]repository.Song, error) { - return s.songRepo.GetAll() + return &SongService{ + songRepo: songRepo, + mediaRepo: mediaRepo, + } } func (s *SongService) GetAllWithDetails() ([]repository.SongDetail, error) { return s.songRepo.GetAllWithDetails() } -func (service *SongService) GetMediaFile(id int) (repository.MediaFile, error) { - song, err := service.songRepo.Get(id) +// GetMediaFilePath returns the file path of a song by its song ID. +func (s *SongService) GetMediaFile(id int) (repository.MediaFile, error) { + song, err := s.songRepo.Get(id) if err != nil { return repository.MediaFile{}, err } - media, err := service.mediaRepo.Get(song.MediaFileID) - if err != nil { - return repository.MediaFile{}, err - } - return media, nil + return s.mediaRepo.Get(song.MediaFileID) }