diff --git a/config.toml.example b/config.toml.example index 8381c5a8..261a4960 100644 --- a/config.toml.example +++ b/config.toml.example @@ -51,6 +51,8 @@ odName = "On Tap" publicMyRadioAPIKey = "DFm1QGGyDZXHvGjjKYxv72ItpZe5oPiNvqTKEJLC2CmPYvdbYQ591DNKhpXwb1U9NVIgBuQ4XOBdSAKbaGzliqHm7pu4H4PxmO7mrH4JvKV6dBZx5n32obnEE2pE9vWC" + liveAudioUrl = "https://audio.ury.org.uk/live-high" + icecastStatusUrl = "https://audio.ury.org.uk/status-json.xsl" [pageContext.indexCountdown] enabled = true diff --git a/controllers/index.go b/controllers/index.go index 932adb26..ddbebceb 100644 --- a/controllers/index.go +++ b/controllers/index.go @@ -86,15 +86,16 @@ func (ic *IndexController) Post(w http.ResponseWriter, r *http.Request) { if err != nil { // Set prompt if send fails data.MsgBoxError = true + // Indicate to the client that this was an error + w.WriteHeader(400) } ic.render(w, data) - } func (ic *IndexController) render(w http.ResponseWriter, data RenderData) { // Render page - err := utils.RenderTemplate(w, ic.config.PageContext, data, "index.tmpl", "elements/current_and_next.tmpl", "elements/banner.tmpl", "elements/message_box.tmpl", "elements/index_countdown.tmpl") + err := utils.RenderTemplate(w, ic.config.PageContext, data, "index.tmpl", "elements/current_and_next.tmpl", "elements/banner.tmpl", "elements/message_box.tmpl", "elements/index_countdown.tmpl", "elements/live_player.tmpl") if err != nil { log.Println(err) return diff --git a/controllers/people.go b/controllers/people.go index 7e8ef4ee..7b686fba 100644 --- a/controllers/people.go +++ b/controllers/people.go @@ -57,7 +57,7 @@ func (pc *PeopleController) Get(w http.ResponseWriter, r *http.Request) { CurrentAndNext: currentAndNext, } - err = utils.RenderTemplate(w, pc.config.PageContext, data, "people.tmpl", "elements/current_and_next.tmpl") + err = utils.RenderTemplate(w, pc.config.PageContext, data, "people.tmpl", "elements/current_and_next.tmpl", "elements/live_player.tmpl") if err != nil { log.Println(err) return diff --git a/controllers/schedule_week.go b/controllers/schedule_week.go index 6dace15f..472898e7 100644 --- a/controllers/schedule_week.go +++ b/controllers/schedule_week.go @@ -169,7 +169,7 @@ func (sc *ScheduleWeekController) makeAndRenderWeek(w http.ResponseWriter, year, Subtypes: subtypes, } - err = utils.RenderTemplate(w, sc.config.PageContext, data, "schedule_week.tmpl", "elements/current_and_next.tmpl") + err = utils.RenderTemplate(w, sc.config.PageContext, data, "schedule_week.tmpl", "elements/current_and_next.tmpl", "elements/live_player.tmpl") if err != nil { log.Println(err) return diff --git a/public/js/currentAndNext.js b/public/js/currentAndNext.js index bd9ab3cc..b736a769 100644 --- a/public/js/currentAndNext.js +++ b/public/js/currentAndNext.js @@ -101,7 +101,7 @@ function success(data) { ' title="Show currently on air: ' + data.payload.current.title + '">' + - '

Now

' + + '

Live now

' + makeContent(data.payload.current) + '', ) @@ -109,7 +109,7 @@ function success(data) { } else { $('.current-and-next-now').replaceWith( '
' + - '

Now

' + + '

Live now

' + makeContent(data.payload.current) + '', ) @@ -121,7 +121,7 @@ function success(data) { // There is no next show (e.g. we're off air) $('.current-and-next-next').replaceWith( '
' + - '

Next

' + + '

Up next

' + '
There\'s nothing up next yet.
' + '', ) @@ -132,14 +132,14 @@ function success(data) { ' title="Show on air next: ' + data.payload.next.title + '.">' + - '

Next

' + + '

Up next

' + makeContent(data.payload.next) + '', ) } else { $('.current-and-next-next').replaceWith( '
' + - '

Next

' + + '

Up next

' + makeContent(data.payload.next) + '', ) diff --git a/public/js/live-player.js b/public/js/live-player.js new file mode 100644 index 00000000..ca926143 --- /dev/null +++ b/public/js/live-player.js @@ -0,0 +1,164 @@ +export function makePlayer(config) { + const { idPrefix, audioUrl, icecastStatusUrl } = config; + let player = new Audio(); + player.preload = 'none'; + const playPause = document.getElementById(`${idPrefix}-play`); + const volume = document.getElementById(`${idPrefix}-volume`); + const currentTrackTitle = document.getElementById(`${idPrefix}-track-title`); + const currentTrackArtist = document.getElementById(`${idPrefix}-track-artist`); + const currentTrackArtistContainer = document.getElementById(`${idPrefix}-track-artist-container`); + const closeButton = document.getElementById(`${idPrefix}-close`); + + closeButton.addEventListener('click', () => { + playbackControls.pause(); + document.querySelector('.current-and-next-player').classList.add('closed'); + document.querySelectorAll('.listen-btn').forEach((ele) => ele.style.display = ''); + }); + + function updateButton() { + if (player.paused) { + playPause.innerHTML = ''; + } else { + playPause.innerHTML = ''; + } + } + + let playbackError = false; + + function markLoading() { + playPause.innerHTML = ''; + } + + function markError() { + playbackError = true; + playPause.disabled = true; + playPause.innerHTML = ''; + } + + function setNowPlaying(title, artist) { + currentTrackTitle.innerText = title; + currentTrackArtist.innerText = artist; + currentTrackArtistContainer.style.display = 'inline'; + if (artist === null) { + currentTrackTitle.innerText = "University Radio York" + currentTrackArtistContainer.style.display = 'none'; + } + } + + let nowPlayingUpdate = null; + + function fetchNowPlaying() { + fetch(icecastStatusUrl) + .then((resp) => { + if (!resp.ok) { + console.error('failed to fetch current track, has the icecastStatus been added to config?', resp.status, resp.statusText); + nowPlayingUpdate = setTimeout(fetchNowPlaying, 10_000); + return; + } else { + return resp.json(); + } + }) + .then((resp) => { + const stream = resp.icestats.source.filter((s) => s.listenurl.indexOf('/live-high-ogg') !== -1)[0]; + const { artist, title } = stream; + + setNowPlaying(title, artist); + + // Update every 10s + nowPlayingUpdate = setTimeout(fetchNowPlaying, 10_000); + }).catch((e) => { + console.error('failed to fetch now playing, has the icecastStatus been added to config?', e); + }); + } + + const playbackControls = { + play() { + if (playbackError) { + console.log('playback error, has the audioUrl been set in config?'); + return; + } + if (!nowPlayingUpdate) { + fetchNowPlaying(); + } + + if (!this.playing) { + player.src = audioUrl; + player.play(); + } + }, + + pause() { + if (playbackError) { + return; + } + if (nowPlayingUpdate) { + clearTimeout(nowPlayingUpdate); + nowPlayingUpdate = null; + } + + player.src = null; + player.srcObject = null; + + updateButton(); + }, + + setVolume(level) { + player.volume = level; + }, + + get playing() { + return player.src !== null && !player.paused; + } + }; + + player.addEventListener('waiting', () => { + if (playbackError) return; + markLoading(); + }) + + player.addEventListener('pause', () => { + if (playbackError) return; + updateButton(); + }); + + player.addEventListener('play', () => { + if (playbackError) return; + updateButton(); + }); + + player.addEventListener('playing', () => { + if (playbackError) return; + updateButton(); + }); + + player.addEventListener('ended', () => { + if (playbackError) return; + player.load(); + }); + + player.addEventListener('error', (ev) => { + console.log(ev); + markError(); + }); + + playPause.addEventListener('click', () => { + if (player.paused) { + playbackControls.play(); + } else { + playbackControls.pause(); + } + }); + + playbackControls.setVolume(parseInt(volume.value) / 11.0); + volume.addEventListener('input', () => { + playbackControls.setVolume(parseInt(volume.value) / 11.0); + }); + + window.onbeforeunload = () => { + if (playbackControls.playing) { + return ''; + } + }; + + return playbackControls; +} diff --git a/sass/elements/_currentAndNext.scss b/sass/elements/_currentAndNext.scss index 7491c78a..f17353e2 100644 --- a/sass/elements/_currentAndNext.scss +++ b/sass/elements/_currentAndNext.scss @@ -1,5 +1,5 @@ .current-next { - background: url("/images/bg-banner-1.jpg") center center no-repeat; + background: url("/images/bg-banner-1.jpg") center no-repeat; background-size: cover; color: white; box-sizing: content-box; @@ -75,4 +75,19 @@ background: $current-next-next-bg; } + .current-and-next-player { + transition: max-height 500ms ease; + overflow: hidden; + max-height: 256px; + + &.closed { + transition: max-height 500ms ease; + max-height: 0; + @include media-breakpoint-down(sm) { + // silly hack since the closed player has a height of 1px, + // even though i tell it that it can have a max height of 0 + background-color: red; + } + } + } } diff --git a/sass/elements/_elements.scss b/sass/elements/_elements.scss index 739d8ec3..a4dba574 100644 --- a/sass/elements/_elements.scss +++ b/sass/elements/_elements.scss @@ -10,3 +10,4 @@ @import "aprilFools"; @import "show"; @import "faq"; +@import "livePlayer"; diff --git a/sass/elements/_livePlayer.scss b/sass/elements/_livePlayer.scss new file mode 100644 index 00000000..88fb3267 --- /dev/null +++ b/sass/elements/_livePlayer.scss @@ -0,0 +1,137 @@ +#live-player-popout-controls { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + margin: 1rem 1rem 0rem 1rem; + font-size: 1.4rem; + + button { + border: none; + background-color: transparent; + color: white; + cursor: pointer; + transition: transform ease 200ms; + &:hover { + transform: scale(1.1); + } + &:focus { + outline: none; + } + &:active { + transform: scale(0.9); + } + } +} + +.current-and-next-player:last-of-type{ + background-color: #dcdee0cc !important; +} + +.live-container { + gap: 4.5rem; +} + +.control-seg { + width: 50%; + flex: 1; + display: flex; + flex-direction: row; + gap: 2rem; + align-items: center; +} + +.control-seg.button-seg { + flex: unset; + width: unset; +} + +.live-player-volume-cont { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.6rem; +} + +.live-now-playing p { + margin: 0; + font-size: large; +} + +@include media-breakpoint-up(sm) { + .live-container { + margin-inline: 4rem; + } +} + +@include media-breakpoint-down(sm) { + .mobile-column { + flex-direction: column; + align-items: center !important; + justify-content: left !important; + text-align: center; + gap: 1rem; + } + .control-seg { + width: unset; + flex-direction: column; + justify-content: center; + align-items: center; + } +} + +.play-pause-button { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + border-radius: 99999px; + background-color: #d51b32; + color: white; + border: none; + outline: none; + width: 48px; + height: 48px; + box-shadow: 0 0.25rem 1rem rgba(0,0,0,0.5); + transition: transform ease 200ms, box-shadow ease 200ms; + &:hover { + transform: scale(1.1); + box-shadow: 0 0.25rem 1rem rgba(0,0,0,0.75); + } + &:focus { + outline: none; + } + &:active { + transform: scale(0.9); + box-shadow: 0 0.25rem 1rem rgba(0,0,0,0.5); + } + + .player-load-dots span { + animation: pulse 2000ms ease infinite; + + &:nth-child(1) { + animation-delay: 0ms; + } + &:nth-child(2) { + animation-delay: 250ms; + } + &:nth-child(3) { + animation-delay: 500ms; + } + } +} + +@keyframes pulse { + from { + opacity: 1; + } + 25% { + opacity: 0; + } + 75% { + opacity: 0; + } + to { + opacity: 1; + } +} diff --git a/structs/config.go b/structs/config.go index 9b704d97..21b8eda6 100644 --- a/structs/config.go +++ b/structs/config.go @@ -48,6 +48,8 @@ type PageContext struct { CINLive string `toml:"cinLive"` IndexCountdown *IndexCountdownConfig `toml:"indexCountdown"` CacheBuster string `toml:"cacheBuster"` + LiveAudioURL string `toml:"liveAudioUrl"` + IcecastStatusURL string `toml:"icecastStatusUrl"` Pages []Page Youtube youtube Osm osm @@ -94,9 +96,9 @@ type Page struct { } type youtube struct { - APIKey string `toml:"apiKey"` - CINPlaylistID string `toml:"cinPlaylistID"` - ChannelURL string `toml:"channelURL"` + APIKey string `toml:"apiKey"` + CINPlaylistID string `toml:"cinPlaylistID"` + ChannelURL string `toml:"channelURL"` } type osm struct { diff --git a/views/elements/current_and_next.tmpl b/views/elements/current_and_next.tmpl index e1ae71e9..dbf1eab9 100644 --- a/views/elements/current_and_next.tmpl +++ b/views/elements/current_and_next.tmpl @@ -1,6 +1,6 @@ {{define "current_and_next"}} -{{with .CurrentAndNext}} +{{with .PageData.CurrentAndNext}}
{{if .Current}}
@@ -14,9 +14,9 @@
{{end}} -
{{end}} +
+ {{template "live_player" .}} + +
+
{{end}} {{define "current_next"}}
diff --git a/views/elements/live_player.tmpl b/views/elements/live_player.tmpl new file mode 100644 index 00000000..bfedd358 --- /dev/null +++ b/views/elements/live_player.tmpl @@ -0,0 +1,50 @@ +{{define "live_player"}} + +
+
+ + + + +
+ +
+ +
+

+ Now playing:
+ University Radio York + +

+
+ +
+ +
+ +
+
+ + + +
+
+ +
+ +
+ + + +{{end}} diff --git a/views/elements/message_box.tmpl b/views/elements/message_box.tmpl index 9ba3cc2d..4479f6a3 100644 --- a/views/elements/message_box.tmpl +++ b/views/elements/message_box.tmpl @@ -2,7 +2,7 @@ {{with .}}

Send a Message


-
+