mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
feat: persist and expose plugin playlist ValidUntil in Subsonic API
Add a ValidUntil field to the Playlist model and persist it from the plugin's GetPlaylistResponse during sync. This allows clients to know when a plugin playlist's data will be refreshed. The value is exposed in the OpenSubsonic playlist response alongside the existing smart playlist ValidUntil calculation. The migration is consolidated into a single multi-statement ExecContext call.
This commit is contained in:
parent
a5fd18dc67
commit
9ddbcbf6b4
@ -12,27 +12,21 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func upAddPluginPlaylistFields(ctx context.Context, tx *sql.Tx) error {
|
func upAddPluginPlaylistFields(ctx context.Context, tx *sql.Tx) error {
|
||||||
_, err := tx.ExecContext(ctx, `ALTER TABLE playlist ADD COLUMN plugin_id VARCHAR(255) NOT NULL DEFAULT '';`)
|
_, err := tx.ExecContext(ctx, `
|
||||||
if err != nil {
|
ALTER TABLE playlist ADD COLUMN plugin_id VARCHAR(255) NOT NULL DEFAULT '';
|
||||||
return err
|
ALTER TABLE playlist ADD COLUMN plugin_playlist_id VARCHAR(255) NOT NULL DEFAULT '';
|
||||||
}
|
ALTER TABLE playlist ADD COLUMN valid_until DATETIME DEFAULT NULL;
|
||||||
_, err = tx.ExecContext(ctx, `ALTER TABLE playlist ADD COLUMN plugin_playlist_id VARCHAR(255) NOT NULL DEFAULT '';`)
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_playlist_plugin ON playlist(plugin_id, plugin_playlist_id, owner_id) WHERE plugin_id != '';
|
||||||
if err != nil {
|
`)
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = tx.ExecContext(ctx, `CREATE UNIQUE INDEX IF NOT EXISTS idx_playlist_plugin ON playlist(plugin_id, plugin_playlist_id, owner_id) WHERE plugin_id != '';`)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func downAddPluginPlaylistFields(ctx context.Context, tx *sql.Tx) error {
|
func downAddPluginPlaylistFields(ctx context.Context, tx *sql.Tx) error {
|
||||||
_, err := tx.ExecContext(ctx, `DROP INDEX IF EXISTS idx_playlist_plugin;`)
|
_, err := tx.ExecContext(ctx, `
|
||||||
if err != nil {
|
DROP INDEX IF EXISTS idx_playlist_plugin;
|
||||||
return err
|
ALTER TABLE playlist DROP COLUMN valid_until;
|
||||||
}
|
ALTER TABLE playlist DROP COLUMN plugin_playlist_id;
|
||||||
_, err = tx.ExecContext(ctx, `ALTER TABLE playlist DROP COLUMN plugin_playlist_id;`)
|
ALTER TABLE playlist DROP COLUMN plugin_id;
|
||||||
if err != nil {
|
`)
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = tx.ExecContext(ctx, `ALTER TABLE playlist DROP COLUMN plugin_id;`)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,8 +32,9 @@ type Playlist struct {
|
|||||||
EvaluatedAt *time.Time `structs:"evaluated_at" json:"evaluatedAt"`
|
EvaluatedAt *time.Time `structs:"evaluated_at" json:"evaluatedAt"`
|
||||||
|
|
||||||
// Plugin playlist attributes
|
// Plugin playlist attributes
|
||||||
PluginID string `structs:"plugin_id" json:"pluginId,omitempty"`
|
PluginID string `structs:"plugin_id" json:"pluginId,omitempty"`
|
||||||
PluginPlaylistID string `structs:"plugin_playlist_id" json:"pluginPlaylistId,omitempty"`
|
PluginPlaylistID string `structs:"plugin_playlist_id" json:"pluginPlaylistId,omitempty"`
|
||||||
|
ValidUntil *time.Time `structs:"valid_until" json:"validUntil,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pls Playlist) IsReadOnly() bool {
|
func (pls Playlist) IsReadOnly() bool {
|
||||||
|
|||||||
@ -200,6 +200,10 @@ func (p *playlistSyncer) syncPlaylist(info capabilities.PlaylistInfo, dbID strin
|
|||||||
PluginID: p.pluginName,
|
PluginID: p.pluginName,
|
||||||
PluginPlaylistID: info.ID,
|
PluginPlaylistID: info.ID,
|
||||||
}
|
}
|
||||||
|
if resp.ValidUntil > 0 {
|
||||||
|
t := time.Unix(resp.ValidUntil, 0)
|
||||||
|
pls.ValidUntil = &t
|
||||||
|
}
|
||||||
|
|
||||||
// Set tracks from matched media files
|
// Set tracks from matched media files
|
||||||
pls.AddMediaFiles(matched)
|
pls.AddMediaFiles(matched)
|
||||||
|
|||||||
@ -75,6 +75,7 @@ var _ = Describe("PlaylistProvider", Ordered, func() {
|
|||||||
Expect(dailyMix1.PluginID).To(Equal("test-playlist-provider"))
|
Expect(dailyMix1.PluginID).To(Equal("test-playlist-provider"))
|
||||||
Expect(dailyMix1.PluginPlaylistID).To(Equal("daily-mix-1"))
|
Expect(dailyMix1.PluginPlaylistID).To(Equal("daily-mix-1"))
|
||||||
Expect(dailyMix1.Public).To(BeFalse())
|
Expect(dailyMix1.Public).To(BeFalse())
|
||||||
|
Expect(dailyMix1.ValidUntil).To(BeNil())
|
||||||
})
|
})
|
||||||
|
|
||||||
It("generates deterministic playlist IDs", func() {
|
It("generates deterministic playlist IDs", func() {
|
||||||
|
|||||||
@ -168,9 +168,10 @@ func buildOSPlaylist(ctx context.Context, p model.Playlist) *responses.OpenSubso
|
|||||||
if p.IsReadOnly() {
|
if p.IsReadOnly() {
|
||||||
pls.Readonly = true
|
pls.Readonly = true
|
||||||
|
|
||||||
// ValidUntil only applies to smart playlists
|
|
||||||
if p.IsSmartPlaylist() && p.EvaluatedAt != nil {
|
if p.IsSmartPlaylist() && p.EvaluatedAt != nil {
|
||||||
pls.ValidUntil = P(p.EvaluatedAt.Add(conf.Server.SmartPlaylistRefreshDelay))
|
pls.ValidUntil = P(p.EvaluatedAt.Add(conf.Server.SmartPlaylistRefreshDelay))
|
||||||
|
} else if p.IsPluginPlaylist() && p.ValidUntil != nil {
|
||||||
|
pls.ValidUntil = p.ValidUntil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
user, ok := request.UserFrom(ctx)
|
user, ok := request.UserFrom(ctx)
|
||||||
|
|||||||
@ -177,13 +177,23 @@ var _ = Describe("buildPlaylist", func() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
It("marks plugin playlist as readonly", func() {
|
It("marks plugin playlist as readonly with no ValidUntil when not set", func() {
|
||||||
ctx = request.WithUser(ctx, model.User{ID: "1234", UserName: "admin"})
|
ctx = request.WithUser(ctx, model.User{ID: "1234", UserName: "admin"})
|
||||||
result := router.buildPlaylist(ctx, playlist)
|
result := router.buildPlaylist(ctx, playlist)
|
||||||
Expect(result.Readonly).To(BeTrue())
|
Expect(result.Readonly).To(BeTrue())
|
||||||
Expect(result.ValidUntil).To(BeNil())
|
Expect(result.ValidUntil).To(BeNil())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("exposes ValidUntil when set on the model", func() {
|
||||||
|
validUntil := time.Date(2023, 3, 1, 12, 0, 0, 0, time.UTC)
|
||||||
|
playlist.ValidUntil = &validUntil
|
||||||
|
|
||||||
|
ctx = request.WithUser(ctx, model.User{ID: "1234", UserName: "admin"})
|
||||||
|
result := router.buildPlaylist(ctx, playlist)
|
||||||
|
Expect(result.Readonly).To(BeTrue())
|
||||||
|
Expect(result.ValidUntil).To(Equal(&validUntil))
|
||||||
|
})
|
||||||
|
|
||||||
It("marks plugin playlist as readonly even for non-owner", func() {
|
It("marks plugin playlist as readonly even for non-owner", func() {
|
||||||
ctx = request.WithUser(ctx, model.User{ID: "other-user", UserName: "other"})
|
ctx = request.WithUser(ctx, model.User{ID: "other-user", UserName: "other"})
|
||||||
result := router.buildPlaylist(ctx, playlist)
|
result := router.buildPlaylist(ctx, playlist)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user