Import your Last.fm and Spotify listening history to the AT Protocol network using the fm.teal.alpha.feed.play lexicon.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore(deps): upgrade atproto stack and bump runtime deps

Update @atproto/api to 0.18, bump ora to v9 (Node 20+), refresh CSV parsing and related transitive dependencies.

+111 -96
+3 -3
README.md
··· 310 310 - **trackName**: The name of the track 311 311 - **artists**: Array of artist objects (requires `artistName`, optional `artistMbId` for Last.fm) 312 312 - **playedTime**: ISO 8601 timestamp of when you listened 313 - - **submissionClientAgent**: Identifies this importer (`lastfm-importer/v0.5.0`) 313 + - **submissionClientAgent**: Identifies this importer (`lastfm-importer/v0.6.0`) 314 314 - **musicServiceBaseDomain**: Set to `last.fm` or `spotify.com` depending on source 315 315 316 316 ### Optional Fields (when available) ··· 337 337 "recordingMbId": "3a390ad3-fe56-45f2-a073-bebc45d6bde1", 338 338 "playedTime": "2025-11-13T23:49:36Z", 339 339 "originUrl": "https://www.last.fm/music/Cjbeards/_/Paint+My+Masterpiece", 340 - "submissionClientAgent": "lastfm-importer/v0.5.0", 340 + "submissionClientAgent": "lastfm-importer/v0.6.0", 341 341 "musicServiceBaseDomain": "last.fm" 342 342 } 343 343 ``` ··· 355 355 "releaseName": "Twenty", 356 356 "playedTime": "2021-09-09T10:34:08Z", 357 357 "originUrl": "https://open.spotify.com/track/3gZqDJkMZipOYCRjlHWgOV", 358 - "submissionClientAgent": "lastfm-importer/v0.5.0", 358 + "submissionClientAgent": "lastfm-importer/v0.6.0", 359 359 "musicServiceBaseDomain": "spotify.com" 360 360 } 361 361 ```
+102 -87
package-lock.json
··· 1 1 { 2 2 "name": "lastfm-importer", 3 - "version": "0.5.0", 3 + "version": "0.6.0", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 8 "name": "lastfm-importer", 9 - "version": "0.5.0", 9 + "version": "0.6.0", 10 10 "license": "AGPL-3.0-only", 11 11 "dependencies": { 12 - "@atproto/api": "^0.13.35", 12 + "@atproto/api": "^0.18.13", 13 13 "chalk": "^5.6.2", 14 14 "cli-progress": "^3.12.0", 15 - "csv-parse": "^5.6.0", 16 - "ora": "^8.2.0" 15 + "csv-parse": "^6.1.0", 16 + "ora": "^9.0.0" 17 17 }, 18 18 "bin": { 19 19 "lastfm-import": "dist/index.js" ··· 24 24 } 25 25 }, 26 26 "node_modules/@atproto/api": { 27 - "version": "0.13.35", 28 - "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.13.35.tgz", 29 - "integrity": "sha512-vsEfBj0C333TLjDppvTdTE0IdKlXuljKSveAeI4PPx/l6eUKNnDTsYxvILtXUVzwUlTDmSRqy5O4Ryh78n1b7g==", 27 + "version": "0.18.13", 28 + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.18.13.tgz", 29 + "integrity": "sha512-CULZ01pSJDltLS/Gc9MMrhFzB6OM3ezyZw7KoeLT/sBfwgA1ddA4mWdTh7DIRosPRigXtA05bnoiCutZbQDo+Q==", 30 30 "license": "MIT", 31 31 "dependencies": { 32 - "@atproto/common-web": "^0.5.0", 33 - "@atproto/lexicon": "^0.4.6", 34 - "@atproto/syntax": "^0.3.2", 35 - "@atproto/xrpc": "^0.6.8", 32 + "@atproto/common-web": "^0.4.11", 33 + "@atproto/lexicon": "^0.6.0", 34 + "@atproto/syntax": "^0.4.2", 35 + "@atproto/xrpc": "^0.7.7", 36 36 "await-lock": "^2.2.2", 37 37 "multiformats": "^9.9.0", 38 38 "tlds": "^1.234.0", ··· 40 40 } 41 41 }, 42 42 "node_modules/@atproto/common-web": { 43 - "version": "0.4.3", 44 - "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.3.tgz", 45 - "integrity": "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==", 43 + "version": "0.4.11", 44 + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.11.tgz", 45 + "integrity": "sha512-VHejNmSABU8/03VrQ3e36AmT5U3UIeio+qSUqCrO1oNgrJcWfGy1rpj0FVtUugWF8Un29+yzkukzWGZfXL70rQ==", 46 46 "license": "MIT", 47 47 "dependencies": { 48 - "graphemer": "^1.4.0", 48 + "@atproto/lex-data": "0.0.7", 49 + "@atproto/lex-json": "0.0.7", 50 + "zod": "^3.23.8" 51 + } 52 + }, 53 + "node_modules/@atproto/lex-data": { 54 + "version": "0.0.7", 55 + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.7.tgz", 56 + "integrity": "sha512-W/Q5o9o7n2Sv3UywckChu01X5lwQUtaiiOkGJLnRsdkQTyC6813nPgY+p2sG7NwwM+82lu+FUV9fE/Ul3VqaJw==", 57 + "license": "MIT", 58 + "dependencies": { 59 + "@atproto/syntax": "0.4.2", 49 60 "multiformats": "^9.9.0", 61 + "tslib": "^2.8.1", 50 62 "uint8arrays": "3.0.0", 51 - "zod": "^3.23.8" 63 + "unicode-segmenter": "^0.14.0" 64 + } 65 + }, 66 + "node_modules/@atproto/lex-json": { 67 + "version": "0.0.7", 68 + "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.7.tgz", 69 + "integrity": "sha512-bjNPD5M/MhLfjNM7tcxuls80UgXpHqxdOxDXEUouAtZQV/nIDhGjmNUvKxOmOgnDsiZRnT2g5y3onrnjH3a44g==", 70 + "license": "MIT", 71 + "dependencies": { 72 + "@atproto/lex-data": "0.0.7", 73 + "tslib": "^2.8.1" 52 74 } 53 75 }, 54 76 "node_modules/@atproto/lexicon": { 55 - "version": "0.4.14", 56 - "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.14.tgz", 57 - "integrity": "sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ==", 77 + "version": "0.6.0", 78 + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.0.tgz", 79 + "integrity": "sha512-5veb8aD+J5M0qszLJ+73KSFsFrJBgAY/nM1TSAJvGY7fNc9ZAT+PSUlmIyrdye9YznAZ07yktalls/TwNV7cHQ==", 58 80 "license": "MIT", 59 81 "dependencies": { 60 - "@atproto/common-web": "^0.4.2", 61 - "@atproto/syntax": "^0.5.0", 82 + "@atproto/common-web": "^0.4.7", 83 + "@atproto/syntax": "^0.4.2", 62 84 "iso-datestring-validator": "^2.2.2", 63 85 "multiformats": "^9.9.0", 64 86 "zod": "^3.23.8" 65 87 } 66 88 }, 67 - "node_modules/@atproto/lexicon/node_modules/@atproto/syntax": { 68 - "version": "0.4.1", 69 - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz", 70 - "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==", 71 - "license": "MIT" 72 - }, 73 89 "node_modules/@atproto/syntax": { 74 - "version": "0.3.4", 75 - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.4.tgz", 76 - "integrity": "sha512-8CNmi5DipOLaVeSMPggMe7FCksVag0aO6XZy9WflbduTKM4dFZVCs4686UeMLfGRXX+X966XgwECHoLYrovMMg==", 90 + "version": "0.4.2", 91 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", 92 + "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", 77 93 "license": "MIT" 78 94 }, 79 95 "node_modules/@atproto/xrpc": { 80 - "version": "0.6.12", 81 - "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.6.12.tgz", 82 - "integrity": "sha512-Ut3iISNLujlmY9Gu8sNU+SPDJDvqlVzWddU8qUr0Yae5oD4SguaUFjjhireMGhQ3M5E0KljQgDbTmnBo1kIZ3w==", 96 + "version": "0.7.7", 97 + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.7.tgz", 98 + "integrity": "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==", 83 99 "license": "MIT", 84 100 "dependencies": { 85 - "@atproto/lexicon": "^0.4.10", 101 + "@atproto/lexicon": "^0.6.0", 86 102 "zod": "^3.23.8" 87 103 } 88 104 }, ··· 154 170 } 155 171 }, 156 172 "node_modules/cli-spinners": { 157 - "version": "2.9.2", 158 - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", 159 - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", 173 + "version": "3.3.0", 174 + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.3.0.tgz", 175 + "integrity": "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ==", 160 176 "license": "MIT", 161 177 "engines": { 162 - "node": ">=6" 178 + "node": ">=18.20" 163 179 }, 164 180 "funding": { 165 181 "url": "https://github.com/sponsors/sindresorhus" 166 182 } 167 183 }, 168 184 "node_modules/csv-parse": { 169 - "version": "5.6.0", 170 - "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.6.0.tgz", 171 - "integrity": "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==", 185 + "version": "6.1.0", 186 + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-6.1.0.tgz", 187 + "integrity": "sha512-CEE+jwpgLn+MmtCpVcPtiCZpVtB6Z2OKPTr34pycYYoL7sxdOkXDdQ4lRiw6ioC0q6BLqhc6cKweCVvral8yhw==", 172 188 "license": "MIT" 173 189 }, 174 190 "node_modules/emoji-regex": { ··· 188 204 "funding": { 189 205 "url": "https://github.com/sponsors/sindresorhus" 190 206 } 191 - }, 192 - "node_modules/graphemer": { 193 - "version": "1.4.0", 194 - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", 195 - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", 196 - "license": "MIT" 197 207 }, 198 208 "node_modules/is-fullwidth-code-point": { 199 209 "version": "3.0.0", ··· 235 245 "license": "MIT" 236 246 }, 237 247 "node_modules/log-symbols": { 238 - "version": "6.0.0", 239 - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", 240 - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", 248 + "version": "7.0.1", 249 + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", 250 + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", 241 251 "license": "MIT", 242 252 "dependencies": { 243 - "chalk": "^5.3.0", 244 - "is-unicode-supported": "^1.3.0" 253 + "is-unicode-supported": "^2.0.0", 254 + "yoctocolors": "^2.1.1" 245 255 }, 246 256 "engines": { 247 257 "node": ">=18" ··· 250 260 "url": "https://github.com/sponsors/sindresorhus" 251 261 } 252 262 }, 253 - "node_modules/log-symbols/node_modules/is-unicode-supported": { 254 - "version": "1.3.0", 255 - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", 256 - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", 257 - "license": "MIT", 258 - "engines": { 259 - "node": ">=12" 260 - }, 261 - "funding": { 262 - "url": "https://github.com/sponsors/sindresorhus" 263 - } 264 - }, 265 263 "node_modules/mimic-function": { 266 264 "version": "5.0.1", 267 265 "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", ··· 296 294 } 297 295 }, 298 296 "node_modules/ora": { 299 - "version": "8.2.0", 300 - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", 301 - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", 297 + "version": "9.0.0", 298 + "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz", 299 + "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==", 302 300 "license": "MIT", 303 301 "dependencies": { 304 - "chalk": "^5.3.0", 302 + "chalk": "^5.6.2", 305 303 "cli-cursor": "^5.0.0", 306 - "cli-spinners": "^2.9.2", 304 + "cli-spinners": "^3.2.0", 307 305 "is-interactive": "^2.0.0", 308 - "is-unicode-supported": "^2.0.0", 309 - "log-symbols": "^6.0.0", 306 + "is-unicode-supported": "^2.1.0", 307 + "log-symbols": "^7.0.1", 310 308 "stdin-discarder": "^0.2.2", 311 - "string-width": "^7.2.0", 312 - "strip-ansi": "^7.1.0" 309 + "string-width": "^8.1.0", 310 + "strip-ansi": "^7.1.2" 313 311 }, 314 312 "engines": { 315 - "node": ">=18" 313 + "node": ">=20" 316 314 }, 317 315 "funding": { 318 316 "url": "https://github.com/sponsors/sindresorhus" 319 317 } 320 - }, 321 - "node_modules/ora/node_modules/emoji-regex": { 322 - "version": "10.6.0", 323 - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", 324 - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", 325 - "license": "MIT" 326 318 }, 327 319 "node_modules/ora/node_modules/string-width": { 328 - "version": "7.2.0", 329 - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", 330 - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", 320 + "version": "8.1.0", 321 + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", 322 + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", 331 323 "license": "MIT", 332 324 "dependencies": { 333 - "emoji-regex": "^10.5.0", 334 - "get-east-asian-width": "^0.5.0", 325 + "get-east-asian-width": "^1.3.0", 335 326 "strip-ansi": "^7.1.0" 336 327 }, 337 328 "engines": { 338 - "node": ">=18" 329 + "node": ">=20" 339 330 }, 340 331 "funding": { 341 332 "url": "https://github.com/sponsors/sindresorhus" ··· 440 431 "tlds": "bin.js" 441 432 } 442 433 }, 434 + "node_modules/tslib": { 435 + "version": "2.8.1", 436 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 437 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 438 + "license": "0BSD" 439 + }, 443 440 "node_modules/typescript": { 444 441 "version": "5.9.3", 445 442 "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", ··· 469 466 "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 470 467 "dev": true, 471 468 "license": "MIT" 469 + }, 470 + "node_modules/unicode-segmenter": { 471 + "version": "0.14.5", 472 + "resolved": "https://registry.npmjs.org/unicode-segmenter/-/unicode-segmenter-0.14.5.tgz", 473 + "integrity": "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==", 474 + "license": "MIT" 475 + }, 476 + "node_modules/yoctocolors": { 477 + "version": "2.1.2", 478 + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", 479 + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", 480 + "license": "MIT", 481 + "engines": { 482 + "node": ">=18" 483 + }, 484 + "funding": { 485 + "url": "https://github.com/sponsors/sindresorhus" 486 + } 472 487 }, 473 488 "node_modules/zod": { 474 489 "version": "3.25.76",
+4 -4
package.json
··· 1 1 { 2 2 "name": "lastfm-importer", 3 - "version": "0.5.0", 3 + "version": "0.6.0", 4 4 "description": "Import Last.fm scrobbles to ATProto with rate limiting", 5 5 "type": "module", 6 6 "main": "./dist/index.js", ··· 29 29 "author": "", 30 30 "license": "AGPL-3.0-only", 31 31 "dependencies": { 32 - "@atproto/api": "^0.13.35", 32 + "@atproto/api": "^0.18.13", 33 33 "chalk": "^5.6.2", 34 34 "cli-progress": "^3.12.0", 35 - "csv-parse": "^5.6.0", 36 - "ora": "^8.2.0" 35 + "csv-parse": "^6.1.0", 36 + "ora": "^9.0.0" 37 37 }, 38 38 "devDependencies": { 39 39 "@types/node": "^20.19.27",
+1 -1
src/config.ts
··· 30 30 const platform = 31 31 PLATFORM_LABELS[process.platform] ?? process.platform; 32 32 33 - return `lastfm-importer/v0.5.0 (${platform}; Node/${process.version})`; 33 + return `lastfm-importer/v0.6.0 (${platform}; Node/${process.version})`; 34 34 } 35 35 36 36 const CLIENT_AGENT = buildClientAgent();
+1 -1
src/lib/cli.ts
··· 26 26 */ 27 27 export function showHelp(): void { 28 28 console.log(` 29 - ${'\x1b[1m'}Last.fm to ATProto Importer v0.5.0${'\x1b[0m'} 29 + ${'\x1b[1m'}Last.fm to ATProto Importer v0.6.0${'\x1b[0m'} 30 30 31 31 ${'\x1b[1m'}USAGE:${'\x1b[0m'} 32 32 npm start [options]