diff --git a/dist/bundle.js b/dist/bundle.js index b1b7080..f50f5ee 100644 --- a/dist/bundle.js +++ b/dist/bundle.js @@ -1,2 +1,2 @@ -"use strict";(()=>{function p(){return new URLSearchParams(window.location.search).get("name")}function m(){return new URLSearchParams(window.location.search).get("id")}var u=["AUDIO_QUALITY_ULTRALOW","AUDIO_QUALITY_LOW","AUDIO_QUALITY_MEDIUM","AUDIO_QUALITY_HIGH"],T={invidiousInstance:"https://vid.puffyan.us/",shaders:!0,volume:50,preferredCodec:"mp4a",maxQuality:"AUDIO_QUALITY_HIGH"};function c(){let t=localStorage.getItem("options");return t?JSON.parse(t):T}function S(t,e){let o=c();o[t]=e,localStorage.setItem("options",JSON.stringify(o))}var y=[];async function h(t){if(y.length>0)return y;let e=c().invidiousInstance,o=e.endsWith("/")?e:e+"/",n=await fetch(`${o}api/v1/playlists/${t}`).then(i=>i.json());return y.push(...n.videos),n.videos}async function P(t){let e=c().invidiousInstance,o=e.endsWith("/")?e:e+"/";return(await fetch(`${o}api/v1/videos/${t}`).then(i=>i.json())).adaptiveFormats.filter(i=>i.type.startsWith("audio/"))}async function v(t){let e=await P(t),{preferredCodec:o}=c(),n=e.filter(s=>s.type.includes(o));return k(n)||n[0]||e[0]}function k(t){let{maxQuality:e}=c(),o=u.filter(i=>u.indexOf(i)<=u.indexOf(e)).sort((i,s)=>u.indexOf(i)-u.indexOf(s));return t.find(i=>o.some(s=>i.audioQuality.includes(s)))}async function I(t){let e=await h(t),o=w(t),n=Date.now()/1e3,i=e.reduce((d,a)=>d+a.lengthSeconds,0),s=Math.floor(n/i)+o;return q(e,s)}function q(t,e){let o=[...t];for(let n=o.length-1;n>0;n--){let i=Math.floor((e+n)%(n+1)),s=o[n];o[n]=o[i],o[i]=s}return o}function w(t){let e=0;for(let o=0;os.videoId===n.song?.videoId);return i+=e,i%=o.length,o[i]}async function B(t,e){let o=document.getElementById("radio-audio"),n=await v(t.videoId);o.src=n.url,o.currentTime=e,o.play();let i=await A(m(),1),s=await v(i.videoId);await D(s)}async function O(){let t=document.getElementById("radio-audio"),{volume:e}=c();t.volume=e/100}async function D(t){console.log("Caching next song: ",t.url);let e=document.createElement("audio");e.src=t.url,e.preload="metadata";let o=document.body.appendChild(e);o.addEventListener("loadeddata",()=>{o.remove()})}document.addEventListener("DOMContentLoaded",()=>{let t=document.getElementById("add-open"),e=document.getElementById("add-close");t.addEventListener("click",()=>{let o=document.getElementById("playlist-input"),n=document.getElementById("radio-name-input"),i=o.value;if(!i)return;let a=new URL(i).searchParams.get("list")??i;window.location.search=`?id=${a}&name=${n.value}`}),e.addEventListener("click",()=>{_("add-dialog")})});function M(t){document.getElementById(t).showModal()}function _(t){document.getElementById(t).close()}function f(t){let e=document.getElementById("radio-audio"),o=document.querySelector("#volume-bar"),n=o.querySelectorAll(".volume-bar-line"),i=document.querySelector("#volume-number");t<0&&(t=0),t>100&&(t=100),t>0?o.classList.remove("muted"):o.classList.add("muted"),e.volume=t/100;let s=Math.round(t/(100/n.length))*(100/n.length);n.forEach(d=>d.classList.remove("active"));for(let d=0;d{document.getElementById("radio-audio").play(),document.removeEventListener("click",t)};document.addEventListener("click",t)}document.addEventListener("DOMContentLoaded",()=>{let t=document.querySelector("#volume-bar");t.addEventListener("click",n=>{let s=Math.floor(n.offsetX/t.clientWidth*100);f(s)}),t.addEventListener("mousedown",n=>{let s=Math.floor(n.offsetX/t.clientWidth*100);f(s),document.addEventListener("mousemove",e),document.addEventListener("mouseup",o)});let e=n=>{let i=n;if(i.target!==t)return;let s=Math.floor(i.offsetX/t.clientWidth*100);f(s)},o=()=>{document.removeEventListener("mousemove",e),document.removeEventListener("mouseup",o)}});if(m()&&p()){let t=p(),e=document.querySelectorAll('meta[name*="title"]'),o=document.querySelectorAll('meta[name*="description"]');for(let n of e)n.setAttribute("content","Titune");for(let n of o)n.setAttribute("content",`Listen along to "${t}" Radio`)}document.addEventListener("DOMContentLoaded",async()=>{document.getElementById("add-icon").addEventListener("click",()=>{M("add-dialog")});let e=document.getElementById("currently-playing"),o=document.getElementById("time-elapsed"),n=document.getElementById("duration"),i=document.getElementById("cover"),s=m(),d="",a=p();document.title=`Titune | ${a}`,O(),f(0),b(),await h(s),setInterval(async()=>{let{song:r,elapsed:l}=await L(s);if(!d.includes(r?.title??"Unknown")&&r){let g=(r.videoThumbnails.length>0&&Array.isArray(r.videoThumbnails[0])?r.videoThumbnails[0]:r.videoThumbnails).reduce((x,E)=>E.width>x.width?E:x).url;i.setAttribute("src",g);let $=document.getElementById("bg-cover");$.style.backgroundImage=`url(${g})`,document.getElementById("favicon").setAttribute("href",g),B(r,l)}if(`${r?.title??"Unknown"}${l}`===d)return;e.textContent=r?.title??"Unknown",o.textContent=`${Math.floor(l/60)}:${Math.floor(l%60).toString().padStart(2,"0")}`,n.textContent=`${Math.floor((r?.lengthSeconds??0)/60)}:${Math.floor((r?.lengthSeconds??0)%60).toString().padStart(2,"0")}`;let U=document.getElementById("progress-bar-fill"),C=l/(r?.lengthSeconds??1)*100;U.style.width=`${C}%`,d=`${r?.title??"Unknown"}${l}`},200)});})(); +"use strict";(()=>{function g(){return new URLSearchParams(window.location.search).get("name")}function l(){return new URLSearchParams(window.location.search).get("id")}var m=["AUDIO_QUALITY_ULTRALOW","AUDIO_QUALITY_LOW","AUDIO_QUALITY_MEDIUM","AUDIO_QUALITY_HIGH"],$={invidiousInstance:"https://vid.puffyan.us/",shaders:!0,volume:50,preferredCodec:"mp4a",maxQuality:"AUDIO_QUALITY_HIGH"};function c(){let t=localStorage.getItem("options");return t?JSON.parse(t):$}function E(t,o){let e=c();e[t]=o,localStorage.setItem("options",JSON.stringify(e))}var v=[];async function I(t){if(v.length>0)return v;let o=c().invidiousInstance,e=o.endsWith("/")?o:o+"/",n=await fetch(`${e}api/v1/playlists/${t}`).then(s=>s.json());return v.push(...n.videos),n.videos}async function P(t){let o=c().invidiousInstance,e=o.endsWith("/")?o:o+"/";return(await fetch(`${e}api/v1/videos/${t}`).then(s=>s.json())).adaptiveFormats.filter(s=>s.type.startsWith("audio/"))}async function L(t){let o=await P(t),{preferredCodec:e}=c(),n=o.filter(i=>i.type.includes(e));return k(n)||n[0]||o[0]}function k(t){let{maxQuality:o}=c(),e=m.filter(s=>m.indexOf(s)<=m.indexOf(o)).sort((s,i)=>m.indexOf(s)-m.indexOf(i));return t.find(s=>e.some(i=>s.audioQuality.includes(i)))}async function S(t){let o=await I(t),e=A(t),n=Date.now()/1e3,s=o.reduce((d,a)=>d+a.lengthSeconds,0),i=Math.floor(n/s)+e;return q(o,i)}function q(t,o){let e=[...t];for(let n=e.length-1;n>0;n--){let s=Math.floor((o+n)%(n+1)),i=e[n];e[n]=e[s],e[s]=i}return e}function A(t){let o=0;for(let e=0;ei.videoId===n.song?.videoId);return s+=o,s%=e.length,e[s]}async function y(t,o){let e=document.getElementById("radio-audio"),n=await L(t.videoId);e.src=n.url,e.currentTime=o,e.play();let s=await O(l(),1),i=await L(s.videoId);await D(i)}async function B(){let t=document.getElementById("radio-audio"),{volume:o}=c();t.volume=o/100}async function D(t){console.log("Caching next song: ",t.url);let o=document.createElement("audio");o.src=t.url,o.preload="metadata";let e=document.body.appendChild(o);e.addEventListener("loadeddata",()=>{e.remove()})}document.addEventListener("DOMContentLoaded",()=>{let t=document.getElementById("add-open"),o=document.getElementById("add-close");t.addEventListener("click",()=>{let e=document.getElementById("playlist-input"),n=document.getElementById("radio-name-input"),s=e.value;if(!s)return;let a=new URL(s).searchParams.get("list")??s;window.location.search=`?id=${a}&name=${n.value}`}),o.addEventListener("click",()=>{_("add-dialog")})});function M(t){document.getElementById(t).showModal()}function _(t){document.getElementById(t).close()}function p(t){let o=document.getElementById("radio-audio"),e=document.querySelector("#volume-bar"),n=e.querySelectorAll(".volume-bar-line"),s=document.querySelector("#volume-number");t<0&&(t=0),t>100&&(t=100),t>0?e.classList.remove("muted"):e.classList.add("muted"),o.volume=t/100;let i=Math.round(t/(100/n.length))*(100/n.length);n.forEach(d=>d.classList.remove("active"));for(let d=0;d{let{song:o,elapsed:e}=await f(l());y(o,e),document.removeEventListener("click",t)};document.addEventListener("click",t)}document.addEventListener("DOMContentLoaded",()=>{let t=document.querySelector("#volume-bar");t.addEventListener("click",n=>{let i=Math.floor(n.offsetX/t.clientWidth*100);p(i)}),t.addEventListener("mousedown",n=>{let i=Math.floor(n.offsetX/t.clientWidth*100);p(i),document.addEventListener("mousemove",o),document.addEventListener("mouseup",e)});let o=n=>{let s=n;if(s.target!==t)return;let i=Math.floor(s.offsetX/t.clientWidth*100);p(i)},e=()=>{document.removeEventListener("mousemove",o),document.removeEventListener("mouseup",e)}});if(l()&&g()){let t=g(),o=document.querySelectorAll('meta[name*="title"]'),e=document.querySelectorAll('meta[name*="description"]');for(let n of o)n.setAttribute("content","Titune");for(let n of e)n.setAttribute("content",`Listen along to "${t}" Radio`)}document.addEventListener("DOMContentLoaded",async()=>{document.getElementById("add-icon").addEventListener("click",()=>{M("add-dialog")});let o=document.getElementById("currently-playing"),e=document.getElementById("time-elapsed"),n=document.getElementById("duration"),s=document.getElementById("cover"),i=l(),d="",a=g();document.title=`Titune | ${a}`,B(),p(0),b(),await I(i),setInterval(async()=>{let{song:r,elapsed:u}=await f(i);if(!d.includes(r?.title??"Unknown")&&r){let h=(r.videoThumbnails.length>0&&Array.isArray(r.videoThumbnails[0])?r.videoThumbnails[0]:r.videoThumbnails).reduce((x,w)=>w.width>x.width?w:x).url;s.setAttribute("src",h);let T=document.getElementById("bg-cover");T.style.backgroundImage=`url(${h})`,document.getElementById("favicon").setAttribute("href",h),y(r,u)}if(`${r?.title??"Unknown"}${u}`===d)return;o.textContent=r?.title??"Unknown",e.textContent=`${Math.floor(u/60)}:${Math.floor(u%60).toString().padStart(2,"0")}`,n.textContent=`${Math.floor((r?.lengthSeconds??0)/60)}:${Math.floor((r?.lengthSeconds??0)%60).toString().padStart(2,"0")}`;let C=document.getElementById("progress-bar-fill"),U=u/(r?.lengthSeconds??1)*100;C.style.width=`${U}%`,d=`${r?.title??"Unknown"}${u}`},200)});})(); //# sourceMappingURL=bundle.js.map diff --git a/dist/bundle.js.map b/dist/bundle.js.map index 75a94ec..1ac0498 100644 --- a/dist/bundle.js.map +++ b/dist/bundle.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["util/meta.js", "util/options.js", "util/youtube.js", "util/radio.js", "util/audioController.js", "util/dialogs.js", "util/volume.js", "index.js"], - "sourcesContent": ["/**\n * We will have two URL params, the \"name\" and the \"id\". Name is used for embedding, ID is used for getting the playlist.\n *\n * The name will not matter, as its just for people reading the URL to identify the playlist (especially for embeding in sites and stuff).\n * The playlist ID does matter. Obviously.\n */\nexport function getRadioName() {\n return new URLSearchParams(window.location.search).get('name');\n}\nexport function getPlaylistId() {\n return new URLSearchParams(window.location.search).get('id');\n}\n", "export const qualityList = [\n 'AUDIO_QUALITY_ULTRALOW',\n 'AUDIO_QUALITY_LOW',\n 'AUDIO_QUALITY_MEDIUM',\n 'AUDIO_QUALITY_HIGH'\n];\nconst defaultOptions = {\n invidiousInstance: 'https://vid.puffyan.us/',\n shaders: true,\n volume: 50,\n preferredCodec: 'mp4a',\n maxQuality: 'AUDIO_QUALITY_HIGH'\n};\nexport function getOptions() {\n const options = localStorage.getItem('options');\n return options ? JSON.parse(options) : defaultOptions;\n}\nexport function setOption(o, v) {\n const options = getOptions();\n options[o] = v;\n localStorage.setItem('options', JSON.stringify(options));\n}\n", "import { getOptions, qualityList } from './options';\nimport { seedFromPlaylistId } from './radio';\nconst cachedPlaylistVideos = [];\nexport function playlistFromLink(link) {\n const playlistId = link.match(/(?<=list=)[a-zA-Z0-9_-]+/)?.[0];\n return playlistId;\n}\nexport async function playlistVideos(playlistId) {\n // Check if we have a cached version of the playlist\n if (cachedPlaylistVideos.length > 0) {\n return cachedPlaylistVideos;\n }\n const inst = getOptions().invidiousInstance;\n // Ensure URL ends with / if it doesn't already\n const url = inst.endsWith('/') ? inst : inst + '/';\n const result = (await fetch(`${url}api/v1/playlists/${playlistId}`).then(r => r.json()));\n // Cache the playlist\n cachedPlaylistVideos.push(...result.videos);\n return result.videos;\n}\n/**\n * Get all of the audio sources for a given video\n */\nexport async function videoAudioSources(videoId) {\n const inst = getOptions().invidiousInstance;\n // Ensure URL ends with / if it doesn't already\n const url = inst.endsWith('/') ? inst : inst + '/';\n const result = (await fetch(`${url}api/v1/videos/${videoId}`).then(r => r.json())).adaptiveFormats;\n return result.filter(r => r.type.startsWith('audio/'));\n}\n/**\n * Get the preferred audio source for a given video. If the preferred codec is not available, it will fall back to the first.\n */\nexport async function videoAudioSource(videoId) {\n const sources = await videoAudioSources(videoId);\n const { preferredCodec } = getOptions();\n // Find the preferred codec\n const codecPreferred = sources.filter(s => s.type.includes(preferredCodec));\n // Within the preferred codec, find the audio source with the highest quality (within the maxQuality option)\n const qualityPreferred = sourceWithClosestQuality(codecPreferred);\n // If the preferred codec is not available, fall back to the first\n return qualityPreferred || codecPreferred[0] || sources[0];\n}\n/**\n * Using the quality list, find the quality closest to the max quality, preferring lower before higher\n */\nexport function sourceWithClosestQuality(sources) {\n const { maxQuality } = getOptions();\n // Find the quality closest to the max quality, preferring lower before higher\n const qualities = qualityList.filter(q => qualityList.indexOf(q) <= qualityList.indexOf(maxQuality)).sort((a, b) => qualityList.indexOf(a) - qualityList.indexOf(b));\n const source = sources.find(s => qualities.some(q => s.audioQuality.includes(q)));\n return source;\n}\nexport async function shuffledPlaylistVideos(playlistId) {\n const videos = await playlistVideos(playlistId);\n const seed = seedFromPlaylistId(playlistId);\n const now = Date.now() / 1000;\n // Get the total length of the playlist in seconds\n const totalLength = videos.reduce((prev, curr) => prev + curr.lengthSeconds, 0);\n // Create a modified seed using the total length of the playlist / the current time\n // This makes it so that whenever the playlist is run through, it will be shuffled in a different way the next time\n const dynSeed = Math.floor(now / totalLength) + seed;\n return shuffle(videos, dynSeed);\n}\n/**\n * Fisher-Yates shuffle algorithm using the seed\n */\nexport function shuffle(array, seed) {\n const shuffled = [...array];\n for (let i = shuffled.length - 1; i > 0; i--) {\n const j = Math.floor((seed + i) % (i + 1));\n const temp = shuffled[i];\n shuffled[i] = shuffled[j];\n shuffled[j] = temp;\n }\n return shuffled;\n}\n", "import { shuffledPlaylistVideos } from './youtube';\n/**\n * Turn playlist id into a numeric 'seed'\n */\nexport function seedFromPlaylistId(playlistId) {\n let seed = 0;\n for (let i = 0; i < playlistId.length; i++) {\n seed += playlistId.charCodeAt(i);\n }\n return seed;\n}\n/**\n * Get the current song and the time elapsed within it using a seed based on playlist ID and the current timestamp\n */\nexport async function getCurrentSong(playlistId) {\n const songs = await shuffledPlaylistVideos(playlistId);\n const now = Date.now();\n // Get the total length of all songs\n let totalLength = 0;\n for (const song of songs) {\n totalLength += song.lengthSeconds;\n }\n // Get the current time elapsed\n const elapsed = Math.floor((now / 1000) % totalLength);\n // Get the current song\n let currentLength = 0;\n let song;\n for (const s of songs) {\n currentLength += s.lengthSeconds;\n if (elapsed < currentLength) {\n song = s;\n break;\n }\n }\n // Get the elapsed time in the song\n const elapsedInSong = elapsed - (currentLength - song.lengthSeconds);\n return { song, elapsed: elapsedInSong };\n}\n/**\n * We may want to get the next song, previous song, or maybe a song played 4 songs ago.\n *\n * For example, getting the next song would be offset = 1, previous song would be offset = -1, and a song played 4 songs ago would be offset = -4.\n * Automatically wraps around the playlist if necessary.\n */\nexport async function getSongRelative(playlistId, offset) {\n const songs = await shuffledPlaylistVideos(playlistId);\n const current = await getCurrentSong(playlistId);\n // Find the index of the current song\n let idx = songs.findIndex(s => s.videoId === current.song?.videoId);\n // Add the offset. Wrap around if necessary\n idx += offset;\n idx %= songs.length;\n return songs[idx];\n}\n", "import { getPlaylistId } from './meta';\nimport { getOptions } from './options';\nimport { getSongRelative } from './radio';\nimport { videoAudioSource } from './youtube';\nexport async function setSongAndTime(song, time) {\n // Get the audio element\n const audio = document.getElementById('radio-audio');\n const src = await videoAudioSource(song.videoId);\n audio.src = src.url;\n audio.currentTime = time;\n audio.play();\n // Whenever we set the song, we should also preload the next song\n const nextSong = await getSongRelative(getPlaylistId(), 1);\n const nextAudio = await videoAudioSource(nextSong.videoId);\n await preloadSong(nextAudio);\n}\n/**\n * Set the volume and stuff\n */\nexport async function initAudioController() {\n const audio = document.getElementById('radio-audio');\n const { volume } = getOptions();\n audio.volume = volume / 100;\n}\n/**\n * Preload a given song\n */\nexport async function preloadSong(song) {\n console.log('Caching next song: ', song.url);\n // Create a disabled audio tag, set the src, and then remove it when it's loaded\n const audio = document.createElement('audio');\n audio.src = song.url;\n audio.preload = 'metadata';\n const inst = document.body.appendChild(audio);\n inst.addEventListener('loadeddata', () => {\n inst.remove();\n });\n}\n", "document.addEventListener('DOMContentLoaded', () => {\n const addOpen = document.getElementById('add-open');\n const addClose = document.getElementById('add-close');\n addOpen.addEventListener('click', () => {\n // Get the playlist URL or ID from the input\n const input = document.getElementById('playlist-input');\n const radioName = document.getElementById('radio-name-input');\n const urlOrId = input.value;\n // If the input is empty, don't do anything\n if (!urlOrId)\n return;\n // If the input is a URL, get the ID from it\n const url = new URL(urlOrId);\n const id = url.searchParams.get('list');\n // If the input is an ID, use it\n const playlistId = id ?? urlOrId;\n // Set new window location\n window.location.search = `?id=${playlistId}&name=${radioName.value}`;\n });\n addClose.addEventListener('click', () => {\n closeModal('add-dialog');\n });\n});\nexport function openModal(id) {\n const diag = document.getElementById(id);\n diag.showModal();\n}\nexport function closeModal(id) {\n const diag = document.getElementById(id);\n diag.close();\n}\n", "import { setOption } from './options';\n/**\n * Set volume\n */\nexport function setVolume(volume) {\n const audio = document.getElementById('radio-audio');\n const volumeBar = document.querySelector('#volume-bar');\n const volumeBarLines = volumeBar.querySelectorAll('.volume-bar-line');\n const volumeNumber = document.querySelector('#volume-number');\n if (volume < 0)\n volume = 0;\n if (volume > 100)\n volume = 100;\n volume > 0 ? volumeBar.classList.remove('muted') : volumeBar.classList.add('muted');\n audio.volume = volume / 100;\n // Find closest multiple of of however many lines there are\n const closest = Math.round(volume / (100 / volumeBarLines.length)) * (100 / volumeBarLines.length);\n // Remove all classes\n volumeBarLines.forEach(line => line.classList.remove('active'));\n // Add classes to the closest multiple of the volume bar lines\n for (let i = 0; i < closest / (100 / volumeBarLines.length); i++) {\n volumeBarLines[i].classList.add('active');\n }\n // Set the volume number\n volumeNumber.textContent = `${volume}%`;\n // Set the volume in options\n setOption('volume', volume);\n}\n/**\n * Create a single-use click listener for the volume bar. Clicking it will trigger the audio element to play\n */\nexport function createTempVolumeListener() {\n const click = () => {\n const audio = document.getElementById('radio-audio');\n audio.play();\n document.removeEventListener('click', click);\n };\n document.addEventListener('click', click);\n}\n// Event listener for dragging/clicking on the volume bar\ndocument.addEventListener('DOMContentLoaded', () => {\n const volumeBar = document.querySelector('#volume-bar');\n volumeBar.addEventListener('click', e => {\n const evt = e;\n const volume = Math.floor((evt.offsetX / volumeBar.clientWidth) * 100);\n setVolume(volume);\n });\n volumeBar.addEventListener('mousedown', (e) => {\n const evt = e;\n const volume = Math.floor((evt.offsetX / volumeBar.clientWidth) * 100);\n setVolume(volume);\n document.addEventListener('mousemove', mouseMove);\n document.addEventListener('mouseup', mouseUp);\n });\n const mouseMove = (e) => {\n const evt = e;\n // target needs to be the volumeBar\n if (evt.target !== volumeBar)\n return;\n const volume = Math.floor((evt.offsetX / volumeBar.clientWidth) * 100);\n setVolume(volume);\n };\n const mouseUp = () => {\n document.removeEventListener('mousemove', mouseMove);\n document.removeEventListener('mouseup', mouseUp);\n };\n});\n", "import { initAudioController, setSongAndTime } from './util/audioController';\nimport { openModal } from './util/dialogs';\nimport { getPlaylistId, getRadioName } from './util/meta';\nimport { getCurrentSong } from './util/radio';\nimport { createTempVolumeListener, setVolume } from './util/volume';\nimport { playlistVideos } from './util/youtube';\n// Change the meta tags to reflect the playlist, if there is one\nif (getPlaylistId() && getRadioName()) {\n const name = getRadioName();\n const titles = document.querySelectorAll('meta[name*=\"title\"]');\n const descriptions = document.querySelectorAll('meta[name*=\"description\"]');\n for (const title of titles) {\n title.setAttribute('content', 'Titune');\n }\n for (const description of descriptions) {\n description.setAttribute('content', `Listen along to \"${name}\" Radio`);\n }\n}\ndocument.addEventListener('DOMContentLoaded', async () => {\n // Create dialog event listeners\n const addButton = document.getElementById('add-icon');\n addButton.addEventListener('click', () => {\n openModal('add-dialog');\n });\n // Every second, update currently-playing and time-elapsed\n const currentlyPlaying = document.getElementById('currently-playing');\n const timeElapsed = document.getElementById('time-elapsed');\n const songLength = document.getElementById('duration');\n const cover = document.getElementById('cover');\n const playlistId = getPlaylistId();\n let identifier = '';\n // Set page title to Titune | \n const radioName = getRadioName();\n document.title = `Titune | ${radioName}`;\n initAudioController();\n // in order to prompt the user to interact with the page, allowing us to play(), we set volume to 0\n setVolume(0);\n createTempVolumeListener();\n // Ensure we call this at least once, to cache the playlist\n await playlistVideos(playlistId);\n setInterval(async () => {\n const { song, elapsed } = await getCurrentSong(playlistId);\n // If the song changes, set the new song and timestamp\n if (!identifier.includes(song?.title ?? 'Unknown') && song) {\n const arrayInArrayMaybe = song.videoThumbnails.length > 0 && Array.isArray(song.videoThumbnails[0]) ? song.videoThumbnails[0] : song.videoThumbnails;\n // @ts-expect-error This array stuff is funky\n const highestWidthThumb = arrayInArrayMaybe.reduce((prev, curr) => curr.width > prev.width ? curr : prev).url;\n cover.setAttribute('src', highestWidthThumb);\n // Also set the body background to the cover\n const bgCover = document.getElementById('bg-cover');\n bgCover.style.backgroundImage = `url(${highestWidthThumb})`;\n // ALSO set the favicon to the cover\n const favicon = document.getElementById('favicon');\n favicon.setAttribute('href', highestWidthThumb);\n setSongAndTime(song, elapsed);\n }\n // Prevents rapid DOM updates\n if (`${song?.title ?? 'Unknown'}${elapsed}` === identifier) {\n return;\n }\n currentlyPlaying.textContent = song?.title ?? 'Unknown';\n timeElapsed.textContent = `${Math.floor(elapsed / 60)}:${Math.floor(elapsed % 60).toString().padStart(2, '0')}`;\n songLength.textContent = `${Math.floor((song?.lengthSeconds ?? 0) / 60)}:${Math.floor((song?.lengthSeconds ?? 0) % 60).toString().padStart(2, '0')}`;\n // Set the progress bar inners width to reflect the percentage of the song that has elapsed\n const progressBarInner = document.getElementById('progress-bar-fill');\n const songPct = (elapsed / (song?.lengthSeconds ?? 1)) * 100;\n progressBarInner.style.width = `${songPct}%`;\n identifier = `${song?.title ?? 'Unknown'}${elapsed}`;\n }, 200);\n});\n"], - "mappings": "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", - "names": ["getRadioName", "getPlaylistId", "qualityList", "defaultOptions", "getOptions", "options", "setOption", "o", "v", "cachedPlaylistVideos", "playlistVideos", "playlistId", "cachedPlaylistVideos", "inst", "getOptions", "url", "result", "r", "videoAudioSources", "videoId", "videoAudioSource", "sources", "preferredCodec", "codecPreferred", "sourceWithClosestQuality", "maxQuality", "qualities", "qualityList", "q", "a", "b", "s", "shuffledPlaylistVideos", "videos", "seed", "seedFromPlaylistId", "now", "totalLength", "prev", "curr", "dynSeed", "shuffle", "array", "shuffled", "i", "j", "temp", "seedFromPlaylistId", "playlistId", "seed", "i", "getCurrentSong", "songs", "shuffledPlaylistVideos", "now", "totalLength", "song", "elapsed", "currentLength", "s", "elapsedInSong", "getSongRelative", "offset", "current", "idx", "setSongAndTime", "song", "time", "audio", "src", "videoAudioSource", "nextSong", "getSongRelative", "getPlaylistId", "nextAudio", "preloadSong", "initAudioController", "volume", "getOptions", "inst", "addOpen", "addClose", "input", "radioName", "urlOrId", "playlistId", "closeModal", "openModal", "id", "setVolume", "volume", "audio", "volumeBar", "volumeBarLines", "volumeNumber", "closest", "line", "i", "setOption", "createTempVolumeListener", "click", "e", "mouseMove", "mouseUp", "evt", "getPlaylistId", "getRadioName", "name", "titles", "descriptions", "title", "description", "openModal", "currentlyPlaying", "timeElapsed", "songLength", "cover", "playlistId", "identifier", "radioName", "initAudioController", "setVolume", "createTempVolumeListener", "playlistVideos", "song", "elapsed", "getCurrentSong", "highestWidthThumb", "prev", "curr", "bgCover", "setSongAndTime", "progressBarInner", "songPct"] + "sourcesContent": ["/**\n * We will have two URL params, the \"name\" and the \"id\". Name is used for embedding, ID is used for getting the playlist.\n *\n * The name will not matter, as its just for people reading the URL to identify the playlist (especially for embeding in sites and stuff).\n * The playlist ID does matter. Obviously.\n */\nexport function getRadioName() {\n return new URLSearchParams(window.location.search).get('name');\n}\nexport function getPlaylistId() {\n return new URLSearchParams(window.location.search).get('id');\n}\n", "export const qualityList = [\n 'AUDIO_QUALITY_ULTRALOW',\n 'AUDIO_QUALITY_LOW',\n 'AUDIO_QUALITY_MEDIUM',\n 'AUDIO_QUALITY_HIGH'\n];\nconst defaultOptions = {\n invidiousInstance: 'https://vid.puffyan.us/',\n shaders: true,\n volume: 50,\n preferredCodec: 'mp4a',\n maxQuality: 'AUDIO_QUALITY_HIGH'\n};\nexport function getOptions() {\n const options = localStorage.getItem('options');\n return options ? JSON.parse(options) : defaultOptions;\n}\nexport function setOption(o, v) {\n const options = getOptions();\n options[o] = v;\n localStorage.setItem('options', JSON.stringify(options));\n}\n", "import { getOptions, qualityList } from './options';\nimport { seedFromPlaylistId } from './radio';\nconst cachedPlaylistVideos = [];\nexport function playlistFromLink(link) {\n const playlistId = link.match(/(?<=list=)[a-zA-Z0-9_-]+/)?.[0];\n return playlistId;\n}\nexport async function playlistVideos(playlistId) {\n // Check if we have a cached version of the playlist\n if (cachedPlaylistVideos.length > 0) {\n return cachedPlaylistVideos;\n }\n const inst = getOptions().invidiousInstance;\n // Ensure URL ends with / if it doesn't already\n const url = inst.endsWith('/') ? inst : inst + '/';\n const result = (await fetch(`${url}api/v1/playlists/${playlistId}`).then(r => r.json()));\n // Cache the playlist\n cachedPlaylistVideos.push(...result.videos);\n return result.videos;\n}\n/**\n * Get all of the audio sources for a given video\n */\nexport async function videoAudioSources(videoId) {\n const inst = getOptions().invidiousInstance;\n // Ensure URL ends with / if it doesn't already\n const url = inst.endsWith('/') ? inst : inst + '/';\n const result = (await fetch(`${url}api/v1/videos/${videoId}`).then(r => r.json())).adaptiveFormats;\n return result.filter(r => r.type.startsWith('audio/'));\n}\n/**\n * Get the preferred audio source for a given video. If the preferred codec is not available, it will fall back to the first.\n */\nexport async function videoAudioSource(videoId) {\n const sources = await videoAudioSources(videoId);\n const { preferredCodec } = getOptions();\n // Find the preferred codec\n const codecPreferred = sources.filter(s => s.type.includes(preferredCodec));\n // Within the preferred codec, find the audio source with the highest quality (within the maxQuality option)\n const qualityPreferred = sourceWithClosestQuality(codecPreferred);\n // If the preferred codec is not available, fall back to the first\n return qualityPreferred || codecPreferred[0] || sources[0];\n}\n/**\n * Using the quality list, find the quality closest to the max quality, preferring lower before higher\n */\nexport function sourceWithClosestQuality(sources) {\n const { maxQuality } = getOptions();\n // Find the quality closest to the max quality, preferring lower before higher\n const qualities = qualityList.filter(q => qualityList.indexOf(q) <= qualityList.indexOf(maxQuality)).sort((a, b) => qualityList.indexOf(a) - qualityList.indexOf(b));\n const source = sources.find(s => qualities.some(q => s.audioQuality.includes(q)));\n return source;\n}\nexport async function shuffledPlaylistVideos(playlistId) {\n const videos = await playlistVideos(playlistId);\n const seed = seedFromPlaylistId(playlistId);\n const now = Date.now() / 1000;\n // Get the total length of the playlist in seconds\n const totalLength = videos.reduce((prev, curr) => prev + curr.lengthSeconds, 0);\n // Create a modified seed using the total length of the playlist / the current time\n // This makes it so that whenever the playlist is run through, it will be shuffled in a different way the next time\n const dynSeed = Math.floor(now / totalLength) + seed;\n return shuffle(videos, dynSeed);\n}\n/**\n * Fisher-Yates shuffle algorithm using the seed\n */\nexport function shuffle(array, seed) {\n const shuffled = [...array];\n for (let i = shuffled.length - 1; i > 0; i--) {\n const j = Math.floor((seed + i) % (i + 1));\n const temp = shuffled[i];\n shuffled[i] = shuffled[j];\n shuffled[j] = temp;\n }\n return shuffled;\n}\n", "import { shuffledPlaylistVideos } from './youtube';\n/**\n * Turn playlist id into a numeric 'seed'\n */\nexport function seedFromPlaylistId(playlistId) {\n let seed = 0;\n for (let i = 0; i < playlistId.length; i++) {\n seed += playlistId.charCodeAt(i);\n }\n return seed;\n}\n/**\n * Get the current song and the time elapsed within it using a seed based on playlist ID and the current timestamp\n */\nexport async function getCurrentSong(playlistId) {\n const songs = await shuffledPlaylistVideos(playlistId);\n const now = Date.now();\n // Get the total length of all songs\n let totalLength = 0;\n for (const song of songs) {\n totalLength += song.lengthSeconds;\n }\n // Get the current time elapsed\n const elapsed = Math.floor((now / 1000) % totalLength);\n // Get the current song\n let currentLength = 0;\n let song;\n for (const s of songs) {\n currentLength += s.lengthSeconds;\n if (elapsed < currentLength) {\n song = s;\n break;\n }\n }\n // Get the elapsed time in the song\n const elapsedInSong = elapsed - (currentLength - song.lengthSeconds);\n return { song, elapsed: elapsedInSong };\n}\n/**\n * We may want to get the next song, previous song, or maybe a song played 4 songs ago.\n *\n * For example, getting the next song would be offset = 1, previous song would be offset = -1, and a song played 4 songs ago would be offset = -4.\n * Automatically wraps around the playlist if necessary.\n */\nexport async function getSongRelative(playlistId, offset) {\n const songs = await shuffledPlaylistVideos(playlistId);\n const current = await getCurrentSong(playlistId);\n // Find the index of the current song\n let idx = songs.findIndex(s => s.videoId === current.song?.videoId);\n // Add the offset. Wrap around if necessary\n idx += offset;\n idx %= songs.length;\n return songs[idx];\n}\n", "import { getPlaylistId } from './meta';\nimport { getOptions } from './options';\nimport { getSongRelative } from './radio';\nimport { videoAudioSource } from './youtube';\nexport async function setSongAndTime(song, time) {\n // Get the audio element\n const audio = document.getElementById('radio-audio');\n const src = await videoAudioSource(song.videoId);\n audio.src = src.url;\n audio.currentTime = time;\n audio.play();\n // Whenever we set the song, we should also preload the next song\n const nextSong = await getSongRelative(getPlaylistId(), 1);\n const nextAudio = await videoAudioSource(nextSong.videoId);\n await preloadSong(nextAudio);\n}\n/**\n * Set the volume and stuff\n */\nexport async function initAudioController() {\n const audio = document.getElementById('radio-audio');\n const { volume } = getOptions();\n audio.volume = volume / 100;\n}\n/**\n * Preload a given song\n */\nexport async function preloadSong(song) {\n console.log('Caching next song: ', song.url);\n // Create a disabled audio tag, set the src, and then remove it when it's loaded\n const audio = document.createElement('audio');\n audio.src = song.url;\n audio.preload = 'metadata';\n const inst = document.body.appendChild(audio);\n inst.addEventListener('loadeddata', () => {\n inst.remove();\n });\n}\n", "document.addEventListener('DOMContentLoaded', () => {\n const addOpen = document.getElementById('add-open');\n const addClose = document.getElementById('add-close');\n addOpen.addEventListener('click', () => {\n // Get the playlist URL or ID from the input\n const input = document.getElementById('playlist-input');\n const radioName = document.getElementById('radio-name-input');\n const urlOrId = input.value;\n // If the input is empty, don't do anything\n if (!urlOrId)\n return;\n // If the input is a URL, get the ID from it\n const url = new URL(urlOrId);\n const id = url.searchParams.get('list');\n // If the input is an ID, use it\n const playlistId = id ?? urlOrId;\n // Set new window location\n window.location.search = `?id=${playlistId}&name=${radioName.value}`;\n });\n addClose.addEventListener('click', () => {\n closeModal('add-dialog');\n });\n});\nexport function openModal(id) {\n const diag = document.getElementById(id);\n diag.showModal();\n}\nexport function closeModal(id) {\n const diag = document.getElementById(id);\n diag.close();\n}\n", "import { setSongAndTime } from './audioController';\nimport { getPlaylistId } from './meta';\nimport { setOption } from './options';\nimport { getCurrentSong } from './radio';\n/**\n * Set volume\n */\nexport function setVolume(volume) {\n const audio = document.getElementById('radio-audio');\n const volumeBar = document.querySelector('#volume-bar');\n const volumeBarLines = volumeBar.querySelectorAll('.volume-bar-line');\n const volumeNumber = document.querySelector('#volume-number');\n if (volume < 0)\n volume = 0;\n if (volume > 100)\n volume = 100;\n volume > 0 ? volumeBar.classList.remove('muted') : volumeBar.classList.add('muted');\n audio.volume = volume / 100;\n // Find closest multiple of of however many lines there are\n const closest = Math.round(volume / (100 / volumeBarLines.length)) * (100 / volumeBarLines.length);\n // Remove all classes\n volumeBarLines.forEach(line => line.classList.remove('active'));\n // Add classes to the closest multiple of the volume bar lines\n for (let i = 0; i < closest / (100 / volumeBarLines.length); i++) {\n volumeBarLines[i].classList.add('active');\n }\n // Set the volume number\n volumeNumber.textContent = `${volume}%`;\n // Set the volume in options\n setOption('volume', volume);\n}\n/**\n * Create a single-use click listener for the volume bar. Clicking it will trigger the audio element to play\n */\nexport function createTempVolumeListener() {\n const click = async () => {\n const { song, elapsed } = await getCurrentSong(getPlaylistId());\n setSongAndTime(song, elapsed);\n document.removeEventListener('click', click);\n };\n document.addEventListener('click', click);\n}\n// Event listener for dragging/clicking on the volume bar\ndocument.addEventListener('DOMContentLoaded', () => {\n const volumeBar = document.querySelector('#volume-bar');\n volumeBar.addEventListener('click', e => {\n const evt = e;\n const volume = Math.floor((evt.offsetX / volumeBar.clientWidth) * 100);\n setVolume(volume);\n });\n volumeBar.addEventListener('mousedown', (e) => {\n const evt = e;\n const volume = Math.floor((evt.offsetX / volumeBar.clientWidth) * 100);\n setVolume(volume);\n document.addEventListener('mousemove', mouseMove);\n document.addEventListener('mouseup', mouseUp);\n });\n const mouseMove = (e) => {\n const evt = e;\n // target needs to be the volumeBar\n if (evt.target !== volumeBar)\n return;\n const volume = Math.floor((evt.offsetX / volumeBar.clientWidth) * 100);\n setVolume(volume);\n };\n const mouseUp = () => {\n document.removeEventListener('mousemove', mouseMove);\n document.removeEventListener('mouseup', mouseUp);\n };\n});\n", "import { initAudioController, setSongAndTime } from './util/audioController';\nimport { openModal } from './util/dialogs';\nimport { getPlaylistId, getRadioName } from './util/meta';\nimport { getCurrentSong } from './util/radio';\nimport { createTempVolumeListener, setVolume } from './util/volume';\nimport { playlistVideos } from './util/youtube';\n// Change the meta tags to reflect the playlist, if there is one\nif (getPlaylistId() && getRadioName()) {\n const name = getRadioName();\n const titles = document.querySelectorAll('meta[name*=\"title\"]');\n const descriptions = document.querySelectorAll('meta[name*=\"description\"]');\n for (const title of titles) {\n title.setAttribute('content', 'Titune');\n }\n for (const description of descriptions) {\n description.setAttribute('content', `Listen along to \"${name}\" Radio`);\n }\n}\ndocument.addEventListener('DOMContentLoaded', async () => {\n // Create dialog event listeners\n const addButton = document.getElementById('add-icon');\n addButton.addEventListener('click', () => {\n openModal('add-dialog');\n });\n // Every second, update currently-playing and time-elapsed\n const currentlyPlaying = document.getElementById('currently-playing');\n const timeElapsed = document.getElementById('time-elapsed');\n const songLength = document.getElementById('duration');\n const cover = document.getElementById('cover');\n const playlistId = getPlaylistId();\n let identifier = '';\n // Set page title to Titune | \n const radioName = getRadioName();\n document.title = `Titune | ${radioName}`;\n initAudioController();\n // in order to prompt the user to interact with the page, allowing us to play(), we set volume to 0\n setVolume(0);\n createTempVolumeListener();\n // Ensure we call this at least once, to cache the playlist\n await playlistVideos(playlistId);\n setInterval(async () => {\n const { song, elapsed } = await getCurrentSong(playlistId);\n // If the song changes, set the new song and timestamp\n if (!identifier.includes(song?.title ?? 'Unknown') && song) {\n const arrayInArrayMaybe = song.videoThumbnails.length > 0 && Array.isArray(song.videoThumbnails[0]) ? song.videoThumbnails[0] : song.videoThumbnails;\n // @ts-expect-error This array stuff is funky\n const highestWidthThumb = arrayInArrayMaybe.reduce((prev, curr) => curr.width > prev.width ? curr : prev).url;\n cover.setAttribute('src', highestWidthThumb);\n // Also set the body background to the cover\n const bgCover = document.getElementById('bg-cover');\n bgCover.style.backgroundImage = `url(${highestWidthThumb})`;\n // ALSO set the favicon to the cover\n const favicon = document.getElementById('favicon');\n favicon.setAttribute('href', highestWidthThumb);\n setSongAndTime(song, elapsed);\n }\n // Prevents rapid DOM updates\n if (`${song?.title ?? 'Unknown'}${elapsed}` === identifier) {\n return;\n }\n currentlyPlaying.textContent = song?.title ?? 'Unknown';\n timeElapsed.textContent = `${Math.floor(elapsed / 60)}:${Math.floor(elapsed % 60).toString().padStart(2, '0')}`;\n songLength.textContent = `${Math.floor((song?.lengthSeconds ?? 0) / 60)}:${Math.floor((song?.lengthSeconds ?? 0) % 60).toString().padStart(2, '0')}`;\n // Set the progress bar inners width to reflect the percentage of the song that has elapsed\n const progressBarInner = document.getElementById('progress-bar-fill');\n const songPct = (elapsed / (song?.lengthSeconds ?? 1)) * 100;\n progressBarInner.style.width = `${songPct}%`;\n identifier = `${song?.title ?? 'Unknown'}${elapsed}`;\n }, 200);\n});\n"], + "mappings": "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", + "names": ["getRadioName", "getPlaylistId", "qualityList", "defaultOptions", "getOptions", "options", "setOption", "o", "v", "cachedPlaylistVideos", "playlistVideos", "playlistId", "cachedPlaylistVideos", "inst", "getOptions", "url", "result", "r", "videoAudioSources", "videoId", "videoAudioSource", "sources", "preferredCodec", "codecPreferred", "s", "sourceWithClosestQuality", "maxQuality", "qualities", "qualityList", "q", "a", "b", "shuffledPlaylistVideos", "videos", "seed", "seedFromPlaylistId", "now", "totalLength", "prev", "curr", "dynSeed", "shuffle", "array", "shuffled", "i", "j", "temp", "seedFromPlaylistId", "playlistId", "seed", "i", "getCurrentSong", "songs", "shuffledPlaylistVideos", "now", "totalLength", "song", "elapsed", "currentLength", "s", "elapsedInSong", "getSongRelative", "offset", "current", "idx", "setSongAndTime", "song", "time", "audio", "src", "videoAudioSource", "nextSong", "getSongRelative", "getPlaylistId", "nextAudio", "preloadSong", "initAudioController", "volume", "getOptions", "inst", "addOpen", "addClose", "input", "radioName", "urlOrId", "playlistId", "closeModal", "openModal", "id", "setVolume", "volume", "audio", "volumeBar", "volumeBarLines", "volumeNumber", "closest", "line", "i", "setOption", "createTempVolumeListener", "click", "song", "elapsed", "getCurrentSong", "getPlaylistId", "setSongAndTime", "e", "mouseMove", "mouseUp", "evt", "getPlaylistId", "getRadioName", "name", "titles", "descriptions", "title", "description", "openModal", "currentlyPlaying", "timeElapsed", "songLength", "cover", "playlistId", "identifier", "radioName", "initAudioController", "setVolume", "createTempVolumeListener", "playlistVideos", "song", "elapsed", "getCurrentSong", "highestWidthThumb", "prev", "curr", "bgCover", "setSongAndTime", "progressBarInner", "songPct"] } diff --git a/dist/util/volume.js b/dist/util/volume.js index 553cebf..7eb0b6b 100644 --- a/dist/util/volume.js +++ b/dist/util/volume.js @@ -1,4 +1,7 @@ +import { setSongAndTime } from './audioController'; +import { getPlaylistId } from './meta'; import { setOption } from './options'; +import { getCurrentSong } from './radio'; /** * Set volume */ @@ -30,9 +33,9 @@ export function setVolume(volume) { * Create a single-use click listener for the volume bar. Clicking it will trigger the audio element to play */ export function createTempVolumeListener() { - const click = () => { - const audio = document.getElementById('radio-audio'); - audio.play(); + const click = async () => { + const { song, elapsed } = await getCurrentSong(getPlaylistId()); + setSongAndTime(song, elapsed); document.removeEventListener('click', click); }; document.addEventListener('click', click); diff --git a/src/util/volume.ts b/src/util/volume.ts index ae7681b..7d466e8 100644 --- a/src/util/volume.ts +++ b/src/util/volume.ts @@ -1,4 +1,7 @@ +import { setSongAndTime } from './audioController' +import { getPlaylistId } from './meta' import { setOption } from './options' +import { getCurrentSong } from './radio' /** * Set volume @@ -38,9 +41,9 @@ export function setVolume(volume: number) { * Create a single-use click listener for the volume bar. Clicking it will trigger the audio element to play */ export function createTempVolumeListener() { - const click = () => { - const audio = document.getElementById('radio-audio') as HTMLAudioElement - audio.play() + const click = async () => { + const { song, elapsed } = await getCurrentSong(getPlaylistId()) + setSongAndTime(song!, elapsed) document.removeEventListener('click', click) }