From 2e95a78632ba0f72893d5132d950f202909142cf Mon Sep 17 00:00:00 2001 From: Topvennie Date: Thu, 19 Dec 2024 15:37:04 +0100 Subject: [PATCH] chore(song): show monthly stats --- db/queries/song.sql | 2 +- internal/pkg/db/dto/song.go | 2 +- internal/pkg/db/sqlc/song.sql.go | 2 +- internal/pkg/song/api.go | 16 +++++- tui/components/stopwatch/stopwatch.go | 5 -- tui/view/song/song.go | 5 +- tui/view/song/style.go | 9 ++- tui/view/song/view.go | 81 +++++++++++++++++++-------- 8 files changed, 86 insertions(+), 36 deletions(-) diff --git a/db/queries/song.sql b/db/queries/song.sql index 9ea92a6..210f367 100644 --- a/db/queries/song.sql +++ b/db/queries/song.sql @@ -79,7 +79,7 @@ FROM ( ) aggregated JOIN song s ON aggregated.song_id = s.id ORDER BY aggregated.created_at DESC -LIMIT 20; +LIMIT 50; -- name: GetTopSongs :many SELECT s.id AS song_id, s.title, COUNT(sh.id) AS play_count diff --git a/internal/pkg/db/dto/song.go b/internal/pkg/db/dto/song.go index b678297..401b3dc 100644 --- a/internal/pkg/db/dto/song.go +++ b/internal/pkg/db/dto/song.go @@ -14,7 +14,7 @@ type Song struct { Album string `json:"album"` SpotifyID string `json:"spotify_id" validate:"required"` DurationMS int32 `json:"duration_ms"` - LyricsType string `json:"lyrics_type"` // Either 'synced' or 'plain' + LyricsType string `json:"lyrics_type"` // Either 'synced' ,'plain' or 'instrumental' Lyrics string `json:"lyrics"` CreatedAt time.Time `json:"created_at"` Artists []SongArtist `json:"artists"` diff --git a/internal/pkg/db/sqlc/song.sql.go b/internal/pkg/db/sqlc/song.sql.go index a237b71..16ff8de 100644 --- a/internal/pkg/db/sqlc/song.sql.go +++ b/internal/pkg/db/sqlc/song.sql.go @@ -308,7 +308,7 @@ FROM ( ) aggregated JOIN song s ON aggregated.song_id = s.id ORDER BY aggregated.created_at DESC -LIMIT 10 +LIMIT 50 ` type GetSongHistoryRow struct { diff --git a/internal/pkg/song/api.go b/internal/pkg/song/api.go index 1f651a7..1758e46 100644 --- a/internal/pkg/song/api.go +++ b/internal/pkg/song/api.go @@ -93,6 +93,7 @@ func (s *Song) getArtist(artist *dto.SongArtist) error { } type lyricsResponse struct { + Instrumental bool `json:"instrumental"` PlainLyrics string `json:"plainLyrics"` SyncedLyrics string `json:"SyncedLyrics"` } @@ -126,18 +127,31 @@ func (s *Song) getLyrics(track *dto.Song) error { return errors.Join(append([]error{errors.New("Song: Lyrics request failed")}, errs...)...) } if status != fiber.StatusOK { + if status == fiber.StatusNotFound { + // Lyrics not found + return nil + } + return fmt.Errorf("Song: Lyrics request wrong status code %d", status) } if (res == &lyricsResponse{}) { return errors.New("Song: Lyrics request returned empty struct") } + zap.S().Info(res) + if res.SyncedLyrics != "" { + // Synced lyrics ? track.LyricsType = "synced" track.Lyrics = res.SyncedLyrics - } else { + } else if res.PlainLyrics != "" { + // Plain lyrics ? track.LyricsType = "plain" track.Lyrics = res.PlainLyrics + } else if res.Instrumental { + // Instrumental ? + track.LyricsType = "instrumental" + track.Lyrics = "" } return nil diff --git a/tui/components/stopwatch/stopwatch.go b/tui/components/stopwatch/stopwatch.go index 6bf702b..0d47840 100644 --- a/tui/components/stopwatch/stopwatch.go +++ b/tui/components/stopwatch/stopwatch.go @@ -92,11 +92,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { case StartStopMsg: if msg.running { // Start - if m.running { - // Already running - return m, nil - } - m.id = nextID() m.duration = msg.startDuration m.running = true diff --git a/tui/view/song/song.go b/tui/view/song/song.go index f7488c2..84eb01d 100644 --- a/tui/view/song/song.go +++ b/tui/view/song/song.go @@ -141,7 +141,7 @@ func (m *Model) Update(msg tea.Msg) (view.View, tea.Cmd) { lyric, ok := m.current.lyrics.Current() if !ok { // Shouldn't happen - zap.S().Error("song: unable to get current lyric in initialization phase: ", m.current.song.Title) + zap.S().Error("song: Unable to get current lyric in initialization phase: ", m.current.song.Title) m.current.playing = false return m, nil } @@ -326,9 +326,6 @@ func updateMonthlyStats(view view.View) (tea.Msg, error) { return nil, err } - // Don't bother checking if anything has changed - // A single extra refresh won't matter - msg := msgStats{monthly: true, stats: []stat{}} // Songs diff --git a/tui/view/song/style.go b/tui/view/song/style.go index d88facb..f9b0f90 100644 --- a/tui/view/song/style.go +++ b/tui/view/song/style.go @@ -39,6 +39,11 @@ var ( sStatEnum = base.Foreground(cSpotify).Width(wStatEnum).Align(lipgloss.Left) sStatEntry = base.Align(lipgloss.Left) sStatAmount = base.Foreground(cZeus).Width(wStatAmount).Align(lipgloss.Right) + + // Specific styles for when no song is playing + sStatCategory = base.Align(lipgloss.Center) + sStatCategoryTitle = base.Foreground(cZeus).Align(lipgloss.Center).Border(lipgloss.NormalBorder(), true, false).BorderForeground(cBorder) + sStatHistory = base.MarginRight(1).PaddingRight(2).Border(lipgloss.ThickBorder(), false, true, false, false).BorderForeground(cBorder) ) // Styles for the lyrics @@ -53,7 +58,7 @@ var ( // Styles for the status var ( - sStatus = base + sStatus = base.MarginTop(1) sStatusSong = base.Align(lipgloss.Center) sStatusStopwatch = base.Faint(true) sStatusBar = base.Foreground(cZeus).Align(lipgloss.Left) @@ -77,6 +82,8 @@ func (m *Model) updateStyles() { // We're full screen sStatOne = sStatOne.Margin(0, 3) } + sStatCategory = sStatCategory.Width(2 * (sStatOne.GetWidth() + view.GetOuterWidth(sStatOne))) + sStatCategoryTitle = sStatCategoryTitle.Width(2*sStatOne.GetWidth() + view.GetOuterWidth(sStatOne)) // Adjust lyrics styles sLyric = sLyric.Width(m.width) diff --git a/tui/view/song/view.go b/tui/view/song/view.go index 33d7651..058ddf7 100644 --- a/tui/view/song/view.go +++ b/tui/view/song/view.go @@ -78,48 +78,85 @@ func (m *Model) viewPlayingLyrics() string { func (m *Model) viewPlayingStats() string { columns := make([]string, 0, 4) - columns = append(columns, m.viewStat(m.history)) - columns = append(columns, m.viewStat(m.stats[0])) - columns = append(columns, m.viewStat(m.stats[1])) - columns = append(columns, m.viewStat(m.stats[2])) + columns = append(columns, m.viewStatPlaying(m.history)) + columns = append(columns, m.viewStatPlaying(m.statsMonthly[0])) + columns = append(columns, m.viewStatPlaying(m.statsMonthly[1])) + columns = append(columns, m.viewStatPlaying(m.statsMonthly[2])) return lipgloss.JoinHorizontal(lipgloss.Top, columns...) } func (m *Model) viewNotPlaying() string { - rows := make([][]string, 0, 2) - for i := 0; i < 2; i++ { + // Render stats + rows := make([][]string, 0, 3) + for i := 0; i < 3; i++ { rows = append(rows, make([]string, 0, 2)) } - rows[0] = append(rows[0], m.viewStat(m.history)) - rows[0] = append(rows[0], m.viewStat(m.stats[0])) - rows[1] = append(rows[1], m.viewStat(m.stats[1])) - rows[1] = append(rows[1], m.viewStat(m.stats[2])) + rows[0] = append(rows[0], m.viewStatPlaying(m.statsMonthly[0], "Monthly")) + rows[0] = append(rows[0], m.viewStatPlaying(m.stats[0], "All Time")) + rows[1] = append(rows[1], m.viewStatPlaying(m.statsMonthly[1], "Monthly")) + rows[1] = append(rows[1], m.viewStatPlaying(m.stats[1], "All Time")) + rows[2] = append(rows[2], m.viewStatPlaying(m.statsMonthly[2], "Monthly")) + rows[2] = append(rows[2], m.viewStatPlaying(m.stats[2], "All Time")) + + renderedRows := make([]string, 0, 3) + var title string + for i, row := range rows { + r := lipgloss.JoinHorizontal(lipgloss.Top, row...) + title = sStatCategory.Render(sStatCategoryTitle.Render(m.stats[i].title)) // HACK: Make border same size as 2 stats next to each other + renderedRows = append(renderedRows, lipgloss.JoinVertical(lipgloss.Left, title, r)) + } + + v := lipgloss.JoinVertical(lipgloss.Left, renderedRows...) + + // Render history + items := make([]string, 0, len(m.history.entries)) - renderedRows := make([]string, 0, 2) - for _, row := range rows { - renderedRows = append(renderedRows, lipgloss.JoinHorizontal(lipgloss.Top, row...)) + // Push it down + for range lipgloss.Height(title) { + items = append(items, "") + } + items = append(items, sStatTitle.Render(m.history.title)) + + for i, entry := range m.history.entries { + enum := sStatEnum.Render(fmt.Sprintf("%d.", i+1)) + body := sStatEntry.Render(entry.name) + amount := sStatAmount.Render(fmt.Sprintf("%d", entry.amount)) + items = append(items, lipgloss.JoinHorizontal(lipgloss.Top, enum, body, amount)) } + items = append(items, "") // HACK: Avoid the last item shifting to the right + list := lipgloss.JoinVertical(lipgloss.Left, items...) + // title := sStatTitle.Render(m.history.title) + history := sStatHistory.Height(lipgloss.Height(v) - 1).MaxHeight(lipgloss.Height(v) - 1).Render(list) // - 1 to compensate for the hack newline at the end - view := lipgloss.JoinVertical(lipgloss.Left, renderedRows...) + v = lipgloss.JoinHorizontal(lipgloss.Top, history, v) - return sAll.Render(view) + return sAll.Render(v) } -func (m *Model) viewStat(stat stat) string { +func (m *Model) viewStatPlaying(stat stat, titleOpt ...string) string { + title := stat.title + if len(titleOpt) > 0 { + title = titleOpt[0] + } + items := make([]string, 0, len(stat.entries)) - for i, stat := range stat.entries { + for i := range stat.entries { + if i >= 10 { + break + } + enum := sStatEnum.Render(fmt.Sprintf("%d.", i+1)) - entry := sStatEntry.Render(stat.name) - amount := sStatAmount.Render(fmt.Sprintf("%d", stat.amount)) + body := sStatEntry.Render(stat.entries[i].name) + amount := sStatAmount.Render(fmt.Sprintf("%d", stat.entries[i].amount)) - items = append(items, lipgloss.JoinHorizontal(lipgloss.Top, enum, entry, amount)) + items = append(items, lipgloss.JoinHorizontal(lipgloss.Top, enum, body, amount)) } items = append(items, "") // HACK: Avoid the last item shifting to the right l := lipgloss.JoinVertical(lipgloss.Left, items...) - title := sStatTitle.Render(stat.title) + t := sStatTitle.Render(title) - return sStatOne.Render(lipgloss.JoinVertical(lipgloss.Left, title, l)) + return sStatOne.Render(lipgloss.JoinVertical(lipgloss.Left, t, l)) }