diff --git a/model/playlist.go b/model/playlist.go
index 2cdb5935d..ff0f1d7b3 100644
--- a/model/playlist.go
+++ b/model/playlist.go
@@ -2,8 +2,6 @@ package model
import (
"time"
-
- "github.com/deluan/rest"
)
type Playlist struct {
@@ -41,7 +39,8 @@ type PlaylistTrack struct {
type PlaylistTracks []PlaylistTrack
type PlaylistTrackRepository interface {
- rest.Repository
+ ResourceRepository
Add(mediaFileIds []string) error
Update(mediaFileIds []string) error
+ Delete(id string) error
}
diff --git a/persistence/playlist_track_repository.go b/persistence/playlist_track_repository.go
index 35823a25a..09f12df0e 100644
--- a/persistence/playlist_track_repository.go
+++ b/persistence/playlist_track_repository.go
@@ -123,12 +123,16 @@ func (r *playlistTrackRepository) Update(mediaFileIds []string) error {
}
}
+ return r.updateStats()
+}
+
+func (r *playlistTrackRepository) updateStats() error {
// Get total playlist duration and count
statsSql := Select("sum(duration) as duration", "count(*) as count").From("media_file").
Join("playlist_tracks f on f.media_file_id = media_file.id").
Where(Eq{"playlist_id": r.playlistId})
var res struct{ Duration, Count float32 }
- err = r.queryOne(statsSql, &res)
+ err := r.queryOne(statsSql, &res)
if err != nil {
return err
}
@@ -142,5 +146,12 @@ func (r *playlistTrackRepository) Update(mediaFileIds []string) error {
return err
}
+func (r *playlistTrackRepository) Delete(id string) error {
+ err := r.delete(And{Eq{"playlist_id": r.playlistId}, Eq{"id": id}})
+ if err != nil {
+ return err
+ }
+ return r.updateStats()
+}
+
var _ model.PlaylistTrackRepository = (*playlistTrackRepository)(nil)
-var _ model.ResourceRepository = (*playlistTrackRepository)(nil)
diff --git a/server/app/app.go b/server/app/app.go
index 714714144..0ad730a47 100644
--- a/server/app/app.go
+++ b/server/app/app.go
@@ -109,6 +109,9 @@ func (app *Router) addPlaylistTrackRoute(r chi.Router) {
r.Route("/{id}", func(r chi.Router) {
r.Use(UrlParams)
r.Get("/", wrapper(rest.Get))
+ r.Delete("/", func(w http.ResponseWriter, r *http.Request) {
+ deleteFromPlaylist(app.ds)(w, r)
+ })
})
r.With(UrlParams).Post("/", func(w http.ResponseWriter, r *http.Request) {
addToPlaylist(app.ds)(w, r)
diff --git a/server/app/playlists.go b/server/app/playlists.go
index b0e35ba71..7df1a77e5 100644
--- a/server/app/playlists.go
+++ b/server/app/playlists.go
@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
+ "github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/navidrome/utils"
)
@@ -13,6 +14,29 @@ type addTracksPayload struct {
Ids []string `json:"ids"`
}
+func deleteFromPlaylist(ds model.DataStore) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ playlistId := utils.ParamString(r, ":playlistId")
+ id := r.URL.Query().Get(":id")
+ tracksRepo := ds.Playlist(r.Context()).Tracks(playlistId)
+ err := tracksRepo.Delete(id)
+ if err == model.ErrNotFound {
+ log.Warn("Track not found in playlist", "playlistId", playlistId, "id", id)
+ http.Error(w, "not found", http.StatusNotFound)
+ return
+ }
+ if err != nil {
+ log.Error("Error deleting track from playlist", "playlistId", playlistId, "id", id, err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ _, err = w.Write([]byte("{}"))
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+ }
+}
+
func addToPlaylist(ds model.DataStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
playlistId := utils.ParamString(r, ":playlistId")
@@ -32,7 +56,7 @@ func addToPlaylist(ds model.DataStore) http.HandlerFunc {
// Must return an object with an ID, to satisfy ReactAdmin `create` call
_, err = w.Write([]byte(fmt.Sprintf(`{"id":"%s"}`, playlistId)))
if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
diff --git a/ui/src/playlist/PlaylistShow.js b/ui/src/playlist/PlaylistShow.js
index 990504cb3..29de548cc 100644
--- a/ui/src/playlist/PlaylistShow.js
+++ b/ui/src/playlist/PlaylistShow.js
@@ -1,12 +1,17 @@
import React from 'react'
+import { useSelector } from 'react-redux'
import { useGetOne } from 'react-admin'
import PlaylistDetails from './PlaylistDetails'
import { Title } from '../common'
import PlaylistSongs from './PlaylistSongs'
import PlaylistActions from './PlaylistActions'
+import PlaylistSongBulkActions from './PlaylistSongBulkActions'
const PlaylistShow = (props) => {
- const { data: record, loading, error } = useGetOne('playlist', props.id)
+ const viewVersion = useSelector((s) => s.admin.ui && s.admin.ui.viewVersion)
+ const { data: record, loading, error } = useGetOne('playlist', props.id, {
+ v: viewVersion,
+ })
if (loading) {
return null
@@ -29,8 +34,7 @@ const PlaylistShow = (props) => {
exporter={false}
perPage={-1}
pagination={null}
- bulkActionButtons={false}
- // bulkActionButtons={}
+ bulkActionButtons={}
/>
>
)
diff --git a/ui/src/playlist/PlaylistSongBulkActions.js b/ui/src/playlist/PlaylistSongBulkActions.js
new file mode 100644
index 000000000..391adbabe
--- /dev/null
+++ b/ui/src/playlist/PlaylistSongBulkActions.js
@@ -0,0 +1,22 @@
+import React, { Fragment, useEffect } from 'react'
+import { BulkDeleteButton, useUnselectAll } from 'react-admin'
+import PropTypes from 'prop-types'
+
+const PlaylistSongBulkActions = ({ playlistId, ...rest }) => {
+ const unselectAll = useUnselectAll()
+ useEffect(() => {
+ unselectAll('playlistTrack')
+ // eslint-disable-next-line
+ }, [])
+ return (
+
+
+
+ )
+}
+
+PlaylistSongBulkActions.propTypes = {
+ playlistId: PropTypes.string.isRequired,
+}
+
+export default PlaylistSongBulkActions