1/** 2 * This script enhances the audio elements on a webpage by adding a custom player interface. 3 * The custom player includes a play/pause button, a progress bar, and a time counter. 4 * It also handles the logic for playing, pausing, and seeking the audio. 5 */ 6 7// Wait for the DOM to be fully loaded before executing the script 8document.addEventListener('DOMContentLoaded', () => { 9 10 /** 11 * Function to format time in seconds to a HH:MM:SS or MM:SS format 12 * @param {number} seconds - The time in seconds 13 * @returns {string} The formatted time 14 */ 15 const formatTime = (seconds) => { 16 const hours = Math.floor(seconds / 3600); 17 const minutes = Math.floor((seconds % 3600) / 60); 18 seconds = Math.floor(seconds % 60); 19 20 if (hours > 0) { 21 return `${hours}:${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}`; 22 } else { 23 return `${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}`; 24 } 25 } 26 27 /** 28 * Function to enhance audio elements on the page 29 * @param {string} selector - The CSS selector for the audio elements to enhance 30 */ 31 const enhanceAudioElements = (selector) => { 32 // Get all audio elements that match the selector and haven't been enhanced yet 33 // Only exclude streamplayer's internal audio elements, not page audio elements 34 const audioElements = document.querySelectorAll('audio:not(.custom-enhanced):not(.ad-audio)'); // Revised 8/20/24 due to debug.log errors. 35 36 audioElements.forEach((audioElement) => { 37 // Get the source of the audio 38 const audioSrc = audioElement.getAttribute('src') || audioElement.querySelector('source').getAttribute('src'); 39 // Get the duration if it exists from RSS feed 40 const rssDuration = audioElement.getAttribute('data-duration'); 41 42 // If there's no source, skip this audio element 43 if (!audioSrc) return; 44 45 // Stop any native playback on the original element before we enhance/hide it. 46 // This prevents double playback (native controls audio + StreamPlayer/mini-player audio). 47 try { 48 audioElement.pause(); 49 audioElement.currentTime = 0; 50 audioElement.muted = true; 51 } catch (err) { 52 // no-op 53 } 54 55 // Skip if this is streamplayer's internal audio element (not page content) 56 if (audioElement.closest('.streamplayer') && audioElement.hasAttribute('data-streamplayer-internal')) { 57 return; 58 } 59 60 // Create the Audio object first. The browser will resolve the src to an absolute URL. 61 const audioObj = new Audio(audioSrc); 62 const absoluteAudioSrc = audioObj.src; 63 64 // Mark the audio element as enhanced 65 audioElement.classList.add('custom-enhanced'); 66 // Create a container for the custom player 67 const container = document.createElement('div'); 68 container.className = 'minimal-player'; 69 container.dataset.audioSrc = absoluteAudioSrc; 70 71 72 if (rssDuration) { 73 container.dataset.duration = rssDuration; 74 } 75 76 // Initial counter display - will be updated once we have duration 77 const initialTimeDisplay = '00:00 / 00:00'; 78 79 // The HTML for the custom player 80 const customPlayerHTML = ` 81 <button class='play-pause-btn' aria-label='Play' aria-pressed='false'><i class="srricons icon-play" aria-hidden="true"></i></button> 82 <div class='progress-container' role='progressbar' aria-valuemin='0' aria-valuemax='100' aria-valuenow='0' aria-label='Audio progress'> 83 <div class='progress-bar'></div> 84 <span class='play-counter'>${initialTimeDisplay}</span> 85 </div>`; 86 87 // Add the custom player to the container 88 container.innerHTML = customPlayerHTML; 89 // Hide the original audio element 90 audioElement.style.display = 'none'; 91 92 // Check for metadata on a parent element and copy it to the container 93 const metadataParent = audioElement.parentNode; 94 if (metadataParent) { 95 const title = metadataParent.dataset.title; 96 const artist = metadataParent.dataset.artist; 97 const image = metadataParent.dataset.image; 98 99 if (title) container.dataset.title = title; 100 if (artist) container.dataset.artist = artist; 101 if (image) container.dataset.image = image; 102 } 103 // Insert the custom player after the original audio element 104 audioElement.parentNode.insertBefore(container, audioElement.nextSibling); 105 106 // Store the Audio object in the container 107 container.audio = audioObj; 108 109 // Update duration display once metadata is loaded 110 audioObj.addEventListener('loadedmetadata', () => { 111 const counter = container.querySelector('.play-counter'); 112 if (counter) { 113 // Check for RSS duration first, fall back to audio duration
114 const duration = rssDuration ? convertITunesDurationToSeconds(rssDuration) : audioObj.duration; 115 if (!isNaN(duration) && isFinite(duration)) { 116 counter.textContent = `00:00 / ${formatTime(duration)}`; 117 } 118 } 119 }); 120 }); 121 } 122 123 // Enhance all audio elements on the page 124 // enhanceAudioElements('audio'); 125 126 // Variables to store the current audio being played and its play/pause button 127 let container, playPauseBtn, audio = new Audio(), progressBar, playCounter, isStreaming = false, audioEventsAttached = false; 128 129 // Function to update mini-player UI based on StreamPlayer state 130 const updateMiniPlayerUI = (isPlaying, audioSrc) => { 131 // Find all minimal players that match the current playing audio source 132 const matchingPlayers = document.querySelectorAll(`.minimal-player[data-audio-src="${audioSrc}"]`); 133 134 document.querySelectorAll('.minimal-player').forEach(player => { 135 const button = player.querySelector('.play-pause-btn'); 136 const isMatch = player.dataset.audioSrc === audioSrc; 137 // Update button icon based on play state and whether this player matches current audio 138 if (isMatch && isPlaying) { 139 button.innerHTML = '<i class="srricons icon-pause" aria-hidden="true"></i>'; button.setAttribute('aria-label', 'Pause'); button.setAttribute('aria-pressed', 'true'); 140 } else { 141 button.innerHTML = '<i class="srricons icon-play" aria-hidden="true"></i>'; button.setAttribute('aria-label', 'Play'); button.setAttribute('aria-pressed', 'false'); 142 } 143 }); 144 }; 145 146 // Function to update progress bar in mini-players 147 const updateMiniPlayerProgress = (audioSrc, currentTime, duration) => { 148 if (!audioSrc || currentTime === undefined || duration === undefined) return; 149 150 // Find all mini-players with matching source 151 document.querySelectorAll(`.minimal-player[data-audio-src="${audioSrc}"]`).forEach(player => { 152 const progressBar = player.querySelector('.progress-bar'); 153 const counter = player.querySelector('.play-counter'); 154 155 if (progressBar) { 156 if (!isNaN(duration) && isFinite(duration) && duration > 0) { 157 const progress = Math.min(100, Math.max(0, (currentTime / duration) * 100)); 158 progressBar.style.width = `${progress}%`; 159 160 if (counter) { 161 counter.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`; 162 } 163 } else if (duration === Infinity || isNaN(duration)) { 164 // Handle streaming audio (live) 165 progressBar.style.width = '100%'; 166 if (counter) { 167 counter.textContent = 'Live'; 168 } 169 } 170 } 171 }); 172 }; 173 174 // Listen for StreamPlayer state changes 175 document.addEventListener('streamPlayerStateChanged', (e) => { 176 if (e.detail) { 177 const { isPlaying, audioSrc, currentTime, duration, isLoading } = e.detail; 178 179 // Update play/pause button state 180 updateMiniPlayerUI(isPlaying, audioSrc); 181 182 // Update progress bar if we have time information 183 if (audioSrc && currentTime !== undefined) { 184 updateMiniPlayerProgress(audioSrc, currentTime, duration); 185 } 186 187 // If loading, add loading indicator 188 if (isLoading) { 189 document.querySelectorAll(`.minimal-player[data-audio-src="${audioSrc}"]`).forEach(player => { 190 player.classList.add('loading'); 191 }); 192 } else { 193 document.querySelectorAll(`.minimal-player[data-audio-src="${audioSrc}"]`).forEach(player => { 194 player.classList.remove('loading'); 195 }); 196 } 197 } 198 }); 199 200 // Handle click events on the page 201 document.addEventListener('click', (e) => { 202 203 // If the click was on a play/pause button 204 if (e.target.closest('.play-pause-btn')) { 205 container = e.target.closest('.minimal-player'); 206 playPauseBtn = container.querySelector('.play-pause-btn'); 207 const audioSrc = container.dataset.audioSrc; 208 const isStreamPlayerActive = document.querySelector('.streamplayer') !== null; 209 210 // If StreamPlayer is active, ensure the minimal player'
210s own Audio instance is not playing. 211 // Without this, the page-level audio (container.audio) can keep playing while StreamPlayer 212 // starts a second audio instance for the same source. 213 if (isStreamPlayerActive && container && container.audio && !container.audio.paused) { 214 try { 215 container.audio.pause(); 216 container.audio.currentTime = 0; 217 } catch (err) { 218 // no-op: best-effort cleanup 219 } 220 } 221 222 // Check if this audio is currently playing in StreamPlayer 223 const isCurrentlyPlaying = window.currentlyPlaying?.src === audioSrc; 224 225 if (isStreamPlayerActive) { 226 // If StreamPlayer is active, use it to control audio 227 audio = window.currentlyPlaying; 228 229 if (audio?.paused || !isCurrentlyPlaying) { 230 // If paused or different source, dispatch event to play in StreamPlayer 231 document.dispatchEvent(new CustomEvent('playInStreamPlayer', { 232 detail: { 233 src: audioSrc, 234 container: container, 235 title: container.dataset.title || audioSrc.split('/').pop().split('.')[0], 236 artist: container.dataset.artist, 237 image: container.dataset.image, 238 originalAudioId: container.dataset.id || null, 239 audioElement: container.audio 240 } 241 })); 242 243 // Update UI immediately for better responsiveness 244 playPauseBtn.innerHTML = '<i class="srricons icon-pause" aria-hidden="true"></i>'; playPauseBtn.setAttribute('aria-label', 'Pause'); playPauseBtn.setAttribute('aria-pressed', 'true'); 245 } else { 246 // If playing, dispatch event to pause in StreamPlayer 247 document.dispatchEvent(new CustomEvent('pauseInStreamPlayer', { 248 detail: { 249 container: container, 250 audioElement: container.audio 251 } 252 })); 253 254 // Update UI immediately for better responsiveness 255 playPauseBtn.innerHTML = '<i class="srricons icon-play" aria-hidden="true"></i>'; playPauseBtn.setAttribute('aria-label', 'Play'); playPauseBtn.setAttribute('aria-pressed', 'false'); 256 } 257 } else { 258 // If StreamPlayer is not active, use the direct audio control 259 audio = container.audio; 260 progressBar = container.querySelector('.progress-bar'); 261 playCounter = container.querySelector('.play-counter'); 262 263 // Attach event listeners once per audio object 264 if (!audio._srrEventsAttached) { 265 attachAudioEvents(audio); 266 audio._srrEventsAttached = true; 267 } 268 269 // If the audio is paused, play it (resume from current position) 270 if (audio?.paused) { 271 if (window.currentlyPlaying && window.currentlyPlaying !== audio) { 272 window.currentlyPlaying.pause(); 273 } 274 window.currentlyPlaying = audio; 275 audio?.play(); 276 } else { 277 // If the audio is playing, pause it (keeps current position) 278 audio?.pause(); 279 } 280 } 281 } 282 283 // If the click was on a progress bar 284 if (e.target.closest('.progress-container')) { 285 const progressContainer = e.target.closest('.progress-container'); 286 const audioContainer = progressContainer.closest('.minimal-player'); 287 const audioSrc = audioContainer.dataset.audioSrc; 288 const isStreamPlayerActive = document.querySelector('.streamplayer') !== null; 289 290 // Get click position 291 const offsetX = e.clientX - progressContainer.getBoundingClientRect().left; 292 const totalWidth = progressContainer.offsetWidth; 293 const percentage = offsetX / totalWidth; 294 295 if (isStreamPlayerActive) { 296 // If StreamPlayer is active and this is not current audio, play it first 297 if (window.currentlyPlaying?.src !== audioSrc) { 298 audioContainer.querySelector('.play-pause-btn').click(); 299 300 // After a small delay to allow for audio loading, set the time 301 setTimeout(() => { 302 if (window.currentlyPlaying && !isNaN(window.currentlyPlaying.duration)) { 303 window.currentlyPlaying.currentTime = percentage * window.currentlyPlaying.duration; 304 } 305 }, 100); 306 } else if (window.currentlyPlaying && !isNaN(window.currentlyPlaying.duration)) { 307 // If already playing in StreamPlayer, just seek 308 window.currentlyPlaying.currentTime = percentage * window.currentlyPlaying.duration; 309 } 310 } else { 311 // Use direct audio control if StreamPlayer is not active 312 const audio = audioContainer.audio; 313 314 // If this is not current audio, switch to it 315 if (window.currentlyPlaying?.src !== audioSrc) { 316 audioContainer.querySelector('.play-pause-btn').click(); 317 } 318 319 // If the audio is loaded, seek to the clicked position 320 if (audio && !isNaN(audio.duration)) { 321 audio.currentTime = percentage * audio.duration; 322 } 323 } 324 } 325 326 }) 327 328 // Attach audio event listeners once and bind to current audio/container via closures over the outer variables 329 function attachAudioEvents(audioObj) { 330 // Update the button icon when the audio is paused 331 audioObj.addEventListener('pause', () => { 332 if (playPauseBtn && container && container.audio === audioObj) { 333 playPauseBtn.innerHTML = '<i class="srricons icon-play" aria-hidden="true"></i>'; playPauseBtn.setAttribute('aria-label', 'Play'); playPauseBtn.setAttribute('aria-pressed', 'false'); 334 if (isStreaming && progressBar) progressBar.style.width = '0%'; 335 } 336 }); 337 338 audioObj.addEventListener('play', () => { 339 // When StreamPlayer is active, do not reset all players here. 340 if (!document.querySelector('.streamplayer')) { 341 resetOtherPlayers(audioObj); 342 } 343 if (playPauseBtn && container && container.audio === audioObj) { 344 playPauseBtn.innerHTML = '<i class="srricons icon-pause" aria-hidden="true"></i>'; playPauseBtn.setAttribute('aria-label', 'Pause'); playPauseBtn.setAttribute('aria-pressed', 'true'); 345 } 346 }); 347 348 audioObj.addEventListener('timeupdate', () => { 349 if (!playPauseBtn || !progressBar || !playCounter) return; 350 if (!container || container.audio !== audioObj) return; 351 352 const rssDuration = container?.dataset?.duration;
353 const duration = rssDuration ? convertITunesDurationToSeconds(rssDuration) : audioObj.duration; 354 355 if ((isNaN(duration) || duration === Infinity)) { 356 isStreaming = true; 357 progressBar.style.width = '100%'; 358 playCounter.textContent = 'Live'; 359 return; 360 } 361 362 isStreaming = false; 363 364 if (isNaN(duration) || !isFinite(duration)) { 365 return; 366 } 367 368 const progress = (audioObj.currentTime / duration) * 100; 369 progressBar.style.width = `${progress}%`; 370 playCounter.textContent = `${formatTime(audioObj.currentTime)} / ${formatTime(duration)}`; 371 372 document.dispatchEvent(new CustomEvent('miniPlayerTimeUpdate', { 373 detail: { 374 audioSrc: audioObj.src, 375 currentTime: audioObj.currentTime, 376 duration: duration 377 } 378 })); 379 }); 380 381 audioObj.addEventListener('ended', () => { 382 if (container && container.audio === audioObj) { 383 var endBtn = container.querySelector('.play-pause-btn'); endBtn.innerHTML = '<i class="srricons icon-play" aria-hidden="true"></i>'; endBtn.setAttribute('aria-label', 'Play'); endBtn.setAttribute('aria-pressed', 'false'); 384 } 385 audioObj.currentTime = 0; 386 }); 387 } 388 389 // Reset all OTHER players (skip the one currently playing) 390 const resetOtherPlayers = (activeAudio) => { 391 document.querySelectorAll('.minimal-player').forEach((player) => { 392 // Skip the player that is currently active 393 if (player.audio === activeAudio) return; 394 395 const btn = player.querySelector('.play-pause-btn'); 396 if (btn) { 397 btn.innerHTML = '<i class="srricons icon-play" aria-hidden="true"></i>'; btn.setAttribute('aria-label', 'Play'); btn.setAttribute('aria-pressed', 'false'); 398 } 399 400 const bar = player.querySelector('.progress-bar'); 401 const counter = player.querySelector('.play-counter'); 402 const playerAudio = player.audio; 403 const rssDuration = player.dataset?.duration; 404 405 let duration = null; 406 if (rssDuration) { 407 duration = convertITunesDurationToSeconds(rssDuration); 408 } else if (playerAudio && !isNaN(playerAudio.duration) && isFinite(playerAudio.duration)) { 409 duration = playerAudio.duration; 410 } 411 412 if (bar) bar.style.width = '0%'; 413 if (counter) { 414 counter.textContent = duration ? `00:00 / ${formatTime(duration)}` : '00:00 / 00:00'; 415 } 416 }); 417 } 418 419 // Listen for StreamPlayer events to sync mini-players 420 document.addEventListener('miniPlayerTimeUpdate', (e) => { 421 if (e.detail) { 422 const { audioSrc, currentTime, duration } = e.detail; 423 424 // Update all mini-players with matching source except the one that triggered the event 425 document.querySelectorAll(`.minimal-player[data-audio-src="${audioSrc}"]`).forEach(player => { 426 // Skip the player that initiated the event (if it's the container variable) 427 if (player === container) return; 428 429 const progressBar = player.querySelector('.progress-bar'); 430 const counter = player.querySelector('.play-counter'); 431 432 if (progressBar && counter) { 433 const progress = (currentTime / duration) * 100; 434 progressBar.style.width = `${progress}%`; 435 counter.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`; 436 } 437 }); 438 } 439 }); 440 441 /** 442 * Convert iTunes duration format (HH:MM:SS or MM:SS or SS) to seconds 443 * @param {string} duration - The duration string from iTunes 444 * @returns {number} Duration in seconds 445 */ 446 const convertITunesDurationToSeconds = (duration) => { 447 if (!duration) return 0; 448 449 const parts = duration.split(':').map(Number); 450 451 if (parts.length === 3) { 452 // HH:MM:SS format 453 return parts[0] * 3600 + parts[1] * 60 + parts[2]; 454 } else if (parts.length === 2) { 455 // MM:SS format 456 return parts[0] * 60 + parts[1]; 457 } else if (parts.length === 1) { 458 // SS format 459 return parts[0]; 460 } 461 462 return 0; 463 } 464 465 const initializeScript = () => { 466 // Your existing code that was inside DOMContentLoaded 467 enhanceAudioElements('audio'); 468 }; 469 470// Run the function once when the DOM is fully loaded 471 document.addEventListener('DOMContentLoaded', initializeScript); 472
473// Observe the body for added nodes and initialize the script for new elements 474 const observer = new MutationObserver((mutations) => { 475 mutations.forEach((mutation) => { 476 if (mutation.addedNodes.length > 0) { 477 initializeScript(); 478 } 479 }); 480 }); 481 482 483 // if (!document.querySelector('.streamplayer')) 484 observer.observe(document.body, {childList: true, subtree: true}); 485});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.