diff --git a/public_html/scripts/m3u-player.js b/public_html/scripts/m3u-player.js
deleted file mode 100644
index 934cf27..0000000
--- a/public_html/scripts/m3u-player.js
+++ /dev/null
@@ -1,315 +0,0 @@
-// @license magnet:?xt=urn:btih:cf05388f2679ee054f2beb29a391d25f4e673ac3&dn=gpl-2.0.txt GPL-v2-or-Later
-// see: m3u-Playlist player in Vanilla Javascript (https://www.draketo.de/software/m3u-player.html)
-// last viewed: 2025-11-15
-const nodes = document.querySelectorAll("audio,video");
-const playlists = {};
-const prefetchedTracks = new Map(); // use a map for insertion order, so we can just blow away old entries.
-// maximum prefetched blobs that are kept.
-const MAX_PREFETCH_KEEP = 10;
-// maximum allowed number of entries in a playlist to prevent OOM attacks against the browser with self-referencing playlists
-const MAX_PLAYLIST_LENGTH = 1000;
-const PLAYLIST_MIME_TYPES = ["audio/x-mpegurl", "audio/mpegurl", "application/vnd.apple.mpegurl","application/mpegurl","application/x-mpegurl"];
-function stripUrlParameters(link) {
- const url = new URL(link, window.location);
- url.search = "";
- url.hash = "";
- return url.href;
-}
-function isPlaylist(link) {
- const linkHref = stripUrlParameters(link);
- return linkHref.endsWith(".m3u") || linkHref.endsWith(".m3u8");
-}
-function isBlob(link) {
- return new URL(link, window.location).protocol == 'blob';
-}
-function parsePlaylist(textContent) {
- return textContent.match(/^(?!#)(?!\s).*$/mg)
- .filter(s => s); // filter removes empty strings
-}
-/**
- * Download the given playlist, parse it, and store the tracks in the
- * global playlists object using the url as key.
- *
- * Runs callback once the playlist downloaded successfully.
- */
-function fetchPlaylist(url, onload, onerror) {
- const playlistFetcher = new XMLHttpRequest();
- playlistFetcher.open("GET", url, true);
- playlistFetcher.responseType = "blob"; // to get a mime type
- playlistFetcher.onload = () => {
- if (PLAYLIST_MIME_TYPES.includes(playlistFetcher.response.type)) { // security check to ensure that filters have run
- const reader = new FileReader();
- const load = onload; // propagate to inner scope
- reader.addEventListener("loadend", e => {
- playlists[url] = parsePlaylist(reader.result);
- onload();
- });
- reader.readAsText(playlistFetcher.response);
- } else {
- console.error("playlist must have one of the playlist MIME type '" + PLAYLIST_MIME_TYPES + "' but it had MIME type '" + playlistFetcher.response.type + "'.");
- onerror();
- }
- };
- playlistFetcher.onerror = onerror;
- playlistFetcher.abort = onerror;
- playlistFetcher.send();
-}
-function servedPartialDataAndCanRequestAll (xhr) {
- if (xhr.status === 206) {
- if (xhr.getResponseHeader("content-range").includes("/")) {
- if (!xhr.getResponseHeader("content-range").includes("/*")) {
- return true;
- }
- }
- }
- return false;
-}
-function prefetchTrack(url, onload) {
- if (prefetchedTracks.has(url)) {
- return;
- }
- // first cleanup: kill the oldest entries until we're back at the allowed size
- while (prefetchedTracks.size > MAX_PREFETCH_KEEP) {
- const key = prefetchedTracks.keys().next().value;
- const track = prefetchedTracks.get(key);
- prefetchedTracks.delete(key);
- }
- // first set the prefetched to the url so we will never request twice
- prefetchedTracks.set(url, url);
- // now start replacing it with a blob
- const xhr = new XMLHttpRequest();
- xhr.open("GET", url, true);
- xhr.responseType = "blob";
- xhr.onload = () => {
- if (servedPartialDataAndCanRequestAll(xhr)) {
- const endRange = Number(xhr.getResponseHeader("content-range").split("/")[1]) - 1;
- const rangeXhr = new XMLHttpRequest();
- rangeXhr.open("GET", url, true);
- rangeXhr.responseType = "blob";
- rangeXhr.setRequestHeader("range", "bytes=0-" + endRange);
- rangeXhr.onload = () => {
- prefetchedTracks.set(url, rangeXhr.response);
- if (onload) {
- onload();
- }
- };
- rangeXhr.send();
- } else {
- prefetchedTracks.set(url, xhr.response);
- if (onload) {
- onload();
- }
- }
- };
- xhr.send();
-}
-
-function showStaticOverlay(mediaTag, canvas) {
- if (mediaTag instanceof Audio) {
- return;
- }
- // take screenshot of video and overlay it to mask short-term flicker.
- const realWidth = mediaTag.getBoundingClientRect().width;
- const realHeight = mediaTag.getBoundingClientRect().height;
- canvas.width = realWidth;
- canvas.height = realHeight;
- // need the actual video size
- const videoAspectRatio = mediaTag.videoHeight / mediaTag.videoWidth;
- const tagAspectRatio = realHeight / realWidth;
- const videoIsPartialHeight = tagAspectRatio > (videoAspectRatio * 1.01); // avoid rounding errors
- const videoIsPartialWidth = videoAspectRatio > (tagAspectRatio * 1.01); // avoid rounding errors
- if (videoIsPartialHeight) {
- canvas.height = realWidth * videoAspectRatio;
- } else if (videoIsPartialWidth) {
- canvas.width = realHeight / videoAspectRatio;
- }
- const context = canvas.getContext("2d");
- context.scale(canvas.width / mediaTag.videoWidth, canvas.height / mediaTag.videoHeight);
- context.drawImage(mediaTag, 0, 0);
- canvas.hidden = true;
- mediaTag.parentNode.insertBefore(canvas, mediaTag.nextSibling);
- canvas.style.position = "absolute";
- // shift canvas to cover only the space where the video actually is
- if (videoIsPartialWidth) {
- canvas.style.marginLeft = "-" + ((realWidth + canvas.width) / 2.) + "px";
- } else {
- canvas.style.marginLeft = "-" + realWidth + "px";
- }
- if (videoIsPartialHeight) {
- canvas.style.marginTop = ((realHeight - canvas.height) / 2.) + "px";
- }
- canvas.hidden = false;
-}
-
-function updateSrc(mediaTag, callback) {
- const playlistUrl = mediaTag.getAttribute("playlist");
- const trackIndex = mediaTag.getAttribute("track-index");
- // deepcopy playlists to avoid shared mutation
- let playlist = [...playlists[playlistUrl]];
- let trackUrl = playlist[trackIndex];
- // download and splice in playlists as needed
- if (isPlaylist(trackUrl)) {
- if (playlist.length >= MAX_PLAYLIST_LENGTH) {
- // skip playlist if we already have too many tracks
- changeTrack(mediaTag, +1);
- } else {
- // do not use the cached playlist here, though it is tempting: it might genuinely change to allow for updates
- fetchPlaylist(
- trackUrl,
- () => {
- playlist.splice(trackIndex, 1, ...playlists[trackUrl]);
- playlists[playlistUrl] = playlist;
- updateSrc(mediaTag, callback);
- },
- () => callback());
- }
- } else {
- let url = prefetchedTracks.has(trackUrl)
- ? prefetchedTracks.get(trackUrl) instanceof Blob
- ? URL.createObjectURL(prefetchedTracks.get(trackUrl))
- : trackUrl : trackUrl;
- const oldUrl = mediaTag.getAttribute("src");
- // prevent size flickering by setting height before src change
- const canvas = document.createElement("canvas");
- if (!isNaN(mediaTag.duration) // already loaded a valid file
- && document.fullscreen !== true) { // overlay does not work for fullscreen
- // mask flickering with a static overlay
- try {
- showStaticOverlay(mediaTag, canvas);
- } catch (error) {
- console.log(error);
- }
- }
- // force sizes to stay constant during loading of the next segment
- mediaTag.style.height = mediaTag.getBoundingClientRect().height.toString() + 'px';
- mediaTag.style.width = mediaTag.getBoundingClientRect().width.toString() + 'px';
- // swich to the next segment
- mediaTag.setAttribute("src", url);
- mediaTag.oncanplaythrough = () => {
- if (!isNaN(mediaTag.duration)) { // already loaded a valid file
- // unset element styles to allow recomputation if sizes changed
- mediaTag.style.height = null;
- mediaTag.style.width = null;
- }
- // remove overlay
- canvas.hidden = true;
- canvas.remove(); // to allow garbage collection
- };
- setTimeout(() => canvas.remove(), 300); // fallback
- // replace the url when done, because a blob from an xhr request
- // is more reliable in the media tag;
- // the normal URL caused jumping prematurely to the next track.
- if (url == trackUrl) {
- prefetchTrack(trackUrl, () => {
- if (mediaTag.paused) {
- if (url == mediaTag.getAttribute("src")) {
- if (mediaTag.currentTime === 0) {
- mediaTag.setAttribute("src", URL.createObjectURL(
- prefetchedTracks.get(url)));
- }
- }
- }
- });
- }
- // allow releasing memory
- if (isBlob(oldUrl)) {
- URL.revokeObjectURL(oldUrl);
- }
- // update title
- mediaTag.parentElement.querySelector(".m3u-player--title").title = trackUrl;
- mediaTag.parentElement.querySelector(".m3u-player--title").textContent = trackUrl;
- // start prefetching the next three tracks.
- for (const i of [1, 2, 3]) {
- if (playlist.length > Number(trackIndex) + i) {
- prefetchTrack(playlist[Number(trackIndex) + i]);
- }
- }
- callback();
- }
-}
-function changeTrack(mediaTag, diff) {
- const currentTrackIndex = Number(mediaTag.getAttribute("track-index"));
- const nextTrackIndex = currentTrackIndex + diff;
- const tracks = playlists[mediaTag.getAttribute("playlist")];
- if (nextTrackIndex >= 0) { // do not collapse the if clauses with double-and, that does not survive inlining
- if (tracks.length > nextTrackIndex) {
- mediaTag.setAttribute("track-index", nextTrackIndex);
- updateSrc(mediaTag, () => mediaTag.play());
- }
- }
-}
-
-/**
- * Turn a media tag into playlist player.
- */
-function initPlayer(mediaTag) {
- mediaTag.setAttribute("playlist", mediaTag.getAttribute("src"));
- mediaTag.setAttribute("track-index", 0);
- const url = mediaTag.getAttribute("playlist");
- const wrapper = mediaTag.parentElement.insertBefore(document.createElement("div"), mediaTag);
- const controls = document.createElement("div");
- const left = document.createElement("span");
- const title = document.createElement("span");
- const right = document.createElement("span");
- controls.appendChild(left);
- controls.appendChild(title);
- controls.appendChild(right);
- left.classList.add("m3u-player--left");
- right.classList.add("m3u-player--right");
- title.classList.add("m3u-player--title");
- title.style.overflow = "hidden";
- title.style.textOverflow = "ellipsis";
- title.style.whiteSpace = "nowrap";
- title.style.opacity = "0.3";
- title.style.direction = "rtl"; // for truncation on the left
- title.style.paddingLeft = "0.5em";
- title.style.paddingRight = "0.5em";
- controls.style.display = "flex";
- controls.style.justifyContent = "space-between";
- const styleTag = document.createElement("style");
- styleTag.innerHTML = ".m3u-player--left:hover, .m3u-player--right:hover {color: wheat; background-color: DarkSlateGray}";
- wrapper.appendChild(styleTag);
- wrapper.appendChild(controls);
- controls.style.width = mediaTag.getBoundingClientRect().width.toString() + "px";
- // appending the media tag to the wrapper removes it from the outer scope but keeps the event listeners
- wrapper.appendChild(mediaTag);
- left.innerHTML = "<"; // not textContent, because we MUST escape
- // the tag here and textContent shows the
- // escaped version
- left.onclick = () => changeTrack(mediaTag, -1);
- right.innerHTML = ">";
- right.onclick = () => changeTrack(mediaTag, +1);
- fetchPlaylist(
- url,
- () => {
- updateSrc(mediaTag, () => null);
- mediaTag.addEventListener("ended", event => {
- if (mediaTag.currentTime >= mediaTag.duration) {
- changeTrack(mediaTag, +1);
- }
- });
- },
- () => null);
- // keep the controls aligned to the media tag
- mediaTag.resizeObserver = new ResizeObserver(entries => {
- controls.style.width = entries[0].contentRect.width.toString() + "px";
- });
- mediaTag.resizeObserver.observe(mediaTag);
-}
-function processTag(mediaTag) {
- const canPlayClaim = mediaTag.canPlayType('audio/x-mpegurl');
- let supportsPlaylists = !!canPlayClaim;
- if (canPlayClaim == 'maybe') { // yes, seriously: specced as you only know when you try
- supportsPlaylists = false;
- }
- if (!supportsPlaylists) {
- if (isPlaylist(mediaTag.getAttribute("src"))) {
- initPlayer(mediaTag);
- }
- }
-}
-document.addEventListener('DOMContentLoaded', () => {
- const nodes = document.querySelectorAll("audio,video");
- nodes.forEach(processTag);
-});
-// @license-end