2020-07-31 13:07:39 -04:00
|
|
|
package subsonic
|
|
|
|
|
|
|
|
|
|
import (
|
2024-10-16 10:59:22 -04:00
|
|
|
"errors"
|
2020-07-31 13:07:39 -04:00
|
|
|
"net/http"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/navidrome/navidrome/model"
|
|
|
|
|
"github.com/navidrome/navidrome/model/request"
|
|
|
|
|
"github.com/navidrome/navidrome/server/subsonic/responses"
|
2023-12-21 17:41:09 -05:00
|
|
|
"github.com/navidrome/navidrome/utils/req"
|
2024-10-22 22:54:31 -04:00
|
|
|
"github.com/navidrome/navidrome/utils/slice"
|
2020-07-31 13:07:39 -04:00
|
|
|
)
|
|
|
|
|
|
2022-11-21 12:57:56 -05:00
|
|
|
func (api *Router) GetBookmarks(r *http.Request) (*responses.Subsonic, error) {
|
2020-07-31 16:56:42 -04:00
|
|
|
user, _ := request.UserFrom(r.Context())
|
|
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
repo := api.ds.MediaFile()
|
|
|
|
|
bookmarks, err := repo.GetBookmarks(r.Context())
|
2020-07-31 16:56:42 -04:00
|
|
|
if err != nil {
|
2020-10-27 15:23:29 -04:00
|
|
|
return nil, err
|
2020-07-31 16:56:42 -04:00
|
|
|
}
|
|
|
|
|
|
2020-08-13 22:11:18 -04:00
|
|
|
response := newResponse()
|
2020-07-31 16:56:42 -04:00
|
|
|
response.Bookmarks = &responses.Bookmarks{}
|
2024-10-22 22:54:31 -04:00
|
|
|
response.Bookmarks.Bookmark = slice.Map(bookmarks, func(bmk model.Bookmark) responses.Bookmark {
|
|
|
|
|
return responses.Bookmark{
|
2021-05-07 09:47:13 -04:00
|
|
|
Entry: childFromMediaFile(r.Context(), bmk.Item),
|
2020-07-31 16:56:42 -04:00
|
|
|
Position: bmk.Position,
|
|
|
|
|
Username: user.UserName,
|
|
|
|
|
Comment: bmk.Comment,
|
|
|
|
|
Created: bmk.CreatedAt,
|
|
|
|
|
Changed: bmk.UpdatedAt,
|
|
|
|
|
}
|
2024-10-22 22:54:31 -04:00
|
|
|
})
|
2020-07-31 16:56:42 -04:00
|
|
|
return response, nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-21 12:57:56 -05:00
|
|
|
func (api *Router) CreateBookmark(r *http.Request) (*responses.Subsonic, error) {
|
2023-12-21 17:41:09 -05:00
|
|
|
p := req.Params(r)
|
|
|
|
|
id, err := p.String("id")
|
2020-07-31 16:56:42 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-21 17:41:09 -05:00
|
|
|
comment, _ := p.String("comment")
|
|
|
|
|
position := p.Int64Or("position", 0)
|
2020-07-31 16:56:42 -04:00
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
repo := api.ds.MediaFile()
|
|
|
|
|
ok, err := repo.Exists(r.Context(), id)
|
fix: apply the per-user library filter to bookmarks, playlists and now-playing (#6179)
On a multi-library instance, a few reads and writes built their own queries
without the per-user library filter that every other media read applies. A
user granted only some libraries could see, and store, tracks from libraries
they had no access to.
- getBookmarks now filters the query. It has to be the query and not the
result: the loop below it pre-sizes the response from the bookmark count,
so a row dropped afterwards would emit an empty bookmark entry.
- createBookmark rejects an id the caller cannot read, returning error 70 to
match getSong. Stored rows are left alone rather than purged, so a
temporary revoke does not lose saved playback positions.
- playlistTrackRepository Read, Count and GetAlbumIDs get the filter their
siblings CountAll and GetMediaFileIDs already had. Read is the one that
mattered most: its id is the integer playlist position, so it needed no
track id at all.
- Playlist track writes are filtered in playlistRepository.addTracks, the
only writer of playlist_tracks rows apart from smart playlists, so Add,
Insert, AddAlbums/AddArtists/AddDiscs and a full replace through Put all
go through it. Insert reserves a slot per requested id, so when the filter
drops one it renumbers to close the hole.
- playTracker.GetNowPlaying honours its context instead of discarding it.
The cache is process-global, so the filter belongs in the tracker rather
than in the Subsonic handler, and any future caller inherits it.
Admins and single-library installs are unaffected: applyLibraryFilter and
HasLibraryAccess both short-circuit for them. Scanner playlist sync runs as
admin, and M3U and CLI imports already resolve tracks through FindByPaths as
the same user, so neither changes.
2026-09-20 12:37:18 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, newError(responses.ErrorDataNotFound, "Song not found")
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
err = repo.AddBookmark(r.Context(), id, comment, position)
|
2020-07-31 16:56:42 -04:00
|
|
|
if err != nil {
|
2020-10-27 15:23:29 -04:00
|
|
|
return nil, err
|
2020-07-31 16:56:42 -04:00
|
|
|
}
|
2020-08-13 22:11:18 -04:00
|
|
|
return newResponse(), nil
|
2020-07-31 16:56:42 -04:00
|
|
|
}
|
|
|
|
|
|
2022-11-21 12:57:56 -05:00
|
|
|
func (api *Router) DeleteBookmark(r *http.Request) (*responses.Subsonic, error) {
|
2023-12-21 17:41:09 -05:00
|
|
|
p := req.Params(r)
|
|
|
|
|
id, err := p.String("id")
|
2020-07-31 16:56:42 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
repo := api.ds.MediaFile()
|
|
|
|
|
err = repo.DeleteBookmark(r.Context(), id)
|
2020-07-31 16:56:42 -04:00
|
|
|
if err != nil {
|
2020-10-27 15:23:29 -04:00
|
|
|
return nil, err
|
2020-07-31 16:56:42 -04:00
|
|
|
}
|
2020-08-13 22:11:18 -04:00
|
|
|
return newResponse(), nil
|
2020-07-31 16:56:42 -04:00
|
|
|
}
|
|
|
|
|
|
2022-11-21 12:57:56 -05:00
|
|
|
func (api *Router) GetPlayQueue(r *http.Request) (*responses.Subsonic, error) {
|
2020-07-31 13:07:39 -04:00
|
|
|
user, _ := request.UserFrom(r.Context())
|
|
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
repo := api.ds.PlayQueue()
|
|
|
|
|
pq, err := repo.RetrieveWithMediaFiles(r.Context(), user.ID)
|
2024-10-16 10:59:22 -04:00
|
|
|
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
2020-10-27 15:23:29 -04:00
|
|
|
return nil, err
|
2020-07-31 13:07:39 -04:00
|
|
|
}
|
2024-10-16 10:59:22 -04:00
|
|
|
if pq == nil || len(pq.Items) == 0 {
|
2026-03-12 12:29:37 +00:00
|
|
|
response := newResponse()
|
|
|
|
|
response.PlayQueue = &responses.PlayQueue{
|
|
|
|
|
Username: user.UserName,
|
|
|
|
|
}
|
|
|
|
|
return response, nil
|
2024-10-16 10:59:22 -04:00
|
|
|
}
|
2020-07-31 13:07:39 -04:00
|
|
|
|
2020-08-13 22:11:18 -04:00
|
|
|
response := newResponse()
|
2025-06-10 23:00:44 -04:00
|
|
|
var currentID string
|
|
|
|
|
if pq.Current >= 0 && pq.Current < len(pq.Items) {
|
|
|
|
|
currentID = pq.Items[pq.Current].ID
|
|
|
|
|
}
|
2020-07-31 13:07:39 -04:00
|
|
|
response.PlayQueue = &responses.PlayQueue{
|
2024-10-22 22:54:31 -04:00
|
|
|
Entry: slice.MapWithArg(pq.Items, r.Context(), childFromMediaFile),
|
2025-06-10 23:00:44 -04:00
|
|
|
Current: currentID,
|
2020-07-31 16:56:42 -04:00
|
|
|
Position: pq.Position,
|
2020-07-31 13:07:39 -04:00
|
|
|
Username: user.UserName,
|
2025-11-09 17:52:05 +00:00
|
|
|
Changed: pq.UpdatedAt,
|
2020-07-31 13:07:39 -04:00
|
|
|
ChangedBy: pq.ChangedBy,
|
|
|
|
|
}
|
|
|
|
|
return response, nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-21 12:57:56 -05:00
|
|
|
func (api *Router) SavePlayQueue(r *http.Request) (*responses.Subsonic, error) {
|
2023-12-21 17:41:09 -05:00
|
|
|
p := req.Params(r)
|
2026-07-18 19:30:04 -04:00
|
|
|
ids := p.Strings("id")
|
2025-06-10 23:00:44 -04:00
|
|
|
currentID, _ := p.String("current")
|
2023-12-21 17:41:09 -05:00
|
|
|
position := p.Int64Or("position", 0)
|
2020-07-31 13:07:39 -04:00
|
|
|
|
|
|
|
|
user, _ := request.UserFrom(r.Context())
|
|
|
|
|
client, _ := request.ClientFrom(r.Context())
|
|
|
|
|
|
2025-06-10 23:00:44 -04:00
|
|
|
items := slice.Map(ids, func(id string) model.MediaFile {
|
|
|
|
|
return model.MediaFile{ID: id}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
currentIndex := 0
|
|
|
|
|
for i, id := range ids {
|
|
|
|
|
if id == currentID {
|
|
|
|
|
currentIndex = i
|
|
|
|
|
break
|
|
|
|
|
}
|
2020-07-31 13:07:39 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pq := &model.PlayQueue{
|
|
|
|
|
UserID: user.ID,
|
2025-06-10 23:00:44 -04:00
|
|
|
Current: currentIndex,
|
2020-07-31 15:32:08 -04:00
|
|
|
Position: position,
|
2020-07-31 13:07:39 -04:00
|
|
|
ChangedBy: client,
|
|
|
|
|
Items: items,
|
|
|
|
|
CreatedAt: time.Time{},
|
|
|
|
|
UpdatedAt: time.Time{},
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
repo := api.ds.PlayQueue()
|
|
|
|
|
err := repo.Store(r.Context(), pq)
|
2020-07-31 13:07:39 -04:00
|
|
|
if err != nil {
|
2020-10-27 15:23:29 -04:00
|
|
|
return nil, err
|
2020-07-31 13:07:39 -04:00
|
|
|
}
|
2020-08-13 22:11:18 -04:00
|
|
|
return newResponse(), nil
|
2020-07-31 13:07:39 -04:00
|
|
|
}
|
2025-11-09 17:52:05 +00:00
|
|
|
|
|
|
|
|
func (api *Router) GetPlayQueueByIndex(r *http.Request) (*responses.Subsonic, error) {
|
|
|
|
|
user, _ := request.UserFrom(r.Context())
|
|
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
repo := api.ds.PlayQueue()
|
|
|
|
|
pq, err := repo.RetrieveWithMediaFiles(r.Context(), user.ID)
|
2025-11-09 17:52:05 +00:00
|
|
|
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if pq == nil || len(pq.Items) == 0 {
|
2026-03-12 12:29:37 +00:00
|
|
|
response := newResponse()
|
|
|
|
|
response.PlayQueueByIndex = &responses.PlayQueueByIndex{
|
|
|
|
|
Username: user.UserName,
|
|
|
|
|
}
|
|
|
|
|
return response, nil
|
2025-11-09 17:52:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response := newResponse()
|
|
|
|
|
|
|
|
|
|
var index *int
|
|
|
|
|
if len(pq.Items) > 0 {
|
|
|
|
|
index = &pq.Current
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response.PlayQueueByIndex = &responses.PlayQueueByIndex{
|
|
|
|
|
Entry: slice.MapWithArg(pq.Items, r.Context(), childFromMediaFile),
|
|
|
|
|
CurrentIndex: index,
|
|
|
|
|
Position: pq.Position,
|
|
|
|
|
Username: user.UserName,
|
|
|
|
|
Changed: pq.UpdatedAt,
|
|
|
|
|
ChangedBy: pq.ChangedBy,
|
|
|
|
|
}
|
|
|
|
|
return response, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (api *Router) SavePlayQueueByIndex(r *http.Request) (*responses.Subsonic, error) {
|
|
|
|
|
p := req.Params(r)
|
2026-07-18 19:30:04 -04:00
|
|
|
ids := p.Strings("id")
|
2025-11-09 17:52:05 +00:00
|
|
|
|
|
|
|
|
position := p.Int64Or("position", 0)
|
|
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
var currentIndex int
|
|
|
|
|
|
|
|
|
|
if len(ids) > 0 {
|
|
|
|
|
currentIndex, err = p.Int("currentIndex")
|
|
|
|
|
if err != nil || currentIndex < 0 || currentIndex >= len(ids) {
|
|
|
|
|
return nil, newError(responses.ErrorMissingParameter, "missing parameter index, err: %s", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
items := slice.Map(ids, func(id string) model.MediaFile {
|
|
|
|
|
return model.MediaFile{ID: id}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
user, _ := request.UserFrom(r.Context())
|
|
|
|
|
client, _ := request.ClientFrom(r.Context())
|
|
|
|
|
|
|
|
|
|
pq := &model.PlayQueue{
|
|
|
|
|
UserID: user.ID,
|
|
|
|
|
Current: currentIndex,
|
|
|
|
|
Position: position,
|
|
|
|
|
ChangedBy: client,
|
|
|
|
|
Items: items,
|
|
|
|
|
CreatedAt: time.Time{},
|
|
|
|
|
UpdatedAt: time.Time{},
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
repo := api.ds.PlayQueue()
|
|
|
|
|
err = repo.Store(r.Context(), pq)
|
2025-11-09 17:52:05 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return newResponse(), nil
|
|
|
|
|
}
|