Last active
August 14, 2023 20:16
-
-
Save RayHollister/f0bb913174afefc2f672d3da01d244e7 to your computer and use it in GitHub Desktop.
This JavaScript fetches and formats the weekly broadcast schedule of a specific NPR radio show from their API. It groups same times on sequential days and displays the schedule on the radio show's webpage. The script runs automatically on page load, if no schedule is already present.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script> | |
const daysOfWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; | |
async function getShowSchedule() { | |
let scheduleDiv = document.querySelector(".RadioShowPage-mediaSchedule"); | |
if (scheduleDiv) return; | |
const programName = document.querySelector(".RadioShowPage-headline").textContent.trim(); | |
try { | |
const today = new Date(); | |
const nextWeek = new Date(); | |
nextWeek.setDate(today.getDate() + 6); | |
const formatDate = (date) => { | |
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`; | |
}; | |
const todayStr = formatDate(today); | |
const nextWeekStr = formatDate(nextWeek); | |
// caching | |
// const response = await fetch(`https://api.composer.nprstations.org/v1/widget/{ucs}/week?date=${todayStr},${nextWeekStr}&format=json`); | |
// cache buster | |
const response = await fetch(`https://api.composer.nprstations.org/v1/widget/{ucs}/week?date=${todayStr},${nextWeekStr}&format=json&nocache=${new Date().getTime()}`); | |
const data = await response.json(); | |
const showSchedules = data.onThisWeek.filter(show => show.program.name === programName); | |
if (!showSchedules.length) { | |
console.error('Invalid API response', data); | |
return; | |
} | |
const timeDayMap = {}; | |
showSchedules.forEach(show => { | |
const showDate = new Date(show.start_utc); | |
let time = formatStartTime(show.start_time); | |
let day; | |
if (time === "12 a.m.") { | |
time = "at midnight"; | |
day = shiftDay(showDate); | |
} else if (time === "12 p.m.") { | |
time = "at noon"; | |
day = showDate.toLocaleDateString('en-US', { weekday: 'long' }); | |
} else { | |
day = showDate.toLocaleDateString('en-US', { weekday: 'long' }); | |
} | |
if (!timeDayMap[time]) { | |
timeDayMap[time] = [day]; | |
} else { | |
timeDayMap[time].push(day); | |
} | |
}); | |
const formattedScheduleTimesArr = combineTimesForDays(timeDayMap); | |
let formattedScheduleTimes = ""; | |
if (formattedScheduleTimesArr.length > 1) { | |
formattedScheduleTimes = formattedScheduleTimesArr.slice(0, -1).join(", ") + " and " + formattedScheduleTimesArr.slice(-1); | |
} else { | |
formattedScheduleTimes = formattedScheduleTimesArr[0]; | |
} | |
scheduleDiv = document.createElement('div'); | |
scheduleDiv.className = "RadioShowPage-mediaSchedule"; | |
const radioTopDiv = document.querySelector('.RadioShowPage-top'); | |
radioTopDiv.insertAdjacentElement('afterend', scheduleDiv); | |
// Capitalize first character | |
formattedScheduleTimes = formattedScheduleTimes.charAt(0).toUpperCase() + formattedScheduleTimes.slice(1); | |
scheduleDiv.textContent = formattedScheduleTimes; | |
} catch (error) { | |
console.error("Error: ", error); | |
} | |
} | |
function combineSequentialDays(days) { | |
let combinedDays = ""; | |
let sequenceStart = days[0]; | |
let previousDay = days[0]; | |
for (let i = 1; i < days.length; i++) { | |
const day = days[i]; | |
const dayIndex = daysOfWeek.indexOf(day); | |
const previousDayIndex = daysOfWeek.indexOf(previousDay); | |
if ((dayIndex !== (previousDayIndex + 1) % 7) && (dayIndex !== previousDayIndex)) { | |
if (sequenceStart === previousDay) { | |
combinedDays += `${sequenceStart}, `; | |
} else if ((daysOfWeek.indexOf(sequenceStart) + 4) % 7 === daysOfWeek.indexOf(previousDay)) { | |
combinedDays += "weekdays, "; | |
} else { | |
combinedDays += `${sequenceStart} through ${previousDay}, `; | |
} | |
sequenceStart = day; | |
} | |
previousDay = day; | |
} | |
if (sequenceStart === previousDay) { | |
combinedDays += `${sequenceStart}`; | |
} else if ((daysOfWeek.indexOf(sequenceStart) + 4) % 7 === daysOfWeek.indexOf(previousDay)) { | |
combinedDays += "weekdays"; | |
} else { | |
combinedDays += `${sequenceStart} through ${previousDay}`; | |
} | |
// Replace the last comma with 'and' if there are only two items | |
if (combinedDays.split(', ').length == 2) { | |
combinedDays = combinedDays.replace(', ', ' and '); | |
} | |
return combinedDays; | |
} | |
function formatStartTime(startTime) { | |
if (!startTime) { | |
console.error('Invalid start time:', startTime); | |
return ''; | |
} | |
const timeParts = startTime.split(":"); | |
let hours = parseInt(timeParts[0]); | |
const minutes = timeParts[1]; | |
let period = "a.m."; | |
if (hours >= 12) { | |
period = "p.m."; | |
if (hours > 12) { | |
hours -= 12; | |
} | |
} | |
if (hours === 0) { | |
hours = 12; | |
} | |
let formattedTime = `${hours}`; | |
if (minutes !== '00') { | |
formattedTime += `:${minutes}`; | |
} | |
formattedTime += ` ${period}`; | |
return formattedTime; | |
} | |
function shiftDay(date) { | |
date.setDate(date.getDate() - 1); | |
return date.toLocaleDateString('en-US', { weekday: 'long' }); | |
} | |
function combineTimesForDays(timeDayMap) { | |
const dayTimeMap = {}; | |
for (let time in timeDayMap) { | |
const days = timeDayMap[time].sort((a, b) => daysOfWeek.indexOf(a) - daysOfWeek.indexOf(b)); | |
const dayString = combineSequentialDays(days); | |
if (!dayTimeMap[dayString]) { | |
dayTimeMap[dayString] = [time]; | |
} else { | |
dayTimeMap[dayString].push(time); | |
} | |
} | |
let formattedScheduleTimesArr = []; | |
for (let days in dayTimeMap) { | |
const times = dayTimeMap[days]; | |
const timeString = times.join(" and "); | |
formattedScheduleTimesArr.push({days: days, timeString: timeString}); | |
} | |
// Sort the array | |
formattedScheduleTimesArr.sort((a, b) => { | |
const timeA = parseInt(a.timeString.split(":")[0]); | |
const timeB = parseInt(b.timeString.split(":")[0]); | |
return timeA - timeB; | |
}); | |
// Convert back to string format | |
return formattedScheduleTimesArr.map(item => `${item.days} ${item.timeString}`); | |
} | |
getShowSchedule(); | |
</script> |
rev 4. Fixed sequential days to show 'through' instead of 'and' between the days.
rev 5. Changed 'Monday through Friday' to 'Weekdays'
Rev 6. Changed 12 p.m. and 12 a.m. to noon and midnight respectively. Changed the day to the previous day if midnight. (Stupid AP Style!!)
Rev 7 added a cache buster to the JSON
Fixed weekdays
Fixed capitalization on weekdays!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Tired of having to update your radio schedules in Composer AND in Grove? This solution solves that! It also allows you to override it with the Grove UI if you want to add something special to the schedule line.
a. Log into Composer.
b. The UCS number can be found in the URL: "https://composer.nprstations.org/{ucs}/".
a. If you don't see the schedule, you might have entered your UCS incorrectly or the radio show's title in Grove doesn't match the title in Composer. Correct the UCS and/or the title of the show in Grove or Composer, and it should start working.
*Updated instructions to implement change across entire Grove site for all Radio Show pages.