Malachite is a tool to import your Last.fm and Spotify listening history to the AT Protocol network using the fm.teal.alpha.feed.play lexicon.
malachite scrobbles importer atproto music
14
fork

Configure Feed

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

feat: add CLI usage

+137
+137
web/src/routes/about/+page.svelte
··· 48 48 </p> 49 49 </section> 50 50 51 + <!-- ── CLI / Local usage ───────────────────────────────────────────────────── --> 52 + <section> 53 + <h2>CLI / Local usage</h2> 54 + <p> 55 + Malachite also ships as a Node.js command-line tool. This is useful if you prefer to 56 + run imports locally, need full control over batch settings, or want to automate things 57 + with scripts. 58 + </p> 59 + 60 + <h3>Prerequisites</h3> 61 + <ul> 62 + <li><a href="https://nodejs.org" target="_blank" rel="noopener">Node.js</a> v18 or later</li> 63 + <li><a href="https://pnpm.io" target="_blank" rel="noopener">pnpm</a> (recommended) — or npm / yarn</li> 64 + </ul> 65 + 66 + <h3>Install &amp; build</h3> 67 + <div class="code-block"> 68 + <pre><code># Clone the repository 69 + git clone https://github.com/ewanc26/malachite.git 70 + cd malachite 71 + 72 + # Install dependencies 73 + pnpm install 74 + 75 + # Build 76 + pnpm build</code></pre> 77 + </div> 78 + 79 + <h3>Interactive mode</h3> 80 + <p>Run without arguments and Malachite will walk you through everything:</p> 81 + <div class="code-block"> 82 + <pre><code>pnpm start</code></pre> 83 + </div> 84 + 85 + <h3>Command-line mode</h3> 86 + <p>Common invocations:</p> 87 + <div class="code-block"> 88 + <pre><code># Import from Last.fm CSV 89 + pnpm start -i lastfm.csv -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y 90 + 91 + # Import from Spotify JSON export 92 + pnpm start -i spotify-export/ -m spotify -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y 93 + 94 + # Merge both sources 95 + pnpm start -i lastfm.csv --spotify-input spotify-export/ -m combined -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y 96 + 97 + # Sync (skip already-imported records) 98 + pnpm start -i lastfm.csv -m sync -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y 99 + 100 + # Remove duplicates from your Teal feed 101 + pnpm start -m deduplicate -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx 102 + 103 + # Preview without publishing 104 + pnpm start -i lastfm.csv --dry-run</code></pre> 105 + </div> 106 + 107 + <h3>Key flags</h3> 108 + <div class="flag-table"> 109 + <div class="flag-row flag-row--header"> 110 + <span>Flag</span><span>Description</span> 111 + </div> 112 + <div class="flag-row"><code>-i &lt;path&gt;</code><span>Input file or directory</span></div> 113 + <div class="flag-row"><code>-h &lt;handle&gt;</code><span>ATProto handle or DID</span></div> 114 + <div class="flag-row"><code>-p &lt;password&gt;</code><span>App password (not your main password)</span></div> 115 + <div class="flag-row"><code>-m &lt;mode&gt;</code><span><code>lastfm</code> · <code>spotify</code> · <code>combined</code> · <code>sync</code> · <code>deduplicate</code></span></div> 116 + <div class="flag-row"><code>-y</code><span>Skip confirmation prompts</span></div> 117 + <div class="flag-row"><code>--dry-run</code><span>Preview without writing records</span></div> 118 + <div class="flag-row"><code>-v</code><span>Verbose / debug output</span></div> 119 + <div class="flag-row"><code>-q</code><span>Quiet mode (warnings &amp; errors only)</span></div> 120 + </div> 121 + 122 + <p> 123 + Full documentation is available at 124 + <a href="https://docs.ewancroft.uk/projects/malachite" target="_blank" rel="noopener">docs.ewancroft.uk/projects/malachite</a>. 125 + </p> 126 + </section> 127 + 51 128 <!-- ── Rate limits ────────────────────────────────────────────────────────── --> 52 129 <section> 53 130 <h2>Rate limits &amp; PDS safety</h2> ··· 229 306 padding: 0.1em 0.35em; 230 307 border-radius: 3px; 231 308 } 309 + 310 + /* ── Code blocks ──────────────────────────────────────────────────────────── */ 311 + .code-block { 312 + background: var(--surface-2); 313 + border: 1px solid var(--border); 314 + border-radius: 8px; 315 + overflow-x: auto; 316 + margin: 0 0 1rem; 317 + } 318 + 319 + .code-block pre { 320 + margin: 0; 321 + padding: 1rem 1.25rem; 322 + } 323 + 324 + .code-block code { 325 + background: none; 326 + padding: 0; 327 + font-size: 0.8rem; 328 + color: var(--text); 329 + line-height: 1.7; 330 + } 331 + 332 + /* ── Flag table ──────────────────────────────────────────────────────────── */ 333 + .flag-table { 334 + border: 1px solid var(--border); 335 + border-radius: 8px; 336 + overflow: hidden; 337 + margin: 0 0 1rem; 338 + font-size: 0.85rem; 339 + } 340 + 341 + .flag-row { 342 + display: grid; 343 + grid-template-columns: 10rem 1fr; 344 + gap: 1rem; 345 + padding: 0.55rem 1rem; 346 + border-bottom: 1px solid var(--border); 347 + align-items: center; 348 + } 349 + 350 + .flag-row:last-child { border-bottom: none; } 351 + 352 + .flag-row--header { 353 + background: var(--surface-2); 354 + font-size: 0.72rem; 355 + font-family: 'JetBrains Mono', monospace; 356 + text-transform: uppercase; 357 + letter-spacing: 0.06em; 358 + color: var(--muted); 359 + } 360 + 361 + .flag-row code { 362 + background: none; 363 + padding: 0; 364 + color: var(--accent); 365 + font-size: 0.82em; 366 + } 367 + 368 + .flag-row span { color: var(--muted); } 232 369 233 370 /* ── Pills ──────────────────────────────────────────────────────────────── */ 234 371 .card {