
{"id":9,"date":"2025-05-01T05:44:36","date_gmt":"2025-05-01T05:44:36","guid":{"rendered":"https:\/\/vocagathering.com\/fall-2025\/?page_id=9"},"modified":"2026-03-16T00:42:50","modified_gmt":"2026-03-16T00:42:50","slug":"discover","status":"publish","type":"page","link":"https:\/\/vocagathering.com\/2026\/discover","title":{"rendered":"Discover"},"content":{"rendered":"\n<div id=\"load-screen\" class=\"center-flex flex-col alignfull\">\n<p><strong>Loading<\/strong><\/p>\n<div class=\"loader\"><\/div>\n<\/div>\n<div class=\"bg-graphic alignfull\" aria-hidden=\"true\">\n    <img decoding=\"async\" src=\"\/wp-content\/uploads\/2026\/02\/bg-pattern.png\">\n<\/div>\n<hr class=\"rainbow\">\n<h1 class=\"neg-bot-mar special-header\">DISCOVERY RADIO<\/h1>\n<hr class=\"rainbow\">\n<section id=\"radio-con\">\n  <div id=\"player-con\" class=\"flex-col\">\n    <div id=\"player-wrapper\">\n      <span id=\"player-btn\" class=\"material-symbols-outlined\">\n        play_circle\n      <\/span>\n      <img decoding=\"async\" alt=\"song thumbnail\" src=\"\">\n      <div id=\"player\"><\/div>\n    <\/div>\n    <div id=\"player-meta-con\">\n      <strong><a id=\"title-link\" href=\"\" target=\"_blank\"><p id=\"title\" class=\"no-bot-mar\"><\/p><\/a><\/strong>\n      <p id=\"author\" class=\"no-top-mar\"><\/p>\n    <\/div>\n  <\/div>\n  <div id=\"ctrl-wrapper\" class=\"flex-col center-flex\">\n    <div class=\"flex-col center-flex\">\n      <div id=\"controls\" class=\"flex-row\">\n        <button id=\"prev\">\n          <span class=\"material-symbols-outlined\">\n            skip_previous\n          <\/span>\n        <\/button>\n        <button id=\"play-toggle\">\n          <span class=\"material-symbols-outlined\">\n            play_circle\n          <\/span>\n        <\/button>\n        <button id=\"next\">\n          <span class=\"material-symbols-outlined\">\n            skip_next\n          <\/span>\n        <\/button>\n      <\/div>\n\n      <div class=\"flex-row center-flex\">\n        <span id=\"vol-icon\" class=\"material-symbols-outlined small-icon\">\n          volume_up\n        <\/span>\n        <div class=\"slidecontainer center-flex\">\n          <input id=\"vol-ctrl\" type=\"range\" min=\"0\" max=\"100\" value=\"50\" class=\"slider\">\n        <\/div>\n      <\/div>\n      <p id=\"skip-txt\" class=\"hidden\">Enabling buttons in 5 seconds&#8230;<\/p>\n      <button id=\"vote-main\" class=\"special-btn btn-hover\" name=\"Vote for this song!\">Vote for this song!<\/button>\n\n    <\/div>\n    <div id=\"voting\" class=\"flex-col\">\n      \n      <div>\n        <p class=\"no-bot-mar\"><strong>Compliments (max 1)<\/strong><\/p>\n        <hr>\n      <\/div>\n      <div id=\"compliments\" class=\"btn-box flex-row\">\n      <button id=\"vote-inst\" name=\"Great instrumental!\">Great instrumental!<\/button>\n      <button id=\"vote-vis\" name=\"Great visuals!\">Great visuals!<\/button>\n      <button id=\"vote-tune\" name=\"Great tuning!\">Great tuning!<\/button>\n    <\/div>\n  <\/div>\n\n  <\/div>\n<\/section>\n\n<section>\n  <button type=\"button\" class=\"collapsible top-collap\"><p>What is the discovery radio?<\/p><\/button>\n    <div class=\"content\">\n      <p>\n          The discovery radio is a randomized playlist of this Vocagathering festival&#8217;s submissions! Discover new producers and vote for songs to support them here!\n          <br><br>\n          Directions:<br>\n          Click play to start the playlist. It will automatically cycle to the next song upon the current song&#8217;s end. <br>\n          <br>\n          You may only vote once per song and leave one compliment per song. \n      <\/p>\n    <\/div>\n<\/section>\n\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Material+Symbols+Outlined\" rel=\"stylesheet\" \/>\n<style>\n.ytp-pause-overlay-container {\n  display: none !important;\n}\n\n.material-symbols-outlined {\n  font-size: 4rem;\n  font-variation-settings:\n  'FILL' 0,\n  'wght' 400,\n  'GRAD' 0,\n  'opsz' 24\n}\n\n.small-icon {\n  font-size: var(--font-size-s);\n}\n\n#radio-con {\n  gap: 1rem;\n  flex-direction: row;\n}\n\n#player-con {\n  width: 100%;\n  overflow: hidden;\n  aspect-ratio: 16\/9;\n  text-align: center;\n  border-radius: 0 0 30px 30px;\n  background: linear-gradient(45deg, var(--color-2-main), var(--color-2-alt-2));\n  box-shadow: var(--darker-bg-color) 0px 0px 15px;\n  margin: 1rem 0rem;\n}\n\n#player-wrapper {\n  height: 100%;\n  position: relative;\n  overflow: hidden;\n  cursor: pointer;\n}\n \n#player-wrapper img {\n  position: absolute;\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n  opacity: 100%;\n}\n\n#player-wrapper img:hover {\n  cursor: pointer;\n}\n\n#player {\n  width: 300%;\n  height: 100%;\n  margin-left: -100%;\n}\n\n#player-btn { \n  position: absolute;\n  z-index: 2;\n  top: calc(50% - 3rem);\n  right: calc(50% - 3rem);\n  font-size: 6rem;\n  background: linear-gradient(45deg, var(--color-2-main), var(--color-2-alt-2));\n  border-radius: 10rem;\n}\n\n#player-btn:hover {\n  scale: 1.05;\n}\n\n#player-btn, #player-wrapper img {\n  transition-duration: 0.2s;\n}\n\n#controls button {\n  background-color: transparent;\n  padding: 0;\n  transition: 70ms;\n}\n\n#controls button:hover {\n  box-shadow: none;\n  scale: 1.15;\n}\n\n#ctrl-wrapper > div {\n  width: 80%;\n}\n\n#ctrl-wrapper, #vote-main{\n  width: 100%;\n}\n\n#compliments button {\n  flex-basis: 33%;\n}\n\n#skip-txt {\n  font-size: var(--font-size-xs);\n  margin: 0.5rem 0 0 0;\n}\n\n.inactive {\n  opacity: 0% !important;\n}\n\n.hidden {\n  display: none;\n}\n\n\n\/* SLIDER DECOR \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/*\/\n.slider {\n  -webkit-appearance: none;\n  width: 100%;\n  height: 0.25rem;\n  border-radius: 1rem;  \n  background: var(--color-1-main);\n  outline: none;\n  -webkit-transition: .2s;\n  margin: 0.5rem 0;\n}\n\n.slider::-webkit-slider-thumb {\n  -webkit-appearance: none;\n  appearance: none;\n  width: 1.2rem;\n  height: 1.2rem;\n  border-radius: 50%;\n  background: var(--color-1-alt-2);\n  cursor: pointer;\n  border: none;\n  transition: 10ms;\n  transition-duration: 10ms;\n  box-shadow: 0 0 5px var(--color-2-alt-2);\n\n}\n\n.slider::-moz-range-thumb {\n  width: 1.2rem;\n  height: 1.2rem;\n  border-radius: 50%;\n  background: var(--color-1-alt-2);\n  cursor: pointer;\n  border: none;\n  transition: 10ms;\n  transition-duration: 10ms;\n  box-shadow: 0 0 5px var(--color-2-alt-2);\n}\n\n.slider::-moz-range-thumb:hover {\n  border: 0px;\n  width: 1.4rem;\n  height: 1.4rem;\n}\n\n.slider::-webkit-slider-thumb:hover {\n  border: 0px;\n  width: 1.4rem;\n  height: 1.4rem;\n}\n\n\/* SLIDER DECOR \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/*\/\n@media (max-width:700px) {\n    .entry-content {\n      padding: 0 !important;\n    }\n\n    .slidecontainer {\n      width: 100%;\n    }\n\n    #radio-con {\n      flex-direction: column;\n    }\n\n    #player-con {\n      border-radius: 0;\n    }\n\n    #voting {\n      width: 80%;\n      gap: 0.8rem;\n    }\n}\n\n@media (max-width:350px) {\n    #compliments {\n      flex-direction: column;\n    }\n}\n<\/style>\n\n\n<script>\nclass votingCtrl {\n  constructor(voteDict) {\n    this.voteDict = voteDict;\n  }\n\n  \/\/ check if already voted for and disable voting if user with ip has already voted\n  async hasVote(songId) { \n\n    \/\/ disable by default\n    this.disableVoting();\n\n    \/\/ check the local storage first\n\n    \/\/ return [boolean, boolean] --> index 0 for general, 1 for compliments\n    \/\/ true if already voted, false if not \n    let voteData = this.#getStorage(songId);\n\n    \/\/ if null, it was not stored yet\n    if (!voteData) { \/\/ get vote info from db\n      voteData = await this.getVoteData(songId);\n      this.#updateStorage(songId, voteData);\n    }\n\n    this.#updateVoteBtns(voteData);\n  }\n\n  #updateVoteBtns(hasVote) {\n    \/\/ update UI\n    const genVoteBtn = document.getElementById('vote-main');\n\n      \/\/ enable voting if eligible\n      if (!hasVote[0]) {\n        \/\/ enable general vote\n        genVoteBtn.disabled = false;\n        genVoteBtn.textContent = genVoteBtn.name;\n      } else {\n        genVoteBtn.textContent = \"Already voted!\";\n      }\n\n      \/\/ enable compliments\n      if (!hasVote[1]) {\n\n        for(let id in this.voteDict) {\n          if (id != 'vote-main') {\n            const compBtn = document.getElementById(id);\n            compBtn.disabled = false;\n            compBtn.textContent = compBtn.name;\n          }\n        };\n      }\n  }\n\n  #getStorage(songId) {\n    return JSON.parse(localStorage.getItem(songId));\n  }\n\n  #updateStorage(songId, voteData) {\n    localStorage.setItem(songId, JSON.stringify(voteData));\n  }\n\n  getVoteData(songId) {\n    try {\n      return jQuery.post(VG_AJAX_URL, {\n        action: \"getVoteData\",\n        dataType: 'json',\n        songId: songId,\n      })\n      .done(function(data) {\n        return data;\n      });\n    } catch (error) {\n        console.error(\"Error getting voting data\")\n    }\n  }\n\n  \/\/ Submit vote to database. \n  async submitVote(songId, elemId) {\n    const type = this.voteDict[elemId];\n\n    \/\/ ajax call to submit vote\n    let data = await jQuery.post(VG_AJAX_URL, {\n        action: \"submitVote\", \/\/ action\n        dataType: 'text',\n        songId: songId,\n        voteType: type\n    });\n    \/\/ check feedback\n    data = JSON.parse(data);\n    if (data.statusCode != 200) {\n      \/\/ TODO: Update display\n      this.voteError(elemId);\n    } else {\n      \/\/ TODO: update display\n      this.confirmVote(elemId);\n\n      \/\/ disable other voting buttons if compliment type\n      if (type != 0) {\n        document.querySelectorAll(\".btn-box > button\").forEach((btn) => {\n          btn.disabled = true;\n        });\n\n        \/\/ update localstorage\n        let voteData = this.#getStorage(songId);\n        this.#updateStorage(songId, [voteData[0], true]);\n      } else {\n        \/\/ update localstorage\n        let voteData = this.#getStorage(songId);\n        this.#updateStorage(songId, [true, voteData[1]]);\n      }\n    }\n  }\n\n  confirmVote(elemId) {\n    document.getElementById(elemId).textContent = \"Vote submitted!\";\n    document.getElementById(elemId).disabled = true;\n\n  }\n\n  voteError(elemId) {\n    document.getElementById(elemId).textContent = \"Error submitting the vote.\";\n  }\n\n  disableVoting() {\n    for (let id in this.voteDict) {\n      document.getElementById(id).disabled = true;\n    }\n  }\n}\n\nclass YouTubePlaylistController {\n  #phase;\n\n  constructor(voteCtrl, elementId = 'player') {\n    this.playerElementId = elementId;\n    this.player = null;\n    this.voteCtrl = voteCtrl;\n    this.playlistIds = null;\n    this.currIndex = 0;\n\n    \/\/ common ctrls\n    this.PLAYER_IMG_ELEM = document.querySelector(\"#player-wrapper img\");\n    this.PLAYER_BTN_ELEM = document.getElementById(\"player-btn\");\n\n    \/\/ Initialize API when ready\n    window.onYouTubeIframeAPIReady = () => this.initPlayer();\n  }\n\n  async initPlayer() {\n    \/\/ get total songs\n    const totalSongs = await this.#getTotalSongs();\n\n    \/\/ check if min number of songs is added\n    \/\/ magic number = minimum number of numbers needed until discovery radio is open\n    if (totalSongs < 30) {\n      this.#placeholder();\n\n      \/\/ remove loading screen\n      if (document.getElementById('load-screen')) {\n          document.getElementById('load-screen').classList.add('slide-out');\n      }\n\n      return;\n    }\n\n    this.playlistIds = await this.#getPlaylistIds();\n    this.playlistIds = this.#shuffleArray(this.playlistIds);\n\n    this.player = new YT.Player(this.playerElementId, {\n      height: '360',\n      width: '640',\n      playerVars: {\n        autoplay: 0,\n        rel: 0,\n        controls: 0,\n        loop: 1,\n        cc_load_policy: 0,\n      },\n      events: {\n        onReady: () => this.onPlayerReady(),\n        onStateChange: (e) => this.onPlayerStateChange(e)\n      }\n    });\n  }\n\n  async onPlayerReady() {\n    \/\/ load video\n    this.currIndex = Math.floor(Math.random() * this.playlistIds.length);\n    const firstId = this.playlistIds[this.currIndex];\n\n    this.player.cueVideoById(firstId);\n\n    \/\/ set up volume btn\n    document.getElementById(\"vol-ctrl\").addEventListener(\"input\", () => { \n      this.changeVolume(document.getElementById(\"vol-ctrl\").value);\n    });\n    this.changeVolume(50);\n\n    \/\/ Play the video if video is paused (state = 2) or unstarted (-1), else pause the video\n    document.getElementById('play-toggle').onclick = () => this.player.getPlayerState() != 1 ? this.#playVideo() : this.#pauseVideo();\n    \/\/ todo: use song data (.isPlayable) to skip primieres that aren't published yet\n    document.getElementById('next').onclick = () => this.#nextVid(document.getElementById('next'));\n    document.getElementById('prev').onclick = () => this.#prevVid();\n\n    \/\/ toggle pause through clicking on player\n    document.getElementById('player-wrapper').onclick = () => this.player.getPlayerState() != 1 ? this.#playVideo() : this.#pauseVideo();\n\n    \/\/ remove loading screen\n    if (document.getElementById('load-screen')) {\n        document.getElementById('load-screen').classList.add('slide-out');\n    }\n  }\n\n  async onPlayerStateChange(event) {\n    if (this.player.getPlayerState() == 0) {\n      this.#nextVid();\n    }\n\n    \/\/ if video is paused\n    if (this.player.getPlayerState() == 2) {\n      \/\/ turn on cover image\n      this.PLAYER_IMG_ELEM.classList.remove(\"inactive\");\n      this.PLAYER_BTN_ELEM.classList.remove(\"inactive\");\n      document.querySelector('#play-toggle > span').textContent = 'play_circle';\n    }\n\n    \/\/ if video playing\n    if(this.player.getPlayerState() == 1) {\n      \/\/ update title and artist\n      this.#setMetadata(this.player.getVideoData());\n\n      \/\/ turn off cover image\n      this.PLAYER_IMG_ELEM.classList.add(\"inactive\");\n      this.PLAYER_BTN_ELEM.classList.add(\"inactive\");\n      document.querySelector('#play-toggle > span').textContent = 'pause_circle';\n\n      if (this.#phase != 3) {\n        await this.voteCtrl.hasVote(this.getSongId());\n      }\n\n    }\n\n    \/\/ after video is cued\n    if (this.player.getPlayerState() == 5) {\n       \/\/ set initial video data\n      this.#setMetadata(this.player.getVideoData());\n\n      \/\/ check phase \n      this.#phase = await this.#getDates();\n\n      \/\/ if after event ends, do not show voting buttons\n      if (this.#phase == 3) {\n        document.getElementById(\"vote-main\").remove();\n        document.getElementById(\"voting\").remove();\n      } else {\n        \/\/ voting control stuff\n        const firstSong = this.getSongId();\n\n        \/\/ enable voting for voting buttons\n        for (let elemId in this.voteCtrl.voteDict) {\n          document.getElementById(elemId).addEventListener( \"click\", () => { \n            const id = this.getSongId();\n            this.voteCtrl.submitVote(id, elemId)});\n        };\n\n          \/\/ check if voting should be disabled for song\n          \/\/ --> upon page load\n          await this.voteCtrl.hasVote(firstSong);\n      }\n      }\n  }\n\n  \/\/ Input: Value - number between 0 and 100\n  changeVolume(value) {\n    this.player.setVolume(value);\n\n    if (value == 0 ) {\n      document.getElementById(\"vol-icon\").textContent = \"no_sound\";\n    } else if (value < 50) {\n      document.getElementById(\"vol-icon\").textContent = \"volume_down\";\n    } else {\n      document.getElementById(\"vol-icon\").textContent = \"volume_up\";\n    }\n  }\n\n  #playVideo() {\n    this.player.playVideo();\n    document.querySelector('#play-toggle > span').textContent = 'pause_circle';\n  }\n\n  #pauseVideo() {\n    this.player.pauseVideo();\n    document.querySelector('#play-toggle > span').textContent = 'play_circle';\n  }\n\n  async #nextVid() {\n    this.currIndex+= 1;\n\n    if (this.currIndex >= this.playlistIds.length) {\n      this.currIndex = 0;\n    }\n\n    this.player.loadVideoById(this.playlistIds[this.currIndex]);\n\n    this.#disableNav();\n  }\n\n  async #prevVid() {\n    this.currIndex-= 1;\n\n    if (this.currIndex < 0) {\n      this.currIndex = this.playlistIds.length - 1;\n    }\n\n    this.player.loadVideoById(this.playlistIds[this.currIndex]);\n\n    this.#disableNav();\n  }\n\n  #disableNav() {\n    \/\/ if during active voting phase\n    if (this.#phase != 3 ) {  \n\n      const displayTxt = document.getElementById(\"skip-txt\");\n      displayTxt.textContent = \"Enabling next button in 5 seconds...\";\n      displayTxt.classList.toggle(\"hidden\");\n\n      const btns = [\"next\", \"prev\"];\n\n      btns.forEach((btn) => {\n        \/\/ disable the next button for 10 seconds\n        document.getElementById(btn).disabled = true;\n      });\n\n      let count = 5;\n      let incrementTimer = setInterval(() => {\n        if (count <= 1) {\n          btns.forEach((btn) => {\n            \/\/ undisable\n            document.getElementById(btn).disabled = false;\n          });\n          clearInterval(incrementTimer);\n          displayTxt.classList.toggle(\"hidden\");\n        }\n        count-=1;\n        displayTxt.textContent = \"Enabling buttons in \" + count + \" seconds...\";\n\n      }, 1000);\n    }\n  }\n\n  \/\/ data = video data returned by player.getVideoData()\n  #setMetadata(data) {\n    document.getElementById('title').textContent = `${data.title}`;\n    document.getElementById('author').textContent = `${data.author}`;\n    document.getElementById('title-link').href = 'https:\/\/www.youtube.com\/watch?v=' + data.video_id;\n\n    \/\/ set thumbnail\n    this.PLAYER_IMG_ELEM.src = \"https:\/\/i.ytimg.com\/vi\/\" + data.video_id + \"\/hqdefault.jpg\"\n  }\n\n  async #getTotalSongs() {\n    let data = await jQuery.post(VG_AJAX_URL, {\n        action: \"getNumSongs\", \/\/ action\n    });\n    return data;\n  }\n\n  #placeholder() {\n    const elem = document.getElementById(\"radio-con\");\n    elem.innerHTML = \"\";\n    let text = document.createElement(\"p\");\n    text.textContent =  \"The discovery radio will open after the minimum number of entries are submitted. Stay tuned!\";\n    elem.append(text);\n  }\n\n  \/\/ exists in utils.js with song list manager... merge later.... todo\n  async #getDates() {\n    try {\n\t\t\tlet data = await jQuery.get(VG_AJAX_URL, {\n\t\t\t\taction: \"getDates\",\n\t\t\t\tdataType: \"json\"\n\t\t\t});\n\t\tconst parsedData = JSON.parse(data).responseText;\n\t\treturn parsedData.phase;\n\n    } catch (error) {\n      console.error(\"error getting dates: \", error);\n    }\n  }\n\n  async #getPlaylistIds() {\n    let data = await jQuery.post(VG_AJAX_URL, {\n      action: \"getRandSongIds\", \/\/ action\n      dataType: 'json',\n    });\n\n    let cleaned = [];\n\n    data.forEach((song) => {\n      cleaned.push(song['youtube_id']);\n    });\n\n    return cleaned;    \n  }\n\n  #shuffleArray(arr) {\n  \tfor (let i = arr.length - 1; i > 0; i--) {\n    \tconst j = Math.floor(Math.random() * (i + 1));\n    \t[arr[i], arr[j]] = [arr[j], arr[i]];\n  \t}\n  \treturn arr;\n  }\n\n  getSongId() {\n    \/\/ get current song's ID\n    return this.player.getVideoUrl().slice(-11); \/\/ assuming id is always last 11 digits\n  }\n}\n\n(async function() {\n  'use strict';\n\n  await init();\n\n  \/\/ Instantiate the controller with playlist ID\n  async function init() {\n\n    \/\/ correlates the element ids of voting buttons to numbers\n    const voteDict = { \/\/ element ID : vote type\n      \"vote-main\": 0,\n      \"vote-inst\": 1,\n      \"vote-vis\": 2,\n      \"vote-tune\": 3,\n    };\n\n    const voteCtrl = new votingCtrl(voteDict);\n    const playlist = new YouTubePlaylistController(voteCtrl);\n  }\n}() );\n\n<\/script>\n<script src=\"https:\/\/www.youtube.com\/iframe_api\"><\/script>\n\n  ","protected":false},"excerpt":{"rendered":"<p>Loading DISCOVERY RADIO play_circle skip_previous play_circle skip_next volume_up Enabling buttons in 5 seconds&#8230; Vote for this song! Compliments (max 1) Great instrumental! Great visuals! Great tuning! What is the discovery radio? The discovery radio is a randomized playlist of this Vocagathering festival&#8217;s submissions! Discover new producers and vote for songs to support them here! Directions: [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"page-no-title","meta":{"footnotes":""},"class_list":["post-9","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/vocagathering.com\/2026\/wp-json\/wp\/v2\/pages\/9","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/vocagathering.com\/2026\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/vocagathering.com\/2026\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/vocagathering.com\/2026\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/vocagathering.com\/2026\/wp-json\/wp\/v2\/comments?post=9"}],"version-history":[{"count":325,"href":"https:\/\/vocagathering.com\/2026\/wp-json\/wp\/v2\/pages\/9\/revisions"}],"predecessor-version":[{"id":913,"href":"https:\/\/vocagathering.com\/2026\/wp-json\/wp\/v2\/pages\/9\/revisions\/913"}],"wp:attachment":[{"href":"https:\/\/vocagathering.com\/2026\/wp-json\/wp\/v2\/media?parent=9"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}