feat(db): add session-vector capture queries; LikeTrack returns row count
ListRecentSessionTracks + UpdatePlayEventVector for the vector capture path inside RecordPlayStarted. LikeTrack switches to :execrows so the contextual-likes capture can detect insert vs already-exists. Existing callers updated to ignore the count for now; later tasks consume it.
This commit is contained in:
@@ -40,7 +40,7 @@ func (h *handlers) handleLikeTrack(w http.ResponseWriter, r *http.Request) {
|
||||
writeErr(w, http.StatusInternalServerError, "server_error", "lookup failed")
|
||||
return
|
||||
}
|
||||
if err := q.LikeTrack(r.Context(), dbq.LikeTrackParams{UserID: user.ID, TrackID: id}); err != nil {
|
||||
if _, err := q.LikeTrack(r.Context(), dbq.LikeTrackParams{UserID: user.ID, TrackID: id}); err != nil {
|
||||
h.logger.Error("api: like track insert", "err", err)
|
||||
writeErr(w, http.StatusInternalServerError, "server_error", "insert failed")
|
||||
return
|
||||
|
||||
@@ -188,6 +188,60 @@ func (q *Queries) InsertSkipEvent(ctx context.Context, arg InsertSkipEventParams
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listRecentSessionTracks = `-- name: ListRecentSessionTracks :many
|
||||
SELECT t.id, t.title, t.album_id, t.artist_id, t.track_number, t.disc_number, t.duration_ms, t.file_path, t.file_size, t.file_format, t.bitrate, t.mbid, t.genre, t.added_at, t.updated_at FROM tracks t
|
||||
JOIN play_events pe ON pe.track_id = t.id
|
||||
WHERE pe.session_id = $1
|
||||
AND pe.started_at < $2
|
||||
ORDER BY pe.started_at DESC
|
||||
LIMIT $3
|
||||
`
|
||||
|
||||
type ListRecentSessionTracksParams struct {
|
||||
SessionID pgtype.UUID
|
||||
StartedAt pgtype.Timestamptz
|
||||
Limit int32
|
||||
}
|
||||
|
||||
// Returns up to $3 tracks in session $1 whose play_event started before
|
||||
// $2, ordered newest-first. Used by playevents.RecordPlayStarted to
|
||||
// build the session_vector for the just-inserted play_event.
|
||||
func (q *Queries) ListRecentSessionTracks(ctx context.Context, arg ListRecentSessionTracksParams) ([]Track, error) {
|
||||
rows, err := q.db.Query(ctx, listRecentSessionTracks, arg.SessionID, arg.StartedAt, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Track
|
||||
for rows.Next() {
|
||||
var i Track
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Title,
|
||||
&i.AlbumID,
|
||||
&i.ArtistID,
|
||||
&i.TrackNumber,
|
||||
&i.DiscNumber,
|
||||
&i.DurationMs,
|
||||
&i.FilePath,
|
||||
&i.FileSize,
|
||||
&i.FileFormat,
|
||||
&i.Bitrate,
|
||||
&i.Mbid,
|
||||
&i.Genre,
|
||||
&i.AddedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const touchPlaySessionLastEvent = `-- name: TouchPlaySessionLastEvent :exec
|
||||
UPDATE play_sessions
|
||||
SET last_event_at = $2,
|
||||
@@ -251,3 +305,21 @@ func (q *Queries) UpdatePlayEventEnded(ctx context.Context, arg UpdatePlayEventE
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updatePlayEventVector = `-- name: UpdatePlayEventVector :exec
|
||||
UPDATE play_events
|
||||
SET session_vector_at_play = $2
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
type UpdatePlayEventVectorParams struct {
|
||||
ID pgtype.UUID
|
||||
SessionVectorAtPlay []byte
|
||||
}
|
||||
|
||||
// Used right after InsertPlayEvent to populate session_vector_at_play
|
||||
// once the vector has been computed.
|
||||
func (q *Queries) UpdatePlayEventVector(ctx context.Context, arg UpdatePlayEventVectorParams) error {
|
||||
_, err := q.db.Exec(ctx, updatePlayEventVector, arg.ID, arg.SessionVectorAtPlay)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func (q *Queries) LikeArtist(ctx context.Context, arg LikeArtistParams) error {
|
||||
return err
|
||||
}
|
||||
|
||||
const likeTrack = `-- name: LikeTrack :exec
|
||||
const likeTrack = `-- name: LikeTrack :execrows
|
||||
INSERT INTO general_likes (user_id, track_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (user_id, track_id) DO NOTHING
|
||||
@@ -87,9 +87,12 @@ type LikeTrackParams struct {
|
||||
TrackID pgtype.UUID
|
||||
}
|
||||
|
||||
func (q *Queries) LikeTrack(ctx context.Context, arg LikeTrackParams) error {
|
||||
_, err := q.db.Exec(ctx, likeTrack, arg.UserID, arg.TrackID)
|
||||
return err
|
||||
func (q *Queries) LikeTrack(ctx context.Context, arg LikeTrackParams) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, likeTrack, arg.UserID, arg.TrackID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const listLikedAlbumIDs = `-- name: ListLikedAlbumIDs :many
|
||||
|
||||
@@ -48,3 +48,21 @@ SELECT * FROM play_events WHERE id = $1;
|
||||
INSERT INTO skip_events (user_id, track_id, session_id, skipped_at, position_ms)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *;
|
||||
|
||||
-- name: ListRecentSessionTracks :many
|
||||
-- Returns up to $3 tracks in session $1 whose play_event started before
|
||||
-- $2, ordered newest-first. Used by playevents.RecordPlayStarted to
|
||||
-- build the session_vector for the just-inserted play_event.
|
||||
SELECT t.* FROM tracks t
|
||||
JOIN play_events pe ON pe.track_id = t.id
|
||||
WHERE pe.session_id = $1
|
||||
AND pe.started_at < $2
|
||||
ORDER BY pe.started_at DESC
|
||||
LIMIT $3;
|
||||
|
||||
-- name: UpdatePlayEventVector :exec
|
||||
-- Used right after InsertPlayEvent to populate session_vector_at_play
|
||||
-- once the vector has been computed.
|
||||
UPDATE play_events
|
||||
SET session_vector_at_play = $2
|
||||
WHERE id = $1;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- name: LikeTrack :exec
|
||||
-- name: LikeTrack :execrows
|
||||
INSERT INTO general_likes (user_id, track_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (user_id, track_id) DO NOTHING;
|
||||
|
||||
@@ -126,7 +126,7 @@ func TestLoadCandidates_ExcludesRecentlyPlayed(t *testing.T) {
|
||||
func TestLoadCandidates_StatJoin(t *testing.T) {
|
||||
f := newFixture(t, 2)
|
||||
// Seed = tracks[0]. Like tracks[1]; play it twice (one skip, one full play) 2 hours ago.
|
||||
if err := f.q.LikeTrack(context.Background(), dbq.LikeTrackParams{UserID: f.user, TrackID: f.tracks[1].ID}); err != nil {
|
||||
if _, err := f.q.LikeTrack(context.Background(), dbq.LikeTrackParams{UserID: f.user, TrackID: f.tracks[1].ID}); err != nil {
|
||||
t.Fatalf("like: %v", err)
|
||||
}
|
||||
twoH := time.Now().UTC().Add(-2 * time.Hour)
|
||||
@@ -201,7 +201,7 @@ func TestLoadCandidates_CrossUserIsolation(t *testing.T) {
|
||||
Username: "bob", PasswordHash: "x", ApiToken: "y", IsAdmin: false,
|
||||
})
|
||||
// Alice likes tracks[1]; Bob shouldn't see it.
|
||||
_ = f.q.LikeTrack(context.Background(), dbq.LikeTrackParams{UserID: f.user, TrackID: f.tracks[1].ID})
|
||||
_, _ = f.q.LikeTrack(context.Background(), dbq.LikeTrackParams{UserID: f.user, TrackID: f.tracks[1].ID})
|
||||
|
||||
got, err := LoadCandidates(context.Background(), f.q, bob.ID, f.tracks[0].ID, 1)
|
||||
if err != nil {
|
||||
|
||||
@@ -38,7 +38,7 @@ func (m *mediaHandlers) handleStar(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if trackID.Valid {
|
||||
if err := q.LikeTrack(r.Context(), dbq.LikeTrackParams{UserID: user.ID, TrackID: trackID}); err != nil {
|
||||
if _, err := q.LikeTrack(r.Context(), dbq.LikeTrackParams{UserID: user.ID, TrackID: trackID}); err != nil {
|
||||
WriteFail(w, r, ErrGeneric, "Could not star track")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ func TestHandleGetStarred2_ReturnsAllThreeArrays(t *testing.T) {
|
||||
q := dbq.New(pool)
|
||||
|
||||
// Star one of each.
|
||||
_ = q.LikeTrack(context.Background(), dbq.LikeTrackParams{UserID: user.ID, TrackID: track.ID})
|
||||
_, _ = q.LikeTrack(context.Background(), dbq.LikeTrackParams{UserID: user.ID, TrackID: track.ID})
|
||||
_ = q.LikeAlbum(context.Background(), dbq.LikeAlbumParams{UserID: user.ID, AlbumID: album.ID})
|
||||
_ = q.LikeArtist(context.Background(), dbq.LikeArtistParams{UserID: user.ID, ArtistID: artist.ID})
|
||||
|
||||
@@ -232,7 +232,7 @@ func TestHandleGetStarred2_CrossUserIsolation(t *testing.T) {
|
||||
bob, _ := q.CreateUser(context.Background(), dbq.CreateUserParams{
|
||||
Username: "bob", PasswordHash: "x", ApiToken: "y", IsAdmin: false,
|
||||
})
|
||||
_ = q.LikeTrack(context.Background(), dbq.LikeTrackParams{UserID: alice.ID, TrackID: track.ID})
|
||||
_, _ = q.LikeTrack(context.Background(), dbq.LikeTrackParams{UserID: alice.ID, TrackID: track.ID})
|
||||
|
||||
m := newStarHandlers(pool)
|
||||
req := httptest.NewRequest(http.MethodGet, "/rest/getStarred2?f=json", nil)
|
||||
|
||||
Reference in New Issue
Block a user