hawk / files / public / js / controls.js
controls.js
2c1835af 2023-10-19 Pavel Safronov | 1 | let pageStatus = { |
| 2 | text: '', | |
| 3 | isError: false | |
| 4 | }; | |
a95a53d1 2023-10-17 Pavel Safronov | 5 | setInterval(() => { |
| 6 | const statusDiv = document.getElementById('status'); | |
2c1835af 2023-10-19 Pavel Safronov | 7 | statusDiv.innerText = pageStatus.text; |
| 8 | if (pageStatus.isError) { | |
| 9 | statusDiv.classList.add('error'); | |
| 10 | } else { | |
| 11 | statusDiv.classList.remove('error'); | |
| 12 | } | |
a95a53d1 2023-10-17 Pavel Safronov | 13 | }, 100); |
| 14 | ||
2c1835af 2023-10-19 Pavel Safronov | 15 | function reportStatus(text, isError = false) { |
| 16 | pageStatus.text = text; | |
| 17 | pageStatus.isError = isError; | |
a95a53d1 2023-10-17 Pavel Safronov | 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) { | |
2c1835af 2023-10-19 Pavel Safronov | 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 | ||
a95a53d1 2023-10-17 Pavel Safronov | 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 | ||
660845f9 2023-10-20 Pavel Safronov | 52 | function setTime(minutes, seconds) { |
| 53 | document.getElementById('minutes-input').value = minutes; | |
| 54 | document.getElementById('seconds-input').value = seconds; | |
| 55 | } | |
| 56 | ||
a95a53d1 2023-10-17 Pavel Safronov | 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 => { | |
c1610592 2023-10-17 Pavel Safronov | 70 | const album = track.album; |
| 71 | const images = album.images; | |
bd4c370c 2023-10-20 Pavel Safronov | 72 | let largestImage = images[0]; |
| 73 | const albumArtUrl = largestImage.url; | |
1e82c571 2023-10-17 Pavel Safronov | 74 | const params = { |
a95a53d1 2023-10-17 Pavel Safronov | 75 | name: track.name, |
c1610592 2023-10-17 Pavel Safronov | 76 | album: track.album.name, |
a95a53d1 2023-10-17 Pavel Safronov | 77 | artist: track.artists[0].name, |
| 78 | duration: formatDuration(track.duration_ms), | |
c1610592 2023-10-17 Pavel Safronov | 79 | albumArtUrl: albumArtUrl, |
0c3c57b6 2023-10-18 Pavel Safronov | 80 | songUrl: track.external_urls.spotify, |
| 81 | albumUrl: album.external_urls.spotify, | |
| 82 | artistUrl: track.artists[0].external_urls.spotify, | |
a95a53d1 2023-10-17 Pavel Safronov | 83 | trackId: track.id, |
| 84 | trackHref: track.external_urls.spotify | |
1e82c571 2023-10-17 Pavel Safronov | 85 | }; |
| 86 | ||
| 87 | const html = createTemplatedHtml('found-song-item', params); | |
a95a53d1 2023-10-17 Pavel Safronov | 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) { | |
1e82c571 2023-10-17 Pavel Safronov | 163 | html = createTemplatedHtml('no-song-playing', {}) |
a95a53d1 2023-10-17 Pavel Safronov | 164 | } else { |
660845f9 2023-10-20 Pavel Safronov | 165 | if (!currentTrack || !currentTrack.item) { |
| 166 | return; | |
| 167 | } | |
a95a53d1 2023-10-17 Pavel Safronov | 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; | |
bd4c370c 2023-10-20 Pavel Safronov | 173 | let largestImage = images[0]; |
| 174 | const albumArtUrl = largestImage.url; | |
a95a53d1 2023-10-17 Pavel Safronov | 175 | const songUrl = item.external_urls.spotify; |
| 176 | const progress = formatDuration(currentTrack.progress_ms); | |
| 177 | const totalDuration = formatDuration(item.duration_ms); | |
1e82c571 2023-10-17 Pavel Safronov | 178 | const params = { |
a95a53d1 2023-10-17 Pavel Safronov | 179 | name: name, |
| 180 | artist: album.artists[0].name, | |
c1610592 2023-10-17 Pavel Safronov | 181 | album: album.name, |
a95a53d1 2023-10-17 Pavel Safronov | 182 | albumArtUrl: albumArtUrl, |
| 183 | songUrl: songUrl, | |
| 184 | progressPercent: progressPercent, | |
| 185 | progress: progress, | |
0c3c57b6 2023-10-18 Pavel Safronov | 186 | duration: totalDuration, |
| 187 | songUrl: item.external_urls.spotify, | |
| 188 | albumUrl: album.external_urls.spotify, | |
| 189 | artistUrl: album.artists[0].external_urls.spotify, | |
1e82c571 2023-10-17 Pavel Safronov | 190 | }; |
| 191 | ||
| 192 | html = createTemplatedHtml('playing-song', params) | |
a95a53d1 2023-10-17 Pavel Safronov | 193 | } |
| 194 | currentSongSpan.innerHTML = html; | |
| 195 | } | |
2c1835af 2023-10-19 Pavel Safronov | 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 | } | |
660845f9 2023-10-20 Pavel Safronov | 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 | } |