package backend import ( "context" "encoding/json" "time" "music-server/internal/db/repository" "github.com/google/uuid" ) // SoundtrackWithSongs represents a soundtrack with its songs for statistics type SoundtrackWithSongs 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,omitempty"` Songs []SongInfoForStats `json:"songs"` } // SongInfoForStats represents a song with soundtrack info for statistics. // The field order matches the generated query rows so they can be converted directly. type SongInfoForStats struct { 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,omitempty"` } // StatisticsSummary holds overall statistics type StatisticsSummary struct { TotalSoundtracks int64 `json:"total_soundtracks"` PlayedSoundtracks int64 `json:"played_soundtracks"` NeverPlayedSoundtracks int64 `json:"never_played_soundtracks"` TotalSoundtrackPlays int64 `json:"total_soundtrack_plays"` AvgSoundtrackPlays float64 `json:"avg_soundtrack_plays"` MaxSoundtrackPlays int64 `json:"max_soundtrack_plays"` MinSoundtrackPlays int64 `json:"min_soundtrack_plays"` } // StatisticsService reads play statistics. type StatisticsService struct { q repository.Querier } // NewStatisticsService creates a StatisticsService. func NewStatisticsService(q repository.Querier) *StatisticsService { return &StatisticsService{q: q} } // soundtrackRow covers the generated rows that share the soundtrack-with-songs shape. type soundtrackRow interface { repository.GetMostPlayedSoundtracksWithSongsRow | repository.GetLeastPlayedSoundtracksWithSongsRow | repository.GetLastPlayedSoundtracksRow | repository.GetOldestPlayedSoundtracksRow | repository.GetAllSoundtracksWithSongsRow } func toSoundtracks[R soundtrackRow](rows []R, err error) ([]SoundtrackWithSongs, error) { if err != nil { return nil, err } result := []SoundtrackWithSongs{} for _, r := range rows { row := repository.GetMostPlayedSoundtracksWithSongsRow(r) result = append(result, SoundtrackWithSongs{ SoundtrackID: row.SoundtrackID, SoundtrackUuid: row.SoundtrackUuid, SoundtrackName: row.SoundtrackName, SoundtrackPlayed: row.SoundtrackPlayed, SoundtrackLastPlayed: row.SoundtrackLastPlayed, Songs: parseSongs(row.Songs), }) } return result, nil } type songRow interface { repository.GetMostPlayedSongsWithSoundtrackRow | repository.GetLeastPlayedSongsWithSoundtrackRow } func toSongs[R songRow](rows []R, err error) ([]SongInfoForStats, error) { if err != nil { return nil, err } result := []SongInfoForStats{} for _, r := range rows { result = append(result, SongInfoForStats(repository.GetMostPlayedSongsWithSoundtrackRow(r))) } return result, nil } func parseSongs(data []byte) []SongInfoForStats { songs := []SongInfoForStats{} if data != nil { if err := json.Unmarshal(data, &songs); err != nil { return []SongInfoForStats{} } } return songs } // AllSoundtracks returns every soundtrack with its songs, sorted by name func (s *StatisticsService) AllSoundtracks(ctx context.Context) ([]SoundtrackWithSongs, error) { return toSoundtracks(s.q.GetAllSoundtracksWithSongs(ctx)) } // MostPlayedSoundtracks returns the top N most played soundtracks with their songs func (s *StatisticsService) MostPlayedSoundtracks(ctx context.Context, limit int32) ([]SoundtrackWithSongs, error) { return toSoundtracks(s.q.GetMostPlayedSoundtracksWithSongs(ctx, limit)) } // LeastPlayedSoundtracks returns the top N least played soundtracks with their songs func (s *StatisticsService) LeastPlayedSoundtracks(ctx context.Context, limit int32) ([]SoundtrackWithSongs, error) { return toSoundtracks(s.q.GetLeastPlayedSoundtracksWithSongs(ctx, limit)) } // LastPlayedSoundtracks returns the most recently played soundtracks func (s *StatisticsService) LastPlayedSoundtracks(ctx context.Context, limit int32) ([]SoundtrackWithSongs, error) { return toSoundtracks(s.q.GetLastPlayedSoundtracks(ctx, limit)) } // OldestPlayedSoundtracks returns the least recently played soundtracks func (s *StatisticsService) OldestPlayedSoundtracks(ctx context.Context, limit int32) ([]SoundtrackWithSongs, error) { return toSoundtracks(s.q.GetOldestPlayedSoundtracks(ctx, limit)) } // NeverPlayedSoundtracks returns soundtracks that have never been played func (s *StatisticsService) NeverPlayedSoundtracks(ctx context.Context) ([]SoundtrackWithSongs, error) { rows, err := s.q.GetNeverPlayedSoundtracks(ctx) if err != nil { return nil, err } result := []SoundtrackWithSongs{} for _, row := range rows { result = append(result, SoundtrackWithSongs{ SoundtrackID: row.SoundtrackID, SoundtrackUuid: row.SoundtrackUuid, SoundtrackName: row.SoundtrackName, SoundtrackPlayed: row.SoundtrackPlayed, Songs: parseSongs(row.Songs), }) } return result, nil } // MostPlayedSongs returns the top N most played songs with their soundtrack info func (s *StatisticsService) MostPlayedSongs(ctx context.Context, limit int32) ([]SongInfoForStats, error) { return toSongs(s.q.GetMostPlayedSongsWithSoundtrack(ctx, limit)) } // LeastPlayedSongs returns the top N least played songs with their soundtrack info func (s *StatisticsService) LeastPlayedSongs(ctx context.Context, limit int32) ([]SongInfoForStats, error) { return toSongs(s.q.GetLeastPlayedSongsWithSoundtrack(ctx, limit)) } // Summary returns overall statistics func (s *StatisticsService) Summary(ctx context.Context) (StatisticsSummary, error) { row, err := s.q.GetStatisticsSummary(ctx) if err != nil { return StatisticsSummary{}, err } return StatisticsSummary(row), nil }