- 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
358 lines
6.9 KiB
Go
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()
|
|
}
|