From 31ec12ae2394a7d074df8c571e835dee8da5332a Mon Sep 17 00:00:00 2001 From: Topvennie Date: Sun, 1 Dec 2024 11:35:34 +0100 Subject: [PATCH] feat(lyric): add view --- config/development.toml | 4 + ...1128115057_alter_song_table_add_lyrics.sql | 6 +- db/queries/song.sql | 39 ++- internal/cmd/tui.go | 9 +- internal/pkg/db/dto/song.go | 63 ++++ internal/pkg/db/sqlc/song.sql.go | 185 ++++++++++- internal/pkg/lyrics/lrc.go | 28 +- internal/pkg/lyrics/lyrics.go | 1 + internal/pkg/lyrics/plain.go | 16 +- ui/screen/cammie.go | 1 - ui/screen/song.go | 50 +++ ui/view/gamification/gamification.go | 28 +- ui/view/message/message.go | 6 +- ui/view/song/song.go | 305 ++++++++++++++++++ ui/view/song/style.go | 24 ++ ui/view/song/util.go | 80 +++++ ui/view/song/view.go | 63 ++++ ui/view/tap/tap.go | 42 +-- ui/view/view.go | 3 +- ui/view/zess/zess.go | 94 +++--- 20 files changed, 931 insertions(+), 116 deletions(-) create mode 100644 ui/screen/song.go create mode 100644 ui/view/song/song.go create mode 100644 ui/view/song/style.go create mode 100644 ui/view/song/util.go create mode 100644 ui/view/song/view.go diff --git a/config/development.toml b/config/development.toml index e8ddddd..4028f99 100644 --- a/config/development.toml +++ b/config/development.toml @@ -65,3 +65,7 @@ interval_s = 60 [tui.gamification] interval_s = 3600 + +[tui.song] +interval_current_s = 5 +interval_top_s = 3600 diff --git a/db/migrations/20241128115057_alter_song_table_add_lyrics.sql b/db/migrations/20241128115057_alter_song_table_add_lyrics.sql index 7219ea7..4189979 100644 --- a/db/migrations/20241128115057_alter_song_table_add_lyrics.sql +++ b/db/migrations/20241128115057_alter_song_table_add_lyrics.sql @@ -13,11 +13,11 @@ ADD COLUMN lyrics TEXT; -- +goose Down -- +goose StatementBegin ALTER TABLE song -DROP COLUMN isrc_id; +DROP COLUMN lyrics; ALTER TABLE song -DROP COLUMN lyrics; +DROP COLUMN lyrics_type; ALTER TABLE song -DROP COLUMN common_id; +DROP COLUMN album; -- +goose StatementEnd diff --git a/db/queries/song.sql b/db/queries/song.sql index 66707ba..cb09a81 100644 --- a/db/queries/song.sql +++ b/db/queries/song.sql @@ -60,7 +60,7 @@ FROM song_artist WHERE name = ?; -- name: GetLastSongFull :many -SELECT s.title AS song_title, s.spotify_id, s.album, s.duration_ms, s.lyrics_type, s.lyrics, a.name AS artist_name, g.genre AS genre +SELECT s.id, s.title AS song_title, s.spotify_id, s.album, s.duration_ms, s.lyrics_type, s.lyrics, sh.created_at, a.id AS artist_id, a.name AS artist_name, a.spotify_id AS artist_spotify_id, a.followers AS artist_followers, a.popularity AS artist_popularity, g.id AS genre_id, g.genre AS genre, sh.created_at FROM song_history sh JOIN song s ON sh.song_id = s.id LEFT JOIN song_artist_song sa ON s.id = sa.song_id @@ -69,3 +69,40 @@ LEFT JOIN song_artist_genre ag ON ag.artist_id = a.id LEFT JOIN song_genre g ON ag.genre_id = g.id WHERE sh.created_at = (SELECT MAX(created_at) FROM song_history) ORDER BY a.name, g.genre; + +-- name: GetSongHistory :many +SELECT s.title +FROM song_history sh +JOIN song s ON sh.song_id = s.id +ORDER BY created_at DESC +LIMIT 5; + +-- name: GetTopSongs :many +SELECT s.id AS song_id, s.title, COUNT(sh.id) AS play_count +FROM song_history sh +JOIN song s ON sh.song_id = s.id +GROUP BY s.id, s.title +ORDER BY play_count DESC +LIMIT 5; + +-- name: GetTopArtists :many +SELECT sa.id AS artist_id, sa.name AS artist_name, COUNT(sh.id) AS total_plays +FROM song_history sh +JOIN song s ON sh.song_id = s.id +JOIN song_artist_song sas ON s.id = sas.song_id +JOIN song_artist sa ON sas.artist_id = sa.id +GROUP BY sa.id, sa.name +ORDER BY total_plays DESC +LIMIT 5; + +-- name: GetTopGenres :many +SELECT g.genre AS genre_name, COUNT(sh.id) AS total_plays +FROM song_history sh +JOIN song s ON sh.song_id = s.id +JOIN song_artist_song sas ON s.id = sas.song_id +JOIN song_artist sa ON sas.artist_id = sa.id +JOIN song_artist_genre sag ON sa.id = sag.artist_id +JOIN song_genre g ON sag.genre_id = g.id +GROUP BY g.genre +ORDER BY total_plays DESC +LIMIT 5; diff --git a/internal/cmd/tui.go b/internal/cmd/tui.go index dccdedf..591b7fd 100644 --- a/internal/cmd/tui.go +++ b/internal/cmd/tui.go @@ -17,6 +17,7 @@ import ( var screens = map[string]func(*db.DB) screen.Screen{ "cammie": screen.NewCammie, + "song": screen.NewSong, "test": screen.NewTest, } @@ -42,7 +43,7 @@ func TUI(db *db.DB) error { for _, updateData := range screen.GetUpdateViews() { done := make(chan bool) dones = append(dones, done) - go tuiPeriodicUpdates(db, p, updateData, done) + go tuiPeriodicUpdates(p, updateData, done) } _, err := p.Run() @@ -54,14 +55,14 @@ func TUI(db *db.DB) error { return err } -func tuiPeriodicUpdates(db *db.DB, p *tea.Program, updateData view.UpdateData, done chan bool) { +func tuiPeriodicUpdates(p *tea.Program, updateData view.UpdateData, done chan bool) { zap.S().Info("TUI: Starting periodic update for ", updateData.Name, " with an interval of ", updateData.Interval, " seconds") ticker := time.NewTicker(time.Duration(updateData.Interval) * time.Second) defer ticker.Stop() // Immediatly update once - msg, err := updateData.Update(db, updateData.View) + msg, err := updateData.Update(updateData.View) if err != nil { zap.S().Error("TUI: Error updating ", updateData.Name, "\n", err) } @@ -77,7 +78,7 @@ func tuiPeriodicUpdates(db *db.DB, p *tea.Program, updateData view.UpdateData, d return case <-ticker.C: // Update - msg, err := updateData.Update(db, updateData.View) + msg, err := updateData.Update(updateData.View) if err != nil { zap.S().Error("TUI: Error updating ", updateData.Name, "\n", err) } diff --git a/internal/pkg/db/dto/song.go b/internal/pkg/db/dto/song.go index 3caaeaa..4839db0 100644 --- a/internal/pkg/db/dto/song.go +++ b/internal/pkg/db/dto/song.go @@ -2,6 +2,7 @@ package dto import ( "database/sql" + "time" "github.com/zeusWPI/scc/internal/pkg/db/sqlc" ) @@ -15,6 +16,7 @@ type Song struct { DurationMS int64 `json:"duration_ms"` LyricsType string `json:"lyrics_type"` // Either 'synced' or 'plain' Lyrics string `json:"lyrics"` + CreatedAt time.Time `json:"created_at"` Artists []SongArtist `json:"artists"` } @@ -56,6 +58,67 @@ func SongDTO(song sqlc.Song) *Song { } } +// SongDTOHistory converts a sqlc.GetLastSongFullRow array to a Song +func SongDTOHistory(songs []sqlc.GetLastSongFullRow) *Song { + if len(songs) == 0 { + return nil + } + + var lyricsType string + if songs[0].LyricsType.Valid { + lyricsType = songs[0].LyricsType.String + } + var lyrics string + if songs[0].Lyrics.Valid { + lyrics = songs[0].Lyrics.String + } + + artistsMap := make(map[int64]SongArtist) + for _, song := range songs { + if !song.ArtistID.Valid { + continue + } + + // Get artist + artist, ok := artistsMap[song.ArtistID.Int64] + if !ok { + // Artist doesn't exist yet, add him + artist = SongArtist{ + ID: song.ArtistID.Int64, + Name: song.ArtistName.String, + SpotifyID: song.ArtistSpotifyID.String, + Followers: song.ArtistFollowers.Int64, + Popularity: song.ArtistPopularity.Int64, + Genres: make([]SongGenre, 0), + } + artistsMap[song.ArtistID.Int64] = artist + } + + // Add genre + artist.Genres = append(artist.Genres, SongGenre{ + ID: song.GenreID.Int64, + Genre: song.Genre.String, + }) + } + + artists := make([]SongArtist, 0, len(artistsMap)) + for _, artist := range artistsMap { + artists = append(artists, artist) + } + + return &Song{ + ID: songs[0].ID, + Title: songs[0].SongTitle, + Album: songs[0].Album, + SpotifyID: songs[0].SpotifyID, + DurationMS: songs[0].DurationMs, + LyricsType: lyricsType, + Lyrics: lyrics, + CreatedAt: songs[0].CreatedAt, + Artists: artists, + } +} + // CreateSongParams converts a Song DTO to a sqlc CreateSongParams object func (s *Song) CreateSongParams() *sqlc.CreateSongParams { lyricsType := sql.NullString{String: s.LyricsType, Valid: false} diff --git a/internal/pkg/db/sqlc/song.sql.go b/internal/pkg/db/sqlc/song.sql.go index 05bdbbc..e37f873 100644 --- a/internal/pkg/db/sqlc/song.sql.go +++ b/internal/pkg/db/sqlc/song.sql.go @@ -8,6 +8,7 @@ package sqlc import ( "context" "database/sql" + "time" ) const createSong = `-- name: CreateSong :one @@ -143,7 +144,7 @@ func (q *Queries) CreateSongHistory(ctx context.Context, songID int64) (SongHist } const getLastSongFull = `-- name: GetLastSongFull :many -SELECT s.title AS song_title, s.spotify_id, s.album, s.duration_ms, s.lyrics_type, s.lyrics, a.name AS artist_name, g.genre AS genre +SELECT s.id, s.title AS song_title, s.spotify_id, s.album, s.duration_ms, s.lyrics_type, s.lyrics, sh.created_at, a.id AS artist_id, a.name AS artist_name, a.spotify_id AS artist_spotify_id, a.followers AS artist_followers, a.popularity AS artist_popularity, g.id AS genre_id, g.genre AS genre, sh.created_at FROM song_history sh JOIN song s ON sh.song_id = s.id LEFT JOIN song_artist_song sa ON s.id = sa.song_id @@ -155,14 +156,22 @@ ORDER BY a.name, g.genre ` type GetLastSongFullRow struct { - SongTitle string - SpotifyID string - Album string - DurationMs int64 - LyricsType sql.NullString - Lyrics sql.NullString - ArtistName sql.NullString - Genre sql.NullString + ID int64 + SongTitle string + SpotifyID string + Album string + DurationMs int64 + LyricsType sql.NullString + Lyrics sql.NullString + CreatedAt time.Time + ArtistID sql.NullInt64 + ArtistName sql.NullString + ArtistSpotifyID sql.NullString + ArtistFollowers sql.NullInt64 + ArtistPopularity sql.NullInt64 + GenreID sql.NullInt64 + Genre sql.NullString + CreatedAt_2 time.Time } func (q *Queries) GetLastSongFull(ctx context.Context) ([]GetLastSongFullRow, error) { @@ -175,14 +184,22 @@ func (q *Queries) GetLastSongFull(ctx context.Context) ([]GetLastSongFullRow, er for rows.Next() { var i GetLastSongFullRow if err := rows.Scan( + &i.ID, &i.SongTitle, &i.SpotifyID, &i.Album, &i.DurationMs, &i.LyricsType, &i.Lyrics, + &i.CreatedAt, + &i.ArtistID, &i.ArtistName, + &i.ArtistSpotifyID, + &i.ArtistFollowers, + &i.ArtistPopularity, + &i.GenreID, &i.Genre, + &i.CreatedAt_2, ); err != nil { return nil, err } @@ -284,3 +301,153 @@ func (q *Queries) GetSongGenreByName(ctx context.Context, genre string) (SongGen err := row.Scan(&i.ID, &i.Genre) return i, err } + +const getSongHistory = `-- name: GetSongHistory :many +SELECT s.title +FROM song_history sh +JOIN song s ON sh.song_id = s.id +ORDER BY created_at DESC +LIMIT 5 +` + +func (q *Queries) GetSongHistory(ctx context.Context) ([]string, error) { + rows, err := q.db.QueryContext(ctx, getSongHistory) + if err != nil { + return nil, err + } + defer rows.Close() + var items []string + for rows.Next() { + var title string + if err := rows.Scan(&title); err != nil { + return nil, err + } + items = append(items, title) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getTopArtists = `-- name: GetTopArtists :many +SELECT sa.id AS artist_id, sa.name AS artist_name, COUNT(sh.id) AS total_plays +FROM song_history sh +JOIN song s ON sh.song_id = s.id +JOIN song_artist_song sas ON s.id = sas.song_id +JOIN song_artist sa ON sas.artist_id = sa.id +GROUP BY sa.id, sa.name +ORDER BY total_plays DESC +LIMIT 5 +` + +type GetTopArtistsRow struct { + ArtistID int64 + ArtistName string + TotalPlays int64 +} + +func (q *Queries) GetTopArtists(ctx context.Context) ([]GetTopArtistsRow, error) { + rows, err := q.db.QueryContext(ctx, getTopArtists) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetTopArtistsRow + for rows.Next() { + var i GetTopArtistsRow + if err := rows.Scan(&i.ArtistID, &i.ArtistName, &i.TotalPlays); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getTopGenres = `-- name: GetTopGenres :many +SELECT g.genre AS genre_name, COUNT(sh.id) AS total_plays +FROM song_history sh +JOIN song s ON sh.song_id = s.id +JOIN song_artist_song sas ON s.id = sas.song_id +JOIN song_artist sa ON sas.artist_id = sa.id +JOIN song_artist_genre sag ON sa.id = sag.artist_id +JOIN song_genre g ON sag.genre_id = g.id +GROUP BY g.genre +ORDER BY total_plays DESC +LIMIT 5 +` + +type GetTopGenresRow struct { + GenreName string + TotalPlays int64 +} + +func (q *Queries) GetTopGenres(ctx context.Context) ([]GetTopGenresRow, error) { + rows, err := q.db.QueryContext(ctx, getTopGenres) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetTopGenresRow + for rows.Next() { + var i GetTopGenresRow + if err := rows.Scan(&i.GenreName, &i.TotalPlays); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getTopSongs = `-- name: GetTopSongs :many +SELECT s.id AS song_id, s.title, COUNT(sh.id) AS play_count +FROM song_history sh +JOIN song s ON sh.song_id = s.id +GROUP BY s.id, s.title +ORDER BY play_count DESC +LIMIT 5 +` + +type GetTopSongsRow struct { + SongID int64 + Title string + PlayCount int64 +} + +func (q *Queries) GetTopSongs(ctx context.Context) ([]GetTopSongsRow, error) { + rows, err := q.db.QueryContext(ctx, getTopSongs) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetTopSongsRow + for rows.Next() { + var i GetTopSongsRow + if err := rows.Scan(&i.SongID, &i.Title, &i.PlayCount); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/pkg/lyrics/lrc.go b/internal/pkg/lyrics/lrc.go index 7c6b3a2..f8f9452 100644 --- a/internal/pkg/lyrics/lrc.go +++ b/internal/pkg/lyrics/lrc.go @@ -30,20 +30,30 @@ func (l *LRC) Previous(amount int) []Lyric { lyrics := make([]Lyric, 0, amount) for i := 1; i <= amount; i++ { - if l.i-i < 0 { + if l.i-i-1 < 0 { break } - lyrics = append(lyrics, l.lyrics[l.i-1]) + lyrics = append([]Lyric{l.lyrics[l.i-i-1]}, lyrics...) } return lyrics } +// Current provides the current lyric if any. +// If the song is finished the boolean is set to false +func (l *LRC) Current() (Lyric, bool) { + if l.i >= len(l.lyrics) { + return Lyric{}, false + } + + return l.lyrics[l.i], true +} + // Next provides the next lyric if any. // If the song is finished the boolean is set to false func (l *LRC) Next() (Lyric, bool) { - if l.i >= len(l.lyrics) { + if l.i+1 >= len(l.lyrics) { return Lyric{}, false } @@ -55,12 +65,12 @@ func (l *LRC) Next() (Lyric, bool) { func (l *LRC) Upcoming(amount int) []Lyric { lyrics := make([]Lyric, 0, amount) - for i := 1; i <= amount; i++ { - if i+l.i >= len(l.lyrics) { + for i := 0; i < amount; i++ { + if l.i+i >= len(l.lyrics) { break } - lyrics = append(lyrics, l.lyrics[i+l.i]) + lyrics = append(lyrics, l.lyrics[l.i+i]) } return lyrics @@ -69,7 +79,7 @@ func (l *LRC) Upcoming(amount int) []Lyric { func parseLRC(text string, totalDuration time.Duration) []Lyric { lines := strings.Split(text, "\n") - lyrics := make([]Lyric, 0, len(lines)) + lyrics := make([]Lyric, 0, len(lines)+1) var previousTimestamp time.Duration re, err := regexp.Compile(`^\[(\d{2}):(\d{2})\.(\d{2})\] (.+)$`) @@ -100,12 +110,12 @@ func parseLRC(text string, totalDuration time.Duration) []Lyric { lyrics = append(lyrics, Lyric{Text: t}) // Set duration of previous lyric - lyrics[i-1].Duration = timestamp - previousTimestamp + lyrics[i].Duration = timestamp - previousTimestamp previousTimestamp = timestamp } // Set duration of last lyric - lyrics[len(lines)-1].Duration = totalDuration - previousTimestamp + lyrics[len(lyrics)-1].Duration = totalDuration - previousTimestamp return lyrics } diff --git a/internal/pkg/lyrics/lyrics.go b/internal/pkg/lyrics/lyrics.go index f66cbd3..80e964f 100644 --- a/internal/pkg/lyrics/lyrics.go +++ b/internal/pkg/lyrics/lyrics.go @@ -11,6 +11,7 @@ import ( type Lyrics interface { GetSong() dto.Song Previous(int) []Lyric + Current() (Lyric, bool) Next() (Lyric, bool) Upcoming(int) []Lyric } diff --git a/internal/pkg/lyrics/plain.go b/internal/pkg/lyrics/plain.go index 4afaa48..6ce3946 100644 --- a/internal/pkg/lyrics/plain.go +++ b/internal/pkg/lyrics/plain.go @@ -27,10 +27,20 @@ func (p *Plain) GetSong() dto.Song { } // Previous provides the previous `amount` of lyrics without affecting the current lyric -func (p *Plain) Previous(amount int) []Lyric { +func (p *Plain) Previous(_ int) []Lyric { return []Lyric{} } +// Current provides the current lyric if any. +// If the song is finished the boolean is set to false +func (p *Plain) Current() (Lyric, bool) { + if p.given { + return Lyric{}, false + } + + return Lyric{}, true +} + // Next provides the next lyric. // If the lyrics are finished the boolean is set to false func (p *Plain) Next() (Lyric, bool) { @@ -38,10 +48,12 @@ func (p *Plain) Next() (Lyric, bool) { return Lyric{}, false } + p.given = true + return p.lyrics, true } // Upcoming provides the next `amount` lyrics without affecting the current lyric -func (p *Plain) Upcoming(amount int) []Lyric { +func (p *Plain) Upcoming(_ int) []Lyric { return []Lyric{} } diff --git a/ui/screen/cammie.go b/ui/screen/cammie.go index 3c1aed2..6e7151d 100644 --- a/ui/screen/cammie.go +++ b/ui/screen/cammie.go @@ -1,4 +1,3 @@ -// Package screen provides difference screens for the tui package screen import ( diff --git a/ui/screen/song.go b/ui/screen/song.go new file mode 100644 index 0000000..6788715 --- /dev/null +++ b/ui/screen/song.go @@ -0,0 +1,50 @@ +package screen + +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/zeusWPI/scc/internal/pkg/db" + "github.com/zeusWPI/scc/ui/view" + "github.com/zeusWPI/scc/ui/view/song" +) + +// Song represents the song screen +type Song struct { + db *db.DB + song view.View +} + +// NewSong creates a new song screen +func NewSong(db *db.DB) Screen { + return &Song{db: db, song: song.NewModel(db)} +} + +// Init initializes the song screen +func (s *Song) Init() tea.Cmd { + return s.song.Init() +} + +// Update updates the song screen +func (s *Song) Update(msg tea.Msg) (Screen, tea.Cmd) { + var cmds []tea.Cmd + switch msg := msg.(type) { + default: + song, cmd := s.song.Update(msg) + s.song = song + + if cmd != nil { + cmds = append(cmds, cmd) + } + } + + return s, tea.Batch(cmds...) +} + +// View returns the song screen view +func (s *Song) View() string { + return s.song.View() +} + +// GetUpdateViews returns all the update functions for the song screen +func (s *Song) GetUpdateViews() []view.UpdateData { + return s.song.GetUpdateDatas() +} diff --git a/ui/view/gamification/gamification.go b/ui/view/gamification/gamification.go index ac25c35..93da300 100644 --- a/ui/view/gamification/gamification.go +++ b/ui/view/gamification/gamification.go @@ -58,25 +58,25 @@ func NewModel(db *db.DB) view.View { } // Init starts the gamification view -func (g *Model) Init() tea.Cmd { +func (m *Model) Init() tea.Cmd { return nil } // Update updates the gamification view -func (g *Model) Update(msg tea.Msg) (view.View, tea.Cmd) { +func (m *Model) Update(msg tea.Msg) (view.View, tea.Cmd) { switch msg := msg.(type) { case Msg: - g.leaderboard = msg.leaderboard + m.leaderboard = msg.leaderboard } - return g, nil + return m, nil } // View draws the gamification view -func (g *Model) View() string { - columns := make([]string, 0, len(g.leaderboard)) +func (m *Model) View() string { + columns := make([]string, 0, len(m.leaderboard)) - for i, item := range g.leaderboard { + for i, item := range m.leaderboard { user := lipgloss.JoinVertical(lipgloss.Left, nameStyles[i%len(nameStyles)].Render(fmt.Sprintf("%d. %s", i+1, item.item.Name)), scoreStyle.Render(strconv.Itoa(int(item.item.Score))), @@ -92,21 +92,21 @@ func (g *Model) View() string { } // GetUpdateDatas get all update functions for the gamification view -func (g *Model) GetUpdateDatas() []view.UpdateData { +func (m *Model) GetUpdateDatas() []view.UpdateData { return []view.UpdateData{ { Name: "gamification leaderboard", - View: g, + View: m, Update: updateLeaderboard, Interval: config.GetDefaultInt("tui.gamification.interval_s", 3600), }, } } -func updateLeaderboard(db *db.DB, view view.View) (tea.Msg, error) { - g := view.(*Model) +func updateLeaderboard(view view.View) (tea.Msg, error) { + m := view.(*Model) - gams, err := db.Queries.GetAllGamificationByScore(context.Background()) + gams, err := m.db.Queries.GetAllGamificationByScore(context.Background()) if err != nil { if err == sql.ErrNoRows { err = nil @@ -116,9 +116,9 @@ func updateLeaderboard(db *db.DB, view view.View) (tea.Msg, error) { // Check if both leaderboards are equal equal := false - if len(g.leaderboard) == len(gams) { + if len(m.leaderboard) == len(gams) { equal = true - for i, l := range g.leaderboard { + for i, l := range m.leaderboard { if !l.item.Equal(*dto.GamificationDTO(gams[i])) { equal = false break diff --git a/ui/view/message/message.go b/ui/view/message/message.go index 4e5d33a..fd71013 100644 --- a/ui/view/message/message.go +++ b/ui/view/message/message.go @@ -79,11 +79,11 @@ func (m *Model) GetUpdateDatas() []view.UpdateData { } } -func updateMessages(db *db.DB, view view.View) (tea.Msg, error) { +func updateMessages(view view.View) (tea.Msg, error) { m := view.(*Model) lastMessageID := m.lastMessageID - message, err := db.Queries.GetLastMessage(context.Background()) + message, err := m.db.Queries.GetLastMessage(context.Background()) if err != nil { if err == sql.ErrNoRows { err = nil @@ -95,7 +95,7 @@ func updateMessages(db *db.DB, view view.View) (tea.Msg, error) { return Msg{lastMessageID: lastMessageID, messages: []string{}}, nil } - messages, err := db.Queries.GetMessageSinceID(context.Background(), lastMessageID) + messages, err := m.db.Queries.GetMessageSinceID(context.Background(), lastMessageID) if err != nil { zap.S().Error("DB: Failed to get messages", err) return Msg{lastMessageID: lastMessageID, messages: []string{}}, err diff --git a/ui/view/song/song.go b/ui/view/song/song.go new file mode 100644 index 0000000..691b8b3 --- /dev/null +++ b/ui/view/song/song.go @@ -0,0 +1,305 @@ +// Package song provides the functions to draw an overview of the song integration +package song + +import ( + "context" + "database/sql" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/zeusWPI/scc/internal/pkg/db" + "github.com/zeusWPI/scc/internal/pkg/db/dto" + "github.com/zeusWPI/scc/internal/pkg/lyrics" + "github.com/zeusWPI/scc/pkg/config" + "github.com/zeusWPI/scc/ui/view" + "go.uber.org/zap" +) + +var ( + previousAmount = 5 // Amount of passed lyrics to show + upcomingAmount = 10 // Amount of upcoming lyrics to show +) + +type playing struct { + song *dto.Song + lyrics lyrics.Lyrics + previous []string // Lyrics already sang + current string // Current lyric + upcoming []string // Lyrics that are coming up +} + +// Model represents the view model for song +type Model struct { + db *db.DB + current playing + history []string + topSongs []topStat + topGenres []topStat + topArtists []topStat +} + +// Msg triggers a song data update +// Required for the View interface +type Msg struct{} + +type msgPlaying struct { + current playing +} + +type msgTop struct { + topSongs []topStat + topGenres []topStat + topArtists []topStat +} + +type msgLyrics struct { + song dto.Song + previous []string + current string + upcoming []string + startNext time.Time + done bool +} + +type topStat struct { + name string + amount int +} + +// NewModel initializes a new song model +func NewModel(db *db.DB) view.View { + // Get history, afterwards it gets updated when a new currentSong is detected + history, _ := db.Queries.GetSongHistory(context.Background()) + + return &Model{ + db: db, + current: playing{}, + history: history, + topSongs: make([]topStat, 0, 5), + topGenres: make([]topStat, 0, 5), + topArtists: make([]topStat, 0, 5), + } +} + +// Init starts the song view +func (m *Model) Init() tea.Cmd { + return nil +} + +// Update updates the song view +func (m *Model) Update(msg tea.Msg) (view.View, tea.Cmd) { + switch msg := msg.(type) { + case msgPlaying: + m.history = append(m.history, msg.current.song.Title) + if len(m.history) > 5 { + m.history = m.history[1:] + } + + m.current = msg.current + // New song, start the commands to update the lyrics + lyric, ok := m.current.lyrics.Current() + zap.S().Info("Song ", ok) + if !ok { + // Song already done (shouldn't happen) + m.current = playing{song: nil} + return m, nil + } + zap.S().Info("Starting") + startTime := m.current.song.CreatedAt.Add(lyric.Duration) + zap.S().Info("Startime: ", startTime) + for startTime.Before(time.Now()) { + lyric, ok := m.current.lyrics.Next() + if !ok { + // We're too late to display lyrics + m.current = playing{song: nil} + return m, nil + } + startTime = startTime.Add(lyric.Duration) + zap.S().Info("Startime: ", startTime) + } + m.current.upcoming = lyricsToString(m.current.lyrics.Upcoming(upcomingAmount)) + return m, updateLyrics(m.current, startTime) + case msgTop: + if msg.topSongs != nil { + m.topSongs = msg.topSongs + } + if msg.topGenres != nil { + m.topGenres = msg.topGenres + } + if msg.topArtists != nil { + m.topArtists = msg.topArtists + } + case msgLyrics: + // Check if it's still relevant + if msg.song.ID != m.current.song.ID { + // We already switched to a new song + return m, nil + } + + if msg.done { + // Song has finished. Reset variables + m.current = playing{song: nil} + return m, nil + } + + // Msg is relevant, update values + m.current.previous = msg.previous + m.current.current = msg.current + m.current.upcoming = msg.upcoming + + // Start the cmd to update the lyrics + return m, updateLyrics(m.current, msg.startNext) + } + + return m, nil +} + +// View draws the song view +func (m *Model) View() string { + if m.current.song != nil { + return m.viewPlaying() + } + + return m.viewNotPlaying() + +} + +// GetUpdateDatas gets all update functions for the song view +func (m *Model) GetUpdateDatas() []view.UpdateData { + return []view.UpdateData{ + { + Name: "update current song", + View: m, + Update: updateCurrentSong, + Interval: config.GetDefaultInt("tui.song.interval_current_s", 5), + }, + { + Name: "top stats", + View: m, + Update: updateTopStats, + Interval: config.GetDefaultInt("tui.song.interval_top_s", 3600), + }, + } +} + +func updateCurrentSong(view view.View) (tea.Msg, error) { + zap.S().Info("Updating current song") + m := view.(*Model) + + songs, err := m.db.Queries.GetLastSongFull(context.Background()) + if err != nil { + if err == sql.ErrNoRows { + err = nil + } + zap.S().Info("Updating song: DB error") + return nil, err + } + if len(songs) == 0 { + zap.S().Info("Updating song: No lenght for songs") + return nil, nil + } + + // Check if song is still playing + if songs[0].CreatedAt.Add(time.Duration(songs[0].DurationMs) * time.Millisecond).Before(time.Now()) { + // Song is finished + zap.S().Info("Updating song: Song finished") + return nil, nil + } + + if m.current.song != nil && songs[0].ID == m.current.song.ID { + // Song is already set to current + zap.S().Info("Updating song: already set to current") + return nil, nil + } + + song := dto.SongDTOHistory(songs) + + zap.S().Info("Updating song: returning") + return msgPlaying{current: playing{song: song, lyrics: lyrics.New(song)}}, nil +} + +func updateTopStats(view view.View) (tea.Msg, error) { + m := view.(*Model) + msg := msgTop{} + change := false + + songs, err := m.db.Queries.GetTopSongs(context.Background()) + if err != nil && err != sql.ErrNoRows { + return nil, err + } + + if !equalTopSongs(m.topSongs, songs) { + msg.topSongs = topStatSqlcSong(songs) + change = true + } + + genres, err := m.db.Queries.GetTopGenres(context.Background()) + if err != nil && err != sql.ErrNoRows { + return nil, err + } + + if !equalTopGenres(m.topGenres, genres) { + msg.topGenres = topStatSqlcGenre(genres) + change = true + } + + artists, err := m.db.Queries.GetTopArtists(context.Background()) + if err != nil && err != sql.ErrNoRows { + return nil, err + } + + if !equalTopArtists(m.topArtists, artists) { + msg.topArtists = topStatSqlcArtist(artists) + change = true + } + + if !change { + return nil, nil + } + + return msg, nil +} + +func updateLyrics(song playing, start time.Time) tea.Cmd { + timeout := time.Duration(0) + now := time.Now() + if start.After(now) { + timeout = start.Sub(now) + } + zap.S().Info("Lyrics: updating in ", timeout) + + return tea.Tick(timeout, func(_ time.Time) tea.Msg { + // Next lyric + zap.S().Info("Lyrics: Getting next lyric") + lyric, ok := song.lyrics.Next() + if !ok { + // Song finished + zap.S().Info("Lyrics: song finished") + return msgLyrics{song: *song.song, done: true} + } + + previous := song.lyrics.Previous(previousAmount) + upcoming := song.lyrics.Upcoming(upcomingAmount) + + end := start.Add(lyric.Duration) + + zap.S().Info("Lyrics: Returning: ", msgLyrics{ + previous: lyricsToString(previous), + current: lyric.Text, + upcoming: lyricsToString(upcoming), + startNext: end, + done: false, + }) + + return msgLyrics{ + song: *song.song, + previous: lyricsToString(previous), + current: lyric.Text, + upcoming: lyricsToString(upcoming), + startNext: end, + done: false, + } + }) +} + +// TODO: It always start at the first lyric but if it's behind it should skip forward. diff --git a/ui/view/song/style.go b/ui/view/song/style.go new file mode 100644 index 0000000..f7a2da2 --- /dev/null +++ b/ui/view/song/style.go @@ -0,0 +1,24 @@ +package song + +import "github.com/charmbracelet/lipgloss" + +// Colors +var ( + cZeus = lipgloss.Color("#FF7F00") + cSpotify = lipgloss.Color("#1DB954") +) + +// Styles +var ( + sBase = lipgloss.NewStyle() + sStat = sBase.MarginRight(3) + sStatTitle = sBase.Foreground(cZeus). + BorderStyle(lipgloss.NormalBorder()).BorderBottom(true).BorderForeground(cSpotify) + sStatAmount = sBase.Foreground(cZeus).MarginLeft(2) + sListEnum = sBase.Foreground(cSpotify).MarginRight(1) + + sLyricBase = sBase.Width(50).Align(lipgloss.Center) + sLyricPrevious = sLyricBase.Foreground(cZeus).Faint(true) + sLyricCurrent = sLyricBase.Foreground(cZeus) + sLyricUpcoming = sLyricBase.Foreground(cSpotify) +) diff --git a/ui/view/song/util.go b/ui/view/song/util.go new file mode 100644 index 0000000..182cf89 --- /dev/null +++ b/ui/view/song/util.go @@ -0,0 +1,80 @@ +package song + +import ( + "github.com/zeusWPI/scc/internal/pkg/db/sqlc" + "github.com/zeusWPI/scc/internal/pkg/lyrics" +) + +func equalTopSongs(s1 []topStat, s2 []sqlc.GetTopSongsRow) bool { + if len(s1) != len(s2) { + return false + } + + for i, s := range s1 { + if s.name != s2[i].Title || s.amount != int(s2[i].PlayCount) { + return false + } + } + + return true +} + +func topStatSqlcSong(songs []sqlc.GetTopSongsRow) []topStat { + topstats := make([]topStat, 0, len(songs)) + for _, s := range songs { + topstats = append(topstats, topStat{name: s.Title, amount: int(s.PlayCount)}) + } + return topstats +} + +func equalTopGenres(s1 []topStat, s2 []sqlc.GetTopGenresRow) bool { + if len(s1) != len(s2) { + return false + } + + for i, s := range s1 { + if s.name != s2[i].GenreName || s.amount != int(s2[i].TotalPlays) { + return false + } + } + + return true +} + +func topStatSqlcGenre(songs []sqlc.GetTopGenresRow) []topStat { + topstats := make([]topStat, 0, len(songs)) + for _, s := range songs { + topstats = append(topstats, topStat{name: s.GenreName, amount: int(s.TotalPlays)}) + } + return topstats +} + +func equalTopArtists(s1 []topStat, s2 []sqlc.GetTopArtistsRow) bool { + if len(s1) != len(s2) { + return false + } + + for i, s := range s1 { + if s.name != s2[i].ArtistName || s.amount != int(s2[i].TotalPlays) { + return false + } + } + + return true +} + +func topStatSqlcArtist(songs []sqlc.GetTopArtistsRow) []topStat { + topstats := make([]topStat, 0, len(songs)) + for _, s := range songs { + topstats = append(topstats, topStat{name: s.ArtistName, amount: int(s.TotalPlays)}) + } + return topstats +} + +func lyricsToString(lyrics []lyrics.Lyric) []string { + text := make([]string, 0, len(lyrics)) + for _, lyric := range lyrics { + text = append(text, lyric.Text) + } + return text +} diff --git a/ui/view/song/view.go b/ui/view/song/view.go new file mode 100644 index 0000000..6ab936a --- /dev/null +++ b/ui/view/song/view.go @@ -0,0 +1,63 @@ +package song + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/list" +) + +func (m *Model) viewPlaying() string { + var previousB strings.Builder + for i, lyric := range m.current.previous { + previousB.WriteString(lyric) + if i != len(m.current.previous)-1 { + previousB.WriteString("\n") + } + } + previous := sLyricPrevious.Render(previousB.String()) + + current := sLyricCurrent.Render(m.current.current) + + var upcomingB strings.Builder + for _, lyric := range m.current.upcoming { + upcomingB.WriteString(lyric) + upcomingB.WriteString("\n") + } + upcoming := sLyricUpcoming.Render(upcomingB.String()) + + return sBase.MarginLeft(5).Render(lipgloss.JoinVertical(lipgloss.Left, previous, current, upcoming)) +} + +func (m *Model) viewNotPlaying() string { + columns := make([]string, 0, 3) + + // Recently played + l := list.New(m.history).Enumerator(list.Arabic).EnumeratorStyle(sListEnum).String() + t := sStatTitle.Width(lipgloss.Width(l)).Align(lipgloss.Center).Render("Recently played") + + column := lipgloss.JoinVertical(lipgloss.Left, t, l) + columns = append(columns, sStat.Render(column)) + + // Top stats + topStats := map[string][]topStat{ + "Top Tracks": m.topSongs, + "Top Artists": m.topArtists, + "Top Genres": m.topGenres, + } + + for title, stat := range topStats { + var statInfos []string + for _, statInfo := range stat { + statInfos = append(statInfos, lipgloss.JoinHorizontal(lipgloss.Top, statInfo.name, sStatAmount.Render(fmt.Sprintf("%d", statInfo.amount)))) + } + l := list.New(statInfos).Enumerator(list.Arabic).EnumeratorStyle(sListEnum).String() + t := sStatTitle.Width(lipgloss.Width(l)).Align(lipgloss.Center).Render(title) + + column := lipgloss.JoinVertical(lipgloss.Left, t, l) + columns = append(columns, sStat.Render(column)) + } + + return lipgloss.JoinHorizontal(lipgloss.Top, columns...) +} diff --git a/ui/view/tap/tap.go b/ui/view/tap/tap.go index 5c39477..5278d6a 100644 --- a/ui/view/tap/tap.go +++ b/ui/view/tap/tap.go @@ -47,44 +47,44 @@ func NewModel(db *db.DB) view.View { } // Init initializes the tap model -func (t *Model) Init() tea.Cmd { +func (m *Model) Init() tea.Cmd { return nil } // Update updates the tap model -func (t *Model) Update(msg tea.Msg) (view.View, tea.Cmd) { +func (m *Model) Update(msg tea.Msg) (view.View, tea.Cmd) { switch msg := msg.(type) { case Msg: - t.lastOrderID = msg.lastOrderID + m.lastOrderID = msg.lastOrderID for _, msg := range msg.items { switch msg.category { case "Mate": - t.mate += msg.amount + m.mate += msg.amount case "Soft": - t.soft += msg.amount + m.soft += msg.amount case "Beer": - t.beer += msg.amount + m.beer += msg.amount case "Food": - t.food += msg.amount + m.food += msg.amount } } - return t, nil + return m, nil } - return t, nil + return m, nil } // View returns the tap view -func (t *Model) View() string { +func (m *Model) View() string { chart := barchart.New(20, 20) barMate := barchart.BarData{ Label: "Mate", Values: []barchart.BarValue{{ Name: "Mate", - Value: t.mate, + Value: m.mate, Style: lipgloss.NewStyle().Foreground(tapCategoryColor["Mate"]), }}, } @@ -92,7 +92,7 @@ func (t *Model) View() string { Label: "Soft", Values: []barchart.BarValue{{ Name: "Soft", - Value: t.soft, + Value: m.soft, Style: lipgloss.NewStyle().Foreground(tapCategoryColor["Soft"]), }}, } @@ -100,7 +100,7 @@ func (t *Model) View() string { Label: "Beer", Values: []barchart.BarValue{{ Name: "Beer", - Value: t.beer, + Value: m.beer, Style: lipgloss.NewStyle().Foreground(tapCategoryColor["Beer"]), }}, } @@ -108,7 +108,7 @@ func (t *Model) View() string { Label: "Food", Values: []barchart.BarValue{{ Name: "Food", - Value: t.food, + Value: m.food, Style: lipgloss.NewStyle().Foreground(tapCategoryColor["Food"]), }}, } @@ -120,22 +120,22 @@ func (t *Model) View() string { } // GetUpdateDatas returns all the update functions for the tap model -func (t *Model) GetUpdateDatas() []view.UpdateData { +func (m *Model) GetUpdateDatas() []view.UpdateData { return []view.UpdateData{ { Name: "tap orders", - View: t, + View: m, Update: updateOrders, Interval: config.GetDefaultInt("tui.tap.interval_s", 60), }, } } -func updateOrders(db *db.DB, view view.View) (tea.Msg, error) { - t := view.(*Model) - lastOrderID := t.lastOrderID +func updateOrders(view view.View) (tea.Msg, error) { + m := view.(*Model) + lastOrderID := m.lastOrderID - order, err := db.Queries.GetLastOrderByOrderID(context.Background()) + order, err := m.db.Queries.GetLastOrderByOrderID(context.Background()) if err != nil { if err == sql.ErrNoRows { err = nil @@ -147,7 +147,7 @@ func updateOrders(db *db.DB, view view.View) (tea.Msg, error) { return Msg{lastOrderID: lastOrderID, items: []tapItem{}}, nil } - orders, err := db.Queries.GetOrderCountByCategorySinceOrderID(context.Background(), lastOrderID) + orders, err := m.db.Queries.GetOrderCountByCategorySinceOrderID(context.Background(), lastOrderID) if err != nil { return Msg{lastOrderID: lastOrderID, items: []tapItem{}}, err } diff --git a/ui/view/view.go b/ui/view/view.go index 8168d26..7770c17 100644 --- a/ui/view/view.go +++ b/ui/view/view.go @@ -3,14 +3,13 @@ package view import ( tea "github.com/charmbracelet/bubbletea" - "github.com/zeusWPI/scc/internal/pkg/db" ) // UpdateData represents the data needed to update a view type UpdateData struct { Name string View View - Update func(db *db.DB, view View) (tea.Msg, error) + Update func(view View) (tea.Msg, error) Interval int } diff --git a/ui/view/zess/zess.go b/ui/view/zess/zess.go index 07681e4..85ec91b 100644 --- a/ui/view/zess/zess.go +++ b/ui/view/zess/zess.go @@ -55,7 +55,7 @@ type seasonMsg struct { // NewModel creates a new zess model view func NewModel(db *db.DB) view.View { - z := &Model{ + m := &Model{ db: db, lastScanID: -1, scans: make([]weekScan, 0), @@ -66,43 +66,43 @@ func NewModel(db *db.DB) view.View { // Populate with data // The order in which this is called is important! - msgScans, err := updateScans(db, z) + msgScans, err := updateScans(m) if err != nil { zap.S().Error("TUI: Unable to update zess scans\n", err) - return z + return m } - _, _ = z.Update(msgScans) + _, _ = m.Update(msgScans) - msgSeason, err := updateSeason(db, z) + msgSeason, err := updateSeason(m) if err != nil { zap.S().Error("TUI: Unable to update zess seasons\n", err) - return z + return m } - _, _ = z.Update(msgSeason) + _, _ = m.Update(msgSeason) - return z + return m } // Init created a new zess model -func (z *Model) Init() tea.Cmd { +func (m *Model) Init() tea.Cmd { return nil } // Update updates the zess model -func (z *Model) Update(msg tea.Msg) (view.View, tea.Cmd) { +func (m *Model) Update(msg tea.Msg) (view.View, tea.Cmd) { switch msg := msg.(type) { // New scan(s) case scanMsg: - z.lastScanID = msg.lastScanID + m.lastScanID = msg.lastScanID // Add new scans for _, newScan := range msg.scans { found := false - for i, modelScan := range z.scans { + for i, modelScan := range m.scans { if newScan.time.equal(modelScan.time) { - z.scans[i].amount++ + m.scans[i].amount++ // Check for maxWeekScans - if z.scans[i].amount > z.maxWeekScans { - z.maxWeekScans = modelScan.amount + if m.scans[i].amount > m.maxWeekScans { + m.maxWeekScans = modelScan.amount } found = true @@ -111,54 +111,54 @@ func (z *Model) Update(msg tea.Msg) (view.View, tea.Cmd) { } if !found { - z.scans = append(z.scans, newScan) + m.scans = append(m.scans, newScan) // Check for maxWeekScans - if newScan.amount > z.maxWeekScans { - z.maxWeekScans = newScan.amount + if newScan.amount > m.maxWeekScans { + m.maxWeekScans = newScan.amount } // Make sure the array doesn't get too big - if len(z.scans) > config.GetDefaultInt("tui.zess.weeks", 10) { - z.scans = z.scans[:1] + if len(m.scans) > config.GetDefaultInt("tui.zess.weeks", 10) { + m.scans = m.scans[:1] } } // Update seasonScans - z.seasonScans += newScan.amount + m.seasonScans += newScan.amount } // New season! // Update variables accordinly case seasonMsg: - z.currentSeason = msg.start - z.seasonScans = 0 - z.maxWeekScans = 0 + m.currentSeason = msg.start + m.seasonScans = 0 + m.maxWeekScans = 0 - validScans := make([]weekScan, 0, len(z.scans)) + validScans := make([]weekScan, 0, len(m.scans)) - for _, scan := range z.scans { + for _, scan := range m.scans { // Add scans if they happend after (or in the same week of) the season start - if scan.time.equal(z.currentSeason) || scan.time.after(z.currentSeason) { + if scan.time.equal(m.currentSeason) || scan.time.after(m.currentSeason) { validScans = append(validScans, scan) - if scan.amount > z.maxWeekScans { - z.maxWeekScans = scan.amount + if scan.amount > m.maxWeekScans { + m.maxWeekScans = scan.amount } - z.seasonScans += scan.amount + m.seasonScans += scan.amount } } - z.scans = validScans + m.scans = validScans } - return z, nil + return m, nil } // View returns the view for the zess model -func (z *Model) View() string { +func (m *Model) View() string { chart := barchart.New(20, 20) - for _, scan := range z.scans { + for _, scan := range m.scans { bar := barchart.BarData{ Label: scan.label, Values: []barchart.BarValue{{ @@ -174,8 +174,8 @@ func (z *Model) View() string { chart.Draw() style := lipgloss.NewStyle().Height(20).Align(lipgloss.Bottom).Render(lipgloss.JoinVertical(lipgloss.Left, - fmt.Sprintf("Season scans\n%d", z.seasonScans), - fmt.Sprintf("Max scans in a week\n%d", z.maxWeekScans), + fmt.Sprintf("Season scans\n%d", m.seasonScans), + fmt.Sprintf("Max scans in a week\n%d", m.maxWeekScans), )) return lipgloss.JoinHorizontal(lipgloss.Top, @@ -185,17 +185,17 @@ func (z *Model) View() string { } // GetUpdateDatas returns all the update functions for the zess model -func (z *Model) GetUpdateDatas() []view.UpdateData { +func (m *Model) GetUpdateDatas() []view.UpdateData { return []view.UpdateData{ { Name: "zess scans", - View: z, + View: m, Update: updateScans, Interval: config.GetDefaultInt("tui.zess.interval_scan_s", 60), }, { Name: "zess season", - View: z, + View: m, Update: updateSeason, Interval: config.GetDefaultInt("tui.zess.interval_season_s", 3600), }, @@ -203,12 +203,12 @@ func (z *Model) GetUpdateDatas() []view.UpdateData { } // Check for any new scans -func updateScans(db *db.DB, view view.View) (tea.Msg, error) { - z := view.(*Model) - lastScanID := z.lastScanID +func updateScans(view view.View) (tea.Msg, error) { + m := view.(*Model) + lastScanID := m.lastScanID // Get new scans - scans, err := db.Queries.GetAllScansSinceID(context.Background(), lastScanID) + scans, err := m.db.Queries.GetAllScansSinceID(context.Background(), lastScanID) if err != nil { if err == sql.ErrNoRows { // No rows shouldn't be considered an error @@ -253,10 +253,10 @@ func updateScans(db *db.DB, view view.View) (tea.Msg, error) { } // Check if a new season started -func updateSeason(db *db.DB, view view.View) (tea.Msg, error) { - z := view.(*Model) +func updateSeason(view view.View) (tea.Msg, error) { + m := view.(*Model) - season, err := db.Queries.GetSeasonCurrent(context.Background()) + season, err := m.db.Queries.GetSeasonCurrent(context.Background()) if err != nil { if err == sql.ErrNoRows { // No rows shouldn't be considered an error @@ -268,7 +268,7 @@ func updateSeason(db *db.DB, view view.View) (tea.Msg, error) { // Check if we have a new season yearNumber, weekNumber := season.Start.ISOWeek() seasonStart := time{year: yearNumber, week: weekNumber} - if z.currentSeason.equal(seasonStart) { + if m.currentSeason.equal(seasonStart) { // Same season return nil, nil }