hawk / files / public / js / api.js
api.js
6e3f55a5 2023-10-13 Pavel Safronov | 1 | const apiExpiry = 3600 * 1000; // 1 hour |
| 2 | ||
e39bdb17 2023-10-14 Pavel Safronov | 3 | function createCallbackState(processedCount, totalCount) { |
| 4 | return { | |
| 5 | processedCount: processedCount, | |
| 6 | totalCount: totalCount | |
| 7 | }; | |
| 8 | } | |
| 9 | ||
8c91be32 2023-10-13 Pavel Safronov | 10 | async function getAlbums(statusCallback) { |
6e3f55a5 2023-10-13 Pavel Safronov | 11 | const db = await openDb(); |
| 12 | const cacheKey = 'albums'; | |
4d32aa41 2023-10-13 Pavel Safronov | 13 | const cachedAlbums = await getWithExpiry(db, cacheKey); |
| 14 | if (cachedAlbums) { | |
8c91be32 2023-10-13 Pavel Safronov | 15 | if (statusCallback) { |
e39bdb17 2023-10-14 Pavel Safronov | 16 | statusCallback(createCallbackState(cachedAlbums.length, cachedAlbums.length)) |
8c91be32 2023-10-13 Pavel Safronov | 17 | } |
4d32aa41 2023-10-13 Pavel Safronov | 18 | return cachedAlbums; |
6e3f55a5 2023-10-13 Pavel Safronov | 19 | } |
| 20 | ||
cc1af884 2023-10-13 Pavel Safronov | 21 | const accessToken = await getAccessToken(); |
4d32aa41 2023-10-13 Pavel Safronov | 22 | const albums = []; |
| 23 | let url = 'https://api.spotify.com/v1/me/albums?limit=50'; | |
| 24 | while (url) { | |
| 25 | const response = await fetch(url, { | |
| 26 | headers: { | |
| 27 | Authorization: 'Bearer ' + accessToken | |
| 28 | } | |
| 29 | }); | |
| 30 | const data = await response.json(); | |
| 31 | albums.push(...data.items); | |
8c91be32 2023-10-13 Pavel Safronov | 32 | if (statusCallback) { |
e39bdb17 2023-10-14 Pavel Safronov | 33 | statusCallback(createCallbackState(albums.length, data.total)) |
8c91be32 2023-10-13 Pavel Safronov | 34 | } |
4d32aa41 2023-10-13 Pavel Safronov | 35 | url = data.next; |
| 36 | } | |
601035fb 2023-10-12 Pavel Safronov | 37 | |
4d32aa41 2023-10-13 Pavel Safronov | 38 | await setWithExpiry(db, cacheKey, albums, apiExpiry); |
| 39 | return albums; | |
8edcc887 2023-10-13 Pavel Safronov | 40 | } |
| 41 | ||
6afcc473 2023-10-14 Pavel Safronov | 42 | async function batchGetTracks(albumIds, statusCallback) { |
| 43 | const batchSize = 20 | |
| 44 | if (albumIds.length == 0) { | |
| 45 | return []; | |
| 46 | } | |
| 47 | if (albumIds.length > batchSize) { | |
| 48 | const allTracks = [] | |
| 49 | while (albumIds.length > 0) { | |
| 50 | const batch = albumIds.splice(0, batchSize); | |
| 51 | albumIds = albumIds.splice(batchSize); | |
| 52 | const tracks = await batchGetTracks(batch, statusCallback); | |
| 53 | allTracks.push(...tracks); | |
| 54 | } | |
| 55 | return allTracks; | |
| 56 | } | |
| 57 | ||
| 58 | const db = await openDb(); | |
| 59 | const cacheKey = `tracks-${albumIds.join('-')}`; | |
| 60 | const cachedTracks = await getWithExpiry(db, cacheKey); | |
| 61 | if (cachedTracks) { | |
| 62 | if (statusCallback) { | |
| 63 | statusCallback(createCallbackState(cachedTracks.length, cachedTracks.length)) | |
| 64 | } | |
| 65 | return cachedTracks; | |
| 66 | } | |
| 67 | ||
| 68 | const tracks = []; | |
| 69 | const accessToken = await getAccessToken(); | |
| 70 | let url = `https://api.spotify.com/v1/albums?ids=${albumIds.join(',')}&market=from_token`; | |
| 71 | while (url) { | |
| 72 | const response = await fetch(url, { | |
| 73 | headers: { | |
| 74 | Authorization: 'Bearer ' + accessToken | |
| 75 | } | |
| 76 | }); | |
| 77 | const data = await response.json(); | |
| 78 | data.albums.forEach(album => { | |
c1610592 2023-10-17 Pavel Safronov | 79 | // combine the album data with the track data |
| 80 | tracks.push(...album.tracks.items.map(track => { | |
| 81 | track.album = { | |
| 82 | id: album.id, | |
| 83 | name: album.name, | |
0c3c57b6 2023-10-18 Pavel Safronov | 84 | images: album.images, |
| 85 | external_urls: album.external_urls | |
c1610592 2023-10-17 Pavel Safronov | 86 | } |
| 87 | return track; | |
| 88 | })); | |
6afcc473 2023-10-14 Pavel Safronov | 89 | }); |
| 90 | if (statusCallback) { | |
| 91 | statusCallback(createCallbackState(tracks.length, tracks.length)) | |
| 92 | } | |
| 93 | url = data.next; | |
| 94 | } | |
| 95 | ||
| 96 | await setWithExpiry(db, cacheKey, tracks, apiExpiry); | |
| 97 | return tracks; | |
| 98 | } | |
| 99 | ||
8c91be32 2023-10-13 Pavel Safronov | 100 | async function getTracks(albumId, statusCallback) { |
6e3f55a5 2023-10-13 Pavel Safronov | 101 | const db = await openDb(); |
| 102 | const cacheKey = `tracks-${albumId}`; | |
4d32aa41 2023-10-13 Pavel Safronov | 103 | const cachedTracks = await getWithExpiry(db, cacheKey); |
| 104 | if (cachedTracks) { | |
8c91be32 2023-10-13 Pavel Safronov | 105 | if (statusCallback) { |
e39bdb17 2023-10-14 Pavel Safronov | 106 | statusCallback(createCallbackState(cachedTracks.length, cachedTracks.length)) |
8c91be32 2023-10-13 Pavel Safronov | 107 | } |
4d32aa41 2023-10-13 Pavel Safronov | 108 | return cachedTracks; |
6e3f55a5 2023-10-13 Pavel Safronov | 109 | } |
| 110 | ||
4d32aa41 2023-10-13 Pavel Safronov | 111 | const tracks = []; |
cc1af884 2023-10-13 Pavel Safronov | 112 | const accessToken = await getAccessToken(); |
4d32aa41 2023-10-13 Pavel Safronov | 113 | let url = `https://api.spotify.com/v1/albums/${albumId}/tracks?limit=50`; |
| 114 | while (url) { | |
| 115 | const response = await fetch(url, { | |
| 116 | headers: { | |
| 117 | Authorization: 'Bearer ' + accessToken | |
| 118 | } | |
| 119 | }); | |
| 120 | const data = await response.json(); | |
| 121 | tracks.push(...data.items); | |
8c91be32 2023-10-13 Pavel Safronov | 122 | if (statusCallback) { |
e39bdb17 2023-10-14 Pavel Safronov | 123 | statusCallback(createCallbackState(tracks.length, data.total)) |
8c91be32 2023-10-13 Pavel Safronov | 124 | } |
4d32aa41 2023-10-13 Pavel Safronov | 125 | url = data.next; |
| 126 | } | |
601035fb 2023-10-12 Pavel Safronov | 127 | |
4d32aa41 2023-10-13 Pavel Safronov | 128 | await setWithExpiry(db, cacheKey, tracks, apiExpiry); |
| 129 | return tracks; | |
8edcc887 2023-10-13 Pavel Safronov | 130 | } |
| 131 | ||
| 132 | async function playTrack(trackId) { | |
3bf5fcbb 2023-10-13 Pavel Safronov | 133 | let accessToken = await getAccessToken(); |
8edcc887 2023-10-13 Pavel Safronov | 134 | console.info(`Playing track ${trackId}`); |
0c3c57b6 2023-10-18 Pavel Safronov | 135 | const response = await fetch(`https://api.spotify.com/v1/me/player/play`, { |
8edcc887 2023-10-13 Pavel Safronov | 136 | method: 'PUT', |
| 137 | headers: { | |
| 138 | Authorization: 'Bearer ' + accessToken | |
| 139 | }, | |
| 140 | body: JSON.stringify({ | |
| 141 | uris: [`spotify:track:${trackId}`] | |
| 142 | }) | |
| 143 | }); | |
0c3c57b6 2023-10-18 Pavel Safronov | 144 | if (response.status != 204) { |
| 145 | const json = await response.json(); | |
| 146 | throw new Error(`Failed to play track ${trackId}: ${response.status}\n${JSON.stringify(json)}`); | |
| 147 | } | |
8edcc887 2023-10-13 Pavel Safronov | 148 | } |
cc1af884 2023-10-13 Pavel Safronov | 149 | |
| 150 | async function getPlayingTrack() { | |
| 151 | const accessToken = await getAccessToken(); | |
| 152 | const response = await fetch("https://api.spotify.com/v1/me/player", { | |
| 153 | headers: { | |
| 154 | Authorization: 'Bearer ' + accessToken | |
| 155 | } | |
| 156 | }); | |
| 157 | ||
4d32aa41 2023-10-13 Pavel Safronov | 158 | if (response.status == 204) { |
| 159 | return null; | |
| 160 | } | |
| 161 | ||
cc1af884 2023-10-13 Pavel Safronov | 162 | const data = await response.json(); |
| 163 | return data; | |
| 164 | } | |
| 165 | ||
| 166 | async function play() { | |
| 167 | const accessToken = await getAccessToken(); | |
| 168 | await fetch("https://api.spotify.com/v1/me/player/play", { | |
| 169 | method: "PUT", | |
| 170 | headers: { | |
| 171 | Authorization: 'Bearer ' + accessToken | |
| 172 | } | |
| 173 | }); | |
| 174 | } | |
| 175 | ||
| 176 | async function pause() { | |
| 177 | const accessToken = await getAccessToken(); | |
| 178 | await fetch("https://api.spotify.com/v1/me/player/pause", { | |
| 179 | method: "PUT", | |
| 180 | headers: { | |
| 181 | Authorization: 'Bearer ' + accessToken | |
| 182 | } | |
| 183 | }); | |
8c91be32 2023-10-13 Pavel Safronov | 184 | } |
2c1835af 2023-10-19 Pavel Safronov | 185 | |
| 186 | async function getAvailableDevices() { | |
| 187 | const accessToken = await getAccessToken(); | |
| 188 | const response = await fetch("https://api.spotify.com/v1/me/player/devices", { | |
| 189 | headers: { | |
| 190 | Authorization: 'Bearer ' + accessToken | |
| 191 | } | |
| 192 | }); | |
| 193 | const data = await response.json(); | |
| 194 | return data.devices; | |
| 195 | } | |
| 196 | ||
| 197 | async function transferPlayback(deviceId) { | |
| 198 | const accessToken = await getAccessToken(); | |
| 199 | const response = await fetch("https://api.spotify.com/v1/me/player", { | |
| 200 | method: "PUT", | |
| 201 | headers: { | |
| 202 | Authorization: 'Bearer ' + accessToken | |
| 203 | }, | |
| 204 | body: JSON.stringify({ | |
| 205 | device_ids: [deviceId] | |
| 206 | }) | |
| 207 | }); | |
| 208 | if (response.status != 204) { | |
| 209 | const json = await response.json(); | |
| 210 | throw new Error(`Failed to transfer playback to ${deviceId}: ${response.status}\n${JSON.stringify(json)}`); | |
| 211 | } | |
| 212 | } |