Files
yellowjacket/backend/playlist/favorites.go
yonlu bdaff478e8 feat(quick-5): add CreatedAt/UpdatedAt to playlist Summary struct
- Add CreatedAt and UpdatedAt string fields to Summary struct
- Format as RFC3339 at all construction sites in playlist.go and favorites.go
- Update TypeScript bindings with new fields in models.ts
2026-03-01 08:51:00 -05:00

358 lines
6.9 KiB
Go

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()
}