package service import ( "butterfliu/config" "butterfliu/internal/model" "butterfliu/internal/repository" "butterfliu/internal/scanner" "errors" "fmt" "log" "path" "path/filepath" "strconv" "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 } func NewLibraryService(repo *repository.LibraryRepository) *LibraryService { return &LibraryService{repo: repo} } func (s *LibraryService) GetAll() ([]model.Library, error) { return s.repo.GetAll() } func (s *LibraryService) GetByID(id int) (model.Library, error) { return s.repo.GetByID(id) } func (s *LibraryService) Create(name, path string) (model.Library, error) { if name = strings.TrimSpace(name); name == "" { return model.Library{}, errors.New("library name cannot be empty") } if path = strings.TrimSpace(path); path == "" { return model.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) } func (s *LibraryService) Delete(id int) error { return s.repo.Delete(id) } func (s *LibraryService) GetSongs(id int) ([]repository.SongDetail, error) { return s.repo.GetSongsByLibraryWithDetails(id) } func (s *LibraryService) GetSongsByArtist(id int) ([]repository.SongDetail, error) { return s.repo.GetSongsByArtistWithDetails(id) } func (s *LibraryService) GetSongsByAlbum(id int) ([]repository.SongDetail, error) { return s.repo.GetSongsByAlbumWithDetails(id) } func (s *LibraryService) GetSongIDsByAlbum(id int) ([]repository.SongDetail, error) { return s.repo.GetSongsByAlbumWithDetails(id) } func (s *LibraryService) GetArtists() ([]model.Artist, error) { return s.repo.GetArtists() } func (s *LibraryService) GetArtist(id int) (model.ArtistDetail, error) { artist, err := s.repo.GetArtist(id) if err != nil { return model.ArtistDetail{}, err } albums, err := s.repo.GetAlbumIDsByArtist(id) if err != nil { return model.ArtistDetail{}, err } songs, err := s.repo.GetSongIDsByArtist(id) if err != nil { return model.ArtistDetail{}, err } return model.ArtistDetail{ ID: artist.ID, Name: artist.Name, Songs: songs, Albums: albums, }, nil } func (s *LibraryService) GetAlbums() ([]model.Album, error) { return s.repo.GetAlbums() } func (s *LibraryService) GetAlbumsByArtistWithDetail(artistID int) ([]model.AlbumDetail, error) { albums, err := s.repo.GetAlbumsByArtist(artistID) if err != nil { return nil, err } details := []model.AlbumDetail{} for _, a := range albums { songs, err := s.repo.GetSongIDsByAlbum(a.ID) if err != nil { break } artist, err := s.repo.GetArtist(artistID) if err != nil { break } details = append(details, model.AlbumDetail{ ID: a.ID, Title: a.Title, Artist: artist.Name, Songs: songs, }) } return details, nil } func (s *LibraryService) GetAlbum(id int) (model.AlbumDetail, error) { album, err := s.repo.GetAlbum(id) if err != nil { return model.AlbumDetail{}, err } songs, err := s.repo.GetSongIDsByAlbum(id) if err != nil { return model.AlbumDetail{}, err } artist, err := s.repo.GetArtist(album.ArtistID) if err != nil { return model.AlbumDetail{}, err } return model.AlbumDetail{ ID: id, Title: album.Title, Songs: songs, Artist: artist.Name, }, nil } func (s *LibraryService) GetAlbumCover(id int) (string, error) { songs, err := s.repo.GetSongIDsByAlbum(id) if err != nil { return "", err } conf := config.LoadConfig() return path.Join(conf.GetCachePath("cover"), strconv.Itoa(songs[0])+".jpg"), nil } func (s *LibraryService) Scan(id int) (*ScanReport, error) { lib, err := s.repo.GetByID(id) if err != nil { return nil, fmt.Errorf("library %d not found: %w", id, err) } scannedSongs, err := scanner.ScanDirectory(lib.Path) if err != nil { return nil, fmt.Errorf("scan directory %s: %w", lib.Path, err) } report := &ScanReport{TotalFiles: len(scannedSongs)} for _, song := range scannedSongs { 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) (model.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) (model.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) (model.Album, error) { al, err := s.repo.GetAlbumByTitleAndArtist(title, artistID) if err == nil { return al, nil } return s.repo.CreateAlbum(title, artistID) }