cec408187d
- Database migration: rename game table to soundtrack - Rename game_name to soundtrack_name, game_id to soundtrack_id - Update all SQL queries in soundtrack.sql, song.sql, song_list.sql, statistics.sql - Regenerate sqlc code (soundtrack.sql.go, song.sql.go, etc.) - Update backend: music.go, sync.go, statistics.go - Update server: musicHandler.go, syncHandler.go, routes.go - Update frontend: hello.go - Keep URL paths as /games for backward compatibility Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
278 lines
7.8 KiB
Go
278 lines
7.8 KiB
Go
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 {
|
|
SoundtrackID int32 `json:"game_id"`
|
|
SoundtrackName string `json:"game_name"`
|
|
SoundtrackPlayed int32 `json:"game_played"`
|
|
SoundtrackLastPlayed *time.Time `json:"game_last_played,omitempty"`
|
|
Songs []SongInfoForStats `json:"songs"`
|
|
}
|
|
|
|
// SongInfoForStats represents a song with game info for statistics
|
|
type SongInfoForStats struct {
|
|
SoundtrackID int32 `json:"game_id"`
|
|
SoundtrackName 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{
|
|
SoundtrackID: row.SoundtrackID,
|
|
SoundtrackName: row.SoundtrackName,
|
|
SoundtrackPlayed: row.SoundtrackPlayed,
|
|
SoundtrackLastPlayed: row.SoundtrackLastPlayed,
|
|
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{
|
|
SoundtrackID: row.SoundtrackID,
|
|
SoundtrackName: row.SoundtrackName,
|
|
SoundtrackPlayed: row.SoundtrackPlayed,
|
|
SoundtrackLastPlayed: row.SoundtrackLastPlayed,
|
|
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{
|
|
SoundtrackID: row.SoundtrackID,
|
|
SoundtrackName: row.SoundtrackName,
|
|
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{
|
|
SoundtrackID: row.SoundtrackID,
|
|
SoundtrackName: row.SoundtrackName,
|
|
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{
|
|
SoundtrackID: row.SoundtrackID,
|
|
SoundtrackName: row.SoundtrackName,
|
|
SoundtrackPlayed: row.SoundtrackPlayed,
|
|
SoundtrackLastPlayed: 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{
|
|
SoundtrackID: row.SoundtrackID,
|
|
SoundtrackName: row.SoundtrackName,
|
|
SoundtrackPlayed: row.SoundtrackPlayed,
|
|
SoundtrackLastPlayed: row.SoundtrackLastPlayed,
|
|
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{
|
|
SoundtrackID: row.SoundtrackID,
|
|
SoundtrackName: row.SoundtrackName,
|
|
SoundtrackPlayed: row.SoundtrackPlayed,
|
|
SoundtrackLastPlayed: row.SoundtrackLastPlayed,
|
|
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.TotalSoundtracks),
|
|
PlayedGames: int64(row.PlayedSoundtracks),
|
|
NeverPlayedGames: int64(row.NeverPlayedSoundtracks),
|
|
TotalGamePlays: int64(row.TotalSoundtrackPlays),
|
|
AvgGamePlays: float64(row.AvgSoundtrackPlays),
|
|
MaxGamePlays: int64(row.MaxSoundtrackPlays),
|
|
MinGamePlays: int64(row.MinSoundtrackPlays),
|
|
}, 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()))
|
|
}
|
|
}
|