document.addEventListener('DOMContentLoaded', () => { // initialize audio player var player = new Player(); player.initialize(); let lastSearch; // function that gets run on content load var wirePage = (e) => { // wire up episode items to play const episodes = e.detail.elt.getElementsByClassName('episode'); for (var episode of episodes) { const title = episode.getAttribute('title'); const series = episode.dataset.series; const cover = episode.dataset.cover; const file = episode.dataset.file; episode.addEventListener('click', () => { player.playEpisode(cover, series, title, file); }); } // wire up series filter inputs var filter = e.detail.elt.getElementsByClassName('filter').item(0); let debouncer; if (filter) { var allSeries = e.detail.elt.getElementsByTagName('section'); // show all series by default when the page loads filter.addEventListener('input', ev => { clearTimeout(debouncer); debouncer = setTimeout(() => { lastSearch = ev.target.value.toLowerCase() var terms = lastSearch.split(' '); for (var series of allSeries) { var match = true; for (var term of terms) { if (term.length > 0 && series.dataset.filter.indexOf(term) < 0) { match = false; break; } } if (match) { series.classList.remove('no-match'); } else { series.classList.add('no-match'); } } }, 499); }); } }; var main = document.getElementsByTagName('main').item(0); document.addEventListener('htmx:historyRestore', (e) => { // repopulate filter on history restore var filter = e.detail.elt.getElementsByClassName('filter').item(0); if (lastSearch && filter) { filter.value = lastSearch; lastSearch = null; } // then wire up the page wirePage(e); }); // set up episode links on content swap main.addEventListener('htmx:afterSwap', wirePage); // load page const pname = location.pathname == '/' ? '/home.html' : location.pathname + '.html'; const path = '/partial' + pname; htmx.ajax('GET', path, 'main'); }); class Player { constructor() { this.playSvg = 'M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM6.79 5.093A.5.5 0 0 0 6 5.5v5a.5.5 0 0 0 .79.407l3.5-2.5a.5.5 0 0 0 0-.814l-3.5-2.5z'; this.pauseSvg = 'M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM6.25 5C5.56 5 5 5.56 5 6.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C7.5 5.56 6.94 5 6.25 5zm3.5 0c-.69 0-1.25.56-1.25 1.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C11 5.56 10.44 5 9.75 5z'; const controls = document.getElementById('controls'); const timeVolume = document.getElementById('timeVolume'); this.nowPlaying = document.getElementById('nowPlaying'); this.rewButton = controls.getElementsByTagName('button').item(0); this.playButton = controls.getElementsByTagName('button').item(1); this.playButtonPath = this.playButton.getElementsByTagName('path').item(0); this.ffwButton = controls.getElementsByTagName('button').item(2); this.cover = this.nowPlaying.getElementsByTagName('img').item(0); this.seriesName = this.nowPlaying.getElementsByTagName('span').item(0); this.episodeName = this.nowPlaying.getElementsByTagName('span').item(1); this.timeDisplay = timeVolume.getElementsByTagName('span').item(0); this.volumeSlider = timeVolume.getElementsByTagName('input').item(0); this.howl = null; this.ticker = null; } initialize() { this.reset(); const player = this; this.playButton.addEventListener('click', () => { player.playPause(); }); this.rewButton.addEventListener('click', () => { player.rewind(); }); this.ffwButton.addEventListener('click', () => { player.fastForward(); }); this.volumeSlider.addEventListener('change', () => { this.setVolume(); }); // set up mediasession in browsers that support it if ('mediaSession' in navigator) { const player = this; navigator.mediaSession.setActionHandler('pause', () => player.playPause()); navigator.mediaSession.setActionHandler('play', () => player.playPause()); navigator.mediaSession.setActionHandler('stop', () => player.playPause()); navigator.mediaSession.setActionHandler('seekforward', () => player.fastForward()); navigator.mediaSession.setActionHandler('seekbackward', () => player.rewind()); navigator.mediaSession.setActionHandler('nexttrack', () => player.fastForward()); navigator.mediaSession.setActionHandler('previoustrack', () => player.rewind()); } } startTicker() { if (!this.ticker) { const player = this; this.ticker = setInterval(() => { player.setTime(); }, 500); } } stopTicker() { if (this.ticker) { clearInterval(this.ticker); this.ticker = null; } } disableControls(disabled) { this.playButton.disabled = disabled === true; this.rewButton.disabled = disabled === true; this.ffwButton.disabled = disabled === true; } setPlay() { this.playButtonPath.setAttribute('d', this.playSvg); } setPause() { this.playButtonPath.setAttribute('d', this.pauseSvg); } setSeries(series, cover) { this.seriesName.innerHTML = series ?? 'No episode playing'; this.seriesName.setAttribute('title', series ?? 'No episode playing'); this.cover.setAttribute('src', cover ?? '/transparent.png'); } setEpisode(episode) { this.episodeName.innerHTML = episode ?? ''; this.episodeName.setAttribute('title', episode ?? ''); } setMetadata(series, episode, cover) { if ('mediaSession' in navigator) { this.metadata = new MediaMetadata({ title: episode, album: series, artist: 'Radiostasis', artwork: [{ src: cover, sizes: '256x256', type: 'image/jpeg', }], }); } } sendMetadata() { if ('mediaSession' in navigator) { navigator.mediaSession.metadata = this.metadata; } } clearMetadata() { if ('mediaSession' in navigator) { navigator.mediaSession.metadata = null; } } _getDisplayTime(time) { const minutes = Math.floor(time / 60); const seconds = Math.round(time - (minutes * 60)); const secStr = seconds < 10 ? '0' + seconds : seconds; return minutes + ':' + secStr; } setTime() { if (this.howl && this.howl.state() === 'loaded') { const total = this.howl.duration(); const current = this.howl.seek(); this.timeDisplay.innerHTML = this._getDisplayTime(current) + ' / ' + this._getDisplayTime(total); } else { this.timeDisplay.innerHTML = '--:-- / --:--'; } } getVolume() { return parseFloat(this.volumeSlider.value) / 100; } setVolume() { if (this.howl) { this.howl.volume(this.getVolume()); } } reset() { this.howl = null; this.setPlay(); this.setSeries(); this.setEpisode(); this.clearMetadata(); this.setTime(); this.nowPlaying.classList.remove('error'); this.disableControls(true); this.stopTicker(); this.metadata = null; } error(message) { this.reset(); this.setSeries('Error playing episode'); this.setEpisode(message); this.nowPlaying.classList.add('error'); } playPause() { if (this.howl && this.howl.playing()) { this.howl.pause(); } else if (this.howl && !this.howl.playing()) { this.howl.play(); } } rewind() { if (this.howl && this.howl.state() === 'loaded') { let newPos = this.howl.seek() - 10; if (newPos < 0) newPos = 0; this.howl.seek(newPos); } } fastForward() { if (this.howl && this.howl.state() === 'loaded') { let newPos = this.howl.seek() + 30; if (newPos > this.howl.duration()) newPos = this.howl.duration(); this.howl.seek(newPos); } } playEpisode(cover, series, episode, file) { if (this.howl) { this.howl.stop(); } this.reset(); this.setPause(); this.disableControls(true); this.setSeries('Loading episode', '/loading.gif'); this.setEpisode(episode); fetch('/api/r/' + file).then(async (res) => { if (!res.ok) { this.error('API returned ' + res.status); return; } const link = await res.json(); const player = this; this.howl = new Howl({ src: link.url + '?Authorization=' + link.token, html5: true, autoplay: true, volume: this.getVolume(), onload: () => { player.setTime(); player.setMetadata(series, episode, cover); }, onplay: () => { player.sendMetadata(); player.setSeries(series, cover); player.setPause(); player.disableControls(false); player.startTicker(); }, onpause: () => { player.setPlay(); player.stopTicker(); }, onend: () => { player.reset(); }, onloaderror: () => { player.reset(); player.error('Playback error'); }, }); }).catch(err => { console.log(err); this.error('API request error'); }); } }