mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-29 22:47:48 +01:00
fix(jellyfin): support playlist deletion via DELETE /Items/{id}
Finamp deletes a playlist with DELETE /Items/{id}, which we didn't route,
so deletion silently failed. Implement it via core/playlists.Delete (which
enforces ownership and removes the cover file). Only playlists are
deletable through this API; non-playlist ids return 404, non-owners 403.
This commit is contained in:
parent
00b9e26d36
commit
03d5ff12e5
3 changed files with 64 additions and 0 deletions
|
|
@ -67,6 +67,7 @@ func (api *Router) routes() http.Handler {
|
|||
r.Get("/Users/{userId}/Items", api.getItems)
|
||||
r.Get("/Items/{itemId}", api.getItem)
|
||||
r.Get("/Users/{userId}/Items/{itemId}", api.getItem)
|
||||
r.Delete("/Items/{itemId}", api.deleteItem)
|
||||
r.Get("/Users/{userId}/Items/Latest", api.getLatest)
|
||||
|
||||
r.Post("/Users/{userId}/FavoriteItems/{itemId}", api.markFavorite)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package jellyfin
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -302,6 +303,25 @@ func (api *Router) getItem(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
}
|
||||
|
||||
// deleteItem handles DELETE /Items/{id}. Only playlists are deletable through this API — albums and
|
||||
// songs come from library scanning, not the client — so any non-playlist id resolves to 404.
|
||||
// core/playlists.Delete enforces ownership (checkWritable) and removes the cover file.
|
||||
func (api *Router) deleteItem(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id := dto.DecodeID(chi.URLParam(r, "itemId"))
|
||||
err := api.playlists.Delete(ctx, id)
|
||||
switch {
|
||||
case err == nil:
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case errors.Is(err, model.ErrNotAuthorized):
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
case errors.Is(err, model.ErrNotFound):
|
||||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
default:
|
||||
api.internalError(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *Router) getLatest(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
opts := filter.AlbumsByNewest()
|
||||
|
|
|
|||
|
|
@ -50,6 +50,14 @@ type fakePlaylists struct {
|
|||
|
||||
removeImagePlaylistID string
|
||||
removeImageErr error
|
||||
|
||||
deletePlaylistID string
|
||||
deleteErr error
|
||||
}
|
||||
|
||||
func (f *fakePlaylists) Delete(_ context.Context, id string) error {
|
||||
f.deletePlaylistID = id
|
||||
return f.deleteErr
|
||||
}
|
||||
|
||||
func (f *fakePlaylists) Create(_ context.Context, _ string, name string, ids []string) (string, error) {
|
||||
|
|
@ -269,6 +277,41 @@ var _ = Describe("Playlists", func() {
|
|||
})
|
||||
})
|
||||
|
||||
Describe("deleteItem", func() {
|
||||
deleteReq := func(id string) *http.Request {
|
||||
r := httptest.NewRequest("DELETE", "/Items/"+dto.EncodeID(id), nil).WithContext(context.Background())
|
||||
return withChiURLParam(r, "itemId", dto.EncodeID(id))
|
||||
}
|
||||
|
||||
It("deletes the playlist and returns 204", func() {
|
||||
w := httptest.NewRecorder()
|
||||
api.deleteItem(w, deleteReq("pl1"))
|
||||
Expect(w.Code).To(Equal(http.StatusNoContent))
|
||||
Expect(fp.deletePlaylistID).To(Equal("pl1"))
|
||||
})
|
||||
|
||||
It("returns 403 when the user doesn't own the playlist", func() {
|
||||
fp.deleteErr = model.ErrNotAuthorized
|
||||
w := httptest.NewRecorder()
|
||||
api.deleteItem(w, deleteReq("pl1"))
|
||||
Expect(w.Code).To(Equal(http.StatusForbidden))
|
||||
})
|
||||
|
||||
It("returns 404 for a missing playlist or non-playlist id", func() {
|
||||
fp.deleteErr = model.ErrNotFound
|
||||
w := httptest.NewRecorder()
|
||||
api.deleteItem(w, deleteReq("al1"))
|
||||
Expect(w.Code).To(Equal(http.StatusNotFound))
|
||||
})
|
||||
|
||||
It("returns 500 on an unexpected error", func() {
|
||||
fp.deleteErr = errors.New("boom")
|
||||
w := httptest.NewRecorder()
|
||||
api.deleteItem(w, deleteReq("pl1"))
|
||||
Expect(w.Code).To(Equal(http.StatusInternalServerError))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("addToPlaylist", func() {
|
||||
It("adds tracks by song id from the lowercase ids param real Jellyfin clients send", func() {
|
||||
w := httptest.NewRecorder()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue