"play last week's show" from ARK using js/html

On our program schedule, we wanted dedicated buttons next to each show title to “play last week’s show”. Came up with some .js that accepts a day and time (24hr format) arguement, and uses that to start an audio player at the most recent (past) occurrence of that day/time. In case this is useful to anyone else:

.js (place at the END of the page):

<script>
    const STATION_ID = "XCSB";
    const STATION_TZ = "America/New_York";

    function calculateRecentTimestamp(dayName, timeStr) {
        const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
        const targetDay = days.indexOf(dayName);
        const [hrs, mins] = timeStr.split(':').map(Number);
        const now = new Date();
        const stationNow = new Date(now.toLocaleString("en-US", {
            timeZone: STATION_TZ
        }));

        let target = new Date(stationNow);
        target.setHours(hrs, mins, 0, 0);

        let diff = stationNow.getDay() - targetDay;
        if (diff < 0 || (diff === 0 && stationNow < target)) {
            diff += 7;
        }
        target.setDate(target.getDate() - diff);

        return target.toISOString().replace(/[:\-]/g, '').split('.')[0] + 'Z';
    }

    function playArchive(name, day, time) {
        const timestamp = calculateRecentTimestamp(day, time);
        // Updated URL format from arkPlayer.js: STATION-TIMESTAMP/index.m3u8
        const streamUrl = `https://ark3.spinitron.com/ark2/${STATION_ID}-${timestamp}/index.m3u8`;

        const audio = document.getElementById('main-audio');
        const bar = document.getElementById('player-bar');
        const label = document.getElementById('now-playing-label');

        label.innerText = `NOW PLAYING: ${name} (Broadcast: ${day} @ ${time} EST)`;
        bar.style.display = 'block';

        if (Hls.isSupported()) {
            const hls = new Hls();
            hls.loadSource(streamUrl);
            hls.attachMedia(audio);
            hls.on(Hls.Events.MANIFEST_PARSED, () => audio.play());
        } else if (audio.canPlayType('application/vnd.apple.mpegurl')) {
            audio.src = streamUrl;
            audio.play();
        }
    }

</script>

(our schedule was in an HTML table, this is the styling for that table, and for the player widget floated to the bottom) CSS:

<style>
    .table_programgrid {
        overflow: auto;
        width: 100%;
    }

    .table_programgrid table {
        border: 1px solid #dededf;
        width: 100%;
        table-layout: auto;
        border-collapse: collapse;
        text-align: left;
    }

    .table_programgrid th {
        border: 1px solid #dededf;
        background-color: #66ccff;
        color: #000000;
        padding: 5px;
    }

    .table_programgrid td {
        border: 1px solid #dededf;
        padding: 5px;
    }

    .table_programgrid tr:nth-child(even) td {
        background-color: #eeeeee;
    }

    .table_programgrid tr:nth-child(odd) td {
        background-color: #dddddd;
    }

    .day-header {
        font-weight: bold;
        background-color: #444 !important;
        color: #0033cc !important;
    }

    .spacer {
        font-weight: bold;
        background-color: #fff !important;
        color: #fff !important;
    }

    /* Persistent Player Bar */
    #player-bar {
        position: fixed;
        bottom: 0;
        left: 0;
        width: 100%;
        background: #222;
        color: white;
        padding: 15px;
        display: none;
        box-shadow: 0 -5px 15px rgba(0, 0, 0, 0.3);
        z-index: 1000;
    }

    .player-inner {
        max-width: 960px;
        margin: 0 auto;
    }

    audio {
        width: 100%;
        height: 35px;
        /* filter: invert(100%) hue-rotate(180deg); */
    }

    #now-playing-label {
        font-size: 12px;
        color: #aaa;
        margin-bottom: 5px;
    }

    .listenbutton {
        cursor: pointer;
        color: #0066cc;
        text-decoration: underline;
        font-weight: bold;
    }

</style>

and then each table cell with the show info and the play button:

   <tr>
      <td>9:00 AM</td>
      <td>Waking the Intonarumori</td>
      <td>bbob</td>
      <td><span class="listenbutton" onclick="playArchive('Waking the Intonarumori', 'Monday', '09:00')">Listen</span></td>
   </tr>

Very slick. I also like the styling of your Player div. Check it out, folks:

bugfix (see below) - the time differential calculation in the first version is based on the client browser’s time, so it will only work properly if the client is in the same timezone as the server. here’s an update that should work from any location:

<script>
        const STATION_ID = "XCSB";
        const STATION_TZ = "America/New_York";

        function calculateRecentTimestamp(dayName, timeStr) {
            const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
            const [targetHrs, targetMins] = timeStr.split(':').map(Number);
            const now = new Date();

            // Iterate backwards up to 7 days to find the most recent matching day
            for (let i = 0; i <= 7; i++) {
                // Subtract days in exact 24-hour milliseconds
                const testDate = new Date(now.getTime() - i * 24 * 60 * 60 * 1000);
                
                // Get the date parts specifically in New York time
                const parts = new Intl.DateTimeFormat('en-US', {
                    timeZone: STATION_TZ,
                    weekday: 'long', year: 'numeric', month: 'numeric', day: 'numeric'
                }).formatToParts(testDate);
                
                const extract = (type) => parts.find(p => p.type === type).value;
                
                if (extract('weekday') === dayName) {
                    const y = extract('year');
                    const mo = extract('month').padStart(2, '0');
                    const d = extract('day').padStart(2, '0');
                    const h = String(targetHrs).padStart(2, '0');
                    const m = String(targetMins).padStart(2, '0');
                    
                    // Construct a base UTC Date simulating the target NY date/time
                    const isoString = `${y}-${mo}-${d}T${h}:${m}:00`;
                    const tempUtcDate = new Date(`${isoString}Z`);
                    
                    // Dynamically find the NY offset at this exact time (handles DST correctly)
                    const formatOpts = { year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: false };
                    const nyTime = tempUtcDate.toLocaleString("en-US", { timeZone: STATION_TZ, ...formatOpts });
                    const utcTime = tempUtcDate.toLocaleString("en-US", { timeZone: "UTC", ...formatOpts });
                    
                    const offsetMs = new Date(nyTime) - new Date(utcTime);
                    
                    // Apply offset to get the true UTC timestamp for the Spinitron archive
                    const finalRealDate = new Date(tempUtcDate.getTime() - offsetMs);
                    
                    // Ensure the calculated show time actually occurred in the past
                    if (finalRealDate <= now) {
                        return finalRealDate.toISOString().replace(/[:\-]/g, '').split('.')[0] + 'Z';
                    }
                }
            }
            return null;
        }

        function playArchive(name, day, time) {
            const timestamp = calculateRecentTimestamp(day, time);
            // Updated URL format from arkPlayer.js: STATION-TIMESTAMP/index.m3u8
            const streamUrl = `https://ark3.spinitron.com/ark2/${STATION_ID}-${timestamp}/index.m3u8`;

            const audio = document.getElementById('main-audio');
            const bar = document.getElementById('player-bar');
            const label = document.getElementById('now-playing-label');

            label.innerText = `NOW PLAYING: ${name} (Broadcast: ${day} @ ${time} EST)`;
            bar.style.display = 'block';

            if (Hls.isSupported()) {
                const hls = new Hls();
                hls.loadSource(streamUrl);
                hls.attachMedia(audio);
                hls.on(Hls.Events.MANIFEST_PARSED, () => audio.play());
            } else if (audio.canPlayType('application/vnd.apple.mpegurl')) {
                audio.src = streamUrl;
                audio.play();
            }
        }
    </script>