Prefer UUIDs in the API (Phase 3 of the UUID migration)
CI / docker (push) Successful in 46s
CI / integration (push) Successful in 23s
CI / test (push) Successful in 30s
CI / publish (push) Skipped

- New /api/v1 endpoints: GET /soundtracks, /soundtracks/{uuid},
  /songs/{uuid} and /songs/{uuid}/file (streams the song, Range support,
  locked during sync)
- UUIDs added to every response with soundtracks or songs: queue info and
  list (SoundtrackUuid, SongUuid) and statistics (soundtrack_uuid,
  song_uuid). Existing fields stay for backward compatibility
- Play counts and broken-song removal use song UUIDs instead of
  soundtrack ID + song name, so songs with the same name are counted
  separately
- Statistics return an empty song list for soundtracks without songs
  instead of a single empty entry
- The legacy .id file cleanup stays until Phase 4 (#31)

Closes #29
This commit is contained in:
Sansan committed 2026-10-06 15:54:41 +02:00
1 parent 8c974a49cc
commit 2c3e97190d
25 files changed
+1521 -117

No files matched your search

+103 -49
View File
@@ -8,21 +8,29 @@ package repository
import (
"context"
"time"
"github.com/google/uuid"
)
const getLastPlayedSoundtracks = `-- name: GetLastPlayedSoundtracks :many
SELECT
g.id as soundtrack_id,
g.uuid as soundtrack_uuid,
g.soundtrack_name,
g.times_played as soundtrack_played,
g.last_played as soundtrack_last_played,
json_agg(
json_build_object(
'song_name', s.song_name,
'path', s.path,
'times_played', s.times_played
)
) as songs
COALESCE(
json_agg(
json_build_object(
'song_uuid', s.uuid,
'soundtrack_uuid', g.uuid,
'song_name', s.song_name,
'path', s.path,
'times_played', s.times_played
)
) FILTER (WHERE s.uuid IS NOT NULL),
'[]'
)::json as songs
FROM soundtrack g
LEFT JOIN song s ON g.id = s.soundtrack_id
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
@@ -33,6 +41,7 @@ LIMIT $1
type GetLastPlayedSoundtracksRow struct {
SoundtrackID int32 `json:"soundtrack_id"`
SoundtrackUuid uuid.UUID `json:"soundtrack_uuid"`
SoundtrackName string `json:"soundtrack_name"`
SoundtrackPlayed int32 `json:"soundtrack_played"`
SoundtrackLastPlayed *time.Time `json:"soundtrack_last_played"`
@@ -51,6 +60,7 @@ func (q *Queries) GetLastPlayedSoundtracks(ctx context.Context, limit int32) ([]
var i GetLastPlayedSoundtracksRow
if err := rows.Scan(
&i.SoundtrackID,
&i.SoundtrackUuid,
&i.SoundtrackName,
&i.SoundtrackPlayed,
&i.SoundtrackLastPlayed,
@@ -69,6 +79,8 @@ func (q *Queries) GetLastPlayedSoundtracks(ctx context.Context, limit int32) ([]
const getLeastPlayedSongsWithSoundtrack = `-- name: GetLeastPlayedSongsWithSoundtrack :many
SELECT
s.soundtrack_id as soundtrack_id,
g.uuid as soundtrack_uuid,
s.uuid as song_uuid,
g.soundtrack_name,
s.song_name,
s.path,
@@ -82,12 +94,14 @@ LIMIT $1
`
type GetLeastPlayedSongsWithSoundtrackRow struct {
SoundtrackID int32 `json:"soundtrack_id"`
SoundtrackName string `json:"soundtrack_name"`
SongName string `json:"song_name"`
Path string `json:"path"`
TimesPlayed int32 `json:"times_played"`
FileName *string `json:"file_name"`
SoundtrackID int32 `json:"soundtrack_id"`
SoundtrackUuid uuid.UUID `json:"soundtrack_uuid"`
SongUuid uuid.UUID `json:"song_uuid"`
SoundtrackName string `json:"soundtrack_name"`
SongName string `json:"song_name"`
Path string `json:"path"`
TimesPlayed int32 `json:"times_played"`
FileName *string `json:"file_name"`
}
// Least played songs with their soundtrack info
@@ -102,6 +116,8 @@ func (q *Queries) GetLeastPlayedSongsWithSoundtrack(ctx context.Context, limit i
var i GetLeastPlayedSongsWithSoundtrackRow
if err := rows.Scan(
&i.SoundtrackID,
&i.SoundtrackUuid,
&i.SongUuid,
&i.SoundtrackName,
&i.SongName,
&i.Path,
@@ -121,17 +137,23 @@ func (q *Queries) GetLeastPlayedSongsWithSoundtrack(ctx context.Context, limit i
const getLeastPlayedSoundtracksWithSongs = `-- name: GetLeastPlayedSoundtracksWithSongs :many
SELECT
g.id as soundtrack_id,
g.uuid as soundtrack_uuid,
g.soundtrack_name,
g.times_played as soundtrack_played,
g.last_played as soundtrack_last_played,
json_agg(
json_build_object(
'song_name', s.song_name,
'path', s.path,
'times_played', s.times_played,
'file_name', s.file_name
)
) as songs
COALESCE(
json_agg(
json_build_object(
'song_uuid', s.uuid,
'soundtrack_uuid', g.uuid,
'song_name', s.song_name,
'path', s.path,
'times_played', s.times_played,
'file_name', s.file_name
)
) FILTER (WHERE s.uuid IS NOT NULL),
'[]'
)::json as songs
FROM soundtrack g
LEFT JOIN song s ON g.id = s.soundtrack_id
WHERE g.deleted IS NULL
@@ -142,6 +164,7 @@ LIMIT $1
type GetLeastPlayedSoundtracksWithSongsRow struct {
SoundtrackID int32 `json:"soundtrack_id"`
SoundtrackUuid uuid.UUID `json:"soundtrack_uuid"`
SoundtrackName string `json:"soundtrack_name"`
SoundtrackPlayed int32 `json:"soundtrack_played"`
SoundtrackLastPlayed *time.Time `json:"soundtrack_last_played"`
@@ -160,6 +183,7 @@ func (q *Queries) GetLeastPlayedSoundtracksWithSongs(ctx context.Context, limit
var i GetLeastPlayedSoundtracksWithSongsRow
if err := rows.Scan(
&i.SoundtrackID,
&i.SoundtrackUuid,
&i.SoundtrackName,
&i.SoundtrackPlayed,
&i.SoundtrackLastPlayed,
@@ -178,6 +202,8 @@ func (q *Queries) GetLeastPlayedSoundtracksWithSongs(ctx context.Context, limit
const getMostPlayedSongsWithSoundtrack = `-- name: GetMostPlayedSongsWithSoundtrack :many
SELECT
s.soundtrack_id as soundtrack_id,
g.uuid as soundtrack_uuid,
s.uuid as song_uuid,
g.soundtrack_name,
s.song_name,
s.path,
@@ -191,12 +217,14 @@ LIMIT $1
`
type GetMostPlayedSongsWithSoundtrackRow struct {
SoundtrackID int32 `json:"soundtrack_id"`
SoundtrackName string `json:"soundtrack_name"`
SongName string `json:"song_name"`
Path string `json:"path"`
TimesPlayed int32 `json:"times_played"`
FileName *string `json:"file_name"`
SoundtrackID int32 `json:"soundtrack_id"`
SoundtrackUuid uuid.UUID `json:"soundtrack_uuid"`
SongUuid uuid.UUID `json:"song_uuid"`
SoundtrackName string `json:"soundtrack_name"`
SongName string `json:"song_name"`
Path string `json:"path"`
TimesPlayed int32 `json:"times_played"`
FileName *string `json:"file_name"`
}
// Most played songs with their soundtrack info
@@ -211,6 +239,8 @@ func (q *Queries) GetMostPlayedSongsWithSoundtrack(ctx context.Context, limit in
var i GetMostPlayedSongsWithSoundtrackRow
if err := rows.Scan(
&i.SoundtrackID,
&i.SoundtrackUuid,
&i.SongUuid,
&i.SoundtrackName,
&i.SongName,
&i.Path,
@@ -230,17 +260,23 @@ func (q *Queries) GetMostPlayedSongsWithSoundtrack(ctx context.Context, limit in
const getMostPlayedSoundtracksWithSongs = `-- name: GetMostPlayedSoundtracksWithSongs :many
SELECT
g.id as soundtrack_id,
g.uuid as soundtrack_uuid,
g.soundtrack_name,
g.times_played as soundtrack_played,
g.last_played as soundtrack_last_played,
json_agg(
json_build_object(
'song_name', s.song_name,
'path', s.path,
'times_played', s.times_played,
'file_name', s.file_name
)
) as songs
COALESCE(
json_agg(
json_build_object(
'song_uuid', s.uuid,
'soundtrack_uuid', g.uuid,
'song_name', s.song_name,
'path', s.path,
'times_played', s.times_played,
'file_name', s.file_name
)
) FILTER (WHERE s.uuid IS NOT NULL),
'[]'
)::json as songs
FROM soundtrack g
LEFT JOIN song s ON g.id = s.soundtrack_id
WHERE g.deleted IS NULL
@@ -251,6 +287,7 @@ LIMIT $1
type GetMostPlayedSoundtracksWithSongsRow struct {
SoundtrackID int32 `json:"soundtrack_id"`
SoundtrackUuid uuid.UUID `json:"soundtrack_uuid"`
SoundtrackName string `json:"soundtrack_name"`
SoundtrackPlayed int32 `json:"soundtrack_played"`
SoundtrackLastPlayed *time.Time `json:"soundtrack_last_played"`
@@ -269,6 +306,7 @@ func (q *Queries) GetMostPlayedSoundtracksWithSongs(ctx context.Context, limit i
var i GetMostPlayedSoundtracksWithSongsRow
if err := rows.Scan(
&i.SoundtrackID,
&i.SoundtrackUuid,
&i.SoundtrackName,
&i.SoundtrackPlayed,
&i.SoundtrackLastPlayed,
@@ -287,16 +325,22 @@ func (q *Queries) GetMostPlayedSoundtracksWithSongs(ctx context.Context, limit i
const getNeverPlayedSoundtracks = `-- name: GetNeverPlayedSoundtracks :many
SELECT
g.id as soundtrack_id,
g.uuid as soundtrack_uuid,
g.soundtrack_name,
g.times_played as soundtrack_played,
g.added,
json_agg(
json_build_object(
'song_name', s.song_name,
'path', s.path,
'times_played', s.times_played
)
) as songs
COALESCE(
json_agg(
json_build_object(
'song_uuid', s.uuid,
'soundtrack_uuid', g.uuid,
'song_name', s.song_name,
'path', s.path,
'times_played', s.times_played
)
) FILTER (WHERE s.uuid IS NOT NULL),
'[]'
)::json as songs
FROM soundtrack g
LEFT JOIN song s ON g.id = s.soundtrack_id
WHERE g.deleted IS NULL AND g.times_played = 0
@@ -306,6 +350,7 @@ ORDER BY g.soundtrack_name
type GetNeverPlayedSoundtracksRow struct {
SoundtrackID int32 `json:"soundtrack_id"`
SoundtrackUuid uuid.UUID `json:"soundtrack_uuid"`
SoundtrackName string `json:"soundtrack_name"`
SoundtrackPlayed int32 `json:"soundtrack_played"`
Added time.Time `json:"added"`
@@ -324,6 +369,7 @@ func (q *Queries) GetNeverPlayedSoundtracks(ctx context.Context) ([]GetNeverPlay
var i GetNeverPlayedSoundtracksRow
if err := rows.Scan(
&i.SoundtrackID,
&i.SoundtrackUuid,
&i.SoundtrackName,
&i.SoundtrackPlayed,
&i.Added,
@@ -342,16 +388,22 @@ func (q *Queries) GetNeverPlayedSoundtracks(ctx context.Context) ([]GetNeverPlay
const getOldestPlayedSoundtracks = `-- name: GetOldestPlayedSoundtracks :many
SELECT
g.id as soundtrack_id,
g.uuid as soundtrack_uuid,
g.soundtrack_name,
g.times_played as soundtrack_played,
g.last_played as soundtrack_last_played,
json_agg(
json_build_object(
'song_name', s.song_name,
'path', s.path,
'times_played', s.times_played
)
) as songs
COALESCE(
json_agg(
json_build_object(
'song_uuid', s.uuid,
'soundtrack_uuid', g.uuid,
'song_name', s.song_name,
'path', s.path,
'times_played', s.times_played
)
) FILTER (WHERE s.uuid IS NOT NULL),
'[]'
)::json as songs
FROM soundtrack g
LEFT JOIN song s ON g.id = s.soundtrack_id
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
@@ -362,6 +414,7 @@ LIMIT $1
type GetOldestPlayedSoundtracksRow struct {
SoundtrackID int32 `json:"soundtrack_id"`
SoundtrackUuid uuid.UUID `json:"soundtrack_uuid"`
SoundtrackName string `json:"soundtrack_name"`
SoundtrackPlayed int32 `json:"soundtrack_played"`
SoundtrackLastPlayed *time.Time `json:"soundtrack_last_played"`
@@ -380,6 +433,7 @@ func (q *Queries) GetOldestPlayedSoundtracks(ctx context.Context, limit int32) (
var i GetOldestPlayedSoundtracksRow
if err := rows.Scan(
&i.SoundtrackID,
&i.SoundtrackUuid,
&i.SoundtrackName,
&i.SoundtrackPlayed,
&i.SoundtrackLastPlayed,