feat: Implement Statistics API with 8 endpoints under /api/v1/statistics/

- Add statistics.sql with 8 SQL queries for play count statistics
- Generate repository code via sqlc
- Add backend/statistics.go with business logic
- Add server/statistics_handler.go with Echo handlers
- Register protected routes under /api/v1/statistics/ with token auth
- Endpoints: games/most-played, games/least-played, games/never-played,
  games/last-played, games/oldest-played, songs/most-played,
  songs/least-played, summary

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-06-01 19:40:22 +02:00
parent 06cbad708d
commit 4c2db11cc5
6 changed files with 1178 additions and 10 deletions
+148
View File
@@ -0,0 +1,148 @@
-- Most played games with their songs
-- name: GetMostPlayedGamesWithSongs :many
SELECT
g.id as game_id,
g.game_name,
g.times_played as game_played,
g.last_played as game_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
FROM game g
LEFT JOIN song s ON g.id = s.game_id
WHERE g.deleted IS NULL
GROUP BY g.id, g.game_name, g.times_played, g.last_played
ORDER BY g.times_played DESC, g.game_name
LIMIT $1;
-- Least played games with their songs
-- name: GetLeastPlayedGamesWithSongs :many
SELECT
g.id as game_id,
g.game_name,
g.times_played as game_played,
g.last_played as game_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
FROM game g
LEFT JOIN song s ON g.id = s.game_id
WHERE g.deleted IS NULL
GROUP BY g.id, g.game_name, g.times_played, g.last_played
ORDER BY g.times_played ASC, g.game_name
LIMIT $1;
-- Most played songs with their game info
-- name: GetMostPlayedSongsWithGame :many
SELECT
s.game_id as game_id,
g.game_name,
s.song_name,
s.path,
s.times_played,
s.file_name
FROM song s
JOIN game g ON s.game_id = g.id
WHERE g.deleted IS NULL
ORDER BY s.times_played DESC, s.song_name
LIMIT $1;
-- Least played songs with their game info
-- name: GetLeastPlayedSongsWithGame :many
SELECT
s.game_id as game_id,
g.game_name,
s.song_name,
s.path,
s.times_played,
s.file_name
FROM song s
JOIN game g ON s.game_id = g.id
WHERE g.deleted IS NULL
ORDER BY s.times_played ASC, s.song_name
LIMIT $1;
-- Games that have never been played (times_played = 0)
-- name: GetNeverPlayedGames :many
SELECT
g.id as game_id,
g.game_name,
g.times_played as game_played,
g.added,
json_agg(
json_build_object(
'song_name', s.song_name,
'path', s.path,
'times_played', s.times_played
)
) as songs
FROM game g
LEFT JOIN song s ON g.id = s.game_id
WHERE g.deleted IS NULL AND g.times_played = 0
GROUP BY g.id, g.game_name, g.times_played, g.added
ORDER BY g.game_name;
-- Last played games (most recently played)
-- name: GetLastPlayedGames :many
SELECT
g.id as game_id,
g.game_name,
g.times_played as game_played,
g.last_played as game_last_played,
json_agg(
json_build_object(
'song_name', s.song_name,
'path', s.path,
'times_played', s.times_played
)
) as songs
FROM game g
LEFT JOIN song s ON g.id = s.game_id
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
GROUP BY g.id, g.game_name, g.times_played, g.last_played
ORDER BY g.last_played DESC
LIMIT $1;
-- Oldest played games (least recently played, but has been played at least once)
-- name: GetOldestPlayedGames :many
SELECT
g.id as game_id,
g.game_name,
g.times_played as game_played,
g.last_played as game_last_played,
json_agg(
json_build_object(
'song_name', s.song_name,
'path', s.path,
'times_played', s.times_played
)
) as songs
FROM game g
LEFT JOIN song s ON g.id = s.game_id
WHERE g.deleted IS NULL AND g.last_played IS NOT NULL
GROUP BY g.id, g.game_name, g.times_played, g.last_played
ORDER BY g.last_played ASC
LIMIT $1;
-- Get statistics summary
-- name: GetStatisticsSummary :one
SELECT
COUNT(*) as total_games,
SUM(CASE WHEN times_played > 0 THEN 1 ELSE 0 END) as played_games,
SUM(CASE WHEN times_played = 0 THEN 1 ELSE 0 END) as never_played_games,
COALESCE(SUM(times_played), 0)::bigint as total_game_plays,
COALESCE(AVG(times_played), 0)::float as avg_game_plays,
COALESCE(MAX(times_played), 0)::bigint as max_game_plays,
COALESCE(MIN(times_played), 0)::bigint as min_game_plays
FROM game
WHERE deleted IS NULL;