Player-js-5

JavaScript ParaNerd 1 Views Size: 26.83 KB Posted on: Dec 10, 25 @ 9:15 PM
  1. 1class RadioPlayer {
  2. 2 constructor(container, config) {
  3. 3 this.container = container;
  4. 4 this.config = config;
  5. 5 this.audio = new Audio();
  6. 6 this.isPlaying = false;
  7. 7 this.currentVolume = 1;
  8. 8 this.elapsed = 0;
  9. 9 this.metadata = null;
  10. 10 this.lastFetchTime = 0;
  11. 11 this.fetchInProgress = false;
  12. 12 this.retryCount = 0;
  13. 13 this.maxRetries = 3;
  14. 14 this.parsedHistory = null;
  15. 15
  16. 16 this.init();
  17. 17 }
  18. 18
  19. 19 init() {
  20. 20 console.log('Initializing Radio Player...');
  21. 21 this.setupAudio();
  22. 22 this.setupElements();
  23. 23 this.attachEventListeners();
  24. 24 this.setupMediaSession();
  25. 25 this.setupVisibilityHandling();
  26. 26 this.setupWebAudio();
  27. 27 this.loadPlayerState();
  28. 28 this.registerServiceWorker();
  29. 29 this.startMetadataPolling();
  30. 30 this.startTimeTracking();
  31. 31 }
  32. 32
  33. 33 setupAudio() {
  34. 34 // Configure audio with better error handling
  35. 35 this.audio.crossOrigin = 'anonymous';
  36. 36 this.audio.preload = 'none';
  37. 37
  38. 38 // Set initial source
  39. 39 this.audio.src = this.config.streamUrl;
  40. 40 console.log('Audio source set to:', this.config.streamUrl);
  41. 41
  42. 42 // Add comprehensive error handling
  43. 43 this.audio.addEventListener('error', (e) => this.handleAudioError(e));
  44. 44 this.audio.addEventListener('canplay', () => console.log('Audio: Ready to play'));
  45. 45 this.audio.addEventListener('loadstart', () => console.log('Audio: Loading started'));
  46. 46 this.audio.addEventListener('stalled', () => this.handleStreamStall());
  47. 47 this.audio.addEventListener('abort', () => console.log('Audio: Load aborted'));
  48. 48 this.audio.addEventListener('emptied', () => console.log('Audio: Media emptied'));
  49. 49 }
  50. 50
  51. 51 setupElements() {
  52. 52 // Get DOM elements
  53. 53 this.artworkImage = this.container.querySelector('.artwork-image');
  54. 54 this.artworkPlaceholder = this.container.querySelector('.artwork-placeholder');
  55. 55 this.songNameDiv = this.container.querySelector('.song-name');
  56. 56 this.artistNameDiv = this.container.querySelector('.artist-name');
  57. 57 this.radioNameDiv = this.container.querySelector('.radio-name');
  58. 58 this.liveContainer = this.container.querySelector('.live-container');
  59. 59 this.playButton = this.container.querySelector('.play-button');
  60. 60 this.volumeButton = this.container.querySelector('.volume-button');
  61. 61 this.volumeSlider = this.container.querySelector('.volume-slider');
  62. 62 this.timeBarProgress = this.container.querySelector('.time-bar-progress');
  63. 63 this.elapsedTimeSpan = this.container.querySelector('.elapsed-time');
  64. 64 this.durationTimeSpan = this.container.querySelector('.duration-time');
  65. 65 this.historyModal = document.querySelector('.history-modal');
  66. 66 this.historyList = document.querySelector('.history-list');
  67. 67
  68. 68 // Set default values
  69. 69 if (this.songNameDiv) this.songNameDiv.textContent = this.config.stationName || 'Paranormal FM';
  70. 70 if (this.artistNameDiv) this.artistNameDiv.textContent = 'Click play to start streaming';
  71. 71 if (this.radioNameDiv) this.radioNameDiv.textContent = this.config.stationName || 'Paranormal FM';
  72. 72
  73. 73 // Initialize volume
  74. 74 if (this.volumeSlider) {
  75. 75 this.volumeSlider.value = 100;
  76. 76 this.isMuted = false;
  77. 77 this.preMuteVolume = 1;
  78. 78 }
  79. 79
  80. 80 this.setupVolumeControls();
  81. 81 this.attachHistoryEventListeners();
  82. 82 }
  83. 83
  84. 84 setupVolumeControls() {
  85. 85 if (!this.volumeButton || !this.volumeSlider) return;
  86. 86
  87. 87 const volumeContainer = this.container.querySelector('.volume-bar-container');
  88. 88 let hideTimeout;
  89. 89
  90. 90 const showVolume = () => {
  91. 91 clearTimeout(hideTimeout);
  92. 92 volumeContainer.classList.add('show');
  93. 93 };
  94. 94
  95. 95 const hideVolume = () => {
  96. 96 hideTimeout = setTimeout(() => {
  97. 97 volumeContainer.classList.remove('show');
  98. 98 }, 200);
  99. 99 };
  100. 100
  101. 101 this.volumeButton.addEventListener('mouseenter', showVolume);
  102. 102 this.volumeButton.addEventListener('mouseleave', hideVolume);
  103. 103 volumeContainer.addEventListener('mouseenter', showVolume);
  104. 104 volumeContainer.addEventListener('mouseleave', hideVolume);
  105. 105
  106. 106 this.volumeButton.addEventListener('click', (e) => {
  107. 107 e.stopPropagation();
  108. 108 this.toggleMute();
  109. 109 });
  110. 110
  111. 111 this.volumeSlider.addEventListener('input', (e) => {
  112. 112 const value = e.target.value / 100;
  113. 113 this.setVolume(value);
  114. 114 });
  115. 115 }
  116. 116
  117. 117 attachEventListeners() {
  118. 118 this.audio.addEventListener('play', () => {
  119. 119 this.isPlaying = true;
  120. 120 this.updatePlayState();
  121. 121 this.savePlayerState();
  122. 122 console.log('Audio: Playing');
  123. 123 });
  124. 124
  125. 125 this.audio.addEventListener('pause', () => {
  126. 126 this.isPlaying = false;
  127. 127 this.updatePlayState();
  128. 128 this.savePlayerState();
  129. 129 console.log('Audio: Paused');
  130. 130 });
  131. 131
  132. 132 this.audio.addEventListener('ended', () => {
  133. 133 console.log('Audio: Ended (should not happen for streams)');
  134. 134 this.isPlaying = false;
  135. 135 this.updatePlayState();
  136. 136 });
  137. 137
  138. 138 if (this.playButton) {
  139. 139 this.playButton.addEventListener('click', () => this.togglePlay());
  140. 140 }
  141. 141 }
  142. 142
  143. 143 setupMediaSession() {
  144. 144 if ('mediaSession' in navigator) {
  145. 145 navigator.mediaSession.setActionHandler('play', () => this.startPlayback());
  146. 146 navigator.mediaSession.setActionHandler('pause', () => this.audio.pause());
  147. 147 navigator.mediaSession.setActionHandler('stop', () => {
  148. 148 this.audio.pause();
  149. 149 this.audio.currentTime = 0;
  150. 150 });
  151. 151 }
  152. 152 }
  153. 153
  154. 154 setupVisibilityHandling() {
  155. 155 document.addEventListener('visibilitychange', () => {
  156. 156 if (document.hidden) {
  157. 157 // Page is hidden - save state
  158. 158 this.savePlayerState();
  159. 159 console.log('Page hidden, state saved');
  160. 160 } else {
  161. 161 // Page is visible again - check if should resume
  162. 162 console.log('Page visible again');
  163. 163 if (this.isPlaying && this.audio.paused) {
  164. 164 console.log('Resuming audio playback');
  165. 165 this.audio.play().catch(console.error);
  166. 166 }
  167. 167 }
  168. 168 });
  169. 169
  170. 170 // Handle page unload
  171. 171 window.addEventListener('beforeunload', () => {
  172. 172 this.savePlayerState();
  173. 173 });
  174. 174 }
  175. 175
  176. 176 setupWebAudio() {
  177. 177 try {
  178. 178 this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
  179. 179 this.source = this.audioContext.createMediaElementSource(this.audio);
  180. 180 this.gainNode = this.audioContext.createGain();
  181. 181
  182. 182 this.source.connect(this.gainNode);
  183. 183 this.gainNode.connect(this.audioContext.destination);
  184. 184
  185. 185 // Keep context alive
  186. 186 document.addEventListener('visibilitychange', () => {
  187. 187 if (this.audioContext.state === 'suspended' && this.isPlaying) {
  188. 188 this.audioContext.resume();
  189. 189 }
  190. 190 });
  191. 191
  192. 192 console.log('Web Audio API setup complete');
  193. 193 } catch (error) {
  194. 194 console.warn('Web Audio API not supported:', error);
  195. 195 }
  196. 196 }
  197. 197
  198. 198 async registerServiceWorker() {
  199. 199 if ('serviceWorker' in navigator) {
  200. 200 try {
  201. 201 const registration = await navigator.serviceWorker.register(this.config.serviceWorkerUrl || '/wp-content/plugins/persistent-radio-player/sw.js');
  202. 202 console.log('Service Worker registered:', registration);
  203. 203 } catch (error) {
  204. 204 console.error('Service Worker registration failed:', error);
  205. 205 }
  206. 206 }
  207. 207 }
  208. 208
  209. 209 savePlayerState() {
  210. 210 const state = {
  211. 211 isPlaying: this.isPlaying,
  212. 212 volume: this.currentVolume,
  213. 213 elapsed: this.elapsed,
  214. 214 lastSong: this.metadata?.now_playing?.song,
  215. 215 timestamp: Date.now()
  216. 216 };
  217. 217 try {
  218. 218 localStorage.setItem('paranormalfm_player_state', JSON.stringify(state));
  219. 219 } catch (error) {
  220. 220 console.warn('Failed to save player state:', error);
  221. 221 }
  222. 222 }
  223. 223
  224. 224 loadPlayerState() {
  225. 225 try {
  226. 226 const saved = localStorage.getItem('paranormalfm_player_state');
  227. 227 if (saved) {
  228. 228 const state = JSON.parse(saved);
  229. 229
  230. 230 // Restore volume
  231. 231 if (state.volume !== undefined) {
  232. 232 this.setVolume(state.volume);
  233. 233 }
  234. 234
  235. 235 // Auto-resume if was playing recently (within 30 minutes)
  236. 236 const timeDiff = Date.now() - state.timestamp;
  237. 237 if (state.isPlaying && timeDiff < 30 * 60 * 1000) {
  238. 238 this.showAutoResumePrompt();
  239. 239 }
  240. 240 }
  241. 241 } catch (error) {
  242. 242 console.warn('Failed to load player state:', error);
  243. 243 }
  244. 244 }
  245. 245
  246. 246 showAutoResumePrompt() {
  247. 247 const resume = confirm('Resume playing Paranormal FM from your last session?');
  248. 248 if (resume) {
  249. 249 this.startPlayback();
  250. 250 }
  251. 251 }
  252. 252
  253. 253 async togglePlay() {
  254. 254 try {
  255. 255 if (this.isPlaying) {
  256. 256 console.log('Pausing audio...');
  257. 257 this.audio.pause();
  258. 258 } else {
  259. 259 console.log('Starting audio playback...');
  260. 260 await this.startPlayback();
  261. 261 }
  262. 262 } catch (error) {
  263. 263 console.error('Error in togglePlay:', error);
  264. 264 this.showError('Failed to start playback: ' + error.message);
  265. 265 }
  266. 266 }
  267. 267
  268. 268 async startPlayback() {
  269. 269 this.showLoadingState();
  270. 270
  271. 271 try {
  272. 272 // Resume audio context if suspended
  273. 273 if (this.audioContext && this.audioContext.state === 'suspended') {
  274. 274 await this.audioContext.resume();
  275. 275 console.log('Audio context resumed');
  276. 276 }
  277. 277
  278. 278 // Reload the audio source to ensure fresh connection
  279. 279 this.audio.load();
  280. 280 console.log('Audio loaded');
  281. 281
  282. 282 // Wait a moment for the browser to initialize
  283. 283 await new Promise(resolve => setTimeout(resolve, 1000));
  284. 284
  285. 285 // Attempt to play
  286. 286 console.log('Attempting to play audio...');
  287. 287 await this.audio.play();
  288. 288
  289. 289 console.log('Playback started successfully');
  290. 290 this.retryCount = 0;
  291. 291 this.hideLoadingState();
  292. 292
  293. 293 } catch (error) {
  294. 294 console.error('Playback failed:', error);
  295. 295
  296. 296 if (this.retryCount < this.maxRetries) {
  297. 297 this.retryCount++;
  298. 298 console.log(`Retrying playback (${this.retryCount}/${this.maxRetries})...`);
  299. 299 setTimeout(() => this.startPlayback(), 2000);
  300. 300 } else {
  301. 301 this.hideLoadingState();
  302. 302 this.handlePlaybackError(error);
  303. 303 }
  304. 304 }
  305. 305 }
  306. 306
  307. 307 handlePlaybackError(error) {
  308. 308 let errorMessage = 'Unable to connect to the radio stream.';
  309. 309
  310. 310 if (error.name === 'NotAllowedError') {
  311. 311 errorMessage = 'Audio playback blocked by browser. Please click the play button again.';
  312. 312 } else if (error.name === 'NotSupportedError') {
  313. 313 errorMessage = 'Audio format not supported by your browser.';
  314. 314 } else if (error.name === 'AbortError') {
  315. 315 errorMessage = 'Audio loading was interrupted.';
  316. 316 }
  317. 317
  318. 318 this.showError(errorMessage);
  319. 319 console.error('Audio error details:', error);
  320. 320 }
  321. 321
  322. 322 handleAudioError(error) {
  323. 323 const target = error.target;
  324. 324 let errorMessage = 'Stream connection error';
  325. 325
  326. 326 if (target && target.error) {
  327. 327 switch (target.error.code) {
  328. 328 case target.error.MEDIA_ERR_ABORTED:
  329. 329 errorMessage = 'Audio loading aborted';
  330. 330 break;
  331. 331 case target.error.MEDIA_ERR_NETWORK:
  332. 332 errorMessage = 'Network error - check your connection';
  333. 333 break;
  334. 334 case target.error.MEDIA_ERR_DECODE:
  335. 335 errorMessage = 'Audio decode error';
  336. 336 break;
  337. 337 case target.error.MEDIA_ERR_SRC_NOT_SUPPORTED:
  338. 338 errorMessage = 'Stream source not supported';
  339. 339 break;
  340. 340 default:
  341. 341 errorMessage = 'Unknown audio error';
  342. 342 }
  343. 343 }
  344. 344
  345. 345 console.error('Audio error:', target?.error);
  346. 346 this.hideLoadingState();
  347. 347 this.showError(errorMessage);
  348. 348
  349. 349 // Auto-retry on network errors
  350. 350 if (target?.error?.code === target.error.MEDIA_ERR_NETWORK && this.retryCount < this.maxRetries) {
  351. 351 this.retryCount++;
  352. 352 setTimeout(() => {
  353. 353 if (this.isPlaying) {
  354. 354 console.log(`Auto-retrying after network error (${this.retryCount}/${this.maxRetries})`);
  355. 355 this.startPlayback();
  356. 356 }
  357. 357 }, 3000);
  358. 358 }
  359. 359 }
  360. 360
  361. 361 handleStreamStall() {
  362. 362 console.log('Stream stalled, attempting recovery...');
  363. 363 if (this.isPlaying) {
  364. 364 setTimeout(() => {
  365. 365 this.audio.load();
  366. 366 this.audio.play().catch(console.error);
  367. 367 }, 1000);
  368. 368 }
  369. 369 }
  370. 370
  371. 371 showLoadingState() {
  372. 372 if (this.playButton) {
  373. 373 this.playButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
  374. 374 this.playButton.disabled = true;
  375. 375 }
  376. 376 }
  377. 377
  378. 378 hideLoadingState() {
  379. 379 if (this.playButton) {
  380. 380 this.playButton.disabled = false;
  381. 381 this.updatePlayState();
  382. 382 }
  383. 383 }
  384. 384
  385. 385 updatePlayState() {
  386. 386 if (this.playButton) {
  387. 387 const icon = this.isPlaying ? 'pause' : 'play';
  388. 388 this.playButton.innerHTML = `<i class="fas fa-${icon}"></i>`;
  389. 389 }
  390. 390 }
  391. 391
  392. 392 setVolume(value) {
  393. 393 this.audio.volume = value;
  394. 394 this.currentVolume = value;
  395. 395 if (this.gainNode) {
  396. 396 this.gainNode.gain.value = value;
  397. 397 }
  398. 398 this.updateVolumeState();
  399. 399 this.savePlayerState();
  400. 400 }
  401. 401
  402. 402 toggleMute() {
  403. 403 if (this.audio.volume > 0) {
  404. 404 this.preMuteVolume = this.audio.volume;
  405. 405 this.setVolume(0);
  406. 406 } else {
  407. 407 this.setVolume(this.preMuteVolume || 0.5);
  408. 408 }
  409. 409 }
  410. 410
  411. 411 updateVolumeState() {
  412. 412 if (this.volumeButton && this.volumeSlider) {
  413. 413 const volume = this.audio.volume;
  414. 414 this.volumeSlider.value = volume * 100;
  415. 415
  416. 416 let icon = 'volume-up';
  417. 417 if (volume === 0) icon = 'volume-mute';
  418. 418 else if (volume < 0.33) icon = 'volume-off';
  419. 419 else if (volume < 0.66) icon = 'volume-down';
  420. 420
  421. 421 this.volumeButton.innerHTML = `<i class="fas fa-${icon}"></i>`;
  422. 422 }
  423. 423 }
  424. 424
  425. 425 startMetadataPolling() {
  426. 426 this.fetchMetadata();
  427. 427 this.metadataInterval = setInterval(() => this.fetchMetadata(), 15000);
  428. 428 }
  429. 429
  430. 430 startTimeTracking() {
  431. 431 this.elapsedInterval = setInterval(() => {
  432. 432 if (this.isPlaying) {
  433. 433 this.elapsed++;
  434. 434 this.updateTimeDisplay();
  435. 435 }
  436. 436 }, 1000);
  437. 437 }
  438. 438
  439. 439 async fetchMetadata() {
  440. 440 if (this.fetchInProgress || Date.now() - this.lastFetchTime < 5000) return;
  441. 441 this.fetchInProgress = true;
  442. 442
  443. 443 try {
  444. 444 const response = await fetch(this.config.directApiUrl);
  445. 445 const data = await response.json();
  446. 446 this.metadata = data;
  447. 447 this.updateMetadata(data);
  448. 448 this.updateMediaSession();
  449. 449 this.lastFetchTime = Date.now();
  450. 450 console.log('Metadata updated successfully');
  451. 451 } catch (error) {
  452. 452 console.error('Error fetching metadata:', error);
  453. 453 } finally {
  454. 454 this.fetchInProgress = false;
  455. 455 }
  456. 456 }
  457. 457
  458. 458 updateMetadata(data) {
  459. 459 try {
  460. 460 // Parse the nested JSON strings from Radiolize API
  461. 461 let nowPlaying = null;
  462. 462 let songHistory = null;
  463. 463
  464. 464 if (data.now_playing) {
  465. 465 if (typeof data.now_playing === 'string') {
  466. 466 nowPlaying = JSON.parse(data.now_playing);
  467. 467 } else {
  468. 468 nowPlaying = data.now_playing;
  469. 469 }
  470. 470 }
  471. 471
  472. 472 if (data.song_history) {
  473. 473 if (typeof data.song_history === 'string') {
  474. 474 songHistory = JSON.parse(data.song_history);
  475. 475 } else {
  476. 476 songHistory = data.song_history;
  477. 477 }
  478. 478 }
  479. 479
  480. 480 if (nowPlaying && nowPlaying.song) {
  481. 481 // Reset elapsed time for new song
  482. 482 this.elapsed = Math.max(0, nowPlaying.elapsed || 0);
  483. 483
  484. 484 // Update song info
  485. 485 if (this.songNameDiv) {
  486. 486 this.songNameDiv.textContent = nowPlaying.song.title || 'Paranormal FM';
  487. 487 }
  488. 488 if (this.artistNameDiv) {
  489. 489 this.artistNameDiv.textContent = nowPlaying.song.artist || 'Now Playing';
  490. 490 }
  491. 491
  492. 492 // Update artwork (only if not generic placeholder)
  493. 493 if (this.artworkImage && nowPlaying.song.art &&
  494. 494 nowPlaying.song.art !== 'https://s76.radiolize.com/static/img/generic_song.png') {
  495. 495 this.artworkImage.src = nowPlaying.song.art;
  496. 496 this.artworkImage.style.display = 'block';
  497. 497 if (this.artworkPlaceholder) {
  498. 498 this.artworkPlaceholder.style.display = 'none';
  499. 499 }
  500. 500 } else {
  501. 501 if (this.artworkImage) this.artworkImage.style.display = 'none';
  502. 502 if (this.artworkPlaceholder) this.artworkPlaceholder.style.display = 'flex';
  503. 503 }
  504. 504 }
  505. 505
  506. 506 // Update station name
  507. 507 if (this.radioNameDiv && data.station?.name) {
  508. 508 this.radioNameDiv.textContent = data.station.name;
  509. 509 }
  510. 510
  511. 511 // Update live status
  512. 512 if (this.liveContainer && data.live) {
  513. 513 const isLive = data.live.is_live;
  514. 514 this.liveContainer.style.display = isLive ? 'flex' : 'none';
  515. 515 const liveText = this.liveContainer.querySelector('.live-text');
  516. 516 if (liveText && isLive && data.live.streamer_name) {
  517. 517 liveText.textContent = data.live.streamer_name;
  518. 518 } else if (liveText) {
  519. 519 liveText.textContent = 'LIVE';
  520. 520 }
  521. 521 }
  522. 522
  523. 523 // Store parsed history for modal
  524. 524 if (songHistory) {
  525. 525 this.parsedHistory = songHistory;
  526. 526 }
  527. 527
  528. 528 // Save state after metadata update
  529. 529 this.savePlayerState();
  530. 530
  531. 531 } catch (error) {
  532. 532 console.error('Error updating metadata:', error);
  533. 533 }
  534. 534 }
  535. 535
  536. 536 updateMediaSession() {
  537. 537 if ('mediaSession' in navigator && this.metadata?.now_playing) {
  538. 538 let nowPlaying;
  539. 539 try {
  540. 540 nowPlaying = typeof this.metadata.now_playing === 'string'
  541. 541 ? JSON.parse(this.metadata.now_playing)
  542. 542 : this.metadata.now_playing;
  543. 543
  544. 544 const song = nowPlaying.song;
  545. 545 navigator.mediaSession.metadata = new MediaMetadata({
  546. 546 title: song.title || 'Paranormal FM',
  547. 547 artist: song.artist || 'Live Stream',
  548. 548 album: 'Paranormal FM Radio',
  549. 549 artwork: [
  550. 550 {
  551. 551 src: song.art || 'https://s76.radiolize.com/static/img/generic_song.png',
  552. 552 sizes: '512x512',
  553. 553 type: 'image/jpeg'
  554. 554 }
  555. 555 ]
  556. 556 });
  557. 557 } catch (e) {
  558. 558 console.error('Error updating media session:', e);
  559. 559 }
  560. 560 }
  561. 561 }
  562. 562
  563. 563 formatTime(seconds) {
  564. 564 const mins = Math.floor(seconds / 60);
  565. 565 const secs = Math.floor(seconds % 60);
  566. 566 return `${mins}:${secs.toString().padStart(2, '0')}`;
  567. 567 }
  568. 568
  569. 569 updateTimeDisplay() {
  570. 570 if (this.elapsedTimeSpan) {
  571. 571 this.elapsedTimeSpan.textContent = this.formatTime(this.elapsed);
  572. 572 }
  573. 573
  574. 574 if (this.timeBarProgress && this.metadata?.now_playing) {
  575. 575 let nowPlaying;
  576. 576 try {
  577. 577 nowPlaying = typeof this.metadata.now_playing === 'string'
  578. 578 ? JSON.parse(this.metadata.now_playing)
  579. 579 : this.metadata.now_playing;
  580. 580
  581. 581 const duration = nowPlaying.duration || 0;
  582. 582 if (duration > 0) {
  583. 583 const progress = (this.elapsed / duration) * 100;
  584. 584 this.timeBarProgress.style.width = `${Math.min(progress, 100)}%`;
  585. 585 }
  586. 586
  587. 587 if (this.durationTimeSpan) {
  588. 588 this.durationTimeSpan.textContent = this.formatTime(duration);
  589. 589 }
  590. 590 } catch (e) {
  591. 591 console.error('Error parsing time data:', e);
  592. 592 }
  593. 593 }
  594. 594 }
  595. 595
  596. 596 showError(message) {
  597. 597 console.error('Radio Player Error:', message);
  598. 598
  599. 599 if (this.artistNameDiv) {
  600. 600 this.artistNameDiv.textContent = message;
  601. 601 }
  602. 602
  603. 603 // Show toast notification
  604. 604 const toast = document.createElement('div');
  605. 605 toast.style.cssText = `
  606. 606 position: fixed;
  607. 607 bottom: 120px;
  608. 608 right: 20px;
  609. 609 background: rgba(255, 0, 0, 0.9);
  610. 610 color: white;
  611. 611 padding: 12px 20px;
  612. 612 border-radius: 8px;
  613. 613 z-index: 10000;
  614. 614 font-family: Arial, sans-serif;
  615. 615 font-size: 14px;
  616. 616 max-width: 300px;
  617. 617 `;
  618. 618 toast.textContent = message;
  619. 619
  620. 620 document.body.appendChild(toast);
  621. 621 setTimeout(() => {
  622. 622 if (document.body.contains(toast)) {
  623. 623 document.body.removeChild(toast);
  624. 624 }
  625. 625 }, 5000);
  626. 626 }
  627. 627
  628. 628 attachHistoryEventListeners() {
  629. 629 const historyButton = this.container.querySelector('.history-button');
  630. 630 const historyModalClose = document.querySelector('.history-modal-close');
  631. 631 const historyModalOverlay = document.querySelector('.history-modal-overlay');
  632. 632
  633. 633 if (historyButton && this.historyModal) {
  634. 634 historyButton.addEventListener('click', () => this.showHistoryModal());
  635. 635 historyModalClose?.addEventListener('click', () => this.hideHistoryModal());
  636. 636 historyModalOverlay?.addEventListener('click', () => this.hideHistoryModal());
  637. 637 }
  638. 638 }
  639. 639
  640. 640 showHistoryModal() {
  641. 641 if (this.historyModal && this.parsedHistory) {
  642. 642 this.updateHistoryList();
  643. 643 this.historyModal.style.display = 'block';
  644. 644 document.body.style.overflow = 'hidden';
  645. 645 }
  646. 646 }
  647. 647
  648. 648 hideHistoryModal() {
  649. 649 if (this.historyModal) {
  650. 650 this.historyModal.style.display = 'none';
  651. 651 document.body.style.overflow = '';
  652. 652 }
  653. 653 }
  654. 654
  655. 655 updateHistoryList() {
  656. 656 if (!this.historyList || !this.parsedHistory) return;
  657. 657
  658. 658 try {
  659. 659 const historyArray = Object.values(this.parsedHistory);
  660. 660 this.historyList.innerHTML = historyArray.map(item => `
  661. 661 <div class="history-item">
  662. 662 <div class="history-artwork">
  663. 663 <img src="${item.song.art || 'https://s76.radiolize.com/static/img/generic_song.png'}"
  664. 664 alt="${item.song.title}" />
  665. 665 </div>
  666. 666 <div class="history-song-info">
  667. 667 <div class="history-song-title">${item.song.title}</div>
  668. 668 <div class="history-artist-name">${item.song.artist}</div>
  669. 669 </div>
  670. 670 </div>
  671. 671 `).join('');
  672. 672 } catch (error) {
  673. 673 console.error('Error updating history list:', error);
  674. 674 }
  675. 675 }
  676. 676
  677. 677 cleanup() {
  678. 678 if (this.elapsedInterval) {
  679. 679 clearInterval(this.elapsedInterval);
  680. 680 }
  681. 681 if (this.metadataInterval) {
  682. 682 clearInterval(this.metadataInterval);
  683. 683 }
  684. 684 if (this.audio) {
  685. 685 this.audio.pause();
  686. 686 }
  687. 687 }
  688. 688}
  689. 689
  690. 690// Initialize the player when DOM is ready
  691. 691document.addEventListener('DOMContentLoaded', () => {
  692. 692 const container = document.getElementById('radio-player');
  693. 693
  694. 694 // Use config from WordPress localization or fallback
  695. 695 const config = window.radioPlayerConfig || {
  696. 696 streamUrl: 'https://s76.radiolize.com:8050/radio.mp3',
  697. 697 directApiUrl: 'https://s76.radiolize.com/api/nowplaying/18',
  698. 698 stationName: 'Paranormal FM'
  699. 699 };
  700. 700
  701. 701 if (container) {
  702. 702 window.radioPlayer = new RadioPlayer(container, config);
  703. 703 console.log('Radio player initialized successfully');
  704. 704 } else {
  705. 705 console.error('Radio player container not found');
  706. 706 }
  707. 707});
  708. 708
  709. 709// Cleanup on page unload
  710. 710window.addEventListener('beforeunload', () => {
  711. 711 if (window.radioPlayer) {
  712. 712 window.radioPlayer.cleanup();
  713. 713 }
  714. 714});

Raw Paste

Comments 0
Login to post a comment.
  • No comments yet. Be the first.
Login to post a comment. Login or Register
We use cookies. To comply with GDPR in the EU and the UK we have to show you these.

We use cookies and similar technologies to keep this website functional (including spam protection via Google reCAPTCHA or Cloudflare Turnstile), and — with your consent — to measure usage and show ads. See Privacy.