a5e2abb8c4
Headline of S4. "Shuffle all" is always present (home app-bar shuffle icon); the pool degrades with reachability: - online → GET /api/library/shuffle?limit=N (new): N random library tracks server-side, per-user quarantine filtered (ListRandomTracksForUser, ORDER BY random()). - offline → ShuffleSource shuffles the whole local cache index (audio_cache_index ∩ cached_tracks, names from cached_artists/ albums) — a UNION over liked AND recently-played, since the two-bucket split is storage-only and never filters playback. Offline gating: refreshable (singleton) system playlists need the live build/shuffle endpoints, so their tile play is disabled when offlineProvider is true — Shuffle all is the offline path. User playlists still play from cache. playlist_card now reads offlineProvider in build → wrapped the direct-render widget test in ProviderScope (offlineProvider is smoke-safe from S1: tracked timers, no connectivity mount). S4b (offline Recently-played / Liked browsable surfaces over the cache index) next. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
120 lines
4.2 KiB
SQL
120 lines
4.2 KiB
SQL
-- name: UpsertTrack :one
|
|
-- file_path is the canonical identity for library scan; mbid is secondary.
|
|
INSERT INTO tracks (
|
|
title, album_id, artist_id, track_number, disc_number,
|
|
duration_ms, file_path, file_size, file_format, bitrate, mbid, genre
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
ON CONFLICT (file_path) DO UPDATE SET
|
|
title = EXCLUDED.title,
|
|
album_id = EXCLUDED.album_id,
|
|
artist_id = EXCLUDED.artist_id,
|
|
track_number = EXCLUDED.track_number,
|
|
disc_number = EXCLUDED.disc_number,
|
|
duration_ms = EXCLUDED.duration_ms,
|
|
file_size = EXCLUDED.file_size,
|
|
file_format = EXCLUDED.file_format,
|
|
bitrate = EXCLUDED.bitrate,
|
|
mbid = EXCLUDED.mbid,
|
|
genre = EXCLUDED.genre,
|
|
updated_at = now()
|
|
RETURNING *;
|
|
|
|
-- name: GetTrackByID :one
|
|
SELECT * FROM tracks WHERE id = $1;
|
|
|
|
-- name: GetTrackByPath :one
|
|
SELECT * FROM tracks WHERE file_path = $1;
|
|
|
|
-- name: ListTracksByAlbum :many
|
|
-- $1 = album_id, $2 = user_id. Pass pgtype.UUID{Valid: false} (NULL)
|
|
-- to skip the per-user quarantine filter; the NOT EXISTS clause on
|
|
-- a NULL user_id never matches a row, so every track passes through.
|
|
SELECT * FROM tracks
|
|
WHERE album_id = $1
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM lidarr_quarantine q
|
|
WHERE q.user_id = $2 AND q.track_id = tracks.id
|
|
)
|
|
ORDER BY disc_number NULLS LAST, track_number NULLS LAST;
|
|
|
|
-- name: CountTracksByAlbum :one
|
|
SELECT count(*) FROM tracks WHERE album_id = $1;
|
|
|
|
-- name: SearchTracks :many
|
|
-- $1 = title query, $2 = user_id (NULL to skip quarantine filter),
|
|
-- $3 = limit, $4 = offset.
|
|
SELECT * FROM tracks
|
|
WHERE title ILIKE '%' || $1::text || '%'
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM lidarr_quarantine q
|
|
WHERE q.user_id = $2 AND q.track_id = tracks.id
|
|
)
|
|
ORDER BY title
|
|
LIMIT $3 OFFSET $4;
|
|
|
|
-- name: CountTracksMatching :one
|
|
-- $1 = title query, $2 = user_id (NULL to skip quarantine filter).
|
|
SELECT COUNT(*) FROM tracks
|
|
WHERE title ILIKE '%' || $1::text || '%'
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM lidarr_quarantine q
|
|
WHERE q.user_id = $2 AND q.track_id = tracks.id
|
|
);
|
|
|
|
-- name: ListArtistTracksForUser :many
|
|
-- M6a: every track for the artist across their albums, with album_title
|
|
-- and artist_name joined. Honors per-user lidarr_quarantine. Used by
|
|
-- /api/artists/{id}/tracks for the artist-card play affordance, which
|
|
-- shuffles client-side. Ordering matches album/track natural order so
|
|
-- the shuffle has a deterministic input.
|
|
SELECT sqlc.embed(t),
|
|
albums.title AS album_title,
|
|
artists.name AS artist_name
|
|
FROM tracks t
|
|
JOIN albums ON albums.id = t.album_id
|
|
JOIN artists ON artists.id = t.artist_id
|
|
WHERE t.artist_id = $1
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM lidarr_quarantine q
|
|
WHERE q.user_id = $2 AND q.track_id = t.id
|
|
)
|
|
ORDER BY albums.release_date NULLS LAST, albums.sort_title,
|
|
t.disc_number NULLS FIRST, t.track_number NULLS FIRST, t.id;
|
|
|
|
-- name: ListRandomTracksForUser :many
|
|
-- #427 S4: backing query for GET /api/library/shuffle — the online
|
|
-- source for "Shuffle all". N random tracks across the whole
|
|
-- library, per-user-quarantine filtered. $1 user_id, $2 limit.
|
|
SELECT sqlc.embed(t),
|
|
albums.title AS album_title,
|
|
artists.name AS artist_name
|
|
FROM tracks t
|
|
JOIN albums ON albums.id = t.album_id
|
|
JOIN artists ON artists.id = t.artist_id
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM lidarr_quarantine q
|
|
WHERE q.user_id = $1 AND q.track_id = t.id
|
|
)
|
|
ORDER BY random()
|
|
LIMIT $2;
|
|
|
|
-- name: CountTracksByArtist :one
|
|
-- Used by request-progress reporting to count tracks ingested under a
|
|
-- matched artist (sum across all the artist's albums) while a request
|
|
-- is still in flight.
|
|
SELECT COUNT(*) FROM tracks WHERE artist_id = $1;
|
|
|
|
-- name: DeleteTrack :one
|
|
-- M7 #372: hard delete with FK cascade. The CASCADE on track_id from
|
|
-- play_events / general_likes_tracks / lidarr_quarantine /
|
|
-- lidarr_quarantine_actions handles their cleanup. RETURNING gives us
|
|
-- album_id + artist_id for the album-empty / artist-empty cascade
|
|
-- checks the service does next.
|
|
DELETE FROM tracks WHERE id = $1
|
|
RETURNING id, album_id, artist_id, file_path, mbid;
|
|
|
|
-- name: GetTracksByIDs :many
|
|
-- Batched lookup used by /api/library/sync to hydrate upsert payloads
|
|
-- (#357). Mirror of GetArtistsByIDs.
|
|
SELECT * FROM tracks WHERE id = ANY($1::uuid[]);
|