Audiobooks
True Crime
Comedy
News
Sport
Science
History
Technology
Documentary
Music
Business
Relationships
Episodes
Your Favourites
Episode Title
Show Name
The Podcast Explorer is a lightweight, intuitive widget designed to sit directly on your website, transforming how visitors interact with your content. It bridges the gap between static links and a truly interactive listening experience.
Buy Me A Coffee
Key Features at a Glance
The Podcast Explorer is built to remove friction from discovery. Here is how it works:
- Curated Categories: Navigate through predefined streams like news, technology, and more. If you don't know what you are looking for, the categories provide an immediate starting point.
- Powerful Search: A dedicated search bar lets you pinpoint specific episodes or topics within seconds. The results appear in a clean, scrollable list that keeps your interface uncluttered.
- Integrated Player: Once you select an episode, the integrated player launches instantly. To keep the focus on the content, the search results auto-collapse, giving the player the screen space it needs.
- Local Favourites: Your visitors can save their preferred episodes to a favourites list. This data is stored directly in the browser cache, ensuring their preferences persist across sessions without requiring a complicated login.
Why This Matters
For creators and bloggers, keeping visitors engaged is a top priority. Instead of forcing users to navigate to third-party platforms to find and play your content, the Podcast Explorer keeps them within your ecosystem. It is designed to be fast, responsive, and easy to implement.
Whether you manage a personal blog or a professional site, this tool is designed to enhance your content delivery and provide your audience with a premium listening experience.
Podcast Explorer
<div class="spw-container">
<style>
.spw-container {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
width: 100%;
max-width: 500px;
border: 1px solid #cbd5e1;
border-radius: 20px;
background: #ffffff;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 12px 30px rgba(0,0,0,0.06);
margin: 15px auto;
box-sizing: border-box;
position: relative;
}
.spw-container * {
box-sizing: border-box;
}
.spw-header {
padding: 20px;
background: linear-gradient(135deg, #475569 0%, #0f172a 100%);
color: #ffffff;
display: flex;
flex-direction: column;
gap: 14px;
}
.spw-header-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.spw-header-title-group {
display: flex;
align-items: center;
gap: 10px;
}
.spw-header h3 {
margin: 0;
font-size: 20px;
font-weight: 700;
color: #ffffff !important;
letter-spacing: -0.3px;
}
.spw-header-icon {
width: 24px;
height: 24px;
fill: none;
stroke: currentColor;
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
}
.spw-author-link {
width: 16px;
height: 16px;
color: #94a3b8;
display: flex;
align-items: center;
transition: color 0.2s ease;
}
.spw-author-link:hover {
color: #ffffff;
}
.spw-author-link svg {
width: 100%;
height: 100%;
fill: none;
stroke: currentColor;
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
}
.spw-search-box {
display: flex;
align-items: center;
background: #ffffff;
border-radius: 30px;
padding: 4px 6px 4px 16px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
gap: 6px;
}
.spw-search-box input {
flex: 1;
border: none !important;
background: transparent !important;
padding: 10px 0 !important;
font-size: 15px !important;
color: #1e293b !important;
outline: none !important;
width: 100%;
}
.spw-btn {
width: 38px;
height: 38px;
border: none !important;
border-radius: 50% !important;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: background 0.2s ease, transform 0.1s ease;
padding: 0 !important;
margin: 0 !important;
}
.spw-btn svg {
width: 18px;
height: 18px;
fill: none;
stroke: currentColor;
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
}
.spw-search-btn {
background: #334155;
color: white;
}
.spw-search-btn:hover {
background: #1e293b;
transform: scale(1.03);
}
.spw-reset-btn {
background: #f1f5f9;
color: #64748b;
}
.spw-reset-btn:hover {
background: #e2e8f0;
color: #0f172a;
transform: scale(1.03);
}
.spw-categories {
display: flex;
gap: 8px;
overflow-x: auto;
scrollbar-width: none;
padding-bottom: 2px;
margin-top: 2px;
}
.spw-categories::-webkit-scrollbar {
display: none;
}
.spw-tag {
background: rgba(255, 255, 255, 0.15);
color: #ffffff;
padding: 6px 14px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s ease;
border: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
align-items: center;
gap: 6px;
}
.spw-tag:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-1px);
}
.spw-tag svg {
width: 14px;
height: 14px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.spw-list-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 16px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
font-size: 13px;
flex-wrap: wrap;
gap: 10px;
}
.spw-toolbar-left {
display: flex;
align-items: center;
gap: 10px;
}
.spw-list-title {
font-weight: 600;
color: #475569;
}
.spw-filter-select {
padding: 4px 8px;
border-radius: 6px;
border: 1px solid #cbd5e1;
font-size: 12px;
color: #475569;
background: #ffffff;
outline: none;
cursor: pointer;
display: none;
}
.spw-toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.spw-toggle-btn {
background: transparent !important;
border: 1px solid transparent !important;
color: #64748b;
font-size: 12px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
padding: 6px 10px !important;
border-radius: 6px;
transition: background 0.2s, color 0.2s;
}
.spw-toggle-btn:hover {
background: #e2e8f0 !important;
color: #0f172a;
}
.spw-chevron {
width: 14px;
height: 14px;
fill: none;
stroke: currentColor;
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform 0.3s ease;
}
.spw-chevron.collapsed {
transform: rotate(180deg);
}
.spw-results {
height: 380px;
overflow-y: auto;
padding: 0 !important;
margin: 0 !important;
list-style: none !important;
background: #fdfdfd;
transition: height 0.3s ease, opacity 0.3s ease;
}
.spw-results.collapsed {
height: 0;
opacity: 0;
overflow: hidden;
border-bottom: none;
}
.spw-results li, .spw-results li::before, .spw-results li::after {
content: none !important;
list-style: none !important;
list-style-type: none !important;
background-image: none !important;
}
.spw-message {
padding: 50px 20px;
text-align: center;
color: #64748b;
font-size: 14px;
line-height: 1.5;
}
.spw-loading {
padding: 15px 20px;
}
.spw-result-item {
display: flex !important;
padding: 16px !important;
border-bottom: 1px solid #f1f5f9 !important;
cursor: pointer;
align-items: flex-start;
gap: 14px;
background: #ffffff !important;
margin: 0 !important;
text-indent: 0 !important;
transition: background 0.2s ease;
position: relative;
}
.spw-result-item:hover {
background: #f8fafc !important;
}
.spw-container .spw-result-art {
width: 68px !important;
height: 68px !important;
min-width: 68px !important;
max-width: 68px !important;
border-radius: 8px !important;
object-fit: cover !important;
background: #cbd5e1 !important;
flex-shrink: 0 !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.05) !important;
margin: 0 !important;
padding: 0 !important;
}
.spw-result-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.spw-result-show {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #475569;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.spw-result-title {
font-size: 14px;
font-weight: 600;
color: #0f172a;
line-height: 1.3;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.spw-result-description {
font-size: 12.5px;
color: #64748b;
line-height: 1.4;
margin: 2px 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.spw-result-meta {
font-size: 12px;
color: #94a3b8;
margin-top: 2px;
display: flex;
justify-content: space-between;
align-items: center;
}
.spw-container .spw-remove-fav-btn {
background: transparent !important;
border: none !important;
color: #ef4444 !important;
cursor: pointer !important;
padding: 4px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
border-radius: 4px !important;
}
.spw-remove-fav-btn:hover {
background: #fee2e2 !important;
}
.spw-remove-fav-btn svg {
width: 14px !important;
height: 14px !important;
fill: currentColor !important;
}
.spw-slide-menu {
position: absolute;
bottom: -100%;
left: 0;
width: 100%;
height: 80%;
background: #ffffff;
z-index: 10;
transition: bottom 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 -4px 20px rgba(0,0,0,0.15);
border-top-left-radius: 20px;
border-top-right-radius: 20px;
display: flex;
flex-direction: column;
}
.spw-slide-menu.open {
bottom: 0;
}
.spw-slide-header {
padding: 16px;
border-bottom: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
background: #f8fafc;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
}
.spw-slide-header h4 {
margin: 0;
font-size: 15px;
color: #0f172a;
}
.spw-close-slide {
background: #e2e8f0;
border: none;
border-radius: 50%;
width: 28px;
height: 28px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #475569;
}
.spw-close-slide:hover {
background: #cbd5e1;
}
.spw-player {
display: none;
flex-direction: column;
padding: 16px;
background: #ffffff;
border-top: 1px solid #e2e8f0;
box-shadow: 0 -4px 15px rgba(0,0,0,0.04);
position: relative;
z-index: 5;
}
.spw-container .spw-player-info {
display: flex !important;
align-items: center !important;
gap: 12px !important;
margin-bottom: 12px !important;
flex-wrap: nowrap !important;
}
.spw-container .spw-player-info img {
width: 52px !important;
height: 52px !important;
min-width: 52px !important;
max-width: 52px !important;
border-radius: 6px !important;
object-fit: cover !important;
flex-shrink: 0 !important;
margin: 0 !important;
padding: 0 !important;
}
.spw-player-details {
flex: 1;
min-width: 0;
}
.spw-player-title {
font-size: 14px;
font-weight: 700;
color: #0f172a;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.spw-player-show {
font-size: 12px;
color: #64748b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.spw-container .spw-player-actions {
display: flex !important;
gap: 8px !important;
align-items: center !important;
flex-shrink: 0 !important;
}
.spw-container .spw-action-btn {
background: #f1f5f9 !important;
border: none !important;
width: 34px !important;
height: 34px !important;
min-width: 34px !important;
border-radius: 50% !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
cursor: pointer !important;
color: #475569 !important;
transition: all 0.2s !important;
text-decoration: none !important;
padding: 0 !important;
margin: 0 !important;
}
.spw-container .spw-action-btn:hover {
background: #e2e8f0 !important;
color: #0f172a !important;
}
.spw-container .spw-action-btn svg {
width: 16px !important;
height: 16px !important;
fill: none !important;
stroke: currentColor !important;
stroke-width: 2 !important;
stroke-linecap: round !important;
stroke-linejoin: round !important;
}
.spw-container .spw-action-btn.active-fav svg {
fill: #ef4444 !important;
stroke: #ef4444 !important;
}
.spw-audio-element {
width: 100%;
height: 36px;
outline: none;
}
</style>
<div class="spw-header">
<div class="spw-header-top">
<div class="spw-header-title-group">
<svg class="spw-header-icon" viewBox="0 0 24 24">
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"></path>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
<line x1="12" y1="19" x2="12" y2="23"></line>
<line x1="8" y1="23" x2="16" y2="23"></line>
</svg>
<h3>Podcast Explorer</h3>
</div>
<a href="https://www.seanduffy.uk/" id="spw-author-link" class="spw-author-link" target="_blank" aria-label="Author link">
<svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>
</div>
<div class="spw-search-box">
<input type="text" id="spw-input" placeholder="Search topics or shows..." autocomplete="off" />
<button id="spw-reset-btn" class="spw-btn spw-reset-btn" aria-label="Reset Search">
<svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
<button id="spw-button" class="spw-btn spw-search-btn" aria-label="Search">
<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</button>
</div>
<div class="spw-categories" id="spw-tags-container">
<span class="spw-tag" data-slug="Audiobooks">
<svg viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>
Audiobooks
</span>
<span class="spw-tag" data-slug="True Crime">
<svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
True Crime
</span>
<span class="spw-tag" data-slug="Comedy">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"></circle><path d="M8 14s1.5 2 4 2 4-2 4-2"></path><line x1="9" y1="9" x2="9.01" y2="9"></line><line x1="15" y1="9" x2="15.01" y2="9"></line></svg>
Comedy
</span>
<span class="spw-tag" data-slug="News">
<svg viewBox="0 0 24 24"><path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16l-3 2z"></path><line x1="14" y1="8" x2="18" y2="8"></line><line x1="10" y1="12" x2="18" y2="12"></line><line x1="10" y1="16" x2="18" y2="16"></line></svg>
News
</span>
<span class="spw-tag" data-slug="Science">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"></circle><ellipse cx="12" cy="12" rx="10" ry="4" transform="rotate(45 12 12)"></ellipse><ellipse cx="12" cy="12" rx="10" ry="4" transform="rotate(-45 12 12)"></ellipse></svg>
Science
</span>
<span class="spw-tag" data-slug="History">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
History
</span>
<span class="spw-tag" data-slug="Technology">
<svg viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="2" y1="21" x2="22" y2="21"></line></svg>
Technology
</span>
<span class="spw-tag" data-slug="Business">
<svg viewBox="0 0 24 24"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path></svg>
Business
</span>
</div>
</div>
<div class="spw-list-header">
<div class="spw-toolbar-left">
<span class="spw-list-title">Episodes</span>
<select id="spw-sort-filter" class="spw-filter-select">
<option value="relevance">Sort: Relevance</option>
<option value="date">Sort: Date</option>
<option value="popularity">Sort: Popularity</option>
</select>
</div>
<div class="spw-toolbar-right">
<button id="spw-favs-menu-btn" class="spw-toggle-btn" aria-label="Open Favourites">
<svg viewBox="0 0 24 24" style="width:14px; height:14px; fill:currentColor; stroke:currentColor; stroke-width:2;"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
Favs
</button>
<button id="spw-toggle-btn" class="spw-toggle-btn" aria-label="Toggle results">
<span class="spw-toggle-text" id="spw-toggle-text">Expand</span>
<svg viewBox="0 0 24 24" class="spw-chevron collapsed" id="spw-chevron"><polyline points="18 15 12 9 6 15"></polyline></svg>
</button>
</div>
</div>
<ul class="spw-results collapsed" id="spw-results-list"></ul>
<div id="spw-slide-menu" class="spw-slide-menu">
<div class="spw-slide-header">
<h4>Your Favourites</h4>
<button id="spw-close-slide" class="spw-close-slide" aria-label="Close">
<svg viewBox="0 0 24 24" style="width:16px;height:16px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
</div>
<ul class="spw-results" id="spw-fav-results-list" style="height: calc(100% - 60px); border-bottom: none;"></ul>
</div>
<div class="spw-player" id="spw-player">
<div class="spw-player-info">
<img id="spw-art" src="" alt="" />
<div class="spw-player-details">
<div class="spw-player-title" id="spw-title">Episode Title</div>
<div class="spw-player-show" id="spw-show">Show Name</div>
</div>
<div class="spw-player-actions">
<button id="spw-btn-fav" class="spw-action-btn" title="Add to Favourites">
<svg viewBox="0 0 24 24"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
</button>
</div>
</div>
<audio class="spw-audio-element" id="spw-audio" controls name="media"></audio>
</div>
<script>
(function() {
const inputEl = document.getElementById('spw-input');
const buttonEl = document.getElementById('spw-button');
const resetEl = document.getElementById('spw-reset-btn');
const resultsList = document.getElementById('spw-results-list');
const tagsContainer = document.getElementById('spw-tags-container');
const playerContainer = document.getElementById('spw-player');
const audioEl = document.getElementById('spw-audio');
const artEl = document.getElementById('spw-art');
const titleEl = document.getElementById('spw-title');
const showEl = document.getElementById('spw-show');
const favBtn = document.getElementById('spw-btn-fav');
const toggleBtn = document.getElementById('spw-toggle-btn');
const toggleText = document.getElementById('spw-toggle-text');
const chevron = document.getElementById('spw-chevron');
const sortFilter = document.getElementById('spw-sort-filter');
const slideMenu = document.getElementById('spw-slide-menu');
const favsMenuBtn = document.getElementById('spw-favs-menu-btn');
const closeSlideBtn = document.getElementById('spw-close-slide');
const favResultsList = document.getElementById('spw-fav-results-list');
const fallbackIcon = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2394a3b8'%3E%3Cpath d='M12 2c-1.66 0-3 1.34-3 3v7c0 1.66 1.34 3 3 3s3-1.34 3-3V5c0-1.66-1.34-3-3-3zm7 10h-2c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-2.08c3.39-.49 6-3.39 6-6.92z'/%3E%3C/svg%3E";
let currentResults = [];
let currentSearchTerm = '';
let currentOffset = 0;
const FETCH_LIMIT = 25;
let currentEpisodeData = null;
let isFetching = false;
let hasMore = true;
let favourites = [];
const CACHE_LIFETIME = 365 * 24 * 60 * 60 * 1000;
function verifyIntegrity() {
const link = document.getElementById('spw-author-link');
if (!link || link.getAttribute('href') !== 'https://www.seanduffy.uk/') {
if (audioEl) audioEl.pause();
playerContainer.style.display = 'none';
resultsList.innerHTML = '<li class="spw-message" style="color:#ef4444;">Widget integrity compromised.</li>';
return false;
}
return true;
}
function loadFavourites() {
try {
const stored = localStorage.getItem('spw_favourites');
if (stored) {
const parsed = JSON.parse(stored);
const now = Date.now();
favourites = parsed.filter(item => (now - item.timestamp) < CACHE_LIFETIME);
if (favourites.length !== parsed.length) {
saveFavourites();
}
}
} catch (e) {
favourites = [];
}
}
function saveFavourites() {
localStorage.setItem('spw_favourites', JSON.stringify(favourites));
}
function isFavourite(url) {
return favourites.some(f => f.episodeUrl === url);
}
function toggleFavourite(epData) {
if (!epData) return;
const index = favourites.findIndex(f => f.episodeUrl === epData.episodeUrl);
if (index > -1) {
favourites.splice(index, 1);
favBtn.classList.remove('active-fav');
} else {
epData.timestamp = Date.now();
favourites.unshift(epData);
favBtn.classList.add('active-fav');
}
saveFavourites();
renderFavourites();
}
function renderFavourites() {
favResultsList.innerHTML = '';
if (favourites.length === 0) {
favResultsList.innerHTML = '<li class="spw-message">You have not saved any favourites yet.</li>';
return;
}
favourites.forEach(ep => {
const li = createEpisodeListItem(ep, true);
favResultsList.appendChild(li);
});
}
function formatMillis(ms) {
if (!ms) return '';
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
return hours > 0
? ${hours}h ${minutes}m
: ${minutes}:${seconds.toString().padStart(2, '0')} mins;
}
function formatDate(dateStr) {
if (!dateStr) return '';
return new Date(dateStr).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
}
function cleanText(htmlStr) {
if (!htmlStr) return '';
return htmlStr.replace(/<[^>]*>/g, '').replace(/&[^;]+;/g, '').trim();
}
function setListState(expand) {
if (expand) {
resultsList.classList.remove('collapsed');
chevron.classList.remove('collapsed');
toggleText.textContent = 'Collapse';
sortFilter.style.display = 'inline-block';
} else {
resultsList.classList.add('collapsed');
chevron.classList.add('collapsed');
toggleText.textContent = 'Expand';
sortFilter.style.display = 'none';
}
}
toggleBtn.addEventListener('click', () => {
const isCollapsed = resultsList.classList.contains('collapsed');
if (isCollapsed && resultsList.children.length === 0) {
resultsList.innerHTML = '<li class="spw-message">Select a category or enter a search term to explore tracks.</li>';
}
setListState(isCollapsed);
});
favsMenuBtn.addEventListener('click', () => {
setListState(false);
renderFavourites();
slideMenu.classList.add('open');
});
closeSlideBtn.addEventListener('click', () => {
slideMenu.classList.remove('open');
});
resetEl.addEventListener('click', () => {
inputEl.value = '';
currentSearchTerm = '';
currentResults = [];
hasMore = true;
resultsList.innerHTML = '';
setListState(false);
playerContainer.style.display = 'none';
audioEl.pause();
audioEl.src = '';
slideMenu.classList.remove('open');
});
function playEpisode(epData) {
if (!verifyIntegrity()) return;
currentEpisodeData = epData;
const highResArtwork = epData.artworkUrl600 || epData.artworkUrl100 || fallbackIcon;
artEl.src = highResArtwork;
titleEl.textContent = epData.trackName;
showEl.textContent = epData.collectionName || 'Podcast';
audioEl.src = epData.episodeUrl;
if (isFavourite(epData.episodeUrl)) {
favBtn.classList.add('active-fav');
} else {
favBtn.classList.remove('active-fav');
}
playerContainer.style.display = 'flex';
setListState(false);
slideMenu.classList.remove('open');
audioEl.play().catch(() => {});
}
favBtn.addEventListener('click', () => {
toggleFavourite(currentEpisodeData);
});
sortFilter.addEventListener('change', () => {
renderResults(true);
});
function createEpisodeListItem(ep, isFavList = false) {
const highResArtwork = ep.artworkUrl600 || ep.artworkUrl100 || fallbackIcon;
const li = document.createElement('li');
li.className = 'spw-result-item';
const img = document.createElement('img');
img.className = 'spw-result-art';
img.src = highResArtwork;
img.onclick = () => playEpisode(ep);
const textDiv = document.createElement('div');
textDiv.className = 'spw-result-text';
textDiv.onclick = () => playEpisode(ep);
const showSpan = document.createElement('span');
showSpan.className = 'spw-result-show';
showSpan.textContent = ep.collectionName || 'Podcast';
const titleDiv = document.createElement('div');
titleDiv.className = 'spw-result-title';
titleDiv.textContent = ep.trackName;
const descDiv = document.createElement('div');
descDiv.className = 'spw-result-description';
descDiv.textContent = cleanText(ep.description || ep.shortDescription || 'No description available.');
const metaDiv = document.createElement('div');
metaDiv.className = 'spw-result-meta';
const pieces = [formatDate(ep.releaseDate), formatMillis(ep.trackTimeMillis)].filter(Boolean);
const infoSpan = document.createElement('span');
infoSpan.textContent = pieces.join(' | ');
metaDiv.appendChild(infoSpan);
if (isFavList) {
const removeBtn = document.createElement('button');
removeBtn.className = 'spw-remove-fav-btn';
removeBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"></path></svg>';
removeBtn.onclick = (e) => {
e.stopPropagation();
toggleFavourite(ep);
};
metaDiv.appendChild(removeBtn);
}
textDiv.appendChild(showSpan);
textDiv.appendChild(titleDiv);
textDiv.appendChild(descDiv);
textDiv.appendChild(metaDiv);
li.appendChild(img);
li.appendChild(textDiv);
return li;
}
function renderResults(resetList = false) {
const previousScrollTop = resultsList.scrollTop;
if (resetList) resultsList.innerHTML = '';
if (currentResults.length === 0 && resetList) {
resultsList.innerHTML = '<li class="spw-message">No results found. Try adjusting terms.</li>';
return;
}
const sortType = sortFilter.value;
let displayData = [...currentResults];
if (sortType === 'date') {
displayData.sort((a, b) => new Date(b.releaseDate || 0) - new Date(a.releaseDate || 0));
} else if (sortType === 'popularity') {
displayData.sort((a, b) => (b.trackTimeMillis || 0) - (a.trackTimeMillis || 0));
}
resultsList.innerHTML = '';
displayData.forEach(ep => {
const li = createEpisodeListItem(ep, false);
resultsList.appendChild(li);
});
if (!resetList) {
resultsList.scrollTop = previousScrollTop;
}
}
function fetchEpisodes(term, isLoadMore = false) {
if (!verifyIntegrity()) return;
if (!term) return;
currentSearchTerm = term;
if (!isLoadMore) {
currentOffset = 0;
currentResults = [];
hasMore = true;
setListState(true);
resultsList.innerHTML = '<li class="spw-message">Searching streaming directories...</li>';
} else {
currentOffset += FETCH_LIMIT;
const loadingLi = document.createElement('li');
loadingLi.className = 'spw-message spw-loading';
loadingLi.textContent = 'Loading more episodes...';
resultsList.appendChild(loadingLi);
}
isFetching = true;
fetch([https://itunes.apple.com/search?media=podcast&entity=podcastEpisode&term=$](https://itunes.apple.com/search?media=podcast&entity=podcastEpisode&term=$){encodeURIComponent(term)}&limit=${FETCH_LIMIT}&offset=${currentOffset})
.then(r => r.json())
.then(data => {
isFetching = false;
const loadingEl = resultsList.querySelector('.spw-loading');
if(loadingEl) loadingEl.remove();
const items = data.results.filter(ep => ep.episodeUrl);
if (items.length < FETCH_LIMIT) hasMore = false;
currentResults = isLoadMore ? currentResults.concat(items) : items;
renderResults(!isLoadMore);
})
.catch(() => {
isFetching = false;
const loadingEl = resultsList.querySelector('.spw-loading');
if(loadingEl) loadingEl.remove();
if (!isLoadMore) resultsList.innerHTML = '<li class="spw-message">An error occurred. Please try again.</li>';
});
}
buttonEl.addEventListener('click', () => fetchEpisodes(inputEl.value.trim()));
inputEl.addEventListener('keypress', (e) => { if (e.key === 'Enter') fetchEpisodes(inputEl.value.trim()); });
tagsContainer.addEventListener('click', (e) => {
const tag = e.target.closest('.spw-tag');
if (tag) {
inputEl.value = tag.dataset.slug;
fetchEpisodes(tag.dataset.slug);
}
});
resultsList.addEventListener('scroll', () => {
if (isFetching || !hasMore || currentResults.length === 0) return;
if (resultsList.scrollTop + resultsList.clientHeight >= resultsList.scrollHeight - 50) {
fetchEpisodes(currentSearchTerm, true);
}
});
loadFavourites();
})();
</script>
</div>
Notice:
Comments are moderated and may not appear immediately. Please keep your comments respectful, and relevant to the post. Spam will not be tolerated. My site. My rules.