package playlist import ( "errors" "fmt" "time" "yellowjacket/backend/database/sql/sqlcgen" "yellowjacket/backend/events" "yellowjacket/backend/favorites" ) var errNoDefaultPlaylist = errors.New( "no default playlist configured", ) // FavoritesConfigProvider is a narrow interface for reading and // writing the default-playlist configuration. type FavoritesConfigProvider interface { GetFavoritesPlaylistID() int64 SetFavoritesPlaylistID(id int64) error GetFavoritesIconStyle() string } // EnsureDefaultPlaylist verifies the configured default playlist // exists in the database. If the playlist is missing or no ID // has been configured yet, a new playlist named "Favorites" is // created and the config is updated. func (s *Service) EnsureDefaultPlaylist() { if s.favoritesConf == nil { s.logger.Warn( "No favorites config provider, skipping", ) return } id := s.favoritesConf.GetFavoritesPlaylistID() // Check whether the playlist still exists. if id > 0 { _, err := s.db.Queries.GetPlaylist( s.db.Ctx, id, ) if err == nil { return // Playlist exists, nothing to do. } s.logger.Warn( "Default playlist not found, recreating", "configuredId", id, ) } // Create a fresh default playlist. created, err := s.db.Queries.CreatePlaylist( s.db.Ctx, favorites.DefaultPlaylistName, ) if err != nil { s.logger.Error( "Failed to create default playlist", "err", err, ) return } s.savePlaylistFile(created.ID, created.Name) if setErr := s.favoritesConf.SetFavoritesPlaylistID( created.ID, ); setErr != nil { s.logger.Error( "Failed to save default playlist ID", "err", setErr, ) } s.logger.Info( "Default playlist created", "id", created.ID, "name", created.Name, ) s.emitEvent(events.PlaylistCreated, Summary{ ID: created.ID, Name: created.Name, CreatedAt: created.CreatedAt.Format(time.RFC3339), UpdatedAt: created.UpdatedAt.Format(time.RFC3339), }) } // GetDefaultPlaylistTrackPaths returns the file paths of all // tracks in the default playlist. func (s *Service) GetDefaultPlaylistTrackPaths() ( []string, error, ) { id := s.defaultPlaylistID() if id == 0 { return []string{}, nil } paths, err := s.db.Queries.GetPlaylistTrackFilePaths( s.db.Ctx, id, ) if err != nil { s.logger.Error( "Failed to get default playlist paths", "playlistId", id, "err", err, ) return nil, fmt.Errorf( "failed to get default playlist paths: %w", err, ) } if paths == nil { paths = []string{} } return paths, nil } // GetDefaultPlaylistInfo returns the ID and name of the default // playlist for display in the frontend. func (s *Service) GetDefaultPlaylistInfo() ( Summary, error, ) { id := s.defaultPlaylistID() if id == 0 { return Summary{}, nil } pl, err := s.db.Queries.GetPlaylist(s.db.Ctx, id) if err != nil { return Summary{}, fmt.Errorf( "failed to get default playlist: %w", err, ) } return Summary{ ID: pl.ID, Name: pl.Name, CreatedAt: pl.CreatedAt.Format(time.RFC3339), UpdatedAt: pl.UpdatedAt.Format(time.RFC3339), }, nil } // ToggleDefaultPlaylistTrack adds or removes a single track // from the default playlist. Returns true if the track is now // in the playlist (was added), false if it was removed. func (s *Service) ToggleDefaultPlaylistTrack( filePath string, ) (bool, error) { id := s.defaultPlaylistID() if id == 0 { return false, errNoDefaultPlaylist } inPlaylist, err := s.db.Queries.IsTrackInPlaylist( s.db.Ctx, sqlcgen.IsTrackInPlaylistParams{ PlaylistID: id, FilePath: filePath, }, ) if err != nil { return false, fmt.Errorf( "failed to check playlist membership: %w", err, ) } if inPlaylist != 0 { // Remove. if rmErr := s.db.Queries.RemovePlaylistTrackByPath( s.db.Ctx, sqlcgen.RemovePlaylistTrackByPathParams{ PlaylistID: id, FilePath: filePath, }, ); rmErr != nil { return false, fmt.Errorf( "failed to remove track: %w", rmErr, ) } s.savePlaylistFileByID(id) s.emitEvent( events.DefaultPlaylistChanged, map[string]any{ "filePath": filePath, "added": false, }, ) s.emitEvent(events.PlaylistTracksChanged, id) return false, nil } // Add. nextPos, posErr := s.db.Queries.GetNextPlaylistTrackPosition( s.db.Ctx, id, ) if posErr != nil { return false, fmt.Errorf( "failed to get next position: %w", posErr, ) } if addErr := s.addSingleTrack( id, filePath, nextPos, ); addErr != nil { return false, fmt.Errorf( "failed to add track: %w", addErr, ) } s.savePlaylistFileByID(id) s.emitEvent( events.DefaultPlaylistChanged, map[string]any{ "filePath": filePath, "added": true, }, ) s.emitEvent(events.PlaylistTracksChanged, id) return true, nil } // AddToDefaultPlaylist adds multiple tracks to the default // playlist, skipping any that are already present. func (s *Service) AddToDefaultPlaylist( filePaths []string, ) error { id := s.defaultPlaylistID() if id == 0 { return errNoDefaultPlaylist } nextPos, err := s.db.Queries.GetNextPlaylistTrackPosition( s.db.Ctx, id, ) if err != nil { return fmt.Errorf( "failed to get next position: %w", err, ) } var added int for _, fp := range filePaths { inPlaylist, chkErr := s.db.Queries.IsTrackInPlaylist( s.db.Ctx, sqlcgen.IsTrackInPlaylistParams{ PlaylistID: id, FilePath: fp, }, ) if chkErr != nil { s.logger.Warn( "Could not check playlist membership", "filePath", fp, "err", chkErr, ) continue } if inPlaylist != 0 { continue } if addErr := s.addSingleTrack( id, fp, nextPos+int64(added), ); addErr != nil { s.logger.Warn( "Could not add track to default playlist", "filePath", fp, "err", addErr, ) continue } added++ } if added > 0 { s.savePlaylistFileByID(id) s.emitEvent( events.DefaultPlaylistChanged, nil, ) s.emitEvent(events.PlaylistTracksChanged, id) } return nil } // RemoveFromDefaultPlaylist removes multiple tracks from the // default playlist. func (s *Service) RemoveFromDefaultPlaylist( filePaths []string, ) error { id := s.defaultPlaylistID() if id == 0 { return errNoDefaultPlaylist } var removed int for _, fp := range filePaths { rmErr := s.db.Queries.RemovePlaylistTrackByPath( s.db.Ctx, sqlcgen.RemovePlaylistTrackByPathParams{ PlaylistID: id, FilePath: fp, }, ) if rmErr != nil { s.logger.Warn( "Could not remove track from default playlist", "filePath", fp, "err", rmErr, ) continue } removed++ } if removed > 0 { s.savePlaylistFileByID(id) s.emitEvent( events.DefaultPlaylistChanged, nil, ) s.emitEvent(events.PlaylistTracksChanged, id) } return nil } // defaultPlaylistID returns the configured default playlist ID, // or 0 if not configured. func (s *Service) defaultPlaylistID() int64 { if s.favoritesConf == nil { return 0 } return s.favoritesConf.GetFavoritesPlaylistID() }