// Player Class class KPFKPlayer { constructor() { this.audio = null; this.episode = null; this.playing = false; this.episodes = []; this.currentPage = 1; this.autoplayNext = true; this.episodesPerPage = 6; this.loading = false; // Cache DOM elements this.elements = { container: null, episodes: null, controls: null, progressBar: null, progressFill: null, currentTime: null, totalTime: null, controlPlay: null }; // Get showCode from the HTML element const container = document.getElementById('kpfk-player'); this.showCode = container?.dataset.show || 'sojourner'; this.elements.container = container; this.init(); } async init() { if (!this.elements.container) return; this.elements.container.innerHTML = '
Loading episodes...
'; try { await this.loadShowInfo(); await this.loadEpisodes(); this.render(); this.bind(); this.cacheElements(); } catch (e) { console.error('Failed to load:', e); this.elements.container.innerHTML = '
Failed to load episodes
'; } } cacheElements() { // Cache frequently used elements after render this.elements.episodes = document.querySelector('.kp-episodes'); this.elements.controls = document.querySelector('.kp-controls'); this.elements.progressBar = document.querySelector('.kp-progress'); this.elements.progressFill = document.querySelector('.kp-progress-fill'); this.elements.currentTime = document.querySelector('.kp-current'); this.elements.totalTime = document.querySelector('.kp-total'); this.elements.controlPlay = document.querySelector('.kp-control-play'); } async loadShowInfo() { try { // Get show info from the API const response = await fetch(`https://confessor.kpfk.org/_nu_do_api.php?req=key&key=${this.showCode}&json=1`); const data = await response.json(); if (data && data.sh_name) { this.showInfo = { title: data.sh_name, subtitle: this.formatShowSchedule(data), image: data.sh_photo ? `https://confessor.kpfk.org/pix/${data.sh_photo}` : null, djname: data.sh_djname || '' }; } else { // Fallback if API doesn't return show info this.showInfo = this.getDefaultShowInfo(); } } catch (e) { console.error('Failed to load show info:', e); this.showInfo = this.getDefaultShowInfo(); } } formatShowSchedule(showData) { if (!showData.sh_days || !showData.sh_ampm) return 'Recent Episodes'; return `${showData.sh_days} • ${showData.sh_ampm}`; } getDefaultShowInfo() { const shows = { sojourner: { title: 'Sojourner Truth', subtitle: 'Justice & Liberation • Saturdays 2PM', image: 'https://mmo.aiircdn.com/237/685d06dede19e.jpg' }, letters: { title: 'Letters and Politics', subtitle: 'Progressive Commentary • Weekdays 4PM' }, dn: { title: 'Democracy Now!', subtitle: 'Independent News • Weekdays 7AM' }, hartmann: { title: 'Thom Hartmann Program', subtitle: 'Progressive Talk • Weekdays 9AM' }, backgroundbriefing: { title: 'Background Briefing', subtitle: 'In-depth Analysis • Weekdays 5PM' }, bradcast2: { title: 'The BradCast', subtitle: 'Green News Report • Weekdays 6PM' }, informap: { title: 'Informa', subtitle: 'News & Analysis' } }; return shows[this.showCode] || { title: 'KPFK Show', subtitle: 'Recent Episodes' }; } async loadEpisodes() { try { // Get archived episodes using the API const response = await fetch(`https://confessor.kpfk.org/_nu_do_api.php?req=fil&id=${this.showCode}&num=50&json=1`); const data = await response.json(); this.episodes = []; if (Array.isArray(data)) { data.forEach(item => { if (item.mp3) { // Parse date from the archive data let date = 'Recent'; let episodeDate = ''; if (item.date) { date = item.date; episodeDate = item.date; } else if (item.def_time) { const d = new Date(item.def_time * 1000); date = d.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }); episodeDate = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); } // Calculate duration from lsecs (length in seconds) let duration = ''; if (item.lsecs) { const mins = Math.floor(item.lsecs / 60); const secs = item.lsecs % 60; duration = `${mins}:${secs.toString().padStart(2, '0')}`; } // Build description from available fields let description = ''; if (item.pubfile && item.pubfile[0]) { const pf = item.pubfile[0]; if (pf.pf_gname) description = pf.pf_gname; if (pf.pf_gtopic && description) description += ' - ' + pf.pf_gtopic; else if (pf.pf_gtopic) description = pf.pf_gtopic; } this.episodes.push({ title: item.title || `${this.showInfo.title} - ${episodeDate}`, date, episodeDate, duration, description: description || '', audioUrl: this.fixAudioURL(item.mp3) }); } }); } } catch (e) { console.error('Failed to load episodes from API:', e); // Fallback to RSS if API fails await this.loadEpisodesFromRSS(); } } fixAudioURL(url) { if (!url) return null; // Fix the confessor internal path to public archive URL if (url.includes('confessor.kpfk.org/home/kpfkarch/public_html/mp3/')) { const filename = url.split('/mp3/')[1]; if (filename) { return `https://archive.kpfk.org/mp3/${filename}`; } } // If it's already a proper archive URL, return as-is if (url.includes('archive.kpfk.org/mp3/')) { return url; } return url; } async loadEpisodesFromRSS() { // Fallback method using RSS const rssUrl = `https://archive.kpfk.org/getrss.php?id=${this.showCode}`; try { const response = await fetch(rssUrl); const text = await response.text(); const parser = new DOMParser(); const xml = parser.parseFromString(text, 'text/xml'); const items = xml.querySelectorAll('item'); this.episodes = []; items.forEach((item, index) => { const title = item.querySelector('title')?.textContent || ''; const desc = item.querySelector('description')?.textContent || ''; const pubDate = item.querySelector('pubDate')?.textContent || ''; const enclosure = item.querySelector('enclosure'); const audioUrl = enclosure?.getAttribute('url'); if (!audioUrl) return; let date = 'Recent'; let episodeDate = ''; if (pubDate) { try { const d = new Date(pubDate); date = d.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }); episodeDate = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); } catch (e) { date = pubDate; episodeDate = pubDate; } } let duration = ''; const durationMatch = desc.match(/(\d{1,3}:\d{2}(?::\d{2})?)/); if (durationMatch) { duration = durationMatch[1]; } let cleanDesc = desc.replace(/<[^>]*>/g, '') .replace(/\d{1,3}:\d{2}(?::\d{2})?/, '') .replace(/\s+/g, ' ') .trim(); if (index !== 0 || title !== this.showInfo.title) { this.episodes.push({ title: title || `Episode ${this.episodes.length + 1}`, date, episodeDate, duration, description: cleanDesc || '', audioUrl }); } }); } catch (e) { console.error('RSS fallback also failed:', e); } } render() { const info = this.showInfo || this.getDefaultShowInfo(); const totalPages = Math.ceil(this.episodes.length / this.episodesPerPage); const start = (this.currentPage - 1) * this.episodesPerPage; const pageEpisodes = this.episodes.slice(start, start + this.episodesPerPage); document.getElementById('kpfk-player').innerHTML = `
${info.image ? `
${info.title}
` : ''}
${info.title}
${info.subtitle}
${pageEpisodes.length > 0 ? pageEpisodes.map((ep, i) => `
${this.escapeHtml(ep.title)}
${ep.date}${ep.duration ? ' • ' + ep.duration : ''}
${ep.description ? `
${this.escapeHtml(ep.description)}
${ep.description.length > 150 ? '' : ''}
` : ''}
♪ Now Playing
`).join('') : '
No episodes available
'}
${totalPages > 1 ? `
Page ${this.currentPage} of ${totalPages} • ${this.episodes.length} episodes
` : ''}
0:00 / 0:00
`; } bind() { // Play buttons document.querySelectorAll('.kp-play').forEach(btn => { btn.onclick = e => this.play(e); }); // Control play button const controlPlay = document.querySelector('.kp-control-play'); if (controlPlay) { controlPlay.onclick = () => this.togglePlayback(); } // Skip buttons document.querySelectorAll('.kp-skip').forEach(btn => { btn.onclick = e => this.skip(e); }); // Progress bar const progress = document.querySelector('.kp-progress'); if (progress) progress.onclick = e => this.seek(e); // Speed controls document.querySelectorAll('.kp-speed').forEach(btn => { btn.onclick = e => this.setSpeed(e); }); // Pagination document.querySelectorAll('.kp-page-btn').forEach(btn => { btn.onclick = e => this.paginate(e); }); // Read more buttons document.querySelectorAll('.kp-read-more').forEach(btn => { btn.onclick = e => this.toggleDescription(e); }); // Download buttons document.querySelectorAll('.kp-download').forEach(btn => { btn.onclick = e => this.downloadEpisode(e); }); // Volume controls const volumeBtn = document.querySelector('.kp-volume-btn'); if (volumeBtn) { volumeBtn.onclick = () => this.toggleMute(); } const volumeSlider = document.querySelector('.kp-volume-slider'); if (volumeSlider) { volumeSlider.onclick = e => this.setVolume(e); } } downloadEpisode(e) { const btn = e.target.closest('.kp-download'); const url = btn.dataset.url; const title = btn.dataset.title; // Create a temporary anchor element to trigger download const a = document.createElement('a'); a.href = url; a.download = `${title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.mp3`; document.body.appendChild(a); a.click(); document.body.removeChild(a); } toggleMute() { if (!this.audio) return; if (this.audio.muted || this.audio.volume === 0) { this.audio.muted = false; this.audio.volume = this.lastVolume || 1; this.updateVolumeUI(); } else { this.lastVolume = this.audio.volume; this.audio.muted = true; this.updateVolumeUI(); } } setVolume(e) { if (!this.audio) return; const rect = e.currentTarget.getBoundingClientRect(); const percent = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); this.audio.volume = percent; this.audio.muted = false; this.updateVolumeUI(); } updateVolumeUI() { if (!this.audio) return; const volumeFill = document.querySelector('.kp-volume-fill'); const volumeBtn = document.querySelector('.kp-volume-btn'); if (volumeFill) { const volume = this.audio.muted ? 0 : this.audio.volume; volumeFill.style.width = `${volume * 100}%`; } if (volumeBtn) { let icon; if (this.audio.muted || this.audio.volume === 0) { icon = ''; } else if (this.audio.volume < 0.5) { icon = ''; } else { icon = ''; } volumeBtn.innerHTML = `${icon}`; } } toggleDescription(e) { const btn = e.target; const wrapper = btn.closest('.kp-desc-wrapper'); const desc = wrapper.querySelector('.kp-desc'); if (desc.classList.contains('expanded')) { desc.classList.remove('expanded'); btn.textContent = 'Read more...'; } else { desc.classList.add('expanded'); btn.textContent = 'Read less'; } } togglePlayback() { if (!this.audio) return; this.audio.paused ? this.audio.play() : this.audio.pause(); } skip(e) { if (!this.audio) return; const seconds = parseInt(e.target.closest('.kp-skip').dataset.skip); this.audio.currentTime += seconds; } play(e) { const btn = e.target.closest('.kp-play'); const episode = btn.closest('.kp-episode'); const index = parseInt(episode.dataset.index); const ep = this.episodes[index]; if (this.episode === episode && this.audio) { this.audio.paused ? this.audio.play() : this.audio.pause(); return; } if (this.audio) { this.audio.pause(); this.reset(); } this.episode = episode; this.currentIndex = index; this.currentEpisode = ep; episode.classList.add('playing'); // Update current info document.querySelector('.kp-current-title').textContent = ep.title; document.querySelector('.kp-current-date').textContent = ep.episodeDate; this.audio = new Audio(ep.audioUrl); this.setupAudio(btn); this.audio.play(); } setupAudio(btn) { this.audio.onloadstart = () => { btn.innerHTML = ''; }; this.audio.onplay = () => { this.playing = true; btn.innerHTML = ''; if (this.elements.controlPlay) { this.elements.controlPlay.innerHTML = ''; } this.showControls(); this.episode.querySelector('.kp-now-playing').classList.add('active'); this.updateVolumeUI(); }; this.audio.onpause = () => { this.playing = false; btn.innerHTML = ''; if (this.elements.controlPlay) { this.elements.controlPlay.innerHTML = ''; } this.episode.querySelector('.kp-now-playing').classList.remove('active'); }; this.audio.ontimeupdate = () => this.updateProgress(); this.audio.onended = () => { if (this.autoplayNext && this.currentIndex < this.episodes.length - 1) { this.playNext(); } else { this.reset(); } }; this.audio.onerror = () => { btn.innerHTML = '❌'; setTimeout(() => this.reset(), 1000); }; } playNext() { const nextIndex = this.currentIndex + 1; const nextEpisode = document.querySelector(`[data-index="${nextIndex}"]`); if (nextEpisode) { const btn = nextEpisode.querySelector('.kp-play'); this.play({ target: btn }); } } showControls() { if (this.elements.controls) { this.elements.controls.classList.add('active'); } } updateProgress() { if (!this.audio?.duration) return; const percent = (this.audio.currentTime / this.audio.duration) * 100; // Use cached elements if (this.elements.progressFill) { this.elements.progressFill.style.width = `${percent}%`; } if (this.elements.currentTime) { this.elements.currentTime.textContent = this.formatTime(this.audio.currentTime); } if (this.elements.totalTime) { this.elements.totalTime.textContent = this.formatTime(this.audio.duration); } } seek(e) { if (!this.audio?.duration) return; const rect = e.target.getBoundingClientRect(); const percent = (e.clientX - rect.left) / rect.width; this.audio.currentTime = percent * this.audio.duration; } setSpeed(e) { const btn = e.target; const speed = parseFloat(btn.dataset.speed); if (this.audio) this.audio.playbackRate = speed; document.querySelectorAll('.kp-speed').forEach(b => b.classList.remove('active')); btn.classList.add('active'); } async paginate(e) { if (this.loading) return; const action = e.target.dataset.action; const totalPages = Math.ceil(this.episodes.length / this.episodesPerPage); if (action === 'next' && this.currentPage < totalPages) { this.currentPage++; } else if (action === 'prev' && this.currentPage > 1) { this.currentPage--; } else { return; } this.loading = true; // Fade out current episodes const episodesContainer = document.querySelector('.kp-episodes'); episodesContainer.style.opacity = '0.5'; // Small delay for visual feedback await new Promise(resolve => setTimeout(resolve, 150)); this.render(); this.bind(); // Restore playing state if needed if (this.audio && this.currentEpisode) { const playingIndex = this.episodes.indexOf(this.currentEpisode); const playingEpisode = document.querySelector(`[data-index="${playingIndex}"]`); if (playingEpisode) { playingEpisode.classList.add('playing'); const btn = playingEpisode.querySelector('.kp-play'); if (this.playing) { btn.innerHTML = ''; playingEpisode.querySelector('.kp-now-playing').classList.add('active'); } } } this.loading = false; } reset() { document.querySelectorAll('.kp-episode').forEach(ep => { ep.classList.remove('playing'); ep.querySelector('.kp-now-playing').classList.remove('active'); }); document.querySelectorAll('.kp-play').forEach(btn => { btn.innerHTML = ''; }); document.querySelector('.kp-controls').classList.remove('active'); document.querySelector('.kp-current-title').textContent = ''; document.querySelector('.kp-current-date').textContent = ''; this.audio = null; this.episode = null; this.playing = false; this.currentEpisode = null; } formatTime(seconds) { if (!seconds || !isFinite(seconds)) return '0:00'; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; } escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } destroy() { // Clean up to prevent memory leaks if (this.audio) { this.audio.pause(); this.audio.src = ''; this.audio.onplay = null; this.audio.onpause = null; this.audio.onended = null; this.audio.ontimeupdate = null; this.audio.onerror = null; this.audio.onloadstart = null; this.audio = null; } // Remove event listeners document.querySelectorAll('.kp-play, .kp-control-play, .kp-skip, .kp-speed, .kp-page-btn, .kp-read-more, .kp-download').forEach(el => { el.onclick = null; }); // Clear cached elements this.elements = { container: null, episodes: null, controls: null, progressBar: null, progressFill: null, currentTime: null, totalTime: null, controlPlay: null }; // Clear data this.episodes = []; this.episode = null; this.currentEpisode = null; } } // Auto-initialize if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => new KPFKPlayer()); } else { new KPFKPlayer(); }

Roosevelt Hotel

7000 Hollywood Blvd
Los Angeles, CA
90028

More Information (KPFK 90.7 FM is not responsible for external websites)

Phone Number: (323) 856-1970

What's On Now

  • Travel Tips for Aztlan

    10:00pm - Midnight

    The best music from the latin youth culture including rock, salsa, cumbia, hip-hop, ska, electronica and LatinX ChicanoX Music, Cultural news and information showcase

Pledge Now!
KPFK is a progressive media outlet challenging corporate media perspectives and providing a voice to voiceless communities. Help keep KPFK a strong and independent source of music, arts, news and information.

Follow us on Social Media