mirror of
https://github.com/riwiwa/muzi.git
synced 2026-03-03 16:51:49 -08:00
add top artists display to profile
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgtype"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type Artist struct {
|
||||
@@ -504,6 +505,80 @@ func GetArtistStats(userId, artistId int) (int, error) {
|
||||
return count, err
|
||||
}
|
||||
|
||||
type TopArtist struct {
|
||||
Artist Artist
|
||||
ListenCount int
|
||||
}
|
||||
|
||||
func GetTopArtists(userId int, limit int, startDate, endDate *time.Time) ([]TopArtist, error) {
|
||||
var err error
|
||||
var rows pgx.Rows
|
||||
|
||||
if startDate == nil && endDate == nil {
|
||||
rows, err = Pool.Query(context.Background(),
|
||||
`SELECT a.id, a.user_id, a.name, a.image_url, a.bio, a.spotify_id, a.musicbrainz_id, COUNT(*) as listen_count
|
||||
FROM artists a
|
||||
JOIN history h ON h.user_id = a.user_id AND a.id = ANY(h.artist_ids)
|
||||
WHERE h.user_id = $1
|
||||
GROUP BY a.id
|
||||
ORDER BY listen_count DESC
|
||||
LIMIT $2`,
|
||||
userId, limit)
|
||||
} else if startDate != nil && endDate == nil {
|
||||
rows, err = Pool.Query(context.Background(),
|
||||
`SELECT a.id, a.user_id, a.name, a.image_url, a.bio, a.spotify_id, a.musicbrainz_id, COUNT(*) as listen_count
|
||||
FROM artists a
|
||||
JOIN history h ON h.user_id = a.user_id AND a.id = ANY(h.artist_ids)
|
||||
WHERE h.user_id = $1 AND h.timestamp >= $2
|
||||
GROUP BY a.id
|
||||
ORDER BY listen_count DESC
|
||||
LIMIT $3`,
|
||||
userId, startDate, limit)
|
||||
} else if startDate == nil && endDate != nil {
|
||||
rows, err = Pool.Query(context.Background(),
|
||||
`SELECT a.id, a.user_id, a.name, a.image_url, a.bio, a.spotify_id, a.musicbrainz_id, COUNT(*) as listen_count
|
||||
FROM artists a
|
||||
JOIN history h ON h.user_id = a.user_id AND a.id = ANY(h.artist_ids)
|
||||
WHERE h.user_id = $1 AND h.timestamp <= $2
|
||||
GROUP BY a.id
|
||||
ORDER BY listen_count DESC
|
||||
LIMIT $3`,
|
||||
userId, endDate, limit)
|
||||
} else {
|
||||
rows, err = Pool.Query(context.Background(),
|
||||
`SELECT a.id, a.user_id, a.name, a.image_url, a.bio, a.spotify_id, a.musicbrainz_id, COUNT(*) as listen_count
|
||||
FROM artists a
|
||||
JOIN history h ON h.user_id = a.user_id AND a.id = ANY(h.artist_ids)
|
||||
WHERE h.user_id = $1 AND h.timestamp >= $2 AND h.timestamp <= $3
|
||||
GROUP BY a.id
|
||||
ORDER BY listen_count DESC
|
||||
LIMIT $4`,
|
||||
userId, startDate, endDate, limit)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var topArtists []TopArtist
|
||||
for rows.Next() {
|
||||
var a Artist
|
||||
var count int
|
||||
var imageUrlPg, bioPg, spotifyIdPg, musicbrainzIdPg pgtype.Text
|
||||
err := rows.Scan(&a.Id, &a.UserId, &a.Name, &imageUrlPg, &bioPg, &spotifyIdPg, &musicbrainzIdPg, &count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.ImageUrl = imageUrlPg.String
|
||||
a.Bio = bioPg.String
|
||||
a.SpotifyId = spotifyIdPg.String
|
||||
a.MusicbrainzId = musicbrainzIdPg.String
|
||||
topArtists = append(topArtists, TopArtist{Artist: a, ListenCount: count})
|
||||
}
|
||||
return topArtists, nil
|
||||
}
|
||||
|
||||
func GetSongStats(userId, songId int) (int, error) {
|
||||
var count int
|
||||
err := Pool.QueryRow(context.Background(),
|
||||
|
||||
60
static/profile.js
Normal file
60
static/profile.js
Normal file
@@ -0,0 +1,60 @@
|
||||
function updateTopArtists() {
|
||||
const period = document.getElementById('period-select').value;
|
||||
const limit = document.getElementById('limit-select').value;
|
||||
const view = document.getElementById('view-select').value;
|
||||
|
||||
const customDates = document.getElementById('custom-dates');
|
||||
if (period === 'custom') {
|
||||
customDates.style.display = 'inline-block';
|
||||
} else {
|
||||
customDates.style.display = 'none';
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set('period', period);
|
||||
params.set('limit', limit);
|
||||
params.set('view', view);
|
||||
|
||||
if (period === 'custom') {
|
||||
const startDate = document.getElementById('start-date').value;
|
||||
const endDate = document.getElementById('end-date').value;
|
||||
if (startDate) params.set('start', startDate);
|
||||
if (endDate) params.set('end', endDate);
|
||||
}
|
||||
|
||||
window.location.search = params.toString();
|
||||
}
|
||||
|
||||
function updateLimitOptions() {
|
||||
const view = document.getElementById('view-select').value;
|
||||
const limitSelect = document.getElementById('limit-select');
|
||||
const maxLimit = view === 'grid' ? 8 : 30;
|
||||
|
||||
for (let option of limitSelect.options) {
|
||||
const value = parseInt(option.value);
|
||||
if (value > maxLimit) {
|
||||
option.style.display = 'none';
|
||||
} else {
|
||||
option.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (parseInt(limitSelect.value) > maxLimit) {
|
||||
limitSelect.value = maxLimit;
|
||||
}
|
||||
}
|
||||
|
||||
function syncGridHeights() {}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const customDates = document.getElementById('custom-dates');
|
||||
const periodSelect = document.getElementById('period-select');
|
||||
|
||||
if (periodSelect && customDates) {
|
||||
if (periodSelect.value === 'custom') {
|
||||
customDates.style.display = 'inline-block';
|
||||
}
|
||||
}
|
||||
|
||||
updateLimitOptions();
|
||||
});
|
||||
447
static/style.css
447
static/style.css
@@ -760,3 +760,450 @@ a.button:hover {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Top Artists Section */
|
||||
.top-artists {
|
||||
width: 50%;
|
||||
margin: 15px 0;
|
||||
padding: 10px;
|
||||
background: #1a1a1a;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.top-artists-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#top-artists-display {
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.top-artists-controls h3 {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.controls-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.controls-row label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.controls-row select {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
background: #333;
|
||||
color: #AFA;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.controls-row select:hover {
|
||||
border-color: #AFA;
|
||||
}
|
||||
|
||||
.controls-row input[type="date"] {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
background: #333;
|
||||
color: #AFA;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Artist Grid - New layout */
|
||||
.artist-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.artist-grid-odd {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.artist-grid-odd .artist-cell-first {
|
||||
aspect-ratio: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.artist-grid-odd .artist-right-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.artist-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.artist-grid-odd .artist-row {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.artist-cell {
|
||||
flex: 1;
|
||||
display: block;
|
||||
background: #2a2a2a;
|
||||
transition: background 0.2s;
|
||||
transition: transform 0.1s ease;
|
||||
border-radius: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.artist-cell-first {
|
||||
display: block;
|
||||
transition: background 0.2s;
|
||||
transition: transform 0.1s ease;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.artist-cell a,
|
||||
.artist-cell-first a {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.artist-cell,
|
||||
.artist-cell-first {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.artist-cell .grid-items-cover-image,
|
||||
.artist-cell-first .grid-items-cover-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.artist-cell .grid-items-cover-image-image,
|
||||
.artist-cell-first .grid-items-cover-image-image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.artist-cell .grid-items-cover-image-image img,
|
||||
.artist-cell-first .grid-items-cover-image-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.artist-cell .artist-placeholder,
|
||||
.artist-cell-first .artist-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #333;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.artist-cell:hover .grid-items-cover-image-image img,
|
||||
.artist-cell-first:hover .grid-items-cover-image-image img,
|
||||
.artist-cell:hover .artist-placeholder,
|
||||
.artist-cell-first:hover .artist-placeholder {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.artist-cell .grid-items-item-details,
|
||||
.artist-cell-first .grid-items-item-details {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
background: linear-gradient(#0000008c, rgba(0,0,0,0.8));
|
||||
box-sizing: border-box;
|
||||
border-radius: 0 0 10px 10px;
|
||||
}
|
||||
|
||||
.artist-cell .grid-items-item-main-text,
|
||||
.artist-cell-first .grid-items-item-main-text {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.artist-cell .grid-items-item-aux-text,
|
||||
.artist-cell-first .grid-items-item-aux-text {
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* List View */
|
||||
.artist-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.grid-large .artist-card-large img,
|
||||
.grid-large .artist-card-large .artist-placeholder-large {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.grid-large .small-group {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.grid-large .small-row {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.grid-large .artist-card-small {
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.grid-large .artist-card-small img,
|
||||
.grid-large .artist-card-small .artist-placeholder-small {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.artist-placeholder-large {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
background: #333;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.grid-small-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.artist-card-large,
|
||||
.artist-card-small {
|
||||
display: block;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.artist-card-large img,
|
||||
.artist-card-small img {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.artist-card-large:hover img,
|
||||
.artist-card-small:hover img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.artist-card-large img,
|
||||
.artist-card-small img {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.artist-card-large:hover img,
|
||||
.artist-card-small:hover img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.artist-placeholder-small {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
background: #333;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.artist-info {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 12px 6px 4px;
|
||||
background: linear-gradient(transparent, rgba(0,0,0,0.9));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.artist-name {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: 9px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.artist-count {
|
||||
color: #AFA;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
/* Grid View - Even */
|
||||
.grid-even {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.grid-even .small-row {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.grid-even .artist-card-small {
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.grid-even .artist-card-small img,
|
||||
.grid-even .artist-card-small .artist-placeholder-small {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.grid-even .artist-card-small img,
|
||||
.grid-even .artist-card-small .artist-placeholder-small {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* List View */
|
||||
.artist-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.artist-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.artist-cell:hover {
|
||||
background: #333;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.artist-cell-first:hover {
|
||||
background: #333;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.artist-row img {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.artist-placeholder-row {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
background: #444;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.artist-row .artist-name {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.artist-row .artist-count {
|
||||
text-align: right;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* Responsive Artist Grid */
|
||||
@media (max-width: 600px) {
|
||||
.artist-grid {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.artist-grid-odd {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.artist-grid-odd .artist-cell-first {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.artist-grid-odd .artist-right-col {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.artist-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.artist-cell {
|
||||
min-width: calc(50% - 4px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.artist-grid-odd .artist-right-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.artist-cell {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
{{ if eq .TemplateName "album"}}{{block "album" .}}{{end}}{{end}}
|
||||
|
||||
<script src="/files/menu.js"></script>
|
||||
{{if eq .TemplateName "profile"}}
|
||||
<script src="/files/profile.js"></script>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
@@ -13,6 +13,150 @@
|
||||
<h3>{{formatInt .ArtistCount}}</h3> <p>Artists<p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-artists">
|
||||
<div class="top-artists-controls">
|
||||
<h3>Top Artists</h3>
|
||||
<div class="controls-row">
|
||||
<label>
|
||||
Period:
|
||||
<select id="period-select" onchange="updateTopArtists()">
|
||||
<option value="all_time" {{if eq .TopArtistsPeriod "all_time"}}selected{{end}}>All Time</option>
|
||||
<option value="week" {{if eq .TopArtistsPeriod "week"}}selected{{end}}>Last 7 Days</option>
|
||||
<option value="month" {{if eq .TopArtistsPeriod "month"}}selected{{end}}>Last 30 Days</option>
|
||||
<option value="year" {{if eq .TopArtistsPeriod "year"}}selected{{end}}>Last Year</option>
|
||||
<option value="custom" {{if eq .TopArtistsPeriod "custom"}}selected{{end}}>Custom</option>
|
||||
</select>
|
||||
</label>
|
||||
<div id="custom-dates" style="display: {{if eq .TopArtistsPeriod "custom"}}inline-block{{else}}none{{end}};">
|
||||
<input type="date" id="start-date" onchange="updateTopArtists()">
|
||||
<input type="date" id="end-date" onchange="updateTopArtists()">
|
||||
</div>
|
||||
<label>
|
||||
Count:
|
||||
<select id="limit-select" onchange="updateTopArtists()">
|
||||
<option value="5" {{if eq .TopArtistsLimit 5}}selected{{end}}>5</option>
|
||||
<option value="6" {{if eq .TopArtistsLimit 6}}selected{{end}}>6</option>
|
||||
<option value="7" {{if eq .TopArtistsLimit 7}}selected{{end}}>7</option>
|
||||
<option value="8" {{if eq .TopArtistsLimit 8}}selected{{end}}>8</option>
|
||||
<option value="9" {{if eq .TopArtistsLimit 9}}selected{{end}}>9</option>
|
||||
<option value="10" {{if eq .TopArtistsLimit 10}}selected{{end}}>10</option>
|
||||
<option value="15" {{if eq .TopArtistsLimit 15}}selected{{end}}>15</option>
|
||||
<option value="20" {{if eq .TopArtistsLimit 20}}selected{{end}}>20</option>
|
||||
<option value="25" {{if eq .TopArtistsLimit 25}}selected{{end}}>25</option>
|
||||
<option value="30" {{if eq .TopArtistsLimit 30}}selected{{end}}>30</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
View:
|
||||
<select id="view-select" onchange="updateLimitOptions(); updateTopArtists()">
|
||||
<option value="grid" {{if eq .TopArtistsView "grid"}}selected{{end}}>Grid</option>
|
||||
<option value="list" {{if eq .TopArtistsView "list"}}selected{{end}}>List</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{{if .TopArtists}}
|
||||
<div id="top-artists-display" class="top-artists-{{.TopArtistsView}}">
|
||||
{{$view := .TopArtistsView}}
|
||||
{{$artists := .TopArtists}}
|
||||
{{$len := len $artists}}
|
||||
{{if eq $view "grid"}}
|
||||
<div class="artist-grid {{if mod $len 2}}artist-grid-odd{{end}}">
|
||||
{{if mod $len 2}}
|
||||
<div class="artist-cell-first">
|
||||
<a href="/profile/{{$.Username}}/artist/{{urlquery (index $artists 0).Artist.Name}}" class="grid-items-cover-image">
|
||||
<div class="grid-items-cover-image-image">
|
||||
{{if (index $artists 0).Artist.ImageUrl}}
|
||||
<img src="{{(index $artists 0).Artist.ImageUrl}}" alt="{{(index $artists 0).Artist.Name}}">
|
||||
{{else}}
|
||||
<div class="artist-placeholder"></div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="grid-items-item-details">
|
||||
<p class="grid-items-item-main-text">{{(index $artists 0).Artist.Name}}</p>
|
||||
<p class="grid-items-item-aux-text">{{formatInt (index $artists 0).ListenCount}} plays</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="artist-right-col">
|
||||
<div class="artist-row">
|
||||
{{range $i, $a := slice $artists 1 (add 1 (div (sub $len 1) 2))}}
|
||||
<div class="artist-cell">
|
||||
<a href="/profile/{{$.Username}}/artist/{{urlquery $a.Artist.Name}}" class="grid-items-cover-image">
|
||||
<div class="grid-items-cover-image-image">
|
||||
{{if $a.Artist.ImageUrl}}<img src="{{$a.Artist.ImageUrl}}" alt="{{$a.Artist.Name}}">{{else}}<div class="artist-placeholder"></div>{{end}}
|
||||
</div>
|
||||
<div class="grid-items-item-details">
|
||||
<p class="grid-items-item-main-text">{{$a.Artist.Name}}</p>
|
||||
<p class="grid-items-item-aux-text">{{formatInt $a.ListenCount}} plays</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="artist-row">
|
||||
{{range $i, $a := slice $artists (add 1 (div (sub $len 1) 2)) $len}}
|
||||
<div class="artist-cell">
|
||||
<a href="/profile/{{$.Username}}/artist/{{urlquery $a.Artist.Name}}" class="grid-items-cover-image">
|
||||
<div class="grid-items-cover-image-image">
|
||||
{{if $a.Artist.ImageUrl}}<img src="{{$a.Artist.ImageUrl}}" alt="{{$a.Artist.Name}}">{{else}}<div class="artist-placeholder"></div>{{end}}
|
||||
</div>
|
||||
<div class="grid-items-item-details">
|
||||
<p class="grid-items-item-main-text">{{$a.Artist.Name}}</p>
|
||||
<p class="grid-items-item-aux-text">{{formatInt $a.ListenCount}} plays</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="artist-row">
|
||||
{{range $i, $a := slice $artists 0 (div $len 2)}}
|
||||
<div class="artist-cell">
|
||||
<a href="/profile/{{$.Username}}/artist/{{urlquery $a.Artist.Name}}" class="grid-items-cover-image">
|
||||
<div class="grid-items-cover-image-image">
|
||||
{{if $a.Artist.ImageUrl}}<img src="{{$a.Artist.ImageUrl}}" alt="{{$a.Artist.Name}}">{{else}}<div class="artist-placeholder"></div>{{end}}
|
||||
</div>
|
||||
<div class="grid-items-item-details">
|
||||
<p class="grid-items-item-main-text">{{$a.Artist.Name}}</p>
|
||||
<p class="grid-items-item-aux-text">{{formatInt $a.ListenCount}} plays</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="artist-row">
|
||||
{{range $i, $a := slice $artists (div $len 2) $len}}
|
||||
<div class="artist-cell">
|
||||
<a href="/profile/{{$.Username}}/artist/{{urlquery $a.Artist.Name}}" class="grid-items-cover-image">
|
||||
<div class="grid-items-cover-image-image">
|
||||
{{if $a.Artist.ImageUrl}}<img src="{{$a.Artist.ImageUrl}}" alt="{{$a.Artist.Name}}">{{else}}<div class="artist-placeholder"></div>{{end}}
|
||||
</div>
|
||||
<div class="grid-items-item-details">
|
||||
<p class="grid-items-item-main-text">{{$a.Artist.Name}}</p>
|
||||
<p class="grid-items-item-aux-text">{{formatInt $a.ListenCount}} plays</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="artist-list">
|
||||
{{range $a := $artists}}
|
||||
<a href="/profile/{{$.Username}}/artist/{{urlquery $a.Artist.Name}}" class="artist-row">
|
||||
{{if $a.Artist.ImageUrl}}<img src="{{$a.Artist.ImageUrl}}" alt="{{$a.Artist.Name}}">{{else}}<div class="artist-placeholder-row"></div>{{end}}
|
||||
<span class="artist-name">{{$a.Artist.Name}}</span>
|
||||
<span class="artist-count">{{formatInt $a.ListenCount}} plays</span>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="history">
|
||||
<h3>Listening History</h3>
|
||||
<table>
|
||||
@@ -39,7 +183,7 @@
|
||||
{{- range $i, $name := $artistNames}}{{if $i}}, {{end}}<a href="/profile/{{$username}}/artist/{{urlquery $name}}">{{$name}}</a>{{end}}
|
||||
</td>
|
||||
<td><a href="/profile/{{$username}}/song/{{urlquery (index $.Artists $index)}}/{{urlquery $title}}">{{$title}}</a></td>
|
||||
<td title="{{formatTimestampFull (index $times $index)}}">{{formatTimestamp (index $times $index)}}</td>
|
||||
<td><span title="{{formatTimestampFull (index $times $index)}}">{{formatTimestamp (index $times $index)}}</span></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
|
||||
@@ -34,6 +34,10 @@ type ProfileData struct {
|
||||
TemplateName string
|
||||
NowPlayingArtist string
|
||||
NowPlayingTitle string
|
||||
TopArtists []db.TopArtist
|
||||
TopArtistsPeriod string
|
||||
TopArtistsLimit int
|
||||
TopArtistsView string
|
||||
}
|
||||
|
||||
// Render a page of the profile in the URL
|
||||
@@ -85,6 +89,72 @@ func profilePageHandler() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
period := r.URL.Query().Get("period")
|
||||
if period == "" {
|
||||
period = "all_time"
|
||||
}
|
||||
|
||||
view := r.URL.Query().Get("view")
|
||||
if view == "" {
|
||||
view = "grid"
|
||||
}
|
||||
|
||||
maxLimit := 30
|
||||
if view == "grid" {
|
||||
maxLimit = 8
|
||||
}
|
||||
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
limit := 10
|
||||
if limitStr != "" {
|
||||
limit, err = strconv.Atoi(limitStr)
|
||||
if err != nil || limit < 5 {
|
||||
limit = 10
|
||||
}
|
||||
if limit > maxLimit {
|
||||
limit = maxLimit
|
||||
}
|
||||
}
|
||||
|
||||
profileData.TopArtistsPeriod = period
|
||||
profileData.TopArtistsLimit = limit
|
||||
profileData.TopArtistsView = view
|
||||
|
||||
var startDate, endDate *time.Time
|
||||
now := time.Now()
|
||||
switch period {
|
||||
case "week":
|
||||
start := now.AddDate(0, 0, -7)
|
||||
startDate = &start
|
||||
case "month":
|
||||
start := now.AddDate(0, -1, 0)
|
||||
startDate = &start
|
||||
case "year":
|
||||
start := now.AddDate(-1, 0, 0)
|
||||
startDate = &start
|
||||
case "custom":
|
||||
startStr := r.URL.Query().Get("start")
|
||||
endStr := r.URL.Query().Get("end")
|
||||
if startStr != "" {
|
||||
if t, err := time.Parse("2006-01-02", startStr); err == nil {
|
||||
startDate = &t
|
||||
}
|
||||
}
|
||||
if endStr != "" {
|
||||
if t, err := time.Parse("2006-01-02", endStr); err == nil {
|
||||
t = t.AddDate(0, 0, 1)
|
||||
endDate = &t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
topArtists, err := db.GetTopArtists(userId, limit, startDate, endDate)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Cannot get top artists: %v\n", err)
|
||||
} else {
|
||||
profileData.TopArtists = topArtists
|
||||
}
|
||||
|
||||
if pageInt == 1 {
|
||||
if np, ok := scrobble.GetNowPlaying(userId); ok {
|
||||
profileData.NowPlayingArtist = np.Artist
|
||||
|
||||
43
web/utils.go
43
web/utils.go
@@ -19,6 +19,49 @@ func add(a int, b int) int {
|
||||
return a + b
|
||||
}
|
||||
|
||||
// Divides two integers (integer division)
|
||||
func div(a int, b int) int {
|
||||
if b == 0 {
|
||||
return 0
|
||||
}
|
||||
return a / b
|
||||
}
|
||||
|
||||
// Returns a % b
|
||||
func mod(a int, b int) int {
|
||||
return a % b
|
||||
}
|
||||
|
||||
// Returns a slice of a slice from start to end
|
||||
func slice(a []db.TopArtist, start int, end int) []db.TopArtist {
|
||||
if start >= len(a) {
|
||||
return []db.TopArtist{}
|
||||
}
|
||||
if end > len(a) {
|
||||
end = len(a)
|
||||
}
|
||||
return a[start:end]
|
||||
}
|
||||
|
||||
func gridReorder(artists []db.TopArtist) []db.TopArtist {
|
||||
if len(artists) < 2 {
|
||||
return artists
|
||||
}
|
||||
if len(artists)%2 == 0 {
|
||||
return artists
|
||||
}
|
||||
remaining := len(artists) - 1
|
||||
perRow := remaining / 2
|
||||
rest := artists[1:]
|
||||
firstRow := rest[:perRow]
|
||||
secondRow := rest[perRow:]
|
||||
var reordered []db.TopArtist
|
||||
reordered = append(reordered, artists[0])
|
||||
reordered = append(reordered, secondRow...)
|
||||
reordered = append(reordered, firstRow...)
|
||||
return reordered
|
||||
}
|
||||
|
||||
// Put a comma in the thousands place, ten-thousands place etc.
|
||||
func formatInt(n int) string {
|
||||
if n < 1000 {
|
||||
|
||||
@@ -33,6 +33,10 @@ func init() {
|
||||
funcMap := template.FuncMap{
|
||||
"sub": sub,
|
||||
"add": add,
|
||||
"div": div,
|
||||
"mod": mod,
|
||||
"slice": slice,
|
||||
"gridReorder": gridReorder,
|
||||
"formatInt": formatInt,
|
||||
"formatTimestamp": formatTimestamp,
|
||||
"formatTimestampFull": formatTimestampFull,
|
||||
|
||||
Reference in New Issue
Block a user