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,277 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"music-server/internal/logging"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GameWithSongs represents a game with its songs for statistics
|
||||||
|
type GameWithSongs struct {
|
||||||
|
GameID int32 `json:"game_id"`
|
||||||
|
GameName string `json:"game_name"`
|
||||||
|
GamePlayed int32 `json:"game_played"`
|
||||||
|
GameLastPlayed *time.Time `json:"game_last_played,omitempty"`
|
||||||
|
Songs []SongInfoForStats `json:"songs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SongInfoForStats represents a song with game info for statistics
|
||||||
|
type SongInfoForStats 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,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatisticsSummary holds overall statistics
|
||||||
|
type StatisticsSummary 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatisticsHandler manages statistics operations
|
||||||
|
type StatisticsHandler struct {
|
||||||
|
// Uses the global backend repo initialized via InitBackend
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStatisticsHandler creates a new StatisticsHandler
|
||||||
|
func NewStatisticsHandler() *StatisticsHandler {
|
||||||
|
return &StatisticsHandler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMostPlayedGamesWithSongs returns the top N most played games with their songs
|
||||||
|
func (h *StatisticsHandler) GetMostPlayedGamesWithSongs(limit int32) ([]GameWithSongs, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
// Get raw results
|
||||||
|
rows, err := queries.GetMostPlayedGamesWithSongs(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to GameWithSongs
|
||||||
|
var result []GameWithSongs
|
||||||
|
for _, row := range rows {
|
||||||
|
var songs []SongInfoForStats
|
||||||
|
if row.Songs != nil {
|
||||||
|
// Parse JSON songs array
|
||||||
|
if err := json.Unmarshal(row.Songs, &songs); err != nil {
|
||||||
|
// Fallback: if JSON parsing fails, create empty song entries
|
||||||
|
songs = make([]SongInfoForStats, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, GameWithSongs{
|
||||||
|
GameID: row.GameID,
|
||||||
|
GameName: row.GameName,
|
||||||
|
GamePlayed: row.GamePlayed,
|
||||||
|
GameLastPlayed: row.GameLastPlayed,
|
||||||
|
Songs: songs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLeastPlayedGamesWithSongs returns the top N least played games with their songs
|
||||||
|
func (h *StatisticsHandler) GetLeastPlayedGamesWithSongs(limit int32) ([]GameWithSongs, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
rows, err := queries.GetLeastPlayedGamesWithSongs(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []GameWithSongs
|
||||||
|
for _, row := range rows {
|
||||||
|
var songs []SongInfoForStats
|
||||||
|
if row.Songs != nil {
|
||||||
|
if err := json.Unmarshal(row.Songs, &songs); err != nil {
|
||||||
|
songs = make([]SongInfoForStats, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, GameWithSongs{
|
||||||
|
GameID: row.GameID,
|
||||||
|
GameName: row.GameName,
|
||||||
|
GamePlayed: row.GamePlayed,
|
||||||
|
GameLastPlayed: row.GameLastPlayed,
|
||||||
|
Songs: songs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMostPlayedSongsWithGame returns the top N most played songs with their game info
|
||||||
|
func (h *StatisticsHandler) GetMostPlayedSongsWithGame(limit int32) ([]SongInfoForStats, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
rows, err := queries.GetMostPlayedSongsWithGame(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []SongInfoForStats
|
||||||
|
for _, row := range rows {
|
||||||
|
result = append(result, SongInfoForStats{
|
||||||
|
GameID: row.GameID,
|
||||||
|
GameName: row.GameName,
|
||||||
|
SongName: row.SongName,
|
||||||
|
Path: row.Path,
|
||||||
|
TimesPlayed: row.TimesPlayed,
|
||||||
|
FileName: row.FileName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLeastPlayedSongsWithGame returns the top N least played songs with their game info
|
||||||
|
func (h *StatisticsHandler) GetLeastPlayedSongsWithGame(limit int32) ([]SongInfoForStats, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
rows, err := queries.GetLeastPlayedSongsWithGame(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []SongInfoForStats
|
||||||
|
for _, row := range rows {
|
||||||
|
result = append(result, SongInfoForStats{
|
||||||
|
GameID: row.GameID,
|
||||||
|
GameName: row.GameName,
|
||||||
|
SongName: row.SongName,
|
||||||
|
Path: row.Path,
|
||||||
|
TimesPlayed: row.TimesPlayed,
|
||||||
|
FileName: row.FileName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNeverPlayedGames returns games that have never been played
|
||||||
|
func (h *StatisticsHandler) GetNeverPlayedGames() ([]GameWithSongs, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
rows, err := queries.GetNeverPlayedGames(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []GameWithSongs
|
||||||
|
for _, row := range rows {
|
||||||
|
var songs []SongInfoForStats
|
||||||
|
if row.Songs != nil {
|
||||||
|
if err := json.Unmarshal(row.Songs, &songs); err != nil {
|
||||||
|
songs = make([]SongInfoForStats, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, GameWithSongs{
|
||||||
|
GameID: row.GameID,
|
||||||
|
GameName: row.GameName,
|
||||||
|
GamePlayed: row.GamePlayed,
|
||||||
|
GameLastPlayed: nil,
|
||||||
|
Songs: songs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastPlayedGames returns the most recently played games
|
||||||
|
func (h *StatisticsHandler) GetLastPlayedGames(limit int32) ([]GameWithSongs, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
rows, err := queries.GetLastPlayedGames(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []GameWithSongs
|
||||||
|
for _, row := range rows {
|
||||||
|
var songs []SongInfoForStats
|
||||||
|
if row.Songs != nil {
|
||||||
|
if err := json.Unmarshal(row.Songs, &songs); err != nil {
|
||||||
|
songs = make([]SongInfoForStats, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, GameWithSongs{
|
||||||
|
GameID: row.GameID,
|
||||||
|
GameName: row.GameName,
|
||||||
|
GamePlayed: row.GamePlayed,
|
||||||
|
GameLastPlayed: row.GameLastPlayed,
|
||||||
|
Songs: songs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOldestPlayedGames returns the least recently played games
|
||||||
|
func (h *StatisticsHandler) GetOldestPlayedGames(limit int32) ([]GameWithSongs, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
rows, err := queries.GetOldestPlayedGames(ctx, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []GameWithSongs
|
||||||
|
for _, row := range rows {
|
||||||
|
var songs []SongInfoForStats
|
||||||
|
if row.Songs != nil {
|
||||||
|
if err := json.Unmarshal(row.Songs, &songs); err != nil {
|
||||||
|
songs = make([]SongInfoForStats, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, GameWithSongs{
|
||||||
|
GameID: row.GameID,
|
||||||
|
GameName: row.GameName,
|
||||||
|
GamePlayed: row.GamePlayed,
|
||||||
|
GameLastPlayed: row.GameLastPlayed,
|
||||||
|
Songs: songs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatisticsSummary returns overall statistics
|
||||||
|
func (h *StatisticsHandler) GetStatisticsSummary() (*StatisticsSummary, error) {
|
||||||
|
queries := BackendRepo()
|
||||||
|
ctx := BackendCtx()
|
||||||
|
|
||||||
|
row, err := queries.GetStatisticsSummary(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &StatisticsSummary{
|
||||||
|
TotalGames: int64(row.TotalGames),
|
||||||
|
PlayedGames: int64(row.PlayedGames),
|
||||||
|
NeverPlayedGames: int64(row.NeverPlayedGames),
|
||||||
|
TotalGamePlays: int64(row.TotalGamePlays),
|
||||||
|
AvgGamePlays: float64(row.AvgGamePlays),
|
||||||
|
MaxGamePlays: int64(row.MaxGamePlays),
|
||||||
|
MinGamePlays: int64(row.MinGamePlays),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log helper for statistics operations
|
||||||
|
func logStatisticsError(err error, operation string) {
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Statistics error",
|
||||||
|
zap.String("operation", operation),
|
||||||
|
zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -122,10 +122,38 @@ func (s *Server) RegisterRoutes() http.Handler {
|
|||||||
// Create token auth middleware with pool access
|
// Create token auth middleware with pool access
|
||||||
tokenAuthMiddleware := middleware.TokenAuthMiddleware(s.db.Pool)
|
tokenAuthMiddleware := middleware.TokenAuthMiddleware(s.db.Pool)
|
||||||
|
|
||||||
// Protected group with token authentication - will be used by VGMQ and Statistics API
|
// Protected group with token authentication
|
||||||
_ = apiV1.Group("", tokenAuthMiddleware)
|
protectedV1 := apiV1.Group("", tokenAuthMiddleware)
|
||||||
|
|
||||||
// Note: Future protected endpoints (VGMQ, Statistics) will be added here
|
// Statistics API endpoints (protected by token auth)
|
||||||
|
statistics := s.statisticsHandler
|
||||||
|
protectedV1.GET("/statistics/games/most-played", func(c *echo.Context) error {
|
||||||
|
return statistics.GetMostPlayedGames(c)
|
||||||
|
})
|
||||||
|
protectedV1.GET("/statistics/games/least-played", func(c *echo.Context) error {
|
||||||
|
return statistics.GetLeastPlayedGames(c)
|
||||||
|
})
|
||||||
|
protectedV1.GET("/statistics/games/never-played", func(c *echo.Context) error {
|
||||||
|
return statistics.GetNeverPlayedGames(c)
|
||||||
|
})
|
||||||
|
protectedV1.GET("/statistics/games/last-played", func(c *echo.Context) error {
|
||||||
|
return statistics.GetLastPlayedGames(c)
|
||||||
|
})
|
||||||
|
protectedV1.GET("/statistics/games/oldest-played", func(c *echo.Context) error {
|
||||||
|
return statistics.GetOldestPlayedGames(c)
|
||||||
|
})
|
||||||
|
protectedV1.GET("/statistics/songs/most-played", func(c *echo.Context) error {
|
||||||
|
return statistics.GetMostPlayedSongs(c)
|
||||||
|
})
|
||||||
|
protectedV1.GET("/statistics/songs/least-played", func(c *echo.Context) error {
|
||||||
|
return statistics.GetLeastPlayedSongs(c)
|
||||||
|
})
|
||||||
|
protectedV1.GET("/statistics/summary", func(c *echo.Context) error {
|
||||||
|
return statistics.GetStatisticsSummary(c)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Future: VGMQ endpoints will be added to protectedV1 group
|
||||||
|
_ = protectedV1 // Use the variable to avoid unused variable error
|
||||||
|
|
||||||
routes := e.Router().Routes()
|
routes := e.Router().Routes()
|
||||||
sort.Slice(routes, func(i, j int) bool {
|
sort.Slice(routes, func(i, j int) bool {
|
||||||
|
|||||||
@@ -15,10 +15,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
port int
|
port int
|
||||||
db *db.Database
|
db *db.Database
|
||||||
tokenHandler *TokenHandler
|
tokenHandler *TokenHandler
|
||||||
httpServer *http.Server
|
statisticsHandler *StatisticsHandler
|
||||||
|
httpServer *http.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -68,11 +69,15 @@ func NewServerInstance() *Server {
|
|||||||
// Initialize token handler with database pool
|
// Initialize token handler with database pool
|
||||||
tokenHandler := NewTokenHandler(database.Pool)
|
tokenHandler := NewTokenHandler(database.Pool)
|
||||||
|
|
||||||
|
// Initialize statistics handler
|
||||||
|
statisticsHandler := NewStatisticsHandler()
|
||||||
|
|
||||||
// Create the server instance
|
// Create the server instance
|
||||||
appServer := &Server{
|
appServer := &Server{
|
||||||
port: port,
|
port: port,
|
||||||
db: database,
|
db: database,
|
||||||
tokenHandler: tokenHandler,
|
tokenHandler: tokenHandler,
|
||||||
|
statisticsHandler: statisticsHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the HTTP server
|
// Create the HTTP server
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"music-server/internal/backend"
|
||||||
|
"music-server/internal/logging"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v5"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatisticsHandler handles statistics-related HTTP requests
|
||||||
|
type StatisticsHandler struct {
|
||||||
|
statsBackend *backend.StatisticsHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStatisticsHandler creates a new StatisticsHandler
|
||||||
|
func NewStatisticsHandler() *StatisticsHandler {
|
||||||
|
return &StatisticsHandler{
|
||||||
|
statsBackend: backend.NewStatisticsHandler(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMostPlayedGames returns top N most played games with songs
|
||||||
|
// GET /api/v1/statistics/games/most-played
|
||||||
|
//
|
||||||
|
// @Summary Get most played games
|
||||||
|
// @Description Returns the top N most played games with their songs
|
||||||
|
// @Tags statistics
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Number of results (default: 10)"
|
||||||
|
// @Success 200 {array} backend.GameWithSongs
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /api/v1/statistics/games/most-played [get]
|
||||||
|
func (h *StatisticsHandler) GetMostPlayedGames(ctx *echo.Context) error {
|
||||||
|
limit := 10 // default
|
||||||
|
limitStr := ctx.QueryParam("limit")
|
||||||
|
if limitStr != "" {
|
||||||
|
var err error
|
||||||
|
limit, err = strconv.Atoi(limitStr)
|
||||||
|
if err != nil || limit <= 0 {
|
||||||
|
return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid limit parameter"})
|
||||||
|
}
|
||||||
|
// Cap at 100 for performance
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
games, err := h.statsBackend.GetMostPlayedGamesWithSongs(int32(limit))
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get most played games", zap.String("error", err.Error()))
|
||||||
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
|
}
|
||||||
|
return ctx.JSON(http.StatusOK, games)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLeastPlayedGames returns top N least played games with songs
|
||||||
|
// GET /api/v1/statistics/games/least-played
|
||||||
|
//
|
||||||
|
// @Summary Get least played games
|
||||||
|
// @Description Returns the top N least played games with their songs
|
||||||
|
// @Tags statistics
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Number of results (default: 10)"
|
||||||
|
// @Success 200 {array} backend.GameWithSongs
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /api/v1/statistics/games/least-played [get]
|
||||||
|
func (h *StatisticsHandler) GetLeastPlayedGames(ctx *echo.Context) error {
|
||||||
|
limit := 10
|
||||||
|
limitStr := ctx.QueryParam("limit")
|
||||||
|
if limitStr != "" {
|
||||||
|
var err error
|
||||||
|
limit, err = strconv.Atoi(limitStr)
|
||||||
|
if err != nil || limit <= 0 {
|
||||||
|
return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid limit parameter"})
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
games, err := h.statsBackend.GetLeastPlayedGamesWithSongs(int32(limit))
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get least played games", zap.String("error", err.Error()))
|
||||||
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
|
}
|
||||||
|
return ctx.JSON(http.StatusOK, games)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMostPlayedSongs returns top N most played songs with game info
|
||||||
|
// GET /api/v1/statistics/songs/most-played
|
||||||
|
//
|
||||||
|
// @Summary Get most played songs
|
||||||
|
// @Description Returns the top N most played songs with their game info
|
||||||
|
// @Tags statistics
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Number of results (default: 10)"
|
||||||
|
// @Success 200 {array} backend.SongInfoForStats
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /api/v1/statistics/songs/most-played [get]
|
||||||
|
func (h *StatisticsHandler) GetMostPlayedSongs(ctx *echo.Context) error {
|
||||||
|
limit := 10
|
||||||
|
limitStr := ctx.QueryParam("limit")
|
||||||
|
if limitStr != "" {
|
||||||
|
var err error
|
||||||
|
limit, err = strconv.Atoi(limitStr)
|
||||||
|
if err != nil || limit <= 0 {
|
||||||
|
return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid limit parameter"})
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
songs, err := h.statsBackend.GetMostPlayedSongsWithGame(int32(limit))
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get most played songs", zap.String("error", err.Error()))
|
||||||
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
|
}
|
||||||
|
return ctx.JSON(http.StatusOK, songs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLeastPlayedSongs returns top N least played songs with game info
|
||||||
|
// GET /api/v1/statistics/songs/least-played
|
||||||
|
//
|
||||||
|
// @Summary Get least played songs
|
||||||
|
// @Description Returns the top N least played songs with their game info
|
||||||
|
// @Tags statistics
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Number of results (default: 10)"
|
||||||
|
// @Success 200 {array} backend.SongInfoForStats
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /api/v1/statistics/songs/least-played [get]
|
||||||
|
func (h *StatisticsHandler) GetLeastPlayedSongs(ctx *echo.Context) error {
|
||||||
|
limit := 10
|
||||||
|
limitStr := ctx.QueryParam("limit")
|
||||||
|
if limitStr != "" {
|
||||||
|
var err error
|
||||||
|
limit, err = strconv.Atoi(limitStr)
|
||||||
|
if err != nil || limit <= 0 {
|
||||||
|
return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid limit parameter"})
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
songs, err := h.statsBackend.GetLeastPlayedSongsWithGame(int32(limit))
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get least played songs", zap.String("error", err.Error()))
|
||||||
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
|
}
|
||||||
|
return ctx.JSON(http.StatusOK, songs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNeverPlayedGames returns games that have never been played
|
||||||
|
// GET /api/v1/statistics/games/never-played
|
||||||
|
//
|
||||||
|
// @Summary Get never played games
|
||||||
|
// @Description Returns all games that have never been played (times_played = 0)
|
||||||
|
// @Tags statistics
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} backend.GameWithSongs
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /api/v1/statistics/games/never-played [get]
|
||||||
|
func (h *StatisticsHandler) GetNeverPlayedGames(ctx *echo.Context) error {
|
||||||
|
games, err := h.statsBackend.GetNeverPlayedGames()
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get never played games", zap.String("error", err.Error()))
|
||||||
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
|
}
|
||||||
|
return ctx.JSON(http.StatusOK, games)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastPlayedGames returns most recently played games
|
||||||
|
// GET /api/v1/statistics/games/last-played
|
||||||
|
//
|
||||||
|
// @Summary Get last played games
|
||||||
|
// @Description Returns the most recently played games
|
||||||
|
// @Tags statistics
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Number of results (default: 10)"
|
||||||
|
// @Success 200 {array} backend.GameWithSongs
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /api/v1/statistics/games/last-played [get]
|
||||||
|
func (h *StatisticsHandler) GetLastPlayedGames(ctx *echo.Context) error {
|
||||||
|
limit := 10
|
||||||
|
limitStr := ctx.QueryParam("limit")
|
||||||
|
if limitStr != "" {
|
||||||
|
var err error
|
||||||
|
limit, err = strconv.Atoi(limitStr)
|
||||||
|
if err != nil || limit <= 0 {
|
||||||
|
return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid limit parameter"})
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
games, err := h.statsBackend.GetLastPlayedGames(int32(limit))
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get last played games", zap.String("error", err.Error()))
|
||||||
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
|
}
|
||||||
|
return ctx.JSON(http.StatusOK, games)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOldestPlayedGames returns least recently played games
|
||||||
|
// GET /api/v1/statistics/games/oldest-played
|
||||||
|
//
|
||||||
|
// @Summary Get oldest played games
|
||||||
|
// @Description Returns the least recently played games (that have been played at least once)
|
||||||
|
// @Tags statistics
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Number of results (default: 10)"
|
||||||
|
// @Success 200 {array} backend.GameWithSongs
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /api/v1/statistics/games/oldest-played [get]
|
||||||
|
func (h *StatisticsHandler) GetOldestPlayedGames(ctx *echo.Context) error {
|
||||||
|
limit := 10
|
||||||
|
limitStr := ctx.QueryParam("limit")
|
||||||
|
if limitStr != "" {
|
||||||
|
var err error
|
||||||
|
limit, err = strconv.Atoi(limitStr)
|
||||||
|
if err != nil || limit <= 0 {
|
||||||
|
return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid limit parameter"})
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
games, err := h.statsBackend.GetOldestPlayedGames(int32(limit))
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get oldest played games", zap.String("error", err.Error()))
|
||||||
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
|
}
|
||||||
|
return ctx.JSON(http.StatusOK, games)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatisticsSummary returns overall statistics
|
||||||
|
// GET /api/v1/statistics/summary
|
||||||
|
//
|
||||||
|
// @Summary Get statistics summary
|
||||||
|
// @Description Returns overall statistics about the music library
|
||||||
|
// @Tags statistics
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} backend.StatisticsSummary
|
||||||
|
// @Failure 500 {object} map[string]string
|
||||||
|
// @Router /api/v1/statistics/summary [get]
|
||||||
|
func (h *StatisticsHandler) GetStatisticsSummary(ctx *echo.Context) error {
|
||||||
|
summary, err := h.statsBackend.GetStatisticsSummary()
|
||||||
|
if err != nil {
|
||||||
|
logging.GetLogger().Error("Failed to get statistics summary", zap.String("error", err.Error()))
|
||||||
|
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to get statistics"})
|
||||||
|
}
|
||||||
|
return ctx.JSON(http.StatusOK, summary)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user