refactor(server/sql): unify *ForUser track queries via nullable user_id (A1)
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
|
"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
|
||||||
"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
|
"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
|
||||||
@@ -53,14 +54,13 @@ func (h *handlers) handleGetAlbum(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeErr(w, apierror.InternalMsg("lookup failed", err))
|
writeErr(w, apierror.InternalMsg("lookup failed", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var tracks []dbq.Track
|
var userID pgtype.UUID
|
||||||
if user, ok := auth.UserFromContext(r.Context()); ok {
|
if user, ok := auth.UserFromContext(r.Context()); ok {
|
||||||
tracks, err = q.ListTracksByAlbumForUser(r.Context(), dbq.ListTracksByAlbumForUserParams{
|
userID = user.ID
|
||||||
AlbumID: album.ID, UserID: user.ID,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
tracks, err = q.ListTracksByAlbum(r.Context(), album.ID)
|
|
||||||
}
|
}
|
||||||
|
tracks, err := q.ListTracksByAlbum(r.Context(), dbq.ListTracksByAlbumParams{
|
||||||
|
AlbumID: album.ID, UserID: userID,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("api: list tracks failed", "err", err)
|
h.logger.Error("api: list tracks failed", "err", err)
|
||||||
writeErr(w, apierror.InternalMsg("lookup failed", err))
|
writeErr(w, apierror.InternalMsg("lookup failed", err))
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func resolveAlbumCoverPath(ctx context.Context, q *dbq.Queries, album dbq.Album)
|
|||||||
return *album.CoverArtPath
|
return *album.CoverArtPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tracks, err := q.ListTracksByAlbum(ctx, album.ID)
|
tracks, err := q.ListTracksByAlbum(ctx, dbq.ListTracksByAlbumParams{AlbumID: album.ID})
|
||||||
if err != nil || len(tracks) == 0 {
|
if err != nil || len(tracks) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-16
@@ -76,29 +76,23 @@ func (h *handlers) handleSearch(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var tracks []dbq.Track
|
// userID is NULL for unauthenticated callers; the unified queries
|
||||||
|
// skip the quarantine filter when user_id IS NULL.
|
||||||
|
var userID pgtype.UUID
|
||||||
if user, ok := auth.UserFromContext(r.Context()); ok {
|
if user, ok := auth.UserFromContext(r.Context()); ok {
|
||||||
tracks, err = dbQ.SearchTracksForUser(r.Context(), dbq.SearchTracksForUserParams{
|
userID = user.ID
|
||||||
Column1: needle, UserID: user.ID, Limit: int32(limit), Offset: int32(offset),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
tracks, err = dbQ.SearchTracks(r.Context(), dbq.SearchTracksParams{
|
|
||||||
Column1: needle, Limit: int32(limit), Offset: int32(offset),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
tracks, err := dbQ.SearchTracks(r.Context(), dbq.SearchTracksParams{
|
||||||
|
Column1: needle, UserID: userID, Limit: int32(limit), Offset: int32(offset),
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("api: search tracks failed", "err", err)
|
h.logger.Error("api: search tracks failed", "err", err)
|
||||||
writeErr(w, apierror.InternalMsg("search failed", err))
|
writeErr(w, apierror.InternalMsg("search failed", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var trackTotal int64
|
trackTotal, err := dbQ.CountTracksMatching(r.Context(), dbq.CountTracksMatchingParams{
|
||||||
if user, ok := auth.UserFromContext(r.Context()); ok {
|
Column1: q, UserID: userID,
|
||||||
trackTotal, err = dbQ.CountTracksMatchingForUser(r.Context(), dbq.CountTracksMatchingForUserParams{
|
})
|
||||||
Column1: q, UserID: user.ID,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
trackTotal, err = dbQ.CountTracksMatching(r.Context(), q)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("api: count tracks matching failed", "err", err)
|
h.logger.Error("api: count tracks matching failed", "err", err)
|
||||||
writeErr(w, apierror.InternalMsg("search failed", err))
|
writeErr(w, apierror.InternalMsg("search failed", err))
|
||||||
|
|||||||
+17
-115
@@ -37,17 +37,6 @@ func (q *Queries) CountTracksByArtist(ctx context.Context, artistID pgtype.UUID)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const countTracksMatching = `-- name: CountTracksMatching :one
|
const countTracksMatching = `-- name: CountTracksMatching :one
|
||||||
SELECT COUNT(*) FROM tracks WHERE title ILIKE '%' || $1::text || '%'
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) CountTracksMatching(ctx context.Context, dollar_1 string) (int64, error) {
|
|
||||||
row := q.db.QueryRow(ctx, countTracksMatching, dollar_1)
|
|
||||||
var count int64
|
|
||||||
err := row.Scan(&count)
|
|
||||||
return count, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const countTracksMatchingForUser = `-- name: CountTracksMatchingForUser :one
|
|
||||||
SELECT COUNT(*) FROM tracks
|
SELECT COUNT(*) FROM tracks
|
||||||
WHERE title ILIKE '%' || $1::text || '%'
|
WHERE title ILIKE '%' || $1::text || '%'
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
@@ -56,14 +45,14 @@ WHERE title ILIKE '%' || $1::text || '%'
|
|||||||
)
|
)
|
||||||
`
|
`
|
||||||
|
|
||||||
type CountTracksMatchingForUserParams struct {
|
type CountTracksMatchingParams struct {
|
||||||
Column1 string
|
Column1 string
|
||||||
UserID pgtype.UUID
|
UserID pgtype.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
// $1 = title query, $2 = user_id.
|
// $1 = title query, $2 = user_id (NULL to skip quarantine filter).
|
||||||
func (q *Queries) CountTracksMatchingForUser(ctx context.Context, arg CountTracksMatchingForUserParams) (int64, error) {
|
func (q *Queries) CountTracksMatching(ctx context.Context, arg CountTracksMatchingParams) (int64, error) {
|
||||||
row := q.db.QueryRow(ctx, countTracksMatchingForUser, arg.Column1, arg.UserID)
|
row := q.db.QueryRow(ctx, countTracksMatching, arg.Column1, arg.UserID)
|
||||||
var count int64
|
var count int64
|
||||||
err := row.Scan(&count)
|
err := row.Scan(&count)
|
||||||
return count, err
|
return count, err
|
||||||
@@ -225,46 +214,6 @@ func (q *Queries) ListArtistTracksForUser(ctx context.Context, arg ListArtistTra
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listTracksByAlbum = `-- name: ListTracksByAlbum :many
|
const listTracksByAlbum = `-- name: ListTracksByAlbum :many
|
||||||
SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at FROM tracks WHERE album_id = $1 ORDER BY disc_number NULLS LAST, track_number NULLS LAST
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ListTracksByAlbum(ctx context.Context, albumID pgtype.UUID) ([]Track, error) {
|
|
||||||
rows, err := q.db.Query(ctx, listTracksByAlbum, albumID)
|
|
||||||
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 listTracksByAlbumForUser = `-- name: ListTracksByAlbumForUser :many
|
|
||||||
SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at FROM tracks
|
SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at FROM tracks
|
||||||
WHERE album_id = $1
|
WHERE album_id = $1
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
@@ -274,15 +223,16 @@ WHERE album_id = $1
|
|||||||
ORDER BY disc_number NULLS LAST, track_number NULLS LAST
|
ORDER BY disc_number NULLS LAST, track_number NULLS LAST
|
||||||
`
|
`
|
||||||
|
|
||||||
type ListTracksByAlbumForUserParams struct {
|
type ListTracksByAlbumParams struct {
|
||||||
AlbumID pgtype.UUID
|
AlbumID pgtype.UUID
|
||||||
UserID pgtype.UUID
|
UserID pgtype.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same as ListTracksByAlbum but excludes tracks the user has quarantined.
|
// $1 = album_id, $2 = user_id. Pass pgtype.UUID{Valid: false} (NULL)
|
||||||
// $1 = album_id, $2 = user_id.
|
// to skip the per-user quarantine filter; the NOT EXISTS clause on
|
||||||
func (q *Queries) ListTracksByAlbumForUser(ctx context.Context, arg ListTracksByAlbumForUserParams) ([]Track, error) {
|
// a NULL user_id never matches a row, so every track passes through.
|
||||||
rows, err := q.db.Query(ctx, listTracksByAlbumForUser, arg.AlbumID, arg.UserID)
|
func (q *Queries) ListTracksByAlbum(ctx context.Context, arg ListTracksByAlbumParams) ([]Track, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listTracksByAlbum, arg.AlbumID, arg.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -319,56 +269,7 @@ func (q *Queries) ListTracksByAlbumForUser(ctx context.Context, arg ListTracksBy
|
|||||||
|
|
||||||
const searchTracks = `-- name: SearchTracks :many
|
const searchTracks = `-- name: SearchTracks :many
|
||||||
SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at FROM tracks
|
SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at FROM tracks
|
||||||
WHERE title ILIKE '%' || $1 || '%'
|
WHERE title ILIKE '%' || $1::text || '%'
|
||||||
ORDER BY title
|
|
||||||
LIMIT $2 OFFSET $3
|
|
||||||
`
|
|
||||||
|
|
||||||
type SearchTracksParams struct {
|
|
||||||
Column1 *string
|
|
||||||
Limit int32
|
|
||||||
Offset int32
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) SearchTracks(ctx context.Context, arg SearchTracksParams) ([]Track, error) {
|
|
||||||
rows, err := q.db.Query(ctx, searchTracks, arg.Column1, arg.Limit, arg.Offset)
|
|
||||||
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 searchTracksForUser = `-- name: SearchTracksForUser :many
|
|
||||||
SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at FROM tracks
|
|
||||||
WHERE title ILIKE '%' || $1 || '%'
|
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM lidarr_quarantine q
|
SELECT 1 FROM lidarr_quarantine q
|
||||||
WHERE q.user_id = $2 AND q.track_id = tracks.id
|
WHERE q.user_id = $2 AND q.track_id = tracks.id
|
||||||
@@ -377,16 +278,17 @@ ORDER BY title
|
|||||||
LIMIT $3 OFFSET $4
|
LIMIT $3 OFFSET $4
|
||||||
`
|
`
|
||||||
|
|
||||||
type SearchTracksForUserParams struct {
|
type SearchTracksParams struct {
|
||||||
Column1 *string
|
Column1 string
|
||||||
UserID pgtype.UUID
|
UserID pgtype.UUID
|
||||||
Limit int32
|
Limit int32
|
||||||
Offset int32
|
Offset int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// $1 = title query, $2 = user_id, $3 = limit, $4 = offset.
|
// $1 = title query, $2 = user_id (NULL to skip quarantine filter),
|
||||||
func (q *Queries) SearchTracksForUser(ctx context.Context, arg SearchTracksForUserParams) ([]Track, error) {
|
// $3 = limit, $4 = offset.
|
||||||
rows, err := q.db.Query(ctx, searchTracksForUser,
|
func (q *Queries) SearchTracks(ctx context.Context, arg SearchTracksParams) ([]Track, error) {
|
||||||
|
rows, err := q.db.Query(ctx, searchTracks,
|
||||||
arg.Column1,
|
arg.Column1,
|
||||||
arg.UserID,
|
arg.UserID,
|
||||||
arg.Limit,
|
arg.Limit,
|
||||||
|
|||||||
@@ -26,23 +26,9 @@ SELECT * FROM tracks WHERE id = $1;
|
|||||||
SELECT * FROM tracks WHERE file_path = $1;
|
SELECT * FROM tracks WHERE file_path = $1;
|
||||||
|
|
||||||
-- name: ListTracksByAlbum :many
|
-- name: ListTracksByAlbum :many
|
||||||
SELECT * FROM tracks WHERE album_id = $1 ORDER BY disc_number NULLS LAST, track_number NULLS LAST;
|
-- $1 = album_id, $2 = user_id. Pass pgtype.UUID{Valid: false} (NULL)
|
||||||
|
-- to skip the per-user quarantine filter; the NOT EXISTS clause on
|
||||||
-- name: CountTracksByAlbum :one
|
-- a NULL user_id never matches a row, so every track passes through.
|
||||||
SELECT count(*) FROM tracks WHERE album_id = $1;
|
|
||||||
|
|
||||||
-- name: SearchTracks :many
|
|
||||||
SELECT * FROM tracks
|
|
||||||
WHERE title ILIKE '%' || $1 || '%'
|
|
||||||
ORDER BY title
|
|
||||||
LIMIT $2 OFFSET $3;
|
|
||||||
|
|
||||||
-- name: CountTracksMatching :one
|
|
||||||
SELECT COUNT(*) FROM tracks WHERE title ILIKE '%' || $1::text || '%';
|
|
||||||
|
|
||||||
-- name: ListTracksByAlbumForUser :many
|
|
||||||
-- Same as ListTracksByAlbum but excludes tracks the user has quarantined.
|
|
||||||
-- $1 = album_id, $2 = user_id.
|
|
||||||
SELECT * FROM tracks
|
SELECT * FROM tracks
|
||||||
WHERE album_id = $1
|
WHERE album_id = $1
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
@@ -51,10 +37,14 @@ WHERE album_id = $1
|
|||||||
)
|
)
|
||||||
ORDER BY disc_number NULLS LAST, track_number NULLS LAST;
|
ORDER BY disc_number NULLS LAST, track_number NULLS LAST;
|
||||||
|
|
||||||
-- name: SearchTracksForUser :many
|
-- name: CountTracksByAlbum :one
|
||||||
-- $1 = title query, $2 = user_id, $3 = limit, $4 = offset.
|
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
|
SELECT * FROM tracks
|
||||||
WHERE title ILIKE '%' || $1 || '%'
|
WHERE title ILIKE '%' || $1::text || '%'
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM lidarr_quarantine q
|
SELECT 1 FROM lidarr_quarantine q
|
||||||
WHERE q.user_id = $2 AND q.track_id = tracks.id
|
WHERE q.user_id = $2 AND q.track_id = tracks.id
|
||||||
@@ -62,8 +52,8 @@ WHERE title ILIKE '%' || $1 || '%'
|
|||||||
ORDER BY title
|
ORDER BY title
|
||||||
LIMIT $3 OFFSET $4;
|
LIMIT $3 OFFSET $4;
|
||||||
|
|
||||||
-- name: CountTracksMatchingForUser :one
|
-- name: CountTracksMatching :one
|
||||||
-- $1 = title query, $2 = user_id.
|
-- $1 = title query, $2 = user_id (NULL to skip quarantine filter).
|
||||||
SELECT COUNT(*) FROM tracks
|
SELECT COUNT(*) FROM tracks
|
||||||
WHERE title ILIKE '%' || $1::text || '%'
|
WHERE title ILIKE '%' || $1::text || '%'
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ func (b *browseHandlers) getAlbum(w http.ResponseWriter, r *http.Request) {
|
|||||||
WriteFail(w, r, ErrGeneric, "Failed to load album artist")
|
WriteFail(w, r, ErrGeneric, "Failed to load album artist")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tracks, err := q.ListTracksByAlbum(r.Context(), id)
|
tracks, err := q.ListTracksByAlbum(r.Context(), dbq.ListTracksByAlbumParams{AlbumID: id})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WriteFail(w, r, ErrGeneric, "Failed to load tracks")
|
WriteFail(w, r, ErrGeneric, "Failed to load tracks")
|
||||||
return
|
return
|
||||||
@@ -377,7 +377,7 @@ func (b *browseHandlers) search3(w http.ResponseWriter, r *http.Request) {
|
|||||||
if songCount > 0 {
|
if songCount > 0 {
|
||||||
tracks, err := q.SearchTracks(r.Context(), dbq.SearchTracksParams{
|
tracks, err := q.SearchTracks(r.Context(), dbq.SearchTracksParams{
|
||||||
Column1: needle, Limit: int32(songCount), Offset: int32(songOffset),
|
Column1: needle, Limit: int32(songCount), Offset: int32(songOffset),
|
||||||
})
|
}) // UserID left zero (NULL) — Subsonic has no per-user quarantine context
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WriteFail(w, r, ErrGeneric, "Song search failed")
|
WriteFail(w, r, ErrGeneric, "Song search failed")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ func resolveAlbumCoverPath(ctx context.Context, q *dbq.Queries, album dbq.Album)
|
|||||||
return *album.CoverArtPath
|
return *album.CoverArtPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tracks, err := q.ListTracksByAlbum(ctx, album.ID)
|
tracks, err := q.ListTracksByAlbum(ctx, dbq.ListTracksByAlbumParams{AlbumID: album.ID})
|
||||||
if err != nil || len(tracks) == 0 {
|
if err != nil || len(tracks) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user