hawk / files / public / js / controls.js
controls.js
| 1 | let pageStatus = { |
| 2 | text: '', |
| 3 | isError: false |
| 4 | }; |
| 5 | setInterval(() => { |
| 6 | const statusDiv = document.getElementById('status'); |
| 7 | statusDiv.innerText = pageStatus.text; |
| 8 | if (pageStatus.isError) { |
| 9 | statusDiv.classList.add('error'); |
| 10 | } else { |
| 11 | statusDiv.classList.remove('error'); |
| 12 | } |
| 13 | }, 100); |
| 14 | |
| 15 | function reportStatus(text, isError = false) { |
| 16 | pageStatus.text = text; |
| 17 | pageStatus.isError = isError; |
| 18 | } |
| 19 | |
| 20 | async function pauseOrPlay() { |
| 21 | const currentTrack = await getPlayingTrack(); |
| 22 | if (currentTrack && currentTrack.is_playing) { |
| 23 | await pause(); |
| 24 | } else { |
| 25 | await play(); |
| 26 | } |
| 27 | setCurrentPlayingSong(); |
| 28 | } |
| 29 | |
| 30 | async function playChosenSong(trackName, trackId) { |
| 31 | // clear status |
| 32 | reportStatus('') |
| 33 | |
| 34 | // get select element |
| 35 | const devicesList = document.getElementById('devices'); |
| 36 | const deviceId = devicesList.value; |
| 37 | if (!deviceId) { |
| 38 | reportStatus('Cannot play track, no device selected', true); |
| 39 | return; |
| 40 | } |
| 41 | |
| 42 | try { |
| 43 | await playTrack(trackId); |
| 44 | } catch (e) { |
| 45 | console.log("Error playing track") |
| 46 | console.log(e); |
| 47 | reportStatus(e); |
| 48 | } |
| 49 | setCurrentPlayingSong(); |
| 50 | } |
| 51 | |
| 52 | function setTime(minutes, seconds) { |
| 53 | document.getElementById('minutes-input').value = minutes; |
| 54 | document.getElementById('seconds-input').value = seconds; |
| 55 | } |
| 56 | |
| 57 | async function searchForMatch() { |
| 58 | const searchButton = document.getElementById('search'); |
| 59 | searchButton.setAttribute('disabled', 'disabled'); |
| 60 | try { |
| 61 | const minutes = document.getElementById('minutes-input').value; |
| 62 | const seconds = document.getElementById('seconds-input').value; |
| 63 | const tracks = await findMatchingTracks(minutes, seconds); |
| 64 | const resultsDiv = document.getElementById('results'); |
| 65 | if (tracks.length == 0) { |
| 66 | resultsDiv.innerHTML = 'No songs found for that duration.'; |
| 67 | } else { |
| 68 | resultsDiv.innerHTML = ''; |
| 69 | tracks.forEach(track => { |
| 70 | const album = track.album; |
| 71 | const images = album.images; |
| 72 | let largestImage = images[0]; |
| 73 | const albumArtUrl = largestImage.url; |
| 74 | const params = { |
| 75 | name: track.name, |
| 76 | album: track.album.name, |
| 77 | artist: track.artists[0].name, |
| 78 | duration: formatDuration(track.duration_ms), |
| 79 | albumArtUrl: albumArtUrl, |
| 80 | songUrl: track.external_urls.spotify, |
| 81 | albumUrl: album.external_urls.spotify, |
| 82 | artistUrl: track.artists[0].external_urls.spotify, |
| 83 | trackId: track.id, |
| 84 | trackHref: track.external_urls.spotify |
| 85 | }; |
| 86 | |
| 87 | const html = createTemplatedHtml('found-song-item', params); |
| 88 | resultsDiv.innerHTML += html; |
| 89 | }); |
| 90 | } |
| 91 | } catch (e) { |
| 92 | console.error(e); |
| 93 | reportStatus(`Encountered error while searching: ${e}`); |
| 94 | } finally { |
| 95 | searchButton.removeAttribute('disabled'); |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | async function findMatchingTracks(minutes, seconds) { |
| 100 | reportStatus(`Retrieving albums`); |
| 101 | const albums = await getAlbums((state) => { |
| 102 | const { processedCount, totalCount } = state; |
| 103 | if (totalCount <= 0) { |
| 104 | totalCount = "???" |
| 105 | } |
| 106 | reportStatus(`Retrieving albums: ${processedCount} of ${totalCount}`); |
| 107 | }); |
| 108 | |
| 109 | const checkTrack = function (track) { |
| 110 | const duration = track.duration_ms; |
| 111 | const durationInSeconds = Math.floor(duration / 1000); |
| 112 | const durationInMinutes = Math.floor(durationInSeconds / 60); |
| 113 | const durationInRemainingSeconds = durationInSeconds % 60; |
| 114 | |
| 115 | const isMatching = (durationInMinutes == minutes && durationInRemainingSeconds == seconds); |
| 116 | return isMatching; |
| 117 | } |
| 118 | |
| 119 | const tracks = await batchGetTracks(albums.map(album => album.album.id), null) |
| 120 | const foundTracks = tracks.filter(track => checkTrack(track)); |
| 121 | |
| 122 | reportStatus(`Found ${foundTracks.length} track(s)`); |
| 123 | return foundTracks; |
| 124 | } |
| 125 | |
| 126 | async function clearDb() { |
| 127 | await deleteDb(); |
| 128 | window.location.reload(); |
| 129 | } |
| 130 | |
| 131 | const hideOrShow = function (elementClass, show) { |
| 132 | const elements = document.getElementsByClassName(elementClass); |
| 133 | for (let i = 0; i < elements.length; i++) { |
| 134 | const element = elements[i]; |
| 135 | if (show) { |
| 136 | element.setAttribute('style', 'display: block'); |
| 137 | } else { |
| 138 | element.setAttribute('style', 'display: none'); |
| 139 | } |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | function formatDuration(ms) { |
| 144 | // Convert milliseconds to total seconds |
| 145 | let totalSeconds = Math.floor(ms / 1000); |
| 146 | |
| 147 | // Calculate minutes and seconds |
| 148 | let minutes = Math.floor(totalSeconds / 60); |
| 149 | let seconds = totalSeconds % 60; |
| 150 | |
| 151 | // Ensure seconds are always 2 digits |
| 152 | let formattedSeconds = seconds.toString().padStart(2, '0'); |
| 153 | |
| 154 | // Return formatted string |
| 155 | return `${minutes}:${formattedSeconds}`; |
| 156 | } |
| 157 | |
| 158 | async function setCurrentPlayingSong() { |
| 159 | const currentSongSpan = document.getElementById('current-song'); |
| 160 | const currentTrack = await getPlayingTrack(); |
| 161 | let html; |
| 162 | if (!currentTrack) { |
| 163 | html = createTemplatedHtml('no-song-playing', {}) |
| 164 | } else { |
| 165 | if (!currentTrack || !currentTrack.item) { |
| 166 | return; |
| 167 | } |
| 168 | const album = currentTrack.item.album; |
| 169 | const item = currentTrack.item; |
| 170 | const name = item.name; |
| 171 | const progressPercent = currentTrack.progress_ms / item.duration_ms * 100; |
| 172 | const images = album.images; |
| 173 | let largestImage = images[0]; |
| 174 | const albumArtUrl = largestImage.url; |
| 175 | const songUrl = item.external_urls.spotify; |
| 176 | const progress = formatDuration(currentTrack.progress_ms); |
| 177 | const totalDuration = formatDuration(item.duration_ms); |
| 178 | const params = { |
| 179 | name: name, |
| 180 | artist: album.artists[0].name, |
| 181 | album: album.name, |
| 182 | albumArtUrl: albumArtUrl, |
| 183 | songUrl: songUrl, |
| 184 | progressPercent: progressPercent, |
| 185 | progress: progress, |
| 186 | duration: totalDuration, |
| 187 | songUrl: item.external_urls.spotify, |
| 188 | albumUrl: album.external_urls.spotify, |
| 189 | artistUrl: album.artists[0].external_urls.spotify, |
| 190 | }; |
| 191 | |
| 192 | html = createTemplatedHtml('playing-song', params) |
| 193 | } |
| 194 | currentSongSpan.innerHTML = html; |
| 195 | } |
| 196 | |
| 197 | async function configureDevicesList() { |
| 198 | // get select element |
| 199 | const devicesList = document.getElementById('devices'); |
| 200 | |
| 201 | await updateDevicesList(devicesList); |
| 202 | setInterval(async () => { |
| 203 | await updateDevicesList(devicesList); |
| 204 | }, 1000 * 5); |
| 205 | devicesList.addEventListener('change', async () => { |
| 206 | const deviceId = devicesList.value; |
| 207 | saveDeviceId(deviceId); |
| 208 | if (deviceId) { |
| 209 | console.info(`Setting device to ${deviceId}`); |
| 210 | await transferPlayback(deviceId); |
| 211 | } |
| 212 | }); |
| 213 | } |
| 214 | |
| 215 | async function updateDevicesList(devicesList) { |
| 216 | // get available devices |
| 217 | const devices = await getAvailableDevices(); |
| 218 | |
| 219 | // add all devices to the select as options |
| 220 | let activeDeviceId = ""; |
| 221 | for (let i = 0; i < devices.length; i++) { |
| 222 | const device = devices[i]; |
| 223 | const ignoreDevice = device.is_restricted || device.is_private_session; |
| 224 | |
| 225 | // if the device is active, save its id |
| 226 | if (device.is_active) { |
| 227 | activeDeviceId = device.id; |
| 228 | } |
| 229 | |
| 230 | // check if the device is already in the list |
| 231 | const existingOption = devicesList.querySelector(`option[value="${device.id}"]`); |
| 232 | if (existingOption) { |
| 233 | if (ignoreDevice) { |
| 234 | // remove the device from the list |
| 235 | devicesList.removeChild(existingOption); |
| 236 | } else { |
| 237 | // update the name of the device, if it has changed |
| 238 | if (existingOption.text != device.name) { |
| 239 | existingOption.text = device.name; |
| 240 | } |
| 241 | // enable the option, if it was disabled |
| 242 | if (existingOption.hasAttribute('disabled')) { |
| 243 | existingOption.removeAttribute('disabled'); |
| 244 | } |
| 245 | } |
| 246 | } else if (!ignoreDevice) { |
| 247 | // add the device to the list |
| 248 | const option = document.createElement('option'); |
| 249 | option.value = device.id; |
| 250 | option.text = device.name; |
| 251 | devicesList.appendChild(option); |
| 252 | } |
| 253 | } |
| 254 | |
| 255 | // disable options that are no longer available |
| 256 | const options = devicesList.querySelectorAll('option'); |
| 257 | for (let i = 0; i < options.length; i++) { |
| 258 | const option = options[i]; |
| 259 | const existingDevice = devices.find(device => device.id == option.value); |
| 260 | if (!existingDevice) { |
| 261 | option.setAttribute('disabled', 'disabled'); |
| 262 | if (option.value == devicesList.value) { |
| 263 | devicesList.value = ""; |
| 264 | devicesList.dispatchEvent(new Event('change')); |
| 265 | } |
| 266 | } |
| 267 | } |
| 268 | |
| 269 | // if there is an active device, select it and save the id |
| 270 | if (activeDeviceId) { |
| 271 | devicesList.value = activeDeviceId; |
| 272 | saveDeviceId(activeDeviceId); |
| 273 | } else { |
| 274 | const savedDeviceId = getSavedDeviceId(); |
| 275 | if (savedDeviceId) { |
| 276 | devicesList.value = savedDeviceId; |
| 277 | } |
| 278 | } |
| 279 | } |
| 280 | |
| 281 | function saveDeviceId(deviceId) { |
| 282 | localStorage.setItem('active_device_id', deviceId); |
| 283 | } |
| 284 | function getSavedDeviceId() { |
| 285 | return localStorage.getItem('active_device_id'); |
| 286 | } |
| 287 | |
| 288 | function configureTimesList() { |
| 289 | timesUl = document.getElementById('times'); |
| 290 | const createLi = (minutes, seconds) => { |
| 291 | const li = document.createElement('li'); |
| 292 | const a = document.createElement('a'); |
| 293 | a.href = '#'; |
| 294 | a.onclick = () => { |
| 295 | setTime(minutes, seconds); |
| 296 | }; |
| 297 | a.innerText = `${minutes}:${seconds.toFixed(2).toString().padStart(5, '0').slice(0, 2)}`; |
| 298 | li.appendChild(a); |
| 299 | return li; |
| 300 | } |
| 301 | timesUl.appendChild(createLi(0, 30)); |
| 302 | timesUl.appendChild(createLi(1, 0)); |
| 303 | timesUl.appendChild(createLi(2, 0)); |
| 304 | timesUl.appendChild(createLi(3, 0)); |
| 305 | timesUl.appendChild(createLi(4, 0)); |
| 306 | timesUl.appendChild(createLi(5, 0)); |
| 307 | } |