Podcast Explorer

This Podcast Explorer serves as a sophisticated, self-contained widget designed to bring rich audio discovery directly to your website. With its refined slate-inspired aesthetic, it blends seamlessly into modern layouts, providing a clean interface that keeps your visitors engaged without the distraction of external page redirects. It transforms a standard sidebar or post area into an interactive hub for media consumption.

Podcast Explorer

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, &#39;0&#39;)} 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&amp;entity=podcastEpisode&amp;term=$](https://itunes.apple.com/search?media=podcast&amp;entity=podcastEpisode&amp;term=$){encodeURIComponent(term)}&amp;limit=${FETCH_LIMIT}&amp;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>
    

    Post a Comment

    0 Comments

    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.

    Post a Comment (0)