Add play buttons to stats page with cover art support

pull/253/head
Broque Thomas 2 months ago
parent 9e75731f6c
commit b59a0eaf95

@ -41919,6 +41919,54 @@ def stats_recent():
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/stats/resolve-track', methods=['POST'])
def stats_resolve_track():
"""Resolve a track by title+artist to get its file_path for playback."""
try:
data = request.get_json()
title = data.get('title', '')
artist = data.get('artist', '')
if not title:
return jsonify({'success': False, 'error': 'Title required'}), 400
database = get_database()
conn = database._get_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT t.id, t.title, t.file_path, t.bitrate, t.duration,
ar.name as artist_name, al.title as album_title,
al.thumb_url, t.artist_id, t.album_id
FROM tracks t
JOIN artists ar ON ar.id = t.artist_id
LEFT JOIN albums al ON al.id = t.album_id
WHERE LOWER(t.title) = LOWER(?) AND LOWER(ar.name) = LOWER(?)
AND t.file_path IS NOT NULL AND t.file_path != ''
LIMIT 1
""", (title.strip(), artist.strip()))
row = cursor.fetchone()
conn.close()
if not row:
return jsonify({'success': False, 'error': 'Track not found in library'})
return jsonify({
'success': True,
'track': {
'id': row[0],
'title': row[1],
'file_path': row[2],
'bitrate': row[3],
'duration': row[4],
'artist_name': row[5],
'album_title': row[6],
'image_url': fix_artist_image_url(row[7]) if row[7] else None,
'artist_id': row[8],
'album_id': row[9],
}
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/listening-stats/sync', methods=['POST'])
def listening_stats_sync():
"""Trigger an immediate listening stats poll."""

@ -42622,6 +42622,7 @@ async function playLibraryTrack(track, albumTitle, artistName) {
}
if (!albumArt) albumArt = artistDetailPageState.enhancedData.artist?.thumb_url;
}
if (!albumArt && track._stats_image) albumArt = track._stats_image;
// Set track info in the media player UI
setTrackInfo({
@ -54944,6 +54945,7 @@ async function loadStatsData() {
<div class="stats-ranked-name">${_esc(item.name)}</div>
<div class="stats-ranked-meta">${item.artist_id ? `<a class="stats-artist-link" onclick="navigateToPage('library');setTimeout(()=>navigateToArtistDetail('${item.artist_id}','${_esc(item.artist||'').replace(/'/g,"\\'")}'),300)">${_esc(item.artist || '')}</a>` : _esc(item.artist || '')}${item.album ? ' · ' + _esc(item.album) : ''}</div>
</div>
<button class="stats-play-btn" onclick="event.stopPropagation();playStatsTrack('${_esc(item.name).replace(/'/g,"\\'")}','${_esc(item.artist||'').replace(/'/g,"\\'")}','${_esc(item.album||'').replace(/'/g,"\\'")}')" title="Play"></button>
<span class="stats-ranked-count">${_fmt(item.play_count)} plays</span>
</div>
`);
@ -55127,6 +55129,33 @@ function _renderLibraryHealth(data) {
}
}
async function playStatsTrack(title, artist, album) {
try {
const resp = await fetch('/api/stats/resolve-track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, artist }),
});
const data = await resp.json();
if (!data.success || !data.track) {
showToast(data.error || 'Track not found in library', 'error');
return;
}
const t = data.track;
playLibraryTrack({
id: t.id,
title: t.title,
file_path: t.file_path,
bitrate: t.bitrate,
artist_id: t.artist_id,
album_id: t.album_id,
_stats_image: t.image_url || null,
}, t.album_title || album || '', t.artist_name || artist || '');
} catch (e) {
showToast('Failed to play track', 'error');
}
}
function _renderRecentPlays(tracks) {
const el = document.getElementById('stats-recent-plays');
if (!el) return;
@ -55150,6 +55179,7 @@ function _renderRecentPlays(tracks) {
el.innerHTML = tracks.map(t => `
<div class="stats-recent-item">
<button class="stats-play-btn stats-play-btn-sm" onclick="event.stopPropagation();playStatsTrack('${_esc(t.title).replace(/'/g,"\\'")}','${_esc(t.artist||'').replace(/'/g,"\\'")}','${_esc(t.album||'').replace(/'/g,"\\'")}')" title="Play"></button>
<span class="stats-recent-title">${_esc(t.title)}</span>
<span class="stats-recent-artist">${_esc(t.artist || '')}</span>
<span class="stats-recent-time">${_ago(t.played_at)}</span>

@ -33064,6 +33064,41 @@ body {
color: rgb(var(--accent-rgb));
}
/* Play buttons */
.stats-play-btn {
width: 28px;
height: 28px;
border-radius: 50%;
border: none;
background: rgba(var(--accent-rgb), 0.15);
color: rgb(var(--accent-rgb));
font-size: 10px;
cursor: pointer;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
opacity: 0;
}
.stats-ranked-item:hover .stats-play-btn,
.stats-recent-item:hover .stats-play-btn {
opacity: 1;
}
.stats-play-btn:hover {
background: rgb(var(--accent-rgb));
color: #fff;
transform: scale(1.1);
}
.stats-play-btn-sm {
width: 22px;
height: 22px;
font-size: 8px;
}
/* Library health */
.stats-health-grid {
display: grid;

Loading…
Cancel
Save