mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
Merge 0e93ebfc73eea68d56ddf23ff6febec820b56b91 into 7e16b6acb5c11e283fcd320a0abb82372a8ab0dd
This commit is contained in:
commit
b2e41587f1
@ -432,7 +432,7 @@ type MediaFileCursor iter.Seq2[MediaFile, error]
|
|||||||
type MediaFileRepository interface {
|
type MediaFileRepository interface {
|
||||||
CountAll(options ...QueryOptions) (int64, error)
|
CountAll(options ...QueryOptions) (int64, error)
|
||||||
CountBySuffix(options ...QueryOptions) (map[string]int64, error)
|
CountBySuffix(options ...QueryOptions) (map[string]int64, error)
|
||||||
Exists(id string) (bool, error)
|
Exists(ids ...string) (bool, error)
|
||||||
Put(m *MediaFile) error
|
Put(m *MediaFile) error
|
||||||
UpdateProbeData(id string, data string) error
|
UpdateProbeData(id string, data string) error
|
||||||
Get(id string) (*MediaFile, error)
|
Get(id string) (*MediaFile, error)
|
||||||
|
|||||||
@ -147,8 +147,29 @@ func (r *mediaFileRepository) CountBySuffix(options ...model.QueryOptions) (map[
|
|||||||
return counts, nil
|
return counts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mediaFileRepository) Exists(id string) (bool, error) {
|
// Exists checks if all given media file IDs exist in the database and are accessible to the current user.
|
||||||
return r.exists(Eq{"media_file.id": id})
|
// If no IDs are provided, it returns true. Duplicate IDs are handled correctly.
|
||||||
|
// If any of the IDs do not exist or are not accessible, it returns false.
|
||||||
|
func (r *mediaFileRepository) Exists(ids ...string) (bool, error) {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
uniqueIds := slice.Unique(ids)
|
||||||
|
|
||||||
|
// Process in batches to avoid hitting SQLITE_MAX_VARIABLE_NUMBER limit (default 999)
|
||||||
|
const batchSize = 300
|
||||||
|
var totalCount int64
|
||||||
|
for batch := range slices.Chunk(uniqueIds, batchSize) {
|
||||||
|
existsQuery := Select("count(*) as exist").From("media_file").Where(Eq{"media_file.id": batch})
|
||||||
|
existsQuery = r.applyLibraryFilter(existsQuery)
|
||||||
|
var res struct{ Exist int64 }
|
||||||
|
err := r.queryOne(existsQuery, &res)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
totalCount += res.Exist
|
||||||
|
}
|
||||||
|
return totalCount == int64(len(uniqueIds)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mediaFileRepository) Put(m *model.MediaFile) error {
|
func (r *mediaFileRepository) Put(m *model.MediaFile) error {
|
||||||
|
|||||||
@ -169,15 +169,26 @@ func (api *Router) Scrobble(r *http.Request) (*responses.Subsonic, error) {
|
|||||||
position := p.IntOr("position", 0)
|
position := p.IntOr("position", 0)
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
|
// Validate all IDs exist before processing (OpenSubsonic compliance)
|
||||||
|
exists, err := api.ds.MediaFile(ctx).Exists(ids...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return nil, newError(responses.ErrorDataNotFound, "Media file not found")
|
||||||
|
}
|
||||||
|
|
||||||
if submission {
|
if submission {
|
||||||
err := api.scrobblerSubmit(ctx, ids, times)
|
err := api.scrobblerSubmit(ctx, ids, times)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(ctx, "Error registering scrobbles", "ids", ids, "times", times, err)
|
log.Error(ctx, "Error registering scrobbles", "ids", ids, "times", times, err)
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err := api.scrobblerNowPlaying(ctx, ids[0], position)
|
err := api.scrobblerNowPlaying(ctx, ids[0], position)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(ctx, "Error setting NowPlaying", "id", ids[0], err)
|
log.Error(ctx, "Error setting NowPlaying", "id", ids[0], err)
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,6 +32,12 @@ var _ = Describe("MediaAnnotationController", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Describe("Scrobble", func() {
|
Describe("Scrobble", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
// Populate mock with valid media files
|
||||||
|
_ = ds.MediaFile(ctx).Put(&model.MediaFile{ID: "12"})
|
||||||
|
_ = ds.MediaFile(ctx).Put(&model.MediaFile{ID: "34"})
|
||||||
|
})
|
||||||
|
|
||||||
It("submit all scrobbles with only the id", func() {
|
It("submit all scrobbles with only the id", func() {
|
||||||
// Back-date the baseline so the assertion still passes on platforms
|
// Back-date the baseline so the assertion still passes on platforms
|
||||||
// with millisecond clock resolution (e.g. Windows).
|
// with millisecond clock resolution (e.g. Windows).
|
||||||
@ -74,10 +80,27 @@ var _ = Describe("MediaAnnotationController", func() {
|
|||||||
Expect(playTracker.Submissions).To(BeEmpty())
|
Expect(playTracker.Submissions).To(BeEmpty())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("returns error when any id is invalid", func() {
|
||||||
|
r := newGetRequest("id=invalid")
|
||||||
|
|
||||||
|
_, err := router.Scrobble(r)
|
||||||
|
|
||||||
|
Expect(err).To(MatchError(ContainSubstring("not found")))
|
||||||
|
Expect(playTracker.Submissions).To(BeEmpty())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns error and does not scrobble when mix of valid and invalid ids", func() {
|
||||||
|
r := newGetRequest("id=12", "id=invalid")
|
||||||
|
|
||||||
|
_, err := router.Scrobble(r)
|
||||||
|
|
||||||
|
Expect(err).To(MatchError(ContainSubstring("not found")))
|
||||||
|
Expect(playTracker.Submissions).To(BeEmpty())
|
||||||
|
})
|
||||||
|
|
||||||
Context("submission=false", func() {
|
Context("submission=false", func() {
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
_ = ds.MediaFile(ctx).Put(&model.MediaFile{ID: "12"})
|
|
||||||
ctx = request.WithPlayer(ctx, model.Player{ID: "player-1"})
|
ctx = request.WithPlayer(ctx, model.Player{ID: "player-1"})
|
||||||
req = newGetRequest("id=12", "submission=false")
|
req = newGetRequest("id=12", "submission=false")
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
@ -99,6 +122,16 @@ var _ = Describe("MediaAnnotationController", func() {
|
|||||||
Expect(playTracker.ReportedPlayback[0].State).To(Equal(scrobbler.StatePlaying))
|
Expect(playTracker.ReportedPlayback[0].State).To(Equal(scrobbler.StatePlaying))
|
||||||
Expect(playTracker.ReportedPlayback[0].ClientId).To(Equal("player-1"))
|
Expect(playTracker.ReportedPlayback[0].ClientId).To(Equal("player-1"))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("returns error when id is invalid", func() {
|
||||||
|
req = newGetRequest("id=invalid", "submission=false")
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
_, err := router.Scrobble(req)
|
||||||
|
|
||||||
|
Expect(err).To(MatchError(ContainSubstring("not found")))
|
||||||
|
Expect(playTracker.Playing).To(BeEmpty())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -44,12 +44,16 @@ func (m *MockMediaFileRepo) SetData(mfs model.MediaFiles) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockMediaFileRepo) Exists(id string) (bool, error) {
|
func (m *MockMediaFileRepo) Exists(ids ...string) (bool, error) {
|
||||||
if m.Err {
|
if m.Err {
|
||||||
return false, errors.New("error")
|
return false, errors.New("error")
|
||||||
}
|
}
|
||||||
_, found := m.Data[id]
|
for _, id := range ids {
|
||||||
return found, nil
|
if _, found := m.Data[id]; !found {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockMediaFileRepo) Get(id string) (*model.MediaFile, error) {
|
func (m *MockMediaFileRepo) Get(id string) (*model.MediaFile, error) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user