diff --git a/db/entities.go b/db/entities.go index dc39ef8..6829863 100644 --- a/db/entities.go +++ b/db/entities.go @@ -510,6 +510,19 @@ type TopArtist struct { ListenCount int } +type TopAlbum struct { + AlbumName string + Artist string + CoverUrl string + ListenCount int +} + +type TopTrack struct { + SongName string + Artist string + ListenCount int +} + func GetTopArtists(userId int, limit int, startDate, endDate *time.Time) ([]TopArtist, error) { var err error var rows pgx.Rows @@ -579,6 +592,130 @@ func GetTopArtists(userId int, limit int, startDate, endDate *time.Time) ([]TopA return topArtists, nil } +func GetTopAlbums(userId int, limit int, startDate, endDate *time.Time) ([]TopAlbum, error) { + var err error + var rows pgx.Rows + + if startDate == nil && endDate == nil { + rows, err = Pool.Query(context.Background(), + `SELECT h.album_name, h.artist, COALESCE(a.cover_url, ''), COUNT(*) as listen_count + FROM history h + LEFT JOIN albums a ON a.user_id = h.user_id AND a.title = h.album_name AND a.artist_id IN (SELECT id FROM artists WHERE user_id = h.user_id AND name = h.artist) + WHERE h.user_id = $1 AND h.album_name IS NOT NULL AND h.album_name != '' + GROUP BY h.album_name, h.artist, a.cover_url + ORDER BY listen_count DESC + LIMIT $2`, + userId, limit) + } else if startDate != nil && endDate == nil { + rows, err = Pool.Query(context.Background(), + `SELECT h.album_name, h.artist, COALESCE(a.cover_url, ''), COUNT(*) as listen_count + FROM history h + LEFT JOIN albums a ON a.user_id = h.user_id AND a.title = h.album_name AND a.artist_id IN (SELECT id FROM artists WHERE user_id = h.user_id AND name = h.artist) + WHERE h.user_id = $1 AND h.timestamp >= $2 AND h.album_name IS NOT NULL AND h.album_name != '' + GROUP BY h.album_name, h.artist, a.cover_url + ORDER BY listen_count DESC + LIMIT $3`, + userId, startDate, limit) + } else if startDate == nil && endDate != nil { + rows, err = Pool.Query(context.Background(), + `SELECT h.album_name, h.artist, COALESCE(a.cover_url, ''), COUNT(*) as listen_count + FROM history h + LEFT JOIN albums a ON a.user_id = h.user_id AND a.title = h.album_name AND a.artist_id IN (SELECT id FROM artists WHERE user_id = h.user_id AND name = h.artist) + WHERE h.user_id = $1 AND h.timestamp <= $2 AND h.album_name IS NOT NULL AND h.album_name != '' + GROUP BY h.album_name, h.artist, a.cover_url + ORDER BY listen_count DESC + LIMIT $3`, + userId, endDate, limit) + } else { + rows, err = Pool.Query(context.Background(), + `SELECT h.album_name, h.artist, COALESCE(a.cover_url, ''), COUNT(*) as listen_count + FROM history h + LEFT JOIN albums a ON a.user_id = h.user_id AND a.title = h.album_name AND a.artist_id IN (SELECT id FROM artists WHERE user_id = h.user_id AND name = h.artist) + WHERE h.user_id = $1 AND h.timestamp >= $2 AND h.timestamp <= $3 AND h.album_name IS NOT NULL AND h.album_name != '' + GROUP BY h.album_name, h.artist, a.cover_url + ORDER BY listen_count DESC + LIMIT $4`, + userId, startDate, endDate, limit) + } + + if err != nil { + return nil, err + } + defer rows.Close() + + var topAlbums []TopAlbum + for rows.Next() { + var albumName, artist, coverUrl string + var count int + err := rows.Scan(&albumName, &artist, &coverUrl, &count) + if err != nil { + return nil, err + } + topAlbums = append(topAlbums, TopAlbum{AlbumName: albumName, Artist: artist, CoverUrl: coverUrl, ListenCount: count}) + } + return topAlbums, nil +} + +func GetTopTracks(userId int, limit int, startDate, endDate *time.Time) ([]TopTrack, error) { + var err error + var rows pgx.Rows + + if startDate == nil && endDate == nil { + rows, err = Pool.Query(context.Background(), + `SELECT song_name, artist, COUNT(*) as listen_count + FROM history + WHERE user_id = $1 + GROUP BY song_name, artist + ORDER BY listen_count DESC + LIMIT $2`, + userId, limit) + } else if startDate != nil && endDate == nil { + rows, err = Pool.Query(context.Background(), + `SELECT song_name, artist, COUNT(*) as listen_count + FROM history + WHERE user_id = $1 AND timestamp >= $2 + GROUP BY song_name, artist + ORDER BY listen_count DESC + LIMIT $3`, + userId, startDate, limit) + } else if startDate == nil && endDate != nil { + rows, err = Pool.Query(context.Background(), + `SELECT song_name, artist, COUNT(*) as listen_count + FROM history + WHERE user_id = $1 AND timestamp <= $2 + GROUP BY song_name, artist + ORDER BY listen_count DESC + LIMIT $3`, + userId, endDate, limit) + } else { + rows, err = Pool.Query(context.Background(), + `SELECT song_name, artist, COUNT(*) as listen_count + FROM history + WHERE user_id = $1 AND timestamp >= $2 AND timestamp <= $3 + GROUP BY song_name, artist + ORDER BY listen_count DESC + LIMIT $4`, + userId, startDate, endDate, limit) + } + + if err != nil { + return nil, err + } + defer rows.Close() + + var topTracks []TopTrack + for rows.Next() { + var songName, artist string + var count int + err := rows.Scan(&songName, &artist, &count) + if err != nil { + return nil, err + } + topTracks = append(topTracks, TopTrack{SongName: songName, Artist: artist, ListenCount: count}) + } + return topTracks, nil +} + func GetSongStats(userId, songId int) (int, error) { var count int err := Pool.QueryRow(context.Background(), diff --git a/static/profile.js b/static/profile.js index 44a0d50..647c322 100644 --- a/static/profile.js +++ b/static/profile.js @@ -32,18 +32,89 @@ function updateLimitOptions() { for (let option of limitSelect.options) { const value = parseInt(option.value); - if (value > maxLimit) { + if (value > maxLimit || (view === 'grid' && value === 7)) { option.style.display = 'none'; } else { option.style.display = ''; } } - if (parseInt(limitSelect.value) > maxLimit) { + if (parseInt(limitSelect.value) > maxLimit || (view === 'grid' && parseInt(limitSelect.value) === 7)) { limitSelect.value = maxLimit; } } +function updateTopAlbums() { + const period = document.getElementById('album-period-select').value; + const limit = document.getElementById('album-limit-select').value; + const view = document.getElementById('album-view-select').value; + + const customDates = document.getElementById('album-custom-dates'); + if (period === 'custom') { + customDates.style.display = 'inline-block'; + } else { + customDates.style.display = 'none'; + } + + const params = new URLSearchParams(window.location.search); + params.set('album_period', period); + params.set('album_limit', limit); + params.set('album_view', view); + + if (period === 'custom') { + const startDate = document.getElementById('album-start-date').value; + const endDate = document.getElementById('album-end-date').value; + if (startDate) params.set('album_start', startDate); + if (endDate) params.set('album_end', endDate); + } + + window.location.search = params.toString(); +} + +function updateTopAlbumsLimitOptions() { + const view = document.getElementById('album-view-select').value; + const limitSelect = document.getElementById('album-limit-select'); + const maxLimit = view === 'grid' ? 8 : 30; + + for (let option of limitSelect.options) { + const value = parseInt(option.value); + if (value > maxLimit || (view === 'grid' && value === 7)) { + option.style.display = 'none'; + } else { + option.style.display = ''; + } + } + + if (parseInt(limitSelect.value) > maxLimit || (view === 'grid' && parseInt(limitSelect.value) === 7)) { + limitSelect.value = maxLimit; + } +} + +function updateTopTracks() { + const period = document.getElementById('track-period-select').value; + const limit = document.getElementById('track-limit-select').value; + + const customDates = document.getElementById('track-custom-dates'); + if (period === 'custom') { + customDates.style.display = 'inline-block'; + } else { + customDates.style.display = 'none'; + } + + const params = new URLSearchParams(window.location.search); + params.set('track_period', period); + params.set('track_limit', limit); + + if (period === 'custom') { + const startDate = document.getElementById('track-start-date').value; + const endDate = document.getElementById('track-end-date').value; + if (startDate) params.set('track_start', startDate); + if (endDate) params.set('track_end', endDate); + } + + window.location.search = params.toString(); +} + function syncGridHeights() {} document.addEventListener('DOMContentLoaded', function() { @@ -57,4 +128,6 @@ document.addEventListener('DOMContentLoaded', function() { } updateLimitOptions(); + + updateTopAlbumsLimitOptions(); }); diff --git a/static/style.css b/static/style.css index b386932..a3269c7 100644 --- a/static/style.css +++ b/static/style.css @@ -770,6 +770,22 @@ a.button:hover { border-radius: 10px; } +.top-albums { + width: 50%; + margin: 15px 0; + padding: 10px; + background: #1a1a1a; + border-radius: 10px; +} + +.top-tracks { + width: 50%; + margin: 15px 0; + padding: 10px; + background: #1a1a1a; + border-radius: 10px; +} + .top-artists-controls { display: flex; flex-direction: column; @@ -777,13 +793,34 @@ a.button:hover { margin-bottom: 10px; } +.top-albums-controls { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 10px; +} + +.top-tracks-controls { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 10px; +} + #top-artists-display { min-height: 150px; } +#top-albums-display { + min-height: 150px; +} + +#top-tracks-display { + min-height: 150px; +} + .top-artists-controls h3 { margin: 0; - color: #fff; } .controls-row { diff --git a/templates/profile.gohtml b/templates/profile.gohtml index d444fa7..3d9fb41 100644 --- a/templates/profile.gohtml +++ b/templates/profile.gohtml @@ -157,6 +157,204 @@ {{end}} +