added "default playlist"/ favorites system
This commit is contained in:
@@ -103,6 +103,7 @@ func NewYellowJacketApp(
|
||||
yjApp.playlist = playlist.NewService(
|
||||
yjApp.logger, yjApp.database, yjApp.appConfig,
|
||||
)
|
||||
yjApp.playlist.SetFavoritesConfig(yjApp.appConfig)
|
||||
|
||||
// create queue (before wails.Run so it can be bound)
|
||||
yjApp.queue = queue.NewQueue(yjApp.logger, yjApp.database)
|
||||
@@ -145,6 +146,7 @@ func (yj *YellowJacketApp) OnStartup(ctx context.Context) {
|
||||
yj.FrontendUtil.SetContext(ctx)
|
||||
yj.library.SetContext(ctx)
|
||||
yj.playlist.SetContext(ctx)
|
||||
yj.playlist.EnsureDefaultPlaylist()
|
||||
|
||||
// Initialize speaker hardware (player struct created in
|
||||
// NewYellowJacketApp for Wails binding registration).
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
|
||||
"yellowjacket/backend/events"
|
||||
"yellowjacket/backend/favorites"
|
||||
"yellowjacket/backend/library"
|
||||
"yellowjacket/backend/system"
|
||||
"yellowjacket/backend/theme"
|
||||
@@ -28,6 +29,7 @@ type Config struct {
|
||||
Theme *theme.Config `toml:"Theme"`
|
||||
Window *WindowConfig `toml:"Window"`
|
||||
TrackList *tracklist.Config `toml:"TrackList"`
|
||||
Favorites *favorites.Config `toml:"Favorites"`
|
||||
}
|
||||
|
||||
// NewConfig creates a new config by loading it from disk.
|
||||
@@ -78,6 +80,12 @@ func (c *Config) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if c.Favorites != nil {
|
||||
if err := c.Favorites.Validate(); err != nil {
|
||||
configErrs = errors.Join(configErrs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if configErrs != nil {
|
||||
return fmt.Errorf(
|
||||
"one or more config parts are invalid: %w",
|
||||
@@ -174,6 +182,12 @@ func (c *Config) applyDefaults() {
|
||||
}
|
||||
|
||||
c.TrackList.ApplyDefaults()
|
||||
|
||||
if c.Favorites == nil {
|
||||
c.Favorites = &favorites.Config{}
|
||||
}
|
||||
|
||||
c.Favorites.ApplyDefaults()
|
||||
}
|
||||
|
||||
// SetContext sets the Wails runtime context for event emission.
|
||||
@@ -436,3 +450,96 @@ func (c *Config) emitTrackListChanged() {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// GetFavoritesPlaylistID returns the configured default playlist ID.
|
||||
func (c *Config) GetFavoritesPlaylistID() int64 {
|
||||
if c.Favorites == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return c.Favorites.PlaylistID
|
||||
}
|
||||
|
||||
// SetFavoritesPlaylistID saves a new default playlist ID.
|
||||
func (c *Config) SetFavoritesPlaylistID(id int64) error {
|
||||
if c.Favorites == nil {
|
||||
c.Favorites = &favorites.Config{}
|
||||
c.Favorites.ApplyDefaults()
|
||||
}
|
||||
|
||||
c.Favorites.PlaylistID = id
|
||||
|
||||
if err := c.Save(); err != nil {
|
||||
return fmt.Errorf(
|
||||
"could not save config: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
c.emitFavoritesChanged()
|
||||
|
||||
c.logger.Info(
|
||||
"favorites playlist ID updated",
|
||||
"playlistId", id,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFavoritesIconStyle returns the configured icon style.
|
||||
func (c *Config) GetFavoritesIconStyle() string {
|
||||
if c.Favorites == nil {
|
||||
return string(favorites.DefaultIconStyle)
|
||||
}
|
||||
|
||||
return string(c.Favorites.IconStyle)
|
||||
}
|
||||
|
||||
// SetFavoritesIconStyle validates and saves a new icon style.
|
||||
func (c *Config) SetFavoritesIconStyle(
|
||||
style string,
|
||||
) error {
|
||||
if c.Favorites == nil {
|
||||
c.Favorites = &favorites.Config{}
|
||||
c.Favorites.ApplyDefaults()
|
||||
}
|
||||
|
||||
c.Favorites.IconStyle = favorites.IconStyle(style)
|
||||
|
||||
if err := c.Favorites.Validate(); err != nil {
|
||||
return fmt.Errorf(
|
||||
"invalid favorites icon style: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
if err := c.Save(); err != nil {
|
||||
return fmt.Errorf(
|
||||
"could not save config: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
c.emitFavoritesChanged()
|
||||
|
||||
c.logger.Info(
|
||||
"favorites icon style updated",
|
||||
"style", style,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// emitFavoritesChanged sends the FavoritesConfigChanged event
|
||||
// to the frontend.
|
||||
func (c *Config) emitFavoritesChanged() {
|
||||
if c.ctx == nil || c.Favorites == nil {
|
||||
return
|
||||
}
|
||||
|
||||
runtime.EventsEmit(
|
||||
c.ctx,
|
||||
events.FavoritesConfigChanged,
|
||||
map[string]any{
|
||||
"PlaylistID": c.Favorites.PlaylistID,
|
||||
"IconStyle": string(c.Favorites.IconStyle),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -88,3 +88,23 @@ DELETE FROM playlist_tracks;
|
||||
-- name: GetNextPlaylistTrackPosition :one
|
||||
SELECT COALESCE(MAX(position), -1) + 1 AS next_position
|
||||
FROM playlist_tracks WHERE playlist_id = ?;
|
||||
|
||||
-- name: GetPlaylistTrackFilePaths :many
|
||||
SELECT af.file_path
|
||||
FROM playlist_tracks pt
|
||||
JOIN audio_files af ON pt.audio_file_id = af.id
|
||||
WHERE pt.playlist_id = ?
|
||||
ORDER BY pt.position;
|
||||
|
||||
-- name: IsTrackInPlaylist :one
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM playlist_tracks pt
|
||||
JOIN audio_files af ON pt.audio_file_id = af.id
|
||||
WHERE pt.playlist_id = ? AND af.file_path = ?
|
||||
) AS in_playlist;
|
||||
|
||||
-- name: RemovePlaylistTrackByPath :exec
|
||||
DELETE FROM playlist_tracks
|
||||
WHERE playlist_id = ? AND audio_file_id = (
|
||||
SELECT id FROM audio_files WHERE file_path = ?
|
||||
);
|
||||
|
||||
@@ -209,6 +209,37 @@ func (q *Queries) GetPlaylist(ctx context.Context, id int64) (Playlist, error) {
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getPlaylistTrackFilePaths = `-- name: GetPlaylistTrackFilePaths :many
|
||||
SELECT af.file_path
|
||||
FROM playlist_tracks pt
|
||||
JOIN audio_files af ON pt.audio_file_id = af.id
|
||||
WHERE pt.playlist_id = ?
|
||||
ORDER BY pt.position
|
||||
`
|
||||
|
||||
func (q *Queries) GetPlaylistTrackFilePaths(ctx context.Context, playlistID int64) ([]string, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getPlaylistTrackFilePaths, playlistID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []string
|
||||
for rows.Next() {
|
||||
var file_path string
|
||||
if err := rows.Scan(&file_path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, file_path)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getPlaylistTracks = `-- name: GetPlaylistTracks :many
|
||||
SELECT pt.id, pt.playlist_id, pt.audio_file_id, pt.position, af.file_path
|
||||
FROM playlist_tracks pt
|
||||
@@ -328,6 +359,26 @@ func (q *Queries) GetPlaylistTracksWithMetadata(ctx context.Context, playlistID
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const isTrackInPlaylist = `-- name: IsTrackInPlaylist :one
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM playlist_tracks pt
|
||||
JOIN audio_files af ON pt.audio_file_id = af.id
|
||||
WHERE pt.playlist_id = ? AND af.file_path = ?
|
||||
) AS in_playlist
|
||||
`
|
||||
|
||||
type IsTrackInPlaylistParams struct {
|
||||
PlaylistID int64
|
||||
FilePath string
|
||||
}
|
||||
|
||||
func (q *Queries) IsTrackInPlaylist(ctx context.Context, arg IsTrackInPlaylistParams) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, isTrackInPlaylist, arg.PlaylistID, arg.FilePath)
|
||||
var in_playlist int64
|
||||
err := row.Scan(&in_playlist)
|
||||
return in_playlist, err
|
||||
}
|
||||
|
||||
const removePlaylistTrack = `-- name: RemovePlaylistTrack :exec
|
||||
DELETE FROM playlist_tracks WHERE id = ?
|
||||
`
|
||||
@@ -337,6 +388,23 @@ func (q *Queries) RemovePlaylistTrack(ctx context.Context, id int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
const removePlaylistTrackByPath = `-- name: RemovePlaylistTrackByPath :exec
|
||||
DELETE FROM playlist_tracks
|
||||
WHERE playlist_id = ? AND audio_file_id = (
|
||||
SELECT id FROM audio_files WHERE file_path = ?
|
||||
)
|
||||
`
|
||||
|
||||
type RemovePlaylistTrackByPathParams struct {
|
||||
PlaylistID int64
|
||||
FilePath string
|
||||
}
|
||||
|
||||
func (q *Queries) RemovePlaylistTrackByPath(ctx context.Context, arg RemovePlaylistTrackByPathParams) error {
|
||||
_, err := q.db.ExecContext(ctx, removePlaylistTrackByPath, arg.PlaylistID, arg.FilePath)
|
||||
return err
|
||||
}
|
||||
|
||||
const updatePlaylistName = `-- name: UpdatePlaylistName :exec
|
||||
UPDATE playlists SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
|
||||
`
|
||||
|
||||
@@ -25,15 +25,17 @@ const (
|
||||
LibraryConfigChanged = "LibraryConfigChanged"
|
||||
ThemeConfigChanged = "ThemeConfigChanged"
|
||||
TrackListConfigChanged = "TrackListConfigChanged"
|
||||
FavoritesConfigChanged = "FavoritesConfigChanged"
|
||||
)
|
||||
|
||||
// Playlist events.
|
||||
const (
|
||||
PlaylistCreated = "PlaylistCreated"
|
||||
PlaylistDeleted = "PlaylistDeleted"
|
||||
PlaylistRenamed = "PlaylistRenamed"
|
||||
PlaylistTracksChanged = "PlaylistTracksChanged"
|
||||
PlaylistsRestored = "PlaylistsRestored"
|
||||
PlaylistCreated = "PlaylistCreated"
|
||||
PlaylistDeleted = "PlaylistDeleted"
|
||||
PlaylistRenamed = "PlaylistRenamed"
|
||||
PlaylistTracksChanged = "PlaylistTracksChanged"
|
||||
PlaylistsRestored = "PlaylistsRestored"
|
||||
DefaultPlaylistChanged = "DefaultPlaylistChanged"
|
||||
)
|
||||
|
||||
// Library events.
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Package favorites manages the default playlist configuration.
|
||||
package favorites
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var errUnknownIconStyle = errors.New(
|
||||
"unknown favorites icon style",
|
||||
)
|
||||
|
||||
// IconStyle controls the icon used to indicate favourited tracks.
|
||||
type IconStyle string
|
||||
|
||||
// Valid IconStyle values.
|
||||
const (
|
||||
// IconHeart uses a heart icon.
|
||||
IconHeart IconStyle = "heart"
|
||||
|
||||
// IconStar uses a star icon.
|
||||
IconStar IconStyle = "star"
|
||||
)
|
||||
|
||||
// DefaultIconStyle is applied when no value has been set.
|
||||
const DefaultIconStyle = IconHeart
|
||||
|
||||
// DefaultPlaylistName is the name given to the auto-created
|
||||
// default playlist.
|
||||
const DefaultPlaylistName = "Favorites"
|
||||
|
||||
// Config holds favourites preferences.
|
||||
type Config struct {
|
||||
PlaylistID int64 `toml:"PlaylistID"`
|
||||
IconStyle IconStyle `toml:"IconStyle"`
|
||||
}
|
||||
|
||||
// ApplyDefaults fills zero-value fields with sensible defaults.
|
||||
func (c *Config) ApplyDefaults() {
|
||||
if c.IconStyle == "" {
|
||||
c.IconStyle = DefaultIconStyle
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks that all values are well-formed.
|
||||
func (c *Config) Validate() error {
|
||||
c.ApplyDefaults()
|
||||
|
||||
switch c.IconStyle {
|
||||
case IconHeart, IconStar:
|
||||
// Valid.
|
||||
default:
|
||||
return fmt.Errorf(
|
||||
"%w: %q",
|
||||
errUnknownIconStyle,
|
||||
c.IconStyle,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
package playlist
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"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,
|
||||
})
|
||||
}
|
||||
|
||||
// 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}, 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()
|
||||
}
|
||||
@@ -96,10 +96,11 @@ type PhantomSearchResult struct {
|
||||
|
||||
// Service manages playlist operations.
|
||||
type Service struct {
|
||||
ctx context.Context
|
||||
logger *slog.Logger
|
||||
db *database.DB
|
||||
libraryDir LibraryDirProvider
|
||||
ctx context.Context
|
||||
logger *slog.Logger
|
||||
db *database.DB
|
||||
libraryDir LibraryDirProvider
|
||||
favoritesConf FavoritesConfigProvider
|
||||
}
|
||||
|
||||
// NewService creates a new playlist service.
|
||||
@@ -115,6 +116,14 @@ func NewService(
|
||||
}
|
||||
}
|
||||
|
||||
// SetFavoritesConfig sets the provider used to read and write
|
||||
// the default-playlist configuration.
|
||||
func (s *Service) SetFavoritesConfig(
|
||||
provider FavoritesConfigProvider,
|
||||
) {
|
||||
s.favoritesConf = provider
|
||||
}
|
||||
|
||||
// SetContext sets the Wails runtime context and runs the
|
||||
// one-time startup migration to bootstrap M3U8 files for
|
||||
// existing playlists.
|
||||
@@ -583,6 +592,8 @@ func (s *Service) RemoveTracksFromPlaylist(
|
||||
}
|
||||
|
||||
// DeletePlaylist deletes a playlist and its M3U8 file.
|
||||
// If the deleted playlist was the default, a new default
|
||||
// playlist is automatically created.
|
||||
func (s *Service) DeletePlaylist(playlistID int64) error {
|
||||
if err := s.db.Queries.DeletePlaylist(
|
||||
s.db.Ctx, playlistID,
|
||||
@@ -606,6 +617,11 @@ func (s *Service) DeletePlaylist(playlistID int64) error {
|
||||
|
||||
s.emitEvent(events.PlaylistDeleted, playlistID)
|
||||
|
||||
// Recreate the default playlist if we just deleted it.
|
||||
if s.defaultPlaylistID() == playlistID {
|
||||
s.EnsureDefaultPlaylist()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user