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:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
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'])
|
@app.route('/api/listening-stats/sync', methods=['POST'])
|
||||||
def listening_stats_sync():
|
def listening_stats_sync():
|
||||||
"""Trigger an immediate listening stats poll."""
|
"""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) albumArt = artistDetailPageState.enhancedData.artist?.thumb_url;
|
||||||
}
|
}
|
||||||
|
if (!albumArt && track._stats_image) albumArt = track._stats_image;
|
||||||
|
|
||||||
// Set track info in the media player UI
|
// Set track info in the media player UI
|
||||||
setTrackInfo({
|
setTrackInfo({
|
||||||
|
|
@ -54944,6 +54945,7 @@ async function loadStatsData() {
|
||||||
<div class="stats-ranked-name">${_esc(item.name)}</div>
|
<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 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>
|
</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>
|
<span class="stats-ranked-count">${_fmt(item.play_count)} plays</span>
|
||||||
</div>
|
</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) {
|
function _renderRecentPlays(tracks) {
|
||||||
const el = document.getElementById('stats-recent-plays');
|
const el = document.getElementById('stats-recent-plays');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
@ -55150,6 +55179,7 @@ function _renderRecentPlays(tracks) {
|
||||||
|
|
||||||
el.innerHTML = tracks.map(t => `
|
el.innerHTML = tracks.map(t => `
|
||||||
<div class="stats-recent-item">
|
<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-title">${_esc(t.title)}</span>
|
||||||
<span class="stats-recent-artist">${_esc(t.artist || '')}</span>
|
<span class="stats-recent-artist">${_esc(t.artist || '')}</span>
|
||||||
<span class="stats-recent-time">${_ago(t.played_at)}</span>
|
<span class="stats-recent-time">${_ago(t.played_at)}</span>
|
||||||
|
|
|
||||||
|
|
@ -33064,6 +33064,41 @@ body {
|
||||||
color: rgb(var(--accent-rgb));
|
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 */
|
/* Library health */
|
||||||
.stats-health-grid {
|
.stats-health-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue