Add play buttons to stats page with cover art support
This commit is contained in:
parent
9e75731f6c
commit
b59a0eaf95
3 changed files with 113 additions and 0 deletions
|
|
@ -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…
Reference in a new issue