davideisinger.com

My personal website
Log | Files | Refs | README

player.js (5328B)


      1 // Track-list buttons and the persistent header control one audio element.
      2 (() => {
      3   const header = document.querySelector('#site-header');
      4   const player = header?.querySelector('audio');
      5   if (!player) return;
      6   const heading = header.querySelector('#latest-track-heading');
      7   const title = header.querySelector('.track-title');
      8   const source = header.querySelector('.track-source a');
      9   let controls = [];
     10   let playRequest = 0;
     11 
     12   function timestamp(seconds) {
     13     if (!Number.isFinite(seconds)) return '0:00';
     14     return `${Math.floor(seconds / 60)}:${String(Math.floor(seconds % 60)).padStart(2, '0')}`;
     15   }
     16 
     17   function update() {
     18     const playing = !player.paused && !player.ended;
     19     for (const track of controls) {
     20       const selected = player.src === track.url;
     21       const active = selected && playing;
     22       track.row.toggleAttribute('data-current-track', selected);
     23       track.icon.textContent = active ? '⏸' : '▶';
     24       track.label.textContent = active ? 'Pause' : 'Play';
     25       track.button.setAttribute('aria-label', `${active ? 'Pause' : 'Play'} ${track.title}`);
     26       track.state.textContent = selected
     27         ? (player.error ? 'Error' : `${timestamp(player.currentTime)} / ${Number.isFinite(player.duration) ? timestamp(player.duration) : '–:––'}`)
     28         : '';
     29       track.state.title = selected && player.error ? 'Unable to play. Try the player above.' : '';
     30     }
     31   }
     32 
     33   async function select(track) {
     34     const request = ++playRequest;
     35     if (player.src === track.url && !player.paused && !player.ended) {
     36       player.pause();
     37       return;
     38     }
     39     if (player.src !== track.url) {
     40       player.pause();
     41       title.textContent = track.title;
     42       source.textContent = track.sourceTitle;
     43       source.href = track.sourceURL;
     44       player.setAttribute('aria-label', track.title);
     45       player.src = track.url;
     46     }
     47     try {
     48       await player.play();
     49     } catch (error) {
     50       if (request !== playRequest || error.name === 'AbortError') return;
     51       // Keep the native header controls available for retrying blocked playback.
     52       track.state.textContent = 'Error';
     53       track.state.title = `Unable to play ${track.title}. Try the player above.`;
     54     }
     55   }
     56 
     57   function preparePostTracks() {
     58     document.querySelectorAll('#content [data-post-music]').forEach(post => {
     59       post.querySelectorAll('audio[src]').forEach(audio => {
     60         if (audio.closest('.music-track')) return;
     61         const url = new URL(audio.src);
     62         if (!url.pathname.toLowerCase().endsWith('.mp3')) return;
     63         let name = url.pathname.split('/').pop().slice(0, -4);
     64         try { name = decodeURIComponent(name); } catch { /* Keep malformed filenames readable. */ }
     65         const row = document.createElement('div');
     66         row.className = 'music-track';
     67         const details = document.createElement('div');
     68         const trackTitle = document.createElement('p');
     69         trackTitle.className = 'track-title';
     70         trackTitle.textContent = name;
     71         const trackSource = document.createElement('p');
     72         trackSource.className = 'track-source';
     73         const link = document.createElement('a');
     74         link.href = post.dataset.postUrl;
     75         link.textContent = post.dataset.postTitle;
     76         trackSource.append('From ', link);
     77         details.append(trackTitle, trackSource);
     78         audio.before(row);
     79         row.append(audio, details);
     80       });
     81     });
     82   }
     83 
     84   function enhance() {
     85     preparePostTracks();
     86     controls = [];
     87     document.querySelectorAll('#content .music-track').forEach(row => {
     88       const audio = row.querySelector('audio');
     89       const trackTitle = row.querySelector('.track-title');
     90       const trackSource = row.querySelector('.track-source a');
     91       if (!audio || !trackTitle || !trackSource) return;
     92       const button = document.createElement('button');
     93       button.type = 'button';
     94       button.className = 'track-toggle';
     95       const icon = document.createElement('span');
     96       icon.className = 'track-toggle-icon';
     97       icon.setAttribute('aria-hidden', 'true');
     98       const label = document.createElement('span');
     99       button.append(icon, label);
    100       const state = document.createElement('span');
    101       state.className = 'track-playback';
    102       const track = { row, button, icon, label, state, url: audio.src, title: trackTitle.textContent,
    103         sourceTitle: trackSource.textContent, sourceURL: trackSource.href };
    104       button.addEventListener('click', () => select(track));
    105       audio.hidden = true;
    106       audio.before(button);
    107       const heading = document.createElement('div');
    108       heading.className = 'track-heading';
    109       trackTitle.before(heading);
    110       heading.append(trackTitle, state);
    111       controls.push(track);
    112     });
    113     update();
    114   }
    115 
    116   player.addEventListener('play', () => {
    117     heading.textContent = 'Now playing';
    118     update();
    119   });
    120   for (const event of ['pause', 'ended', 'timeupdate', 'loadedmetadata', 'durationchange', 'emptied', 'error']) {
    121     player.addEventListener(event, update);
    122   }
    123   document.addEventListener('play', event => {
    124     if (!(event.target instanceof HTMLAudioElement)) return;
    125     document.querySelectorAll('audio').forEach(audio => {
    126       if (audio !== event.target) audio.pause();
    127     });
    128   }, true);
    129   document.addEventListener('site:navigated', enhance);
    130   enhance();
    131 })();