"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var Player = /** @class */ (function () { function Player(playlist) { var _this = this; this.playlist = playlist; this.playlist.setPlayEpisodeHandler(function (episode) { return _this.playEpisode(episode); }); var controls = getOrThrow(document.getElementById('controls')); var timeVolume = getOrThrow(document.getElementById('timeVolume')); this.nowPlaying = getOrThrow(document.getElementById('nowPlaying')); this.rewButton = getOrThrow(controls.getElementsByTagName('button').item(0)); this.playButton = getOrThrow(controls.getElementsByTagName('button').item(1)); this.playButtonPath = getOrThrow(this.playButton.getElementsByTagName('path').item(0)); this.ffwButton = getOrThrow(controls.getElementsByTagName('button').item(2)); this.skipButton = getOrThrow(controls.getElementsByTagName('button').item(3)); this.cover = getOrThrow(this.nowPlaying.getElementsByTagName('img').item(0)); this.seriesName = getOrThrow(this.nowPlaying.getElementsByTagName('span').item(0)); this.episodeName = getOrThrow(this.nowPlaying.getElementsByTagName('span').item(1)); this.timeDisplay = getOrThrow(timeVolume.getElementsByTagName('span').item(0)); this.volumeSlider = getOrThrow(timeVolume.getElementsByTagName('input').item(0)); this.progress = getOrThrow(document.getElementById('progress')); this.ticker = null; this.howl = null; this.episode = null; // initialize to stopped state this.stopPlaybackAndResetUi(); // wire up static ui elements this.playButton.addEventListener('click', function () { return _this.playPause(); }); this.rewButton.addEventListener('click', function () { return _this.rewind(); }); this.ffwButton.addEventListener('click', function () { return _this.fastForward(); }); this.skipButton.addEventListener('click', function () { return _this.nextEpisode(); }); this.volumeSlider.addEventListener('change', function () { return _this.setVolume(); }); // wire up mediaSession events if ('mediaSession' in navigator) { navigator.mediaSession.setActionHandler('pause', function () { return _this.playPause(); }); navigator.mediaSession.setActionHandler('play', function () { return _this.playPause(); }); navigator.mediaSession.setActionHandler('stop', function () { return _this.stopPlaybackAndResetUi(); }); navigator.mediaSession.setActionHandler('seekforward', function () { return _this.fastForward(); }); navigator.mediaSession.setActionHandler('seekbackward', function () { return _this.rewind(); }); navigator.mediaSession.setActionHandler('nexttrack', function () { return _this.nextEpisode(); }); // don't support previous track yet, queue removes them once finished // wire this up to rewind instead navigator.mediaSession.setActionHandler('previoustrack', function () { return _this.rewind(); }); } // set up playlist changed handler this.playlist.addPlaylistChangedHandler(function () { _this.skipButton.disabled = !_this.playlist.hasNextEpisode(); }); } Player.prototype.currentEpisode = function () { return this.episode; }; Player.prototype.playEpisode = function (episode, paused) { var _this = this; if (paused === void 0) { paused = false; } this.stopPlaybackAndResetUi(); this.setPauseButtonUI(); this.episode = episode; this.playlist.removeEpisode(episode, false, false); this.updateNowPlayingUI(true); void fetch("/api/r/".concat(episode.file)) .then(function (res) { return __awaiter(_this, void 0, void 0, function () { var link; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!res.ok) { this.setErrorUI("Error fetching episode (".concat(res.status, ")")); return [2 /*return*/]; } return [4 /*yield*/, res.json()]; case 1: link = _a.sent(); this.howl = new Howl({ src: "".concat(link.url, "?Authorization=").concat(link.token), html5: true, autoplay: !paused, volume: this.getVolume(), onload: function () { _this.updateTimeUI(); _this.sendMediaSessionMetadata(); _this.updateNowPlayingUI(); _this.sendMediaSessionMetadata(); if (paused) _this.setPlayButtonUI(); }, onplay: function () { _this.setPauseButtonUI(); _this.startTicker(); }, onpause: function () { _this.setPlayButtonUI(); _this.stopTicker(); }, onend: function () { return _this.nextEpisode(); }, onloaderror: function () { return _this.setErrorUI('Error playing episode.'); }, }); return [2 /*return*/]; } }); }); }) .catch(function () { _this.setErrorUI('Error downloading episode.'); }); }; Player.prototype.isPlaying = function () { if (this.howl) { return this.howl.playing(); } return false; }; Player.prototype.setErrorUI = function (message) { this.stopPlaybackAndResetUi(); Toastify({ text: message, gravity: 'bottom', position: 'right', style: { marginBottom: '10ex', background: '#a00', color: '#fff', }, }).showToast(); }; Player.prototype.playPause = function () { if (this.howl && this.howl.playing()) this.howl.pause(); else if (this.howl && !this.howl.playing()) this.howl.play(); }; Player.prototype.rewind = function () { if (this.howl && this.howl.state() === 'loaded') { var newPos = this.howl.seek() - 10; if (newPos < 0) newPos = 0; this.howl.seek(newPos); this.updateTimeUI(); } }; Player.prototype.fastForward = function () { if (this.howl && this.howl.state() === 'loaded') { var newPos = this.howl.seek() + 30; if (newPos > this.howl.duration()) newPos = this.howl.duration(); this.howl.seek(newPos); this.updateTimeUI(); } }; Player.prototype.nextEpisode = function () { var next = this.playlist.nextEpisode(); if (next) this.playEpisode(next); else { this.stopPlaybackAndResetUi(); // manually trigger playlist changed // so that currently playing media gets wiped this.playlist.triggerPlaylistChanged(); } }; Player.prototype.setPlayButtonUI = function () { this.playButtonPath.setAttribute('d', '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'); }; Player.prototype.setPauseButtonUI = function () { this.playButtonPath.setAttribute('d', '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'); }; Player.prototype.getVolume = function () { return parseFloat(this.volumeSlider.value) / 100; }; Player.prototype.setVolume = function () { var _a; (_a = this.howl) === null || _a === void 0 ? void 0 : _a.volume(this.getVolume()); }; Player.prototype.updateNowPlayingUI = function (loading) { if (loading === void 0) { loading = false; } this.nowPlaying.classList.remove('error'); if (this.episode) { this.seriesName.innerHTML = loading ? 'Loading episode' : this.episode.series.title; this.episodeName.innerHTML = this.episode.title; this.cover.src = loading ? '/loading.gif' : this.episode.series.cover; this.nowPlaying.title = "".concat(this.episode.series.title, "\n").concat(this.episode.title); } else { this.seriesName.innerHTML = 'No episode playing'; this.episodeName.innerHTML = ''; this.cover.src = '/transparent.png'; this.nowPlaying.title = 'No episode playing'; } if (loading || !this.episode) { this.playButton.disabled = true; this.rewButton.disabled = true; this.ffwButton.disabled = true; this.skipButton.disabled = true; } else { this.playButton.disabled = false; this.rewButton.disabled = false; this.ffwButton.disabled = false; this.skipButton.disabled = !this.playlist.hasNextEpisode(); } }; Player.prototype.sendMediaSessionMetadata = function () { if ('mediaSession' in navigator) { if (this.episode) { navigator.mediaSession.metadata = new MediaMetadata({ title: this.episode.title, album: 'Radiostasis', artist: this.episode.series.title, artwork: [ { src: this.episode.series.cover, sizes: '256x256', type: 'image/jpeg', }, ], }); } else { navigator.mediaSession.metadata = null; } } }; Player.prototype.timeToDisplayString = function (time) { var mins = Math.floor(time / 60); var secs = Math.floor(time - mins * 60); var secStr = secs < 10 ? "0".concat(secs) : secs.toString(); return "".concat(mins, ":").concat(secStr); }; Player.prototype.updateTimeUI = function () { if (this.howl && this.howl.state() === 'loaded') { var total = this.howl.duration(); var current = this.howl.seek(); var pct = "".concat(Math.round((current / total) * 1000) / 10, "%"); var timeStamp = "".concat(this.timeToDisplayString(current), " / ").concat(this.timeToDisplayString(total)); // set the new values if they've changed since the last tick if (this.timeDisplay.innerHTML !== timeStamp) this.timeDisplay.innerHTML = timeStamp; if (this.progress.style.width !== pct) this.progress.style.width = pct; } else { this.timeDisplay.innerHTML = '--:-- / --:--'; this.progress.style.width = '0%'; } }; Player.prototype.startTicker = function () { var _this = this; if (!this.ticker) { this.ticker = setInterval(function () { return _this.updateTimeUI(); }, 500); } }; Player.prototype.stopTicker = function () { if (this.ticker) { clearInterval(this.ticker); this.ticker = null; } }; Player.prototype.stopPlaybackAndResetUi = function () { var _a; (_a = this.howl) === null || _a === void 0 ? void 0 : _a.unload(); this.howl = null; this.episode = null; this.setPlayButtonUI(); this.updateNowPlayingUI(); this.sendMediaSessionMetadata(); this.stopTicker(); this.updateTimeUI(); }; return Player; }()); var Playlist = /** @class */ (function () { function Playlist() { var _this = this; this.queueExpandedHeight = 'calc(100% - 6ex)'; // event handlers this.changedHandlers = []; this.playEpisodeHandler = null; // the actual episode queue this.episodes = []; this.episodeHash = new Set(); this.queueContainer = getOrThrow(document.getElementById('queue-container')); this.queueInitialHeight = getComputedStyle(this.queueContainer).height; this.queueTab = getOrThrow(this.queueContainer.getElementsByTagName('h2').item(0)); this.queueList = getOrThrow(this.queueContainer.getElementsByTagName('ol').item(0)); this.overlay = getOrThrow(document.getElementById('overlay')); this.clearButton = getOrThrow(document.getElementById('clear-playlist')); // wire up global playlist controls this.queueTab.addEventListener('click', function () { return _this.toggleQueueUI(); }); this.overlay.addEventListener('click', function () { return _this.toggleQueueUI(); }); this.clearButton.addEventListener('click', function () { return _this.clearPlaylist(); }); } Playlist.prototype.addPlaylistChangedHandler = function (handler) { this.changedHandlers.push(handler); }; Playlist.prototype.setPlayEpisodeHandler = function (handler) { this.playEpisodeHandler = handler; }; Playlist.prototype.hasNextEpisode = function () { return this.episodes.length > 0; }; Playlist.prototype.nextEpisode = function () { if (this.episodes.length > 0) { return this.episodes[0][0]; } return null; }; Playlist.prototype.pushEpisode = function (episode) { this.pushEpisodes([episode]); }; Playlist.prototype.pushEpisodes = function (episodes) { for (var _i = 0, episodes_1 = episodes; _i < episodes_1.length; _i++) { var episode = episodes_1[_i]; if (this.isQueued(episode)) this.removeEpisode(episode, true, false); var litem = this.createQueueListItem(episode); this.episodes.push([episode, litem]); this.episodeHash.add(episode.id); this.queueList.appendChild(litem); } this.notify(episodes.length == 1 ? 'Episode added to queue.' : "".concat(episodes.length, " episodes added to queue.")); this.playlistChanged(); }; Playlist.prototype.unshiftEpisodes = function (episodes) { for (var i = episodes.length - 1; i >= 0; i--) { var episode = episodes[i]; if (this.isQueued(episode)) this.removeEpisode(episode, true, false); var litem = this.createQueueListItem(episode); this.episodes.unshift([episode, litem]); this.episodeHash.add(episode.id); this.queueList.prepend(litem); } this.notify(episodes.length == 1 ? 'Episode added to queue.' : "".concat(episodes.length, " episodes added to queue.")); this.playlistChanged(); }; Playlist.prototype.removeEpisode = function (episode, ignoreChanged, notify) { if (ignoreChanged === void 0) { ignoreChanged = false; } if (notify === void 0) { notify = true; } if (this.isQueued(episode)) { var idx = this.episodes.findIndex(function (e) { return e[0].id == episode.id; }); var deleted = this.episodes.splice(idx, 1); this.episodeHash.delete(episode.id); deleted[0][1].remove(); } if (!ignoreChanged) this.playlistChanged(); if (notify) this.notify('Episode removed from queue.'); }; Playlist.prototype.isQueued = function (episode) { return this.episodeHash.has(episode.id); }; Playlist.prototype.triggerPlaylistChanged = function () { this.playlistChanged(); }; Playlist.prototype.clearPlaylist = function () { var removed = this.episodes.length; this.episodes.length = 0; this.episodeHash.clear(); this.queueList.innerHTML = ''; this.playlistChanged(); if (removed > 0) { this.notify('Playlist cleared.'); } }; Playlist.prototype.playlistChanged = function () { for (var _i = 0, _a = this.changedHandlers; _i < _a.length; _i++) { var handler = _a[_i]; handler(); } }; Playlist.prototype.toggleQueueUI = function () { if (this.queueContainer.style.height !== this.queueExpandedHeight) { this.queueContainer.style.height = this.queueExpandedHeight; this.overlay.style.backgroundColor = 'rgba(255, 255, 255, 0.75)'; this.overlay.style.backdropFilter = 'blur(5px)'; this.overlay.style.pointerEvents = 'auto'; this.queueTab.innerHTML = 'Close'; this.queueTab.classList.add('expanded'); } else { this.queueContainer.style.height = this.queueInitialHeight; this.overlay.style.backgroundColor = 'rgba(255, 255, 255, 0)'; this.overlay.style.backdropFilter = 'none'; this.overlay.style.pointerEvents = 'none'; this.queueTab.innerHTML = 'Playlist'; this.queueTab.classList.remove('expanded'); } }; Playlist.prototype.createQueueListItem = function (episode) { var _this = this; var item = document.createElement('li'); item.classList.add('episode'); item.title = episode.title; var label = document.createElement('label'); label.innerHTML = episode.title; var sspan = document.createElement('span'); sspan.innerHTML = episode.series.title; var aside = document.createElement('aside'); aside.appendChild(sspan); var controls = document.createElement('div'); controls.classList.add('controls'); var playBtn = document.createElement('a'); playBtn.href = '#'; playBtn.innerHTML = 'Play Now'; playBtn.addEventListener('click', function () { if (_this.playEpisodeHandler) _this.playEpisodeHandler(episode); }); var remBtn = document.createElement('a'); remBtn.href = '#'; remBtn.innerHTML = 'Remove'; remBtn.addEventListener('click', function () { return _this.removeEpisode(episode); }); controls.appendChild(playBtn); controls.appendChild(remBtn); item.appendChild(label); item.appendChild(aside); item.appendChild(controls); return item; }; Playlist.prototype.notify = function (message) { Toastify({ text: message, gravity: 'bottom', position: 'right', style: { marginBottom: '10ex', background: '#090', color: '#fff', }, }).showToast(); }; return Playlist; }()); var Radiostasis = /** @class */ (function () { function Radiostasis() { var _this = this; var _a; this.lastSearch = null; this.debouncer = null; this.playlist = new Playlist(); this.player = new Player(this.playlist); this.main = getOrThrow(document.getElementsByTagName('main').item(0)); // set up htmx event handlers document.addEventListener('htmx:historyRestore', function () { return _this.wireLoadedFragment(); }); (_a = document .getElementsByTagName('main') .item(0)) === null || _a === void 0 ? void 0 : _a.addEventListener('htmx:afterSwap', function () { return _this.wireLoadedFragment(); }); // set up playlist changed handler this.playlist.addPlaylistChangedHandler(function () { return _this.episodeStateChanged(); }); // set up prompt to prevent accidentally leaving the page while playing window.addEventListener('beforeunload', function (e) { if (!_this.player.isPlaying()) return undefined; e.preventDefault(); e.returnValue = ''; }); } Radiostasis.prototype.initialize = function () { var path = location.pathname == '/' ? '/partial/home.html' : "/partial/".concat(location.pathname, ".html"); htmx.ajax('GET', path, 'main'); }; Radiostasis.prototype.wireLoadedFragment = function () { var _this = this; var _a, _b, _c, _d, _e; // save a list of all episodes for the series play/queue buttons var seriesEpisodes = []; // episode play and queue buttons var episodes = this.main.getElementsByClassName('episode'); var _loop_1 = function (i) { var el = episodes.item(i); if (!el || !el.dataset.episode) return "continue"; var episode = JSON.parse(el.dataset.episode); // save and store this model for later use seriesEpisodes.push(episode); // play button (_a = el.getElementsByTagName('a') .item(0)) === null || _a === void 0 ? void 0 : _a.addEventListener('click', function (e) { var _a; if (((_a = _this.player.currentEpisode()) === null || _a === void 0 ? void 0 : _a.id) !== episode.id) { _this.player.playEpisode(episode); } e.preventDefault(); }); // queue button (_b = el.getElementsByTagName('a') .item(1)) === null || _b === void 0 ? void 0 : _b.addEventListener('click', function (e) { var _a; if (!_this.player.currentEpisode()) { // if nothing is playing, first queued item gets loaded // into the player but does not autoplay _this.player.playEpisode(episode, true); } else if (((_a = _this.player.currentEpisode()) === null || _a === void 0 ? void 0 : _a.id) !== episode.id) { if (_this.playlist.isQueued(episode)) { _this.playlist.removeEpisode(episode); } else { _this.playlist.pushEpisode(episode); } } e.preventDefault(); }); // set playing/queued state this_1.updateEpisodeElementFromState(el, episode); }; var this_1 = this; for (var i = 0; i < episodes.length; i++) { _loop_1(i); } // series play and queue buttons var series = (_c = document .getElementsByClassName('seriesDetails') .item(0)) === null || _c === void 0 ? void 0 : _c.getElementsByTagName('section').item(0); if (series) { // play series button (_d = series .getElementsByTagName('a') .item(0)) === null || _d === void 0 ? void 0 : _d.addEventListener('click', function () { var _a; _this.playlist.unshiftEpisodes(seriesEpisodes); if (((_a = _this.player.currentEpisode()) === null || _a === void 0 ? void 0 : _a.id) !== seriesEpisodes[0].id) { _this.player.playEpisode(seriesEpisodes[0]); } else { _this.playlist.removeEpisode(seriesEpisodes[0]); } }); // queue series button (_e = series .getElementsByTagName('a') .item(1)) === null || _e === void 0 ? void 0 : _e.addEventListener('click', function () { var toQueue = seriesEpisodes.filter(function (e) { var _a; return ((_a = _this.player.currentEpisode()) === null || _a === void 0 ? void 0 : _a.id) !== e.id; }); _this.playlist.pushEpisodes(toQueue); // if nothing is playing, peel off the first episode and load it // into the player without autoplay if (!_this.player.currentEpisode()) { _this.player.playEpisode(seriesEpisodes[0], true); } }); } // series filter input var filter = (this.main.getElementsByClassName('filter').item(0)); if (filter) { if (this.lastSearch) { filter.value = this.lastSearch; this.lastSearch = null; } var allSeries_1 = this.main.getElementsByTagName('section'); filter.addEventListener('input', function () { if (_this.debouncer) clearTimeout(_this.debouncer); _this.debouncer = setTimeout(function () { _this.lastSearch = filter.value.toLowerCase(); var terms = _this.lastSearch.split(' '); for (var i = 0; i < allSeries_1.length; i++) { var series_1 = allSeries_1.item(i); if (!series_1 || !series_1.dataset.filter) continue; var match = true; for (var _i = 0, terms_1 = terms; _i < terms_1.length; _i++) { var term = terms_1[_i]; if (term.length > 0 && series_1.dataset.filter.indexOf(term) < 0) { match = false; break; } } if (match) series_1.classList.remove('no-match'); else series_1.classList.add('no-match'); } }, 499); }); } }; Radiostasis.prototype.updateEpisodeElementFromState = function (litem, episode) { var _a; if (((_a = this.player.currentEpisode()) === null || _a === void 0 ? void 0 : _a.id) === episode.id) { litem.classList.add('playing'); getOrThrow(litem.getElementsByTagName('a').item(0)).innerHTML = 'Playing'; } else { litem.classList.remove('playing'); getOrThrow(litem.getElementsByTagName('a').item(0)).innerHTML = 'Play Episode'; } var queueBtn = getOrThrow(litem.getElementsByTagName('a').item(1)); if (this.playlist.isQueued(episode)) { queueBtn.classList.add('queued'); queueBtn.innerHTML = 'Remove from Queue'; } else { queueBtn.classList.remove('queued'); queueBtn.innerHTML = 'Queue Episode'; } }; Radiostasis.prototype.episodeStateChanged = function () { var episodes = this.main.getElementsByClassName('episode'); for (var i = 0; i < episodes.length; i++) { var el = episodes.item(i); if (!el || !el.dataset.episode) continue; var episode = JSON.parse(el.dataset.episode); this.updateEpisodeElementFromState(el, episode); } }; return Radiostasis; }()); function getOrThrow(element) { if (!element) { throw new Error("tried to get an element that doesn't exist"); } return element; } document.addEventListener('DOMContentLoaded', function () { // start application new Radiostasis().initialize(); });