2020-08-01 12:17:06 -04:00
|
|
|
package persistence
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
|
|
|
|
|
"github.com/navidrome/navidrome/log"
|
|
|
|
|
"github.com/navidrome/navidrome/model"
|
|
|
|
|
"github.com/navidrome/navidrome/model/request"
|
2022-07-26 16:47:16 -04:00
|
|
|
. "github.com/onsi/ginkgo/v2"
|
2020-08-01 12:17:06 -04:00
|
|
|
. "github.com/onsi/gomega"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var _ = Describe("sqlBookmarks", func() {
|
|
|
|
|
var mr model.MediaFileRepository
|
2026-09-25 18:06:10 -04:00
|
|
|
var ctx context.Context
|
2020-08-01 12:17:06 -04:00
|
|
|
|
|
|
|
|
BeforeEach(func() {
|
2026-09-25 18:06:10 -04:00
|
|
|
ctx = request.WithUser(log.NewContext(GinkgoT().Context()), model.User{ID: "userid"})
|
|
|
|
|
mr = NewMediaFileRepository(GetDBXBuilder())
|
2020-08-01 12:17:06 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
Describe("Bookmarks", func() {
|
|
|
|
|
It("returns an empty collection if there are no bookmarks", func() {
|
2026-09-25 18:06:10 -04:00
|
|
|
Expect(mr.GetBookmarks(ctx)).To(BeEmpty())
|
2020-08-01 12:17:06 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("saves and overrides bookmarks", func() {
|
|
|
|
|
By("Saving the bookmark")
|
2026-09-25 18:06:10 -04:00
|
|
|
Expect(mr.AddBookmark(ctx, songAntenna.ID, "this is a comment", 123)).To(BeNil())
|
2020-08-01 12:17:06 -04:00
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
bms, err := mr.GetBookmarks(ctx)
|
2023-12-09 13:52:17 -05:00
|
|
|
Expect(err).ToNot(HaveOccurred())
|
2020-08-01 12:17:06 -04:00
|
|
|
|
|
|
|
|
Expect(bms).To(HaveLen(1))
|
|
|
|
|
Expect(bms[0].Item.ID).To(Equal(songAntenna.ID))
|
|
|
|
|
Expect(bms[0].Item.Title).To(Equal(songAntenna.Title))
|
|
|
|
|
Expect(bms[0].Comment).To(Equal("this is a comment"))
|
|
|
|
|
Expect(bms[0].Position).To(Equal(int64(123)))
|
|
|
|
|
created := bms[0].CreatedAt
|
|
|
|
|
updated := bms[0].UpdatedAt
|
|
|
|
|
Expect(created.IsZero()).To(BeFalse())
|
|
|
|
|
Expect(updated).To(BeTemporally(">=", created))
|
|
|
|
|
|
|
|
|
|
By("Overriding the bookmark")
|
2026-09-25 18:06:10 -04:00
|
|
|
Expect(mr.AddBookmark(ctx, songAntenna.ID, "another comment", 333)).To(BeNil())
|
2020-08-01 12:17:06 -04:00
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
bms, err = mr.GetBookmarks(ctx)
|
2023-12-09 13:52:17 -05:00
|
|
|
Expect(err).ToNot(HaveOccurred())
|
2020-08-01 12:17:06 -04:00
|
|
|
|
|
|
|
|
Expect(bms[0].Item.ID).To(Equal(songAntenna.ID))
|
|
|
|
|
Expect(bms[0].Comment).To(Equal("another comment"))
|
|
|
|
|
Expect(bms[0].Position).To(Equal(int64(333)))
|
|
|
|
|
Expect(bms[0].CreatedAt).To(Equal(created))
|
|
|
|
|
Expect(bms[0].UpdatedAt).To(BeTemporally(">=", updated))
|
|
|
|
|
|
|
|
|
|
By("Saving another bookmark")
|
2026-09-25 18:06:10 -04:00
|
|
|
Expect(mr.AddBookmark(ctx, songComeTogether.ID, "one more comment", 444)).To(BeNil())
|
|
|
|
|
bms, err = mr.GetBookmarks(ctx)
|
2023-12-09 13:52:17 -05:00
|
|
|
Expect(err).ToNot(HaveOccurred())
|
2020-08-01 12:17:06 -04:00
|
|
|
Expect(bms).To(HaveLen(2))
|
|
|
|
|
|
|
|
|
|
By("Delete bookmark")
|
2026-09-25 18:06:10 -04:00
|
|
|
Expect(mr.DeleteBookmark(ctx, songAntenna.ID)).To(Succeed())
|
|
|
|
|
bms, err = mr.GetBookmarks(ctx)
|
2023-12-09 13:52:17 -05:00
|
|
|
Expect(err).ToNot(HaveOccurred())
|
2020-08-01 12:17:06 -04:00
|
|
|
Expect(bms).To(HaveLen(1))
|
|
|
|
|
Expect(bms[0].Item.ID).To(Equal(songComeTogether.ID))
|
|
|
|
|
Expect(bms[0].Item.Title).To(Equal(songComeTogether.Title))
|
2023-12-09 13:52:17 -05:00
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
Expect(mr.DeleteBookmark(ctx, songComeTogether.ID)).To(Succeed())
|
|
|
|
|
Expect(mr.GetBookmarks(ctx)).To(BeEmpty())
|
2020-08-01 12:17:06 -04:00
|
|
|
})
|
|
|
|
|
})
|
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
|
|
|
|
|
|
|
|
Describe("library access", func() {
|
|
|
|
|
var otherLib model.Library
|
|
|
|
|
var restrictedUser model.User
|
2026-09-25 18:06:10 -04:00
|
|
|
var adminCtx, userCtx context.Context
|
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
|
|
|
var userMr model.MediaFileRepository
|
|
|
|
|
|
|
|
|
|
BeforeEach(func() {
|
|
|
|
|
adminCtx, otherLib, restrictedUser = restrictedFixture("bmk")
|
|
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
adminMr := NewMediaFileRepository(GetDBXBuilder())
|
|
|
|
|
Expect(adminMr.Put(adminCtx, &model.MediaFile{
|
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
|
|
|
ID: "bmk-otherlib-track", LibraryID: otherLib.ID,
|
|
|
|
|
Path: "hidden/bookmarked.mp3", Title: "Hidden Bookmarked",
|
|
|
|
|
})).To(Succeed())
|
2026-09-25 18:06:10 -04:00
|
|
|
DeferCleanup(func() { _ = adminMr.Delete(adminCtx, "bmk-otherlib-track") })
|
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
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
userCtx = request.WithUser(ctx, restrictedUser)
|
|
|
|
|
userMr = NewMediaFileRepository(GetDBXBuilder())
|
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
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("does not return bookmarks for tracks outside the user's libraries", func() {
|
2026-09-25 18:06:10 -04:00
|
|
|
Expect(userMr.AddBookmark(userCtx, "bmk-otherlib-track", "sneaky", 1)).To(Succeed())
|
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
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
Expect(userMr.GetBookmarks(userCtx)).To(BeEmpty())
|
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
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("still returns the bookmark for an admin", func() {
|
2026-09-25 18:06:10 -04:00
|
|
|
adminMr := NewMediaFileRepository(GetDBXBuilder())
|
|
|
|
|
Expect(adminMr.AddBookmark(adminCtx, "bmk-otherlib-track", "mine", 1)).To(Succeed())
|
|
|
|
|
DeferCleanup(func() { _ = adminMr.DeleteBookmark(adminCtx, "bmk-otherlib-track") })
|
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
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
bms, err := adminMr.GetBookmarks(adminCtx)
|
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
|
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
|
Expect(bms).To(HaveLen(1))
|
|
|
|
|
Expect(bms[0].Item.ID).To(Equal("bmk-otherlib-track"))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
It("keeps returning bookmarks for tracks inside the user's libraries", func() {
|
2026-09-25 18:06:10 -04:00
|
|
|
Expect(userMr.AddBookmark(userCtx, songAntenna.ID, "allowed", 5)).To(Succeed())
|
|
|
|
|
DeferCleanup(func() { _ = userMr.DeleteBookmark(userCtx, songAntenna.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
|
|
|
|
2026-09-25 18:06:10 -04:00
|
|
|
bms, err := userMr.GetBookmarks(userCtx)
|
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
|
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
|
Expect(bms).To(HaveLen(1))
|
|
|
|
|
Expect(bms[0].Item.ID).To(Equal(songAntenna.ID))
|
|
|
|
|
})
|
|
|
|
|
})
|
2020-08-01 12:17:06 -04:00
|
|
|
})
|