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:
@@ -0,0 +1,435 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// source: statistics.sql
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
const getLastPlayedGames = `-- 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
|
||||
`
|
||||
|
||||
type GetLastPlayedGamesRow struct {
|
||||
GameID int32 `json:"game_id"`
|
||||
GameName string `json:"game_name"`
|
||||
GamePlayed int32 `json:"game_played"`
|
||||
GameLastPlayed *time.Time `json:"game_last_played"`
|
||||
Songs []byte `json:"songs"`
|
||||
}
|
||||
|
||||
// Last played games (most recently played)
|
||||
func (q *Queries) GetLastPlayedGames(ctx context.Context, limit int32) ([]GetLastPlayedGamesRow, error) {
|
||||
rows, err := q.db.Query(ctx, getLastPlayedGames, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetLastPlayedGamesRow
|
||||
for rows.Next() {
|
||||
var i GetLastPlayedGamesRow
|
||||
if err := rows.Scan(
|
||||
&i.GameID,
|
||||
&i.GameName,
|
||||
&i.GamePlayed,
|
||||
&i.GameLastPlayed,
|
||||
&i.Songs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getLeastPlayedGamesWithSongs = `-- 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
|
||||
`
|
||||
|
||||
type GetLeastPlayedGamesWithSongsRow struct {
|
||||
GameID int32 `json:"game_id"`
|
||||
GameName string `json:"game_name"`
|
||||
GamePlayed int32 `json:"game_played"`
|
||||
GameLastPlayed *time.Time `json:"game_last_played"`
|
||||
Songs []byte `json:"songs"`
|
||||
}
|
||||
|
||||
// Least played games with their songs
|
||||
func (q *Queries) GetLeastPlayedGamesWithSongs(ctx context.Context, limit int32) ([]GetLeastPlayedGamesWithSongsRow, error) {
|
||||
rows, err := q.db.Query(ctx, getLeastPlayedGamesWithSongs, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetLeastPlayedGamesWithSongsRow
|
||||
for rows.Next() {
|
||||
var i GetLeastPlayedGamesWithSongsRow
|
||||
if err := rows.Scan(
|
||||
&i.GameID,
|
||||
&i.GameName,
|
||||
&i.GamePlayed,
|
||||
&i.GameLastPlayed,
|
||||
&i.Songs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getLeastPlayedSongsWithGame = `-- 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
|
||||
`
|
||||
|
||||
type GetLeastPlayedSongsWithGameRow struct {
|
||||
GameID int32 `json:"game_id"`
|
||||
GameName string `json:"game_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 game info
|
||||
func (q *Queries) GetLeastPlayedSongsWithGame(ctx context.Context, limit int32) ([]GetLeastPlayedSongsWithGameRow, error) {
|
||||
rows, err := q.db.Query(ctx, getLeastPlayedSongsWithGame, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetLeastPlayedSongsWithGameRow
|
||||
for rows.Next() {
|
||||
var i GetLeastPlayedSongsWithGameRow
|
||||
if err := rows.Scan(
|
||||
&i.GameID,
|
||||
&i.GameName,
|
||||
&i.SongName,
|
||||
&i.Path,
|
||||
&i.TimesPlayed,
|
||||
&i.FileName,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getMostPlayedGamesWithSongs = `-- 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
|
||||
`
|
||||
|
||||
type GetMostPlayedGamesWithSongsRow struct {
|
||||
GameID int32 `json:"game_id"`
|
||||
GameName string `json:"game_name"`
|
||||
GamePlayed int32 `json:"game_played"`
|
||||
GameLastPlayed *time.Time `json:"game_last_played"`
|
||||
Songs []byte `json:"songs"`
|
||||
}
|
||||
|
||||
// Most played games with their songs
|
||||
func (q *Queries) GetMostPlayedGamesWithSongs(ctx context.Context, limit int32) ([]GetMostPlayedGamesWithSongsRow, error) {
|
||||
rows, err := q.db.Query(ctx, getMostPlayedGamesWithSongs, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetMostPlayedGamesWithSongsRow
|
||||
for rows.Next() {
|
||||
var i GetMostPlayedGamesWithSongsRow
|
||||
if err := rows.Scan(
|
||||
&i.GameID,
|
||||
&i.GameName,
|
||||
&i.GamePlayed,
|
||||
&i.GameLastPlayed,
|
||||
&i.Songs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getMostPlayedSongsWithGame = `-- 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
|
||||
`
|
||||
|
||||
type GetMostPlayedSongsWithGameRow struct {
|
||||
GameID int32 `json:"game_id"`
|
||||
GameName string `json:"game_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 game info
|
||||
func (q *Queries) GetMostPlayedSongsWithGame(ctx context.Context, limit int32) ([]GetMostPlayedSongsWithGameRow, error) {
|
||||
rows, err := q.db.Query(ctx, getMostPlayedSongsWithGame, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetMostPlayedSongsWithGameRow
|
||||
for rows.Next() {
|
||||
var i GetMostPlayedSongsWithGameRow
|
||||
if err := rows.Scan(
|
||||
&i.GameID,
|
||||
&i.GameName,
|
||||
&i.SongName,
|
||||
&i.Path,
|
||||
&i.TimesPlayed,
|
||||
&i.FileName,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getNeverPlayedGames = `-- 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
|
||||
`
|
||||
|
||||
type GetNeverPlayedGamesRow struct {
|
||||
GameID int32 `json:"game_id"`
|
||||
GameName string `json:"game_name"`
|
||||
GamePlayed int32 `json:"game_played"`
|
||||
Added time.Time `json:"added"`
|
||||
Songs []byte `json:"songs"`
|
||||
}
|
||||
|
||||
// Games that have never been played (times_played = 0)
|
||||
func (q *Queries) GetNeverPlayedGames(ctx context.Context) ([]GetNeverPlayedGamesRow, error) {
|
||||
rows, err := q.db.Query(ctx, getNeverPlayedGames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetNeverPlayedGamesRow
|
||||
for rows.Next() {
|
||||
var i GetNeverPlayedGamesRow
|
||||
if err := rows.Scan(
|
||||
&i.GameID,
|
||||
&i.GameName,
|
||||
&i.GamePlayed,
|
||||
&i.Added,
|
||||
&i.Songs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getOldestPlayedGames = `-- 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
|
||||
`
|
||||
|
||||
type GetOldestPlayedGamesRow struct {
|
||||
GameID int32 `json:"game_id"`
|
||||
GameName string `json:"game_name"`
|
||||
GamePlayed int32 `json:"game_played"`
|
||||
GameLastPlayed *time.Time `json:"game_last_played"`
|
||||
Songs []byte `json:"songs"`
|
||||
}
|
||||
|
||||
// Oldest played games (least recently played, but has been played at least once)
|
||||
func (q *Queries) GetOldestPlayedGames(ctx context.Context, limit int32) ([]GetOldestPlayedGamesRow, error) {
|
||||
rows, err := q.db.Query(ctx, getOldestPlayedGames, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetOldestPlayedGamesRow
|
||||
for rows.Next() {
|
||||
var i GetOldestPlayedGamesRow
|
||||
if err := rows.Scan(
|
||||
&i.GameID,
|
||||
&i.GameName,
|
||||
&i.GamePlayed,
|
||||
&i.GameLastPlayed,
|
||||
&i.Songs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getStatisticsSummary = `-- 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
|
||||
`
|
||||
|
||||
type GetStatisticsSummaryRow struct {
|
||||
TotalGames int64 `json:"total_games"`
|
||||
PlayedGames int64 `json:"played_games"`
|
||||
NeverPlayedGames int64 `json:"never_played_games"`
|
||||
TotalGamePlays int64 `json:"total_game_plays"`
|
||||
AvgGamePlays float64 `json:"avg_game_plays"`
|
||||
MaxGamePlays int64 `json:"max_game_plays"`
|
||||
MinGamePlays int64 `json:"min_game_plays"`
|
||||
}
|
||||
|
||||
// Get statistics summary
|
||||
func (q *Queries) GetStatisticsSummary(ctx context.Context) (GetStatisticsSummaryRow, error) {
|
||||
row := q.db.QueryRow(ctx, getStatisticsSummary)
|
||||
var i GetStatisticsSummaryRow
|
||||
err := row.Scan(
|
||||
&i.TotalGames,
|
||||
&i.PlayedGames,
|
||||
&i.NeverPlayedGames,
|
||||
&i.TotalGamePlays,
|
||||
&i.AvgGamePlays,
|
||||
&i.MaxGamePlays,
|
||||
&i.MinGamePlays,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
Reference in New Issue
Block a user