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