feat(subsonic): getAlbumList2 recent and frequent read the caller's play history (#6293)
release / govulncheck (push) Successful in 20s
release / web (push) Successful in 1m37s
release / go (push) Successful in 1m45s
release / integration (push) Failing after 5m27s
release / android (push) Successful in 5m40s
release / Build signed APK (releases and dev) (push) Successful in 5m55s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 20s
release / web (push) Successful in 1m37s
release / go (push) Successful in 1m45s
release / integration (push) Failing after 5m27s
release / android (push) Successful in 5m40s
release / Build signed APK (releases and dev) (push) Successful in 5m55s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
type=recent orders the caller's albums by their latest play (skips included); type=frequent orders them by completed plays (skips excluded). Plays of tracks whose file is gone do not count, and both page by size/offset like the other types. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -585,6 +585,69 @@ func (q *Queries) ListAlbumsByGenre(ctx context.Context, arg ListAlbumsByGenrePa
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listAlbumsFrequentlyPlayedByUser = `-- name: ListAlbumsFrequentlyPlayedByUser :many
|
||||
WITH plays AS (
|
||||
SELECT track_id, count(*) AS cnt
|
||||
FROM play_events
|
||||
WHERE user_id = $3 AND was_skipped = false
|
||||
GROUP BY track_id
|
||||
), per_album AS (
|
||||
SELECT t.album_id, sum(p.cnt) AS cnt
|
||||
FROM plays p
|
||||
JOIN tracks t ON t.id = p.track_id
|
||||
WHERE t.missing_since IS NULL
|
||||
GROUP BY t.album_id
|
||||
)
|
||||
SELECT albums.id, albums.title, albums.sort_title, albums.artist_id, albums.release_date, albums.mbid, albums.cover_art_path, albums.created_at, albums.updated_at, albums.cover_art_source, albums.cover_art_sources_version, albums.release_group_mbid
|
||||
FROM per_album pa
|
||||
JOIN albums ON albums.id = pa.album_id
|
||||
ORDER BY pa.cnt DESC, albums.id
|
||||
LIMIT $2 OFFSET $1
|
||||
`
|
||||
|
||||
type ListAlbumsFrequentlyPlayedByUserParams struct {
|
||||
Off int32
|
||||
Lim int32
|
||||
UserID pgtype.UUID
|
||||
}
|
||||
|
||||
// The user's albums, most played first: an album's count is the user's
|
||||
// completed plays across its tracks. Skips do not count, so skipping
|
||||
// through an album does not make it a favourite. Plays of tracks whose file
|
||||
// is gone do not count. Serves Subsonic getAlbumList2?type=frequent.
|
||||
func (q *Queries) ListAlbumsFrequentlyPlayedByUser(ctx context.Context, arg ListAlbumsFrequentlyPlayedByUserParams) ([]Album, error) {
|
||||
rows, err := q.db.Query(ctx, listAlbumsFrequentlyPlayedByUser, arg.Off, arg.Lim, arg.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Album
|
||||
for rows.Next() {
|
||||
var i Album
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Title,
|
||||
&i.SortTitle,
|
||||
&i.ArtistID,
|
||||
&i.ReleaseDate,
|
||||
&i.Mbid,
|
||||
&i.CoverArtPath,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.CoverArtSource,
|
||||
&i.CoverArtSourcesVersion,
|
||||
&i.ReleaseGroupMbid,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listAlbumsMissingMbidWithTrack = `-- name: ListAlbumsMissingMbidWithTrack :many
|
||||
SELECT a.id AS album_id,
|
||||
a.artist_id AS artist_id,
|
||||
@@ -720,6 +783,73 @@ func (q *Queries) ListAlbumsRandom(ctx context.Context, limit int32) ([]Album, e
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listAlbumsRecentlyPlayedByUser = `-- name: ListAlbumsRecentlyPlayedByUser :many
|
||||
WITH plays AS (
|
||||
SELECT track_id, max(started_at) AS last_played
|
||||
FROM play_events
|
||||
WHERE user_id = $3
|
||||
GROUP BY track_id
|
||||
), per_album AS (
|
||||
SELECT t.album_id, max(p.last_played) AS last_played
|
||||
FROM plays p
|
||||
JOIN tracks t ON t.id = p.track_id
|
||||
WHERE t.missing_since IS NULL
|
||||
GROUP BY t.album_id
|
||||
)
|
||||
SELECT albums.id, albums.title, albums.sort_title, albums.artist_id, albums.release_date, albums.mbid, albums.cover_art_path, albums.created_at, albums.updated_at, albums.cover_art_source, albums.cover_art_sources_version, albums.release_group_mbid
|
||||
FROM per_album pa
|
||||
JOIN albums ON albums.id = pa.album_id
|
||||
ORDER BY pa.last_played DESC, albums.id
|
||||
LIMIT $2 OFFSET $1
|
||||
`
|
||||
|
||||
type ListAlbumsRecentlyPlayedByUserParams struct {
|
||||
Off int32
|
||||
Lim int32
|
||||
UserID pgtype.UUID
|
||||
}
|
||||
|
||||
// The user's albums, most recently played first: an album's time is its
|
||||
// latest play of any of its tracks, skips included, since a skip is still
|
||||
// the user reaching for that album. Plays of tracks whose file is gone do
|
||||
// not count. Serves Subsonic getAlbumList2?type=recent.
|
||||
//
|
||||
// Aggregates play_events per track first (play_events_user_track_idx), then
|
||||
// folds tracks into albums, so the join runs over distinct tracks rather
|
||||
// than every play.
|
||||
func (q *Queries) ListAlbumsRecentlyPlayedByUser(ctx context.Context, arg ListAlbumsRecentlyPlayedByUserParams) ([]Album, error) {
|
||||
rows, err := q.db.Query(ctx, listAlbumsRecentlyPlayedByUser, arg.Off, arg.Lim, arg.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Album
|
||||
for rows.Next() {
|
||||
var i Album
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Title,
|
||||
&i.SortTitle,
|
||||
&i.ArtistID,
|
||||
&i.ReleaseDate,
|
||||
&i.Mbid,
|
||||
&i.CoverArtPath,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.CoverArtSource,
|
||||
&i.CoverArtSourcesVersion,
|
||||
&i.ReleaseGroupMbid,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listRecentlyAddedAlbumsWithArtist = `-- name: ListRecentlyAddedAlbumsWithArtist :many
|
||||
SELECT albums.id, albums.title, albums.sort_title, albums.artist_id, albums.release_date, albums.mbid, albums.cover_art_path, albums.created_at, albums.updated_at, albums.cover_art_source, albums.cover_art_sources_version, albums.release_group_mbid, artists.name AS artist_name
|
||||
FROM albums
|
||||
|
||||
@@ -99,6 +99,56 @@ WHERE EXISTS (
|
||||
ORDER BY albums.sort_title, albums.id
|
||||
LIMIT sqlc.arg(lim) OFFSET sqlc.arg(off);
|
||||
|
||||
-- name: ListAlbumsRecentlyPlayedByUser :many
|
||||
-- The user's albums, most recently played first: an album's time is its
|
||||
-- latest play of any of its tracks, skips included, since a skip is still
|
||||
-- the user reaching for that album. Plays of tracks whose file is gone do
|
||||
-- not count. Serves Subsonic getAlbumList2?type=recent.
|
||||
--
|
||||
-- Aggregates play_events per track first (play_events_user_track_idx), then
|
||||
-- folds tracks into albums, so the join runs over distinct tracks rather
|
||||
-- than every play.
|
||||
WITH plays AS (
|
||||
SELECT track_id, max(started_at) AS last_played
|
||||
FROM play_events
|
||||
WHERE user_id = sqlc.arg(user_id)
|
||||
GROUP BY track_id
|
||||
), per_album AS (
|
||||
SELECT t.album_id, max(p.last_played) AS last_played
|
||||
FROM plays p
|
||||
JOIN tracks t ON t.id = p.track_id
|
||||
WHERE t.missing_since IS NULL
|
||||
GROUP BY t.album_id
|
||||
)
|
||||
SELECT albums.*
|
||||
FROM per_album pa
|
||||
JOIN albums ON albums.id = pa.album_id
|
||||
ORDER BY pa.last_played DESC, albums.id
|
||||
LIMIT sqlc.arg(lim) OFFSET sqlc.arg(off);
|
||||
|
||||
-- name: ListAlbumsFrequentlyPlayedByUser :many
|
||||
-- The user's albums, most played first: an album's count is the user's
|
||||
-- completed plays across its tracks. Skips do not count, so skipping
|
||||
-- through an album does not make it a favourite. Plays of tracks whose file
|
||||
-- is gone do not count. Serves Subsonic getAlbumList2?type=frequent.
|
||||
WITH plays AS (
|
||||
SELECT track_id, count(*) AS cnt
|
||||
FROM play_events
|
||||
WHERE user_id = sqlc.arg(user_id) AND was_skipped = false
|
||||
GROUP BY track_id
|
||||
), per_album AS (
|
||||
SELECT t.album_id, sum(p.cnt) AS cnt
|
||||
FROM plays p
|
||||
JOIN tracks t ON t.id = p.track_id
|
||||
WHERE t.missing_since IS NULL
|
||||
GROUP BY t.album_id
|
||||
)
|
||||
SELECT albums.*
|
||||
FROM per_album pa
|
||||
JOIN albums ON albums.id = pa.album_id
|
||||
ORDER BY pa.cnt DESC, albums.id
|
||||
LIMIT sqlc.arg(lim) OFFSET sqlc.arg(off);
|
||||
|
||||
-- name: SearchAlbums :many
|
||||
SELECT * FROM albums
|
||||
WHERE title ILIKE '%' || $1 || '%'
|
||||
|
||||
Reference in New Issue
Block a user