+2
.env.example
+2
.env.example
+17
package.json
+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
+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
+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();