update: listen setlist as youtube playlist

This commit is contained in:
Nathan FONTEYNE 2026-07-08 17:20:40 +02:00
parent 09b7b0bc10
commit 1b792eecfb
8 changed files with 65 additions and 5 deletions

View file

@ -301,6 +301,26 @@ h3 { font-size: 1rem; margin: 0 0 0.5rem; }
.section-header h2 { margin: 0; }
.search-bar {
position: relative;
max-width: 360px;
margin: 0 0 1rem;
}
.search-bar input {
padding-left: 2.2rem;
}
.search-icon {
position: absolute;
left: 0.65rem;
top: 50%;
transform: translateY(-50%);
color: var(--muted);
display: flex;
pointer-events: none;
}
/* ---------- Cards / grids ---------- */
.card-grid {

View file

@ -38,6 +38,11 @@
</form>
</div>
<div class="search-bar">
<span class="search-icon" aria-hidden="true"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"></circle><path d="M21 21l-4.35-4.35"></path></svg></span>
<input type="search" id="song-search-input" placeholder="Rechercher un morceau ou un artiste...">
</div>
<div id="songs-list" class="card-grid"></div>
</main>

View file

@ -32,10 +32,12 @@ function timelineSongRow(song) {
function timelineConcertBlock(concert) {
const main = concert.songs.filter((s) => !s.is_encore);
const encore = concert.songs.filter((s) => s.is_encore);
const playlistUrl = youtubePlaylistUrl(concert.songs.map((s) => s.youtube_url).filter(Boolean));
return `
<div class="card timeline-concert">
<div class="card-title">${escapeHtml(concert.name || 'Concert')}</div>
<div class="card-subtitle">${escapeHtml(concert.venue || '')} · ${formatDate(concert.concert_date)}</div>
${playlistUrl ? `<div class="song-links"><a class="pill-link youtube" href="${playlistUrl}" target="_blank" rel="noopener">&#9658; Écouter la setlist sur YouTube</a></div>` : ''}
<div class="setlist-section">
<h3>Setlist</h3>

View file

@ -40,6 +40,15 @@ function youtubeThumbnailUrl(url) {
return videoId ? `https://img.youtube.com/vi/${videoId}/mqdefault.jpg` : null;
}
// Builds a link that opens several videos back-to-back in YouTube's
// temporary "mix" player — no API call, no auth, just the video IDs we
// already have from each song's youtube_url. Returns null if none of the
// given URLs have a recognizable YouTube video ID.
function youtubePlaylistUrl(urls) {
const ids = urls.map(youtubeVideoId).filter(Boolean);
return ids.length ? `https://www.youtube.com/watch_videos?video_ids=${ids.join(',')}` : null;
}
function initials(name) {
return String(name || '?')
.trim()

View file

@ -1,9 +1,16 @@
let me = null;
let instruments = [];
let songs = [];
let searchQuery = '';
const expanded = new Set();
const editing = new Set();
function matchesSearch(song, query) {
const q = query.trim().toLowerCase();
if (!q) return true;
return song.title.toLowerCase().includes(q) || song.artist.toLowerCase().includes(q);
}
async function loadInstruments() {
instruments = await api.get('/api/instruments');
}
@ -131,7 +138,11 @@ function tutorialCardTemplate(t) {
function renderSongs() {
const container = document.getElementById('songs-list');
renderList(container, songs, songCardTemplate, 'Aucun morceau au répertoire pour le moment.');
const visibleSongs = songs.filter((s) => matchesSearch(s, searchQuery));
const emptyMessage = searchQuery.trim()
? 'Aucun morceau ne correspond à la recherche.'
: 'Aucun morceau au répertoire pour le moment.';
renderList(container, visibleSongs, songCardTemplate, emptyMessage);
document.querySelectorAll('.toggle-tutorials').forEach((btn) => {
btn.addEventListener('click', () => onToggleTutorials(parseInt(btn.dataset.id, 10)));
@ -316,5 +327,9 @@ async function onSongCandidateSelected(candidate) {
dropdownId: 'song-title-dropdown',
onSelect: onSongCandidateSelected,
});
document.getElementById('song-search-input').addEventListener('input', (e) => {
searchQuery = e.target.value;
renderSongs();
});
await loadSongs();
})();

View file

@ -32,10 +32,12 @@ function createSetlistEditor({
const main = setlist.songs.filter((s) => !s.is_encore);
const encore = setlist.songs.filter((s) => s.is_encore);
const rowHtml = (s) => `<li><span class="row-title">${escapeHtml(s.title)}</span> <span class="row-artist">— ${escapeHtml(s.artist)}</span>${s.note ? `<span class="note">${escapeHtml(s.note)}</span>` : ''}</li>`;
const playlistUrl = youtubePlaylistUrl([...main, ...encore].map((s) => s.youtube_url).filter(Boolean));
return `
<div class="card">
<div class="card-title">${escapeHtml(setlist.name || 'Concert')}</div>
<div class="card-subtitle">${escapeHtml(setlist.venue || '')} · ${formatDate(setlist.concert_date)}</div>
${playlistUrl ? `<div class="song-links"><a class="pill-link youtube" href="${playlistUrl}" target="_blank" rel="noopener">&#9658; Écouter la setlist sur YouTube</a></div>` : ''}
</div>
<div class="setlist-section">
<h3>Setlist</h3>