Files
minstrel/internal/db/dbq/playlists.sql.go
T
bvandeusen 9bf4b504b3 feat(server): GET /api/library/sync endpoint
Returns batched upserts + deletes since the supplied cursor. Empty cursor
returns full snapshot; subsequent calls pull deltas. Per-user entities
(likes, playlists) are scoped to the authed user. Composite-key entities
(likes, playlist_tracks) use stable string ids encoded by sync.EncodeLikeID
/ sync.EncodePlaylistTrackID.

Behavior:
  204 No Content - no changes since cursor
  200 OK         - JSON syncResponse {cursor, upserts, deletes}
  410 Gone       - cursor older than oldest log row; client must reset
  401 / 500      - standard envelope errors

Adds sqlc queries GetAlbumsByIDs, GetTracksByIDs, GetPlaylistsByIDs to
mirror the existing GetArtistsByIDs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 22:36:26 -04:00

483 lines
13 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: playlists.sql
package dbq
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const appendPlaylistTrack = `-- name: AppendPlaylistTrack :one
INSERT INTO playlist_tracks (playlist_id, position, track_id, title, artist_name, album_title, duration_sec)
SELECT
$1::uuid,
COALESCE((SELECT MAX(position) + 1 FROM playlist_tracks WHERE playlist_id = $1::uuid), 0),
t.id,
t.title,
artists.name,
albums.title,
(t.duration_ms / 1000)::integer
FROM tracks t
JOIN albums ON albums.id = t.album_id
JOIN artists ON artists.id = t.artist_id
WHERE t.id = $2::uuid
RETURNING playlist_id, position, track_id, title, artist_name, album_title, duration_sec, added_at
`
type AppendPlaylistTrackParams struct {
PlaylistID pgtype.UUID
TrackID pgtype.UUID
}
// Inserts at the next available position. Snapshot fields are copied
// from the tracks/albums/artists join at insert time. tracks.duration_ms
// is converted to seconds for the snapshot.
func (q *Queries) AppendPlaylistTrack(ctx context.Context, arg AppendPlaylistTrackParams) (PlaylistTrack, error) {
row := q.db.QueryRow(ctx, appendPlaylistTrack, arg.PlaylistID, arg.TrackID)
var i PlaylistTrack
err := row.Scan(
&i.PlaylistID,
&i.Position,
&i.TrackID,
&i.Title,
&i.ArtistName,
&i.AlbumTitle,
&i.DurationSec,
&i.AddedAt,
)
return i, err
}
const createPlaylist = `-- name: CreatePlaylist :one
INSERT INTO playlists (user_id, name, description, is_public)
VALUES ($1, $2, $3, $4)
RETURNING id, user_id, name, description, is_public, cover_path, track_count, duration_sec, created_at, updated_at, kind, system_variant, seed_artist_id
`
type CreatePlaylistParams struct {
UserID pgtype.UUID
Name string
Description string
IsPublic bool
}
func (q *Queries) CreatePlaylist(ctx context.Context, arg CreatePlaylistParams) (Playlist, error) {
row := q.db.QueryRow(ctx, createPlaylist,
arg.UserID,
arg.Name,
arg.Description,
arg.IsPublic,
)
var i Playlist
err := row.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.IsPublic,
&i.CoverPath,
&i.TrackCount,
&i.DurationSec,
&i.CreatedAt,
&i.UpdatedAt,
&i.Kind,
&i.SystemVariant,
&i.SeedArtistID,
)
return i, err
}
const deletePlaylist = `-- name: DeletePlaylist :one
DELETE FROM playlists WHERE id = $1
RETURNING id, cover_path
`
type DeletePlaylistRow struct {
ID pgtype.UUID
CoverPath *string
}
// Returns cover_path so the caller can clean up the cached collage on disk.
func (q *Queries) DeletePlaylist(ctx context.Context, id pgtype.UUID) (DeletePlaylistRow, error) {
row := q.db.QueryRow(ctx, deletePlaylist, id)
var i DeletePlaylistRow
err := row.Scan(&i.ID, &i.CoverPath)
return i, err
}
const deletePlaylistTrack = `-- name: DeletePlaylistTrack :exec
DELETE FROM playlist_tracks
WHERE playlist_id = $1 AND position = $2
`
type DeletePlaylistTrackParams struct {
PlaylistID pgtype.UUID
Position int32
}
// Two-step: delete the row at `position`, then renumber subsequent rows
// to close the gap. The renumber is a single UPDATE; the service layer
// runs both in one transaction.
func (q *Queries) DeletePlaylistTrack(ctx context.Context, arg DeletePlaylistTrackParams) error {
_, err := q.db.Exec(ctx, deletePlaylistTrack, arg.PlaylistID, arg.Position)
return err
}
const getPlaylist = `-- name: GetPlaylist :one
SELECT p.id, p.user_id, p.name, p.description, p.is_public, p.cover_path, p.track_count, p.duration_sec, p.created_at, p.updated_at, p.kind, p.system_variant, p.seed_artist_id, u.username AS owner_username
FROM playlists p
JOIN users u ON u.id = p.user_id
WHERE p.id = $1
`
type GetPlaylistRow struct {
ID pgtype.UUID
UserID pgtype.UUID
Name string
Description string
IsPublic bool
CoverPath *string
TrackCount int32
DurationSec int32
CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
Kind string
SystemVariant *string
SeedArtistID pgtype.UUID
OwnerUsername string
}
func (q *Queries) GetPlaylist(ctx context.Context, id pgtype.UUID) (GetPlaylistRow, error) {
row := q.db.QueryRow(ctx, getPlaylist, id)
var i GetPlaylistRow
err := row.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.IsPublic,
&i.CoverPath,
&i.TrackCount,
&i.DurationSec,
&i.CreatedAt,
&i.UpdatedAt,
&i.Kind,
&i.SystemVariant,
&i.SeedArtistID,
&i.OwnerUsername,
)
return i, err
}
const getPlaylistsByIDs = `-- name: GetPlaylistsByIDs :many
SELECT id, user_id, name, description, is_public, cover_path, track_count, duration_sec, created_at, updated_at, kind, system_variant, seed_artist_id FROM playlists WHERE id = ANY($1::uuid[])
`
// Batched lookup used by /api/library/sync to hydrate upsert payloads
// (#357). Mirror of GetArtistsByIDs.
func (q *Queries) GetPlaylistsByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Playlist, error) {
rows, err := q.db.Query(ctx, getPlaylistsByIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Playlist
for rows.Next() {
var i Playlist
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.IsPublic,
&i.CoverPath,
&i.TrackCount,
&i.DurationSec,
&i.CreatedAt,
&i.UpdatedAt,
&i.Kind,
&i.SystemVariant,
&i.SeedArtistID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listAllPlaylistTracksForCollage = `-- name: ListAllPlaylistTracksForCollage :many
SELECT pt.position,
albums.cover_art_path AS album_cover_path
FROM playlist_tracks pt
LEFT JOIN tracks t ON t.id = pt.track_id
LEFT JOIN albums ON albums.id = t.album_id
WHERE pt.playlist_id = $1
ORDER BY pt.position
LIMIT $2
`
type ListAllPlaylistTracksForCollageParams struct {
PlaylistID pgtype.UUID
Limit int32
}
type ListAllPlaylistTracksForCollageRow struct {
Position int32
AlbumCoverPath *string
}
// First N tracks for the collage. Uses LEFT JOIN on albums for the
// cover_path; rows with NULL cover_path get the glyph fallback.
func (q *Queries) ListAllPlaylistTracksForCollage(ctx context.Context, arg ListAllPlaylistTracksForCollageParams) ([]ListAllPlaylistTracksForCollageRow, error) {
rows, err := q.db.Query(ctx, listAllPlaylistTracksForCollage, arg.PlaylistID, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListAllPlaylistTracksForCollageRow
for rows.Next() {
var i ListAllPlaylistTracksForCollageRow
if err := rows.Scan(&i.Position, &i.AlbumCoverPath); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listPlaylistTracks = `-- name: ListPlaylistTracks :many
SELECT pt.playlist_id, pt.position, pt.track_id, pt.title, pt.artist_name, pt.album_title, pt.duration_sec, pt.added_at,
t.id AS live_track_id,
albums.id AS album_id,
artists.id AS artist_id
FROM playlist_tracks pt
LEFT JOIN tracks t ON t.id = pt.track_id
LEFT JOIN albums ON albums.id = t.album_id
LEFT JOIN artists ON artists.id = t.artist_id
WHERE pt.playlist_id = $1
ORDER BY pt.position
`
type ListPlaylistTracksRow struct {
PlaylistID pgtype.UUID
Position int32
TrackID pgtype.UUID
Title string
ArtistName string
AlbumTitle string
DurationSec int32
AddedAt pgtype.Timestamptz
LiveTrackID pgtype.UUID
AlbumID pgtype.UUID
ArtistID pgtype.UUID
}
// Joined to tracks for the live track id (the service layer derives the
// stream URL from it); LEFT JOIN preserves the row when track_id is NULL
// (track was removed from the library). The denormalized snapshot fields
// on playlist_tracks remain authoritative for title/artist/album text.
func (q *Queries) ListPlaylistTracks(ctx context.Context, playlistID pgtype.UUID) ([]ListPlaylistTracksRow, error) {
rows, err := q.db.Query(ctx, listPlaylistTracks, playlistID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListPlaylistTracksRow
for rows.Next() {
var i ListPlaylistTracksRow
if err := rows.Scan(
&i.PlaylistID,
&i.Position,
&i.TrackID,
&i.Title,
&i.ArtistName,
&i.AlbumTitle,
&i.DurationSec,
&i.AddedAt,
&i.LiveTrackID,
&i.AlbumID,
&i.ArtistID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listPlaylistsForUser = `-- name: ListPlaylistsForUser :many
SELECT p.id, p.user_id, p.name, p.description, p.is_public, p.cover_path, p.track_count, p.duration_sec, p.created_at, p.updated_at, p.kind, p.system_variant, p.seed_artist_id, u.username AS owner_username
FROM playlists p
JOIN users u ON u.id = p.user_id
WHERE p.user_id = $1 OR p.is_public = true
ORDER BY p.updated_at DESC
`
type ListPlaylistsForUserRow struct {
ID pgtype.UUID
UserID pgtype.UUID
Name string
Description string
IsPublic bool
CoverPath *string
TrackCount int32
DurationSec int32
CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
Kind string
SystemVariant *string
SeedArtistID pgtype.UUID
OwnerUsername string
}
// Owner's playlists (any visibility) + other users' public playlists.
// Ordered by updated_at desc so newly-edited ones float to the top.
func (q *Queries) ListPlaylistsForUser(ctx context.Context, userID pgtype.UUID) ([]ListPlaylistsForUserRow, error) {
rows, err := q.db.Query(ctx, listPlaylistsForUser, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListPlaylistsForUserRow
for rows.Next() {
var i ListPlaylistsForUserRow
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.IsPublic,
&i.CoverPath,
&i.TrackCount,
&i.DurationSec,
&i.CreatedAt,
&i.UpdatedAt,
&i.Kind,
&i.SystemVariant,
&i.SeedArtistID,
&i.OwnerUsername,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const renumberPlaylistTracksAfter = `-- name: RenumberPlaylistTracksAfter :exec
UPDATE playlist_tracks
SET position = position - 1
WHERE playlist_id = $1 AND position > $2
`
type RenumberPlaylistTracksAfterParams struct {
PlaylistID pgtype.UUID
Position int32
}
// Used after DeletePlaylistTrack to close the gap.
func (q *Queries) RenumberPlaylistTracksAfter(ctx context.Context, arg RenumberPlaylistTracksAfterParams) error {
_, err := q.db.Exec(ctx, renumberPlaylistTracksAfter, arg.PlaylistID, arg.Position)
return err
}
const setPlaylistCover = `-- name: SetPlaylistCover :exec
UPDATE playlists SET cover_path = $2, updated_at = now() WHERE id = $1
`
type SetPlaylistCoverParams struct {
ID pgtype.UUID
CoverPath *string
}
func (q *Queries) SetPlaylistCover(ctx context.Context, arg SetPlaylistCoverParams) error {
_, err := q.db.Exec(ctx, setPlaylistCover, arg.ID, arg.CoverPath)
return err
}
const updatePlaylist = `-- name: UpdatePlaylist :one
UPDATE playlists
SET
name = CASE WHEN $1::boolean THEN $2::text ELSE name END,
description = CASE WHEN $3::boolean THEN $4::text ELSE description END,
is_public = CASE WHEN $5::boolean THEN $6::boolean ELSE is_public END,
updated_at = now()
WHERE id = $7
RETURNING id, user_id, name, description, is_public, cover_path, track_count, duration_sec, created_at, updated_at, kind, system_variant, seed_artist_id
`
type UpdatePlaylistParams struct {
UpdateName bool
Name string
UpdateDescription bool
Description string
UpdateIsPublic bool
IsPublic bool
ID pgtype.UUID
}
// Updates only the fields whose corresponding `updateX` flag is true.
// The flags let the service layer keep PATCH semantics (only-touch-what-the-caller-sent)
// without writing N variants.
func (q *Queries) UpdatePlaylist(ctx context.Context, arg UpdatePlaylistParams) (Playlist, error) {
row := q.db.QueryRow(ctx, updatePlaylist,
arg.UpdateName,
arg.Name,
arg.UpdateDescription,
arg.Description,
arg.UpdateIsPublic,
arg.IsPublic,
arg.ID,
)
var i Playlist
err := row.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.IsPublic,
&i.CoverPath,
&i.TrackCount,
&i.DurationSec,
&i.CreatedAt,
&i.UpdatedAt,
&i.Kind,
&i.SystemVariant,
&i.SeedArtistID,
)
return i, err
}
const updatePlaylistRollups = `-- name: UpdatePlaylistRollups :exec
UPDATE playlists
SET
track_count = (SELECT COUNT(*) FROM playlist_tracks pt WHERE pt.playlist_id = $1),
duration_sec = (SELECT COALESCE(SUM(pt.duration_sec), 0) FROM playlist_tracks pt WHERE pt.playlist_id = $1),
updated_at = now()
WHERE id = $1
`
// Set track_count + duration_sec from a fresh aggregate. Called after
// every mutation that touches playlist_tracks. Cheap; the table is small.
func (q *Queries) UpdatePlaylistRollups(ctx context.Context, playlistID pgtype.UUID) error {
_, err := q.db.Exec(ctx, updatePlaylistRollups, playlistID)
return err
}