From f266e4fcf75215523c609fa43851976c9bb91735 Mon Sep 17 00:00:00 2001 From: "Alexander \"Arav\" Andreev" Date: Wed, 15 May 2024 23:51:10 +0400 Subject: [PATCH] Restructurised Statistics. --- internal/statistics/db/sqlite/db.go | 155 +++++----------------------- internal/statistics/statistics.go | 127 ++++++++++++++++++++++- 2 files changed, 153 insertions(+), 129 deletions(-) diff --git a/internal/statistics/db/sqlite/db.go b/internal/statistics/db/sqlite/db.go index 8f4b0d1..1dcbad7 100644 --- a/internal/statistics/db/sqlite/db.go +++ b/internal/statistics/db/sqlite/db.go @@ -2,11 +2,9 @@ package sqlite import ( "database/sql" - "dwelling-radio/internal/radio" "dwelling-radio/internal/statistics" _ "embed" "fmt" - "time" _ "github.com/mattn/go-sqlite3" "github.com/pkg/errors" @@ -32,52 +30,8 @@ var ( queryMostSimultaneousListeners string ) -var ( - stmtSongAdd *sql.Stmt - stmtHistoryAdd *sql.Stmt - stmtLastNSongs *sql.Stmt - stmtMostPopularSongs *sql.Stmt - stmtMostSimultaneousListeners *sql.Stmt -) - type SQLiteStatistics struct { - db *sql.DB -} - -func initDBStatements(db *sql.DB) error { - db.Exec("PRAGMA foreign_keys = ON;") - - _, err := db.Exec(querySchema) - if err != nil { - return errors.Wrap(err, "failed to init schema") - } - - stmtSongAdd, err = db.Prepare(querySongAdd) - if err != nil { - return errors.Wrap(err, "failed to prepare querySongAdd") - } - - stmtHistoryAdd, err = db.Prepare(queryHistoryAdd) - if err != nil { - return errors.Wrap(err, "failed to prepare queryHistoryAdd") - } - - stmtLastNSongs, err = db.Prepare(queryLastNSongs) - if err != nil { - return errors.Wrap(err, "failed to prepare queryLastNSongs") - } - - stmtMostPopularSongs, err = db.Prepare(queryMostPopularSongs) - if err != nil { - return errors.Wrap(err, "failed to prepare queryMostPopularSongs") - } - - stmtMostSimultaneousListeners, err = db.Prepare(queryMostSimultaneousListeners) - if err != nil { - return errors.Wrap(err, "failed to prepare queryMostSimultaneousListeners") - } - - return nil + statistics.BaseStatistics } func New(path string) (statistics.Statistics, error) { @@ -86,106 +40,51 @@ func New(path string) (statistics.Statistics, error) { return nil, err } - if err := initDBStatements(db); err != nil { - return nil, err - } + stats := &SQLiteStatistics{ + BaseStatistics: statistics.BaseStatistics{ + Db: db, DbDateFormat: dbDateFormat}} - return &SQLiteStatistics{db: db}, nil -} + db.Exec("PRAGMA foreign_keys = ON;") -func (s *SQLiteStatistics) Add(song *radio.Song) error { - if song == nil || song.Artist == "" || song.Title == "" { - return errors.New("No song or an empty one was passed.") - } - - tx, err := s.db.Begin() + _, err = db.Exec(querySchema) if err != nil { - return err - } - defer tx.Rollback() - - row := tx.Stmt(stmtSongAdd).QueryRow(song.Artist, song.Title) - if row.Err() != nil { - return row.Err() + return nil, errors.Wrap(err, + statistics.ErrPrepareStmt{Name: "initial schema"}.Error()) } - var songID int64 - if err := row.Scan(&songID); err != nil { - return err - } - - res, err := tx.Stmt(stmtHistoryAdd).Exec(song.StartAt.UTC().Format(dbDateFormat), - songID, song.Listeners, song.PeakListeners) + stats.BaseStatistics.StmtSongAdd, err = db.Prepare(querySongAdd) if err != nil { - return err - } else if ra, err := res.RowsAffected(); ra == 0 || err != nil { - return errors.New("a song wasn't added to history, but there were no errors") + return nil, errors.Wrap(err, + statistics.ErrPrepareStmt{Name: "song_add"}.Error()) } - tx.Commit() - - return nil -} - -func (s *SQLiteStatistics) LastNSongs(n int64) ([]radio.Song, error) { - if n == 0 { - return nil, nil - } - - tx, err := s.db.Begin() + stats.BaseStatistics.StmtHistoryAdd, err = db.Prepare(queryHistoryAdd) if err != nil { - return nil, err + return nil, errors.Wrap(err, + statistics.ErrPrepareStmt{Name: "history_add"}.Error()) } - defer tx.Rollback() - rows, err := tx.Stmt(stmtLastNSongs).Query(n) + stats.BaseStatistics.StmtLastNSongs, err = db.Prepare(queryLastNSongs) if err != nil { - return nil, err + return nil, errors.Wrap(err, + statistics.ErrPrepareStmt{Name: "last N songs"}.Error()) } - songs := make([]radio.Song, n) - - i := 0 - for rows.Next() { - var startAt string - - if err := rows.Scan(&startAt, &songs[i].Artist, &songs[i].Title, - &songs[i].Listeners, &songs[i].PeakListeners); err != nil { - return nil, err - } - - songs[i].StartAt, err = time.Parse(dbDateFormat, startAt) - if err != nil { - return nil, err - } - - i++ + stats.BaseStatistics.StmtMostPopularSongs, err = db.Prepare(queryMostPopularSongs) + if err != nil { + return nil, errors.Wrap(err, + statistics.ErrPrepareStmt{Name: "most popular song"}.Error()) } - tx.Commit() - - if i == 0 { - return nil, nil + stats.BaseStatistics.StmtMostSimultaneousListeners, err = db.Prepare(queryMostSimultaneousListeners) + if err != nil { + return nil, errors.Wrap(err, + statistics.ErrPrepareStmt{Name: "most simultaneous listeners"}.Error()) } - lst := make([]radio.Song, i) - copy(lst, songs[:]) - - return lst, nil -} - -func (s *SQLiteStatistics) MostNPopularSongs(n int64) ([]radio.Song, error) { - if n == 0 { - return nil, nil - } - - return nil, nil -} - -func (s *SQLiteStatistics) MostSimultaneousListeners() (radio.Song, error) { - return radio.Song{}, nil + return stats, nil } func (s *SQLiteStatistics) Close() error { - return s.db.Close() + return s.BaseStatistics.Close() } diff --git a/internal/statistics/statistics.go b/internal/statistics/statistics.go index 3b6dda5..5e9b706 100644 --- a/internal/statistics/statistics.go +++ b/internal/statistics/statistics.go @@ -1,6 +1,13 @@ package statistics -import "dwelling-radio/internal/radio" +import ( + "database/sql" + "dwelling-radio/internal/radio" + "fmt" + "time" + + "github.com/pkg/errors" +) const MostListenedDateFormat string = "02 January 2006 at 15:04:05 MST" @@ -11,3 +18,121 @@ type Statistics interface { MostSimultaneousListeners() (radio.Song, error) Close() error } + +type ErrPrepareStmt struct { + Name string +} + +func (e ErrPrepareStmt) Error() string { + return fmt.Sprintf("failed to prepare an SQL statement '%s'", e.Name) +} + +var ErrNoSong = errors.New("no song was passed (a struct is nil or empty)") +var ErrSongNotAdded = errors.New("song was not added") + +type BaseStatistics struct { + Db *sql.DB + DbDateFormat string + + StmtHistoryAdd *sql.Stmt + StmtSongAdd *sql.Stmt + + StmtLastNSongs *sql.Stmt + StmtMostPopularSongs *sql.Stmt + StmtMostSimultaneousListeners *sql.Stmt +} + +func (s *BaseStatistics) Add(song *radio.Song) error { + if song == nil || song.Artist == "" || song.Title == "" { + return ErrNoSong + } + + tx, err := s.Db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + row := tx.Stmt(s.StmtSongAdd).QueryRow(song.Artist, song.Title) + if row.Err() != nil { + return row.Err() + } + + var songID int64 + if err := row.Scan(&songID); err != nil { + return err + } + + _, err = tx.Stmt(s.StmtHistoryAdd).Exec(song.StartAt.UTC().Format(s.DbDateFormat), + songID, song.Listeners, song.PeakListeners) + if err != nil { + return errors.Wrap(err, ErrSongNotAdded.Error()) + } + + tx.Commit() + + return nil +} + +func (s *BaseStatistics) LastNSongs(n int64) ([]radio.Song, error) { + if n == 0 { + return nil, nil + } + + tx, err := s.Db.Begin() + if err != nil { + return nil, err + } + defer tx.Rollback() + + rows, err := tx.Stmt(s.StmtLastNSongs).Query(n) + if err != nil { + return nil, err + } + + songs := make([]radio.Song, n) + + i := 0 + for rows.Next() { + var startAt string + + if err := rows.Scan(&startAt, &songs[i].Artist, &songs[i].Title, + &songs[i].Listeners, &songs[i].PeakListeners); err != nil { + return nil, err + } + + songs[i].StartAt, err = time.Parse(s.DbDateFormat, startAt) + if err != nil { + return nil, err + } + + i++ + } + + tx.Commit() + + if i == 0 { + return nil, nil + } + + lst := make([]radio.Song, i) + copy(lst, songs[:]) + + return lst, nil +} + +func (s *BaseStatistics) MostNPopularSongs(n int64) ([]radio.Song, error) { + if n == 0 { + return nil, nil + } + + return nil, nil +} + +func (s *BaseStatistics) MostSimultaneousListeners() (radio.Song, error) { + return radio.Song{}, nil +} + +func (s *BaseStatistics) Close() error { + return s.Db.Close() +}