grab info from your spotify playlists

fetch playlists

+2
.env.example
··· 1 + SPOTIFY_CLIENT_ID=your_client_id_here 2 + SPOTIFY_CLIENT_SECRET=your_client_secret_here
+3
.gitignore
··· 1 + node_modules/ 2 + .env 3 + *.log
+17
package.json
··· 1 + { 2 + "name": "spotify-albums", 3 + "version": "1.0.0", 4 + "type": "module", 5 + "scripts": { 6 + "playlists": "tsx scripts/playlists.ts" 7 + }, 8 + "dependencies": { 9 + "dotenv": "^16.4.5", 10 + "node-fetch": "^3.3.2" 11 + }, 12 + "devDependencies": { 13 + "@types/node": "^20.11.5", 14 + "typescript": "^5.3.3", 15 + "tsx": "^4.7.0" 16 + } 17 + }
+401
pnpm-lock.yaml
··· 1 + lockfileVersion: '9.0' 2 + 3 + settings: 4 + autoInstallPeers: true 5 + excludeLinksFromLockfile: false 6 + 7 + importers: 8 + 9 + .: 10 + dependencies: 11 + dotenv: 12 + specifier: ^16.4.5 13 + version: 16.6.1 14 + node-fetch: 15 + specifier: ^3.3.2 16 + version: 3.3.2 17 + devDependencies: 18 + '@types/node': 19 + specifier: ^20.11.5 20 + version: 20.19.27 21 + tsx: 22 + specifier: ^4.7.0 23 + version: 4.21.0 24 + typescript: 25 + specifier: ^5.3.3 26 + version: 5.9.3 27 + 28 + packages: 29 + 30 + '@esbuild/aix-ppc64@0.27.2': 31 + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} 32 + engines: {node: '>=18'} 33 + cpu: [ppc64] 34 + os: [aix] 35 + 36 + '@esbuild/android-arm64@0.27.2': 37 + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} 38 + engines: {node: '>=18'} 39 + cpu: [arm64] 40 + os: [android] 41 + 42 + '@esbuild/android-arm@0.27.2': 43 + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} 44 + engines: {node: '>=18'} 45 + cpu: [arm] 46 + os: [android] 47 + 48 + '@esbuild/android-x64@0.27.2': 49 + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} 50 + engines: {node: '>=18'} 51 + cpu: [x64] 52 + os: [android] 53 + 54 + '@esbuild/darwin-arm64@0.27.2': 55 + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} 56 + engines: {node: '>=18'} 57 + cpu: [arm64] 58 + os: [darwin] 59 + 60 + '@esbuild/darwin-x64@0.27.2': 61 + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} 62 + engines: {node: '>=18'} 63 + cpu: [x64] 64 + os: [darwin] 65 + 66 + '@esbuild/freebsd-arm64@0.27.2': 67 + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} 68 + engines: {node: '>=18'} 69 + cpu: [arm64] 70 + os: [freebsd] 71 + 72 + '@esbuild/freebsd-x64@0.27.2': 73 + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} 74 + engines: {node: '>=18'} 75 + cpu: [x64] 76 + os: [freebsd] 77 + 78 + '@esbuild/linux-arm64@0.27.2': 79 + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} 80 + engines: {node: '>=18'} 81 + cpu: [arm64] 82 + os: [linux] 83 + 84 + '@esbuild/linux-arm@0.27.2': 85 + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} 86 + engines: {node: '>=18'} 87 + cpu: [arm] 88 + os: [linux] 89 + 90 + '@esbuild/linux-ia32@0.27.2': 91 + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} 92 + engines: {node: '>=18'} 93 + cpu: [ia32] 94 + os: [linux] 95 + 96 + '@esbuild/linux-loong64@0.27.2': 97 + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} 98 + engines: {node: '>=18'} 99 + cpu: [loong64] 100 + os: [linux] 101 + 102 + '@esbuild/linux-mips64el@0.27.2': 103 + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} 104 + engines: {node: '>=18'} 105 + cpu: [mips64el] 106 + os: [linux] 107 + 108 + '@esbuild/linux-ppc64@0.27.2': 109 + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} 110 + engines: {node: '>=18'} 111 + cpu: [ppc64] 112 + os: [linux] 113 + 114 + '@esbuild/linux-riscv64@0.27.2': 115 + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} 116 + engines: {node: '>=18'} 117 + cpu: [riscv64] 118 + os: [linux] 119 + 120 + '@esbuild/linux-s390x@0.27.2': 121 + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} 122 + engines: {node: '>=18'} 123 + cpu: [s390x] 124 + os: [linux] 125 + 126 + '@esbuild/linux-x64@0.27.2': 127 + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} 128 + engines: {node: '>=18'} 129 + cpu: [x64] 130 + os: [linux] 131 + 132 + '@esbuild/netbsd-arm64@0.27.2': 133 + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} 134 + engines: {node: '>=18'} 135 + cpu: [arm64] 136 + os: [netbsd] 137 + 138 + '@esbuild/netbsd-x64@0.27.2': 139 + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} 140 + engines: {node: '>=18'} 141 + cpu: [x64] 142 + os: [netbsd] 143 + 144 + '@esbuild/openbsd-arm64@0.27.2': 145 + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} 146 + engines: {node: '>=18'} 147 + cpu: [arm64] 148 + os: [openbsd] 149 + 150 + '@esbuild/openbsd-x64@0.27.2': 151 + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} 152 + engines: {node: '>=18'} 153 + cpu: [x64] 154 + os: [openbsd] 155 + 156 + '@esbuild/openharmony-arm64@0.27.2': 157 + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} 158 + engines: {node: '>=18'} 159 + cpu: [arm64] 160 + os: [openharmony] 161 + 162 + '@esbuild/sunos-x64@0.27.2': 163 + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} 164 + engines: {node: '>=18'} 165 + cpu: [x64] 166 + os: [sunos] 167 + 168 + '@esbuild/win32-arm64@0.27.2': 169 + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} 170 + engines: {node: '>=18'} 171 + cpu: [arm64] 172 + os: [win32] 173 + 174 + '@esbuild/win32-ia32@0.27.2': 175 + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} 176 + engines: {node: '>=18'} 177 + cpu: [ia32] 178 + os: [win32] 179 + 180 + '@esbuild/win32-x64@0.27.2': 181 + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} 182 + engines: {node: '>=18'} 183 + cpu: [x64] 184 + os: [win32] 185 + 186 + '@types/node@20.19.27': 187 + resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==} 188 + 189 + data-uri-to-buffer@4.0.1: 190 + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} 191 + engines: {node: '>= 12'} 192 + 193 + dotenv@16.6.1: 194 + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} 195 + engines: {node: '>=12'} 196 + 197 + esbuild@0.27.2: 198 + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} 199 + engines: {node: '>=18'} 200 + hasBin: true 201 + 202 + fetch-blob@3.2.0: 203 + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} 204 + engines: {node: ^12.20 || >= 14.13} 205 + 206 + formdata-polyfill@4.0.10: 207 + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} 208 + engines: {node: '>=12.20.0'} 209 + 210 + fsevents@2.3.3: 211 + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 212 + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 213 + os: [darwin] 214 + 215 + get-tsconfig@4.13.0: 216 + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} 217 + 218 + node-domexception@1.0.0: 219 + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} 220 + engines: {node: '>=10.5.0'} 221 + deprecated: Use your platform's native DOMException instead 222 + 223 + node-fetch@3.3.2: 224 + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} 225 + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 226 + 227 + resolve-pkg-maps@1.0.0: 228 + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 229 + 230 + tsx@4.21.0: 231 + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} 232 + engines: {node: '>=18.0.0'} 233 + hasBin: true 234 + 235 + typescript@5.9.3: 236 + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 237 + engines: {node: '>=14.17'} 238 + hasBin: true 239 + 240 + undici-types@6.21.0: 241 + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} 242 + 243 + web-streams-polyfill@3.3.3: 244 + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} 245 + engines: {node: '>= 8'} 246 + 247 + snapshots: 248 + 249 + '@esbuild/aix-ppc64@0.27.2': 250 + optional: true 251 + 252 + '@esbuild/android-arm64@0.27.2': 253 + optional: true 254 + 255 + '@esbuild/android-arm@0.27.2': 256 + optional: true 257 + 258 + '@esbuild/android-x64@0.27.2': 259 + optional: true 260 + 261 + '@esbuild/darwin-arm64@0.27.2': 262 + optional: true 263 + 264 + '@esbuild/darwin-x64@0.27.2': 265 + optional: true 266 + 267 + '@esbuild/freebsd-arm64@0.27.2': 268 + optional: true 269 + 270 + '@esbuild/freebsd-x64@0.27.2': 271 + optional: true 272 + 273 + '@esbuild/linux-arm64@0.27.2': 274 + optional: true 275 + 276 + '@esbuild/linux-arm@0.27.2': 277 + optional: true 278 + 279 + '@esbuild/linux-ia32@0.27.2': 280 + optional: true 281 + 282 + '@esbuild/linux-loong64@0.27.2': 283 + optional: true 284 + 285 + '@esbuild/linux-mips64el@0.27.2': 286 + optional: true 287 + 288 + '@esbuild/linux-ppc64@0.27.2': 289 + optional: true 290 + 291 + '@esbuild/linux-riscv64@0.27.2': 292 + optional: true 293 + 294 + '@esbuild/linux-s390x@0.27.2': 295 + optional: true 296 + 297 + '@esbuild/linux-x64@0.27.2': 298 + optional: true 299 + 300 + '@esbuild/netbsd-arm64@0.27.2': 301 + optional: true 302 + 303 + '@esbuild/netbsd-x64@0.27.2': 304 + optional: true 305 + 306 + '@esbuild/openbsd-arm64@0.27.2': 307 + optional: true 308 + 309 + '@esbuild/openbsd-x64@0.27.2': 310 + optional: true 311 + 312 + '@esbuild/openharmony-arm64@0.27.2': 313 + optional: true 314 + 315 + '@esbuild/sunos-x64@0.27.2': 316 + optional: true 317 + 318 + '@esbuild/win32-arm64@0.27.2': 319 + optional: true 320 + 321 + '@esbuild/win32-ia32@0.27.2': 322 + optional: true 323 + 324 + '@esbuild/win32-x64@0.27.2': 325 + optional: true 326 + 327 + '@types/node@20.19.27': 328 + dependencies: 329 + undici-types: 6.21.0 330 + 331 + data-uri-to-buffer@4.0.1: {} 332 + 333 + dotenv@16.6.1: {} 334 + 335 + esbuild@0.27.2: 336 + optionalDependencies: 337 + '@esbuild/aix-ppc64': 0.27.2 338 + '@esbuild/android-arm': 0.27.2 339 + '@esbuild/android-arm64': 0.27.2 340 + '@esbuild/android-x64': 0.27.2 341 + '@esbuild/darwin-arm64': 0.27.2 342 + '@esbuild/darwin-x64': 0.27.2 343 + '@esbuild/freebsd-arm64': 0.27.2 344 + '@esbuild/freebsd-x64': 0.27.2 345 + '@esbuild/linux-arm': 0.27.2 346 + '@esbuild/linux-arm64': 0.27.2 347 + '@esbuild/linux-ia32': 0.27.2 348 + '@esbuild/linux-loong64': 0.27.2 349 + '@esbuild/linux-mips64el': 0.27.2 350 + '@esbuild/linux-ppc64': 0.27.2 351 + '@esbuild/linux-riscv64': 0.27.2 352 + '@esbuild/linux-s390x': 0.27.2 353 + '@esbuild/linux-x64': 0.27.2 354 + '@esbuild/netbsd-arm64': 0.27.2 355 + '@esbuild/netbsd-x64': 0.27.2 356 + '@esbuild/openbsd-arm64': 0.27.2 357 + '@esbuild/openbsd-x64': 0.27.2 358 + '@esbuild/openharmony-arm64': 0.27.2 359 + '@esbuild/sunos-x64': 0.27.2 360 + '@esbuild/win32-arm64': 0.27.2 361 + '@esbuild/win32-ia32': 0.27.2 362 + '@esbuild/win32-x64': 0.27.2 363 + 364 + fetch-blob@3.2.0: 365 + dependencies: 366 + node-domexception: 1.0.0 367 + web-streams-polyfill: 3.3.3 368 + 369 + formdata-polyfill@4.0.10: 370 + dependencies: 371 + fetch-blob: 3.2.0 372 + 373 + fsevents@2.3.3: 374 + optional: true 375 + 376 + get-tsconfig@4.13.0: 377 + dependencies: 378 + resolve-pkg-maps: 1.0.0 379 + 380 + node-domexception@1.0.0: {} 381 + 382 + node-fetch@3.3.2: 383 + dependencies: 384 + data-uri-to-buffer: 4.0.1 385 + fetch-blob: 3.2.0 386 + formdata-polyfill: 4.0.10 387 + 388 + resolve-pkg-maps@1.0.0: {} 389 + 390 + tsx@4.21.0: 391 + dependencies: 392 + esbuild: 0.27.2 393 + get-tsconfig: 4.13.0 394 + optionalDependencies: 395 + fsevents: 2.3.3 396 + 397 + typescript@5.9.3: {} 398 + 399 + undici-types@6.21.0: {} 400 + 401 + web-streams-polyfill@3.3.3: {}
+140
scripts/playlists.ts
··· 1 + import dotenv from 'dotenv'; 2 + import fetch from 'node-fetch'; 3 + import readline from 'readline'; 4 + import { createServer } from 'http'; 5 + import { writeFile, mkdir } from 'fs/promises'; 6 + import { join } from 'path'; 7 + 8 + dotenv.config(); 9 + 10 + interface SpotifyPlaylist { 11 + id: string; 12 + name: string; 13 + tracks: { 14 + total: number; 15 + }; 16 + } 17 + 18 + interface PlaylistsResponse { 19 + items: SpotifyPlaylist[]; 20 + next: string | null; 21 + } 22 + 23 + interface TokenResponse { 24 + access_token: string; 25 + refresh_token: string; 26 + expires_in: number; 27 + } 28 + 29 + interface SpotifyTrack { 30 + track: { 31 + name: string; 32 + artists: { name: string }[]; 33 + album: { name: string }; 34 + }; 35 + } 36 + 37 + interface TracksResponse { 38 + items: SpotifyTrack[]; 39 + next: string | null; 40 + } 41 + 42 + const PORT = 8888; 43 + const REDIRECT_URI = `http://127.0.0.1:${PORT}/callback`; 44 + const SCOPES = 'playlist-read-private user-library-read'; 45 + const OUTPUT_DIR = 'playlists'; 46 + 47 + function getAuthUrl(): string { 48 + const params = new URLSearchParams({ 49 + client_id: process.env.SPOTIFY_CLIENT_ID!, 50 + response_type: 'code', 51 + redirect_uri: REDIRECT_URI, 52 + scope: SCOPES 53 + }); 54 + return `https://accounts.spotify.com/authorize?${params}`; 55 + } 56 + 57 + async function getAccessToken(code: string): Promise<TokenResponse> { 58 + const response = await fetch('https://accounts.spotify.com/api/token', { 59 + method: 'POST', 60 + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 61 + body: new URLSearchParams({ 62 + grant_type: 'authorization_code', 63 + code, 64 + redirect_uri: REDIRECT_URI, 65 + client_id: process.env.SPOTIFY_CLIENT_ID!, 66 + client_secret: process.env.SPOTIFY_CLIENT_SECRET! 67 + }) 68 + }); 69 + 70 + if (!response.ok) { 71 + throw new Error(`Failed to get access token: ${response.statusText}`); 72 + } 73 + 74 + return await response.json() as TokenResponse; 75 + } 76 + 77 + async function getAllPlaylists(token: string): Promise<SpotifyPlaylist[]> { 78 + let url: string | null = 'https://api.spotify.com/v1/me/playlists?limit=50'; 79 + const playlists: SpotifyPlaylist[] = []; 80 + 81 + while (url) { 82 + const response = await fetch(url, { 83 + headers: { 'Authorization': `Bearer ${token}` } 84 + }); 85 + 86 + if (!response.ok) { 87 + throw new Error(`Failed to fetch playlists: ${response.statusText}`); 88 + } 89 + 90 + const data = await response.json() as PlaylistsResponse; 91 + playlists.push(...data.items); 92 + url = data.next; 93 + } 94 + 95 + return playlists; 96 + } 97 + 98 + function waitForAuthCode(): Promise<string> { 99 + return new Promise((resolve, reject) => { 100 + const server = createServer((req, res) => { 101 + const url = new URL(req.url!, `http://${req.headers.host}`); 102 + const code = url.searchParams.get('code'); 103 + 104 + if (code) { 105 + res.writeHead(200, { 'Content-Type': 'text/html' }); 106 + res.end('<h1>Authentication successful! You can close this window.</h1>'); 107 + server.close(); 108 + resolve(code); 109 + } else { 110 + const error = url.searchParams.get('error'); 111 + res.writeHead(400, { 'Content-Type': 'text/html' }); 112 + res.end(`<h1>Authentication failed: ${error}</h1>`); 113 + server.close(); 114 + reject(new Error(`Authentication failed: ${error}`)); 115 + } 116 + }); 117 + 118 + server.listen(PORT, () => { 119 + console.log(`Server listening on port ${PORT}`); 120 + }); 121 + }); 122 + } 123 + 124 + async function main() { 125 + try { 126 + console.log('Opening browser for authentication...'); 127 + console.log(getAuthUrl()); 128 + 129 + const code = await waitForAuthCode(); 130 + const { access_token } = await getAccessToken(code); 131 + 132 + const playlists = await getAllPlaylists(access_token); 133 + console.log(`\nFound ${playlists.length} playlists:`); 134 + playlists.forEach(p => console.log(`- ${p.name} (${p.tracks.total} tracks)`)); 135 + } catch (error) { 136 + console.error('Error:', (error as Error).message); 137 + } 138 + } 139 + 140 + main();
+12
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "ES2022", 5 + "moduleResolution": "node", 6 + "strict": true, 7 + "esModuleInterop": true, 8 + "skipLibCheck": true, 9 + "forceConsistentCasingInFileNames": true, 10 + "resolveJsonModule": true 11 + } 12 + }