Malachite#
Import your Last.fm and Spotify listening history to the AT Protocol network using the fm.teal.alpha.feed.play lexicon.
Repository: malachite
Also available on Tangled
Table of Contents#
- ⚠️ Important: Rate Limits
- What's with the name?
- Web App
- Quick Start
- Features
- Usage Examples
- Command Line Options
- Getting Your Data
- Data Format
- How It Works
- Logging and Output
- Error Handling
- Troubleshooting
- Development
- File Storage
- Project Structure
- Technical Details
- Contributing
- License
- Credits
⚠️ Important: Rate Limits#
CRITICAL: Bluesky's AppView has rate limits on PDS instances. Exceeding 10K records per day can rate limit your ENTIRE PDS, affecting all users on your instance.
This importer automatically protects your PDS by:
- Dynamic batch sizing (1-200 records) that adapts to available quota in real-time
- 15% headroom buffer prevents quota exhaustion before hitting the limit
- Limiting imports to 7,500 records per day (with 75% safety margin)
- Calculating optimal batch sizes and delays
- Graceful degradation - scales down smoothly as quota depletes
- Instant recovery - immediately returns to maximum speed after quota resets
- Pausing 24 hours between days for large imports
- Providing clear progress tracking and time estimates
- Persisting state across restarts for safe resume
How Dynamic Batch Sizing Works#
Malachite continuously monitors your rate limit quota and automatically adjusts batch size:
Fresh Quota (5000 points) → Batch Size: 200 records (maximum speed)
Half Depleted (2500 points) → Batch Size: 200 records (still optimal)
Approaching Limit (1200) → Batch Size: 150 records (scaling down)
Near Headroom (900) → Batch Size: 50 records (conservative)
Below Headroom (700) → Batch Size: 1 record (minimal progress)
[Quota Resets] → Batch Size: 200 records (instant recovery)
Benefits:
- ✅ 2x faster when quota is fresh (200 vs 100 records/batch)
- ✅ Never hits rate limits - proactive scaling with 15% buffer
- ✅ Always makes progress - even with minimal quota (batch size 1)
- ✅ Automatic recovery - no manual intervention needed
- ✅ Transparent - logs all batch size changes with reasons
For more details, see the Bluesky Rate Limits Documentation.
What's with the name?#
It used to be called atproto-lastfm-importer — generic as fuck. That name told you what it did and nothing about why it mattered, and it sounded like a disposable weekend script. So I renamed it.
At the moment, the repository is still called atproto-lastfm-importer on Tangled, but the GitHub link has been updated to malachite. I do not know if this can be resolved.
Malachite is a greenish-blue copper mineral associated with preservation and transformation. That's exactly what this tool does: it preserves your scrobbles and transforms them into proper fm.teal.alpha.feed.play records on the AT Protocol. The colour match isn't an accident — malachite sits squarely in the teal/green range, a deliberate nod to the teal lexicon it publishes to.
Web App#
Malachite also ships a browser-based web app (web/) built with SvelteKit. It supports all five import modes and signs in via ATProto OAuth — no app password required.
Running the web app in development:
cd web
pnpm install
pnpm dev # starts at http://127.0.0.1:5173
Note: The dev server must run on
127.0.0.1:5173exactly. This is enforced invite.config.tsbecause the OAuth loopbackredirect_uriis pinned to that origin (RFC 8252 §7.3). Do not change the host or port without updating the OAuth client metadata.
The web app fetches existing records using the same CAR-export path as the CLI (com.atproto.sync.getRepo) so it costs zero AppView write-quota points to check for duplicates.
Quick Start#
Note: You must build the project first, then run with arguments.
Interactive Mode (Recommended for First-Time Users)#
Just run without any arguments and Malachite will guide you through the process:
# Install dependencies and build
pnpm install
pnpm build
# Run in interactive mode
pnpm start
The interactive mode will:
- Present a menu of available actions
- Prompt for all required information (handle, password, files)
- Ask for optional settings (dry run, verbose logging, etc.)
- Provide helpful descriptions for each option
Command Line Mode#
For automation or if you prefer command-line arguments:
# Show help
pnpm start --help
# Run with command line arguments
pnpm start -i lastfm.csv -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y
# Alternative: run directly with node
node dist/index.js -i lastfm.csv -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y
Features#
Import Capabilities#
- ✅ Last.fm Import: Full support for Last.fm CSV exports with MusicBrainz IDs
- ✅ Spotify Import: Import Extended Streaming History JSON files
- ✅ Combined Import: Merge Last.fm and Spotify exports with intelligent deduplication
- ✅ Re-Sync Mode: Import only new scrobbles without creating duplicates
- ✅ Duplicate Removal: Clean up accidentally imported duplicate records
Performance & Safety#
- ✅ Automatic Duplicate Prevention: Fetches your existing Teal records via CAR export and skips anything already imported
- ✅ Input Deduplication: Removes duplicate entries within the source file before submission
- ✅ Dynamic Batch Sizing: Automatically adjusts batch size (1-200 records) based on available rate limit quota
- ✅ Batch Operations: Uses
com.atproto.repo.applyWritesfor efficient batch publishing (up to 200 records per call) - ✅ Zero-cost sync check: Existing record fetching uses
com.atproto.sync.getRepo(CAR export) — a separate, far more generous rate-limit envelope that costs zero AppView write-quota points - ✅ Intelligent Rate Limiting: Real-time quota monitoring with 15% headroom buffer prevents rate limit exhaustion
- ✅ Adaptive Recovery: Automatically scales back to maximum speed after quota resets
- ✅ Multi-Day Imports: Large imports automatically span multiple days with 24-hour pauses
- ✅ Resume Support: Safe to stop (Ctrl+C) and restart - continues from where it left off
- ✅ Graceful Cancellation: Press Ctrl+C to stop after the current batch completes
User Experience#
- ✅ Structured Logging: Color-coded output with debug/verbose modes
- ✅ Progress Tracking: Real-time progress with time estimates
- ✅ Dry Run Mode: Preview records without publishing
- ✅ Interactive Mode: Simple prompts guide you through the process
- ✅ Command Line Mode: Full automation support for scripting
- ✅ Web App: Browser-based UI with ATProto OAuth sign-in
Technical Features#
- ✅ TID-based Record Keys: Timestamp-based identifiers for chronological ordering
- ✅ Identity Resolution: Resolves ATProto handles/DIDs using Slingshot
- ✅ PDS Auto-Discovery: Automatically connects to your personal PDS
- ✅ MusicBrainz Support: Preserves MusicBrainz IDs when available (Last.fm)
- ✅ Chronological Ordering: Processes oldest first (or newest with
-rflag) - ✅ Error Handling: Continues on errors with detailed reporting
Usage Examples#
Combined Import (Last.fm + Spotify)#
Merge your Last.fm and Spotify listening history into a single, deduplicated import:
# Preview the merged import
pnpm start -i lastfm.csv --spotify-input spotify-export/ -m combined --dry-run
# Perform the combined import
pnpm start -i lastfm.csv --spotify-input spotify-export/ -m combined -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y
What combined mode does:
- Parses both Last.fm CSV and Spotify JSON exports
- Normalizes track names and artist names for comparison
- Identifies duplicate plays (same track within 5 minutes)
- Chooses the best version of each play (prefers Last.fm with MusicBrainz IDs)
- Merges into a single chronological timeline
- Shows detailed statistics about the merge
Re-Sync Mode#
Sync your Last.fm export with Teal without creating duplicates:
# Preview what will be synced
pnpm start -i lastfm.csv -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -m sync --dry-run
# Perform the sync
pnpm start -i lastfm.csv -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -m sync -y
Perfect for:
- Re-running imports with updated Last.fm exports
- Recovering from interrupted imports
- Adding recent scrobbles without duplicating old ones
Note: Sync mode requires authentication even in dry-run mode to fetch existing records.
Remove Duplicates#
Clean up accidentally imported duplicate records:
# Preview duplicates (dry run)
pnpm start -m deduplicate -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx --dry-run
# Remove duplicates (keeps first occurrence)
pnpm start -m deduplicate -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx
Import from Spotify#
# Import single Spotify JSON file
pnpm start -i Streaming_History_Audio_2021-2023_0.json -m spotify -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y
# Import directory with multiple Spotify files (recommended)
pnpm start -i '/path/to/Spotify Extended Streaming History' -m spotify -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y
Import from Last.fm#
# Standard Last.fm import
pnpm start -i lastfm.csv -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y
# Preview without publishing
pnpm start -i lastfm.csv --dry-run
# Process newest tracks first
pnpm start -i lastfm.csv -h alice.bsky.social -r -y
# Verbose debug output
pnpm start -i lastfm.csv --dry-run -v
# Quiet mode (only warnings and errors)
pnpm start -i lastfm.csv -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -q -y
Advanced Options#
# Development mode (verbose + file logging + smaller batches for debugging)
pnpm start -i lastfm.csv --dev --dry-run
# Custom batch settings (advanced users only)
pnpm start -i lastfm.csv -h alice.bsky.social -b 20 -d 3000
# Full automation with all flags
pnpm start -i lastfm.csv -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y -q
Command Line Options#
Note: When importing data (not in deduplicate mode), you must provide --input, --handle, and --password. The --yes flag skips confirmation prompts for automation.
Required Options#
| Option | Short | Description | Example |
|---|---|---|---|
--input <path> |
-i |
Path to Last.fm CSV or Spotify JSON file/directory | -i lastfm.csv |
--handle <handle> |
-h |
ATProto handle or DID | -h alice.bsky.social |
--password <pass> |
-p |
ATProto app password | -p xxxx-xxxx-xxxx-xxxx |
Import Mode#
| Option | Short | Description | Default |
|---|---|---|---|
--mode <mode> |
-m |
Import mode | lastfm |
Available modes:
lastfm- Import Last.fm export onlyspotify- Import Spotify export onlycombined- Merge Last.fm + Spotify exportssync- Skip existing records (sync mode)deduplicate- Remove duplicate records
Additional Options#
| Option | Short | Description | Default |
|---|---|---|---|
--spotify-input <path> |
Path to Spotify export (for combined mode) | - | |
--reverse |
-r |
Process newest first | false |
--yes |
-y |
Skip confirmation prompts | false |
--dry-run |
Preview without importing | false |
|
--verbose |
-v |
Enable debug logging | false |
--quiet |
-q |
Suppress non-essential output | false |
--dev |
Development mode (verbose + file logging + smaller batches) | false |
|
--batch-size <num> |
-b |
Initial batch size (1-200, dynamically adjusted) | Auto-calculated |
--batch-delay <ms> |
-d |
Delay between batches in ms | 500 (min) |
--help |
Show help message | - |
PDS Override#
If you already know the base URL of your Personal Data Server (PDS) you can bypass the Slingshot identity resolver and provide it directly with the --pds flag. This is useful for private instances, testing, or when the resolver is unreliable.
| Option | Description |
|---|---|
--pds <url> |
PDS base URL to use for authentication and API calls (e.g. https://pds.example.com). When provided, Malachite will skip Slingshot lookup and use this URL directly. |
Notes:
- The
--pdsflag overrides the configured Slingshot resolver for identity lookup. If--pdsis given, Malachite will attempt to authenticate directly against the supplied PDS using your handle/DID and app password. - Use the full base URL (including scheme), e.g.
https://pds.example.com. - If authentication fails when using
--pds, try removing the flag so Malachite can resolve your PDS automatically via Slingshot.
Legacy Flags (Backwards Compatible)#
These old flags still work but are deprecated:
--file→ Use--input--identifier→ Use--handle--spotify-file→ Use--spotify-input--reverse-chronological→ Use--reverse--spotify→ Use--mode spotify--combined→ Use--mode combined--sync→ Use--mode sync--remove-duplicates→ Use--mode deduplicate
Getting Your Data#
Last.fm Export#
- Visit Last.fm Export Tool
- Request your data export in CSV format
- Download the CSV file when ready
- Use the CSV file path with this importer
Spotify Export#
- Go to Spotify Privacy Settings
- Scroll to "Download your data" and request your data
- Select "Extended streaming history" (can take up to 30 days)
- When ready, download and extract the ZIP file
- Use either:
- A single JSON file:
Streaming_History_Audio_2021-2023_0.json - The entire extracted directory (recommended)
- A single JSON file:
Note: The importer automatically:
- Reads all
Streaming_History_Audio_*.jsonfiles in a directory - Filters out podcasts, audiobooks, and non-music content
- Combines all music tracks into a single import
Data Format#
Each scrobble becomes an fm.teal.alpha.feed.play record with:
Required Fields#
- trackName: The name of the track
- artists: Array of artist objects (requires
artistName, optionalartistMbIdfor Last.fm) - playedTime: ISO 8601 timestamp of when you listened
- submissionClientAgent: Identifies this importer (
malachite/v0.10.0for CLI,malachite/v0.3.0 (web)for the web app) - musicServiceBaseDomain: Set to
last.fmorspotify.com
Optional Fields#
- releaseName: Album/release name
- releaseMbId: MusicBrainz release ID (Last.fm only)
- recordingMbId: MusicBrainz recording/track ID (Last.fm only)
- originUrl: Link to the track on Last.fm or Spotify
Example Records#
Last.fm Record:
{
"$type": "fm.teal.alpha.feed.play",
"trackName": "Paint My Masterpiece",
"artists": [
{
"artistName": "Cjbeards",
"artistMbId": "c8d4f4bf-1b82-4d4d-9d73-05909faaff89"
}
],
"releaseName": "Masquerade",
"releaseMbId": "fdb2397b-78d5-4019-8fad-656d286e4d33",
"recordingMbId": "3a390ad3-fe56-45f2-a073-bebc45d6bde1",
"playedTime": "2025-11-13T23:49:36Z",
"originUrl": "https://www.last.fm/music/Cjbeards/_/Paint+My+Masterpiece",
"submissionClientAgent": "malachite/v0.10.0",
"musicServiceBaseDomain": "last.fm"
}
Spotify Record:
{
"$type": "fm.teal.alpha.feed.play",
"trackName": "Don't Give Up",
"artists": [
{
"artistName": "Chicane"
}
],
"releaseName": "Twenty",
"playedTime": "2021-09-09T10:34:08Z",
"originUrl": "https://open.spotify.com/track/3gZqDJkMZipOYCRjlHWgOV",
"submissionClientAgent": "malachite/v0.10.0",
"musicServiceBaseDomain": "spotify.com"
}
How It Works#
Processing Flow#
- Parses input file(s):
- Last.fm: CSV using
csv-parselibrary - Spotify: JSON files (single or multiple in directory)
- Last.fm: CSV using
- Filters data:
- Spotify: Automatically removes podcasts, audiobooks, and non-music content
- Converts to schema: Maps to
fm.teal.alpha.feed.playformat - Deduplicates input: Removes duplicate entries from the source data (keeps first occurrence)
- Checks Teal: Downloads the entire repo as a CAR file (
com.atproto.sync.getRepo) and skips any records already imported — costs zero AppView write-quota points - Sorts records: Chronologically (oldest first) or reverse with
-rflag - Generates TID-based keys: From
playedTimefor chronological ordering - Validates fields: Ensures required fields are present
- Publishes in batches: Uses
com.atproto.repo.applyWrites(up to 200 records per call)
Automatic Duplicate Prevention#
The importer has two layers of duplicate prevention to ensure you never import the same record twice:
Step 1: Input File Deduplication#
Removes duplicates within your source file(s):
How duplicates are identified:
- Same track name (case-insensitive)
- Same artist name (case-insensitive)
- Same timestamp (exact match)
What happens:
- First occurrence is kept
- Subsequent duplicates are removed
- Shows message: "No duplicates found in input data" or "Removed X duplicate(s)"
Step 2: Teal Comparison via CAR Export#
Automatically checks your existing Teal records by downloading your entire repo as a CARv1 file:
- One HTTP request fetches the whole repo (
com.atproto.sync.getRepo) - The CAR file is parsed locally in memory — no AppView quota consumed
- Compares every record against your input and skips anything already imported
- Shows: "Skipped X already-imported record(s)"
This means:
- ✅ Safe to re-run imports with updated exports
- ✅ Won't create duplicates if you run the import twice
- ✅ Zero AppView write-quota cost for the sync check
- ✅ Works automatically - no special mode needed
Note:
- Credentials are required even for
--dry-runto fetch the CAR export - Sync mode (
-m sync): Shows detailed statistics about what's being skipped - Deduplicate mode (
-m deduplicate): Removes duplicates from already-imported Teal records (cleanup tool)
Rate Limiting Algorithm#
- Calculates safe daily limit (75% of 10K = 7,500 records/day by default)
- Determines how many days needed for your import
- Monitors rate limit quota in real-time before each batch
- Dynamically adjusts batch size (1-200 records) based on available points
- Preserves 15% headroom buffer to prevent exhaustion
- Automatically waits when quota is exhausted (with countdown timer)
- Instantly scales back up to maximum batch size after quota resets
- Enforces minimum delay between batches
- Shows clear schedule and real-time batch size adjustments
Multi-Day Imports#
For imports exceeding the daily limit, the importer automatically:
- Calculates a schedule: Splits your import across multiple days
- Shows the plan: Displays which records will be imported each day
- Processes Day 1: Imports the first batch of records
- Pauses 24 hours: Waits a full day before continuing
- Repeats: Continues until all records are imported
Important notes:
- You can safely stop (Ctrl+C) and restart
- Progress is preserved - continues where it left off
- Each day's progress is clearly displayed
- Time estimates account for multi-day duration
Logging and Output#
The importer uses color-coded output for clarity:
- Green (✓): Success messages
- Cyan (→): Progress updates
- Yellow (⚠️): Warnings
- Red (✗): Errors
- Bold Red (🛑): Fatal errors
Verbosity Levels#
Default Mode: Standard operational messages
pnpm start -i lastfm.csv -h alice.bsky.social -p pass
Verbose Mode (-v): Detailed debug information including batch timing and API calls
pnpm start -i lastfm.csv -h alice.bsky.social -p pass -v
Quiet Mode (-q): Only warnings and errors
pnpm start -i lastfm.csv -h alice.bsky.social -p pass -q
Development Mode (--dev): Verbose logging + file logging to ~/.malachite/logs/ + smaller batch sizes
pnpm start -i lastfm.csv --dev --dry-run
Development mode is perfect for:
- Debugging import issues with detailed logs
- Testing changes with smaller batches (20 records max)
- Preserving logs for later analysis
- Troubleshooting problems with support
Error Handling#
The importer is designed to be resilient:
- Network errors: Failed records are logged but don't stop the import
- Invalid data: Skipped with error messages
- Authentication issues: Clear error messages with suggested fixes
- Rate limit hits: Automatic adjustment and retry logic
- Ctrl+C handling: Gracefully stops after current batch
Troubleshooting#
Authentication Issues#
"Handle not found"
- Verify your ATProto handle is correct (e.g.,
alice.bsky.social) - Ensure you're using a valid DID or handle
"Invalid credentials"
- Use an app password, not your main account password
- Generate app passwords in your account settings
Performance Issues#
"Rate limit exceeded"
- The importer should prevent this automatically
- If you see this, wait 24 hours before retrying
- Consider reducing batch size with
-bflag
Import seems stuck
- Check progress messages - large imports take time
- Multi-day imports pause for 24 hours between days
- You can safely stop (Ctrl+C) and resume later
- Use
--verboseflag to see detailed progress
Connection Issues#
"Connection refused"
- Check your internet connection
- Verify your PDS is accessible
- Some PDSs may have firewall rules
Output Control#
Too much output
- Use
--quietflag to suppress non-essential messages - Only warnings and errors will be shown
Need more details
- Use
--verboseflag to see debug-level information - Shows batch timing, API calls, and detailed progress
Development#
# Type checking
pnpm run type-check
# Build
pnpm run build
# Development mode (rebuild + run)
pnpm run dev
# Run tests
pnpm run test
# Clean build artifacts
pnpm run clean
File Storage#
Malachite stores all its data in ~/.malachite/:
~/.malachite/
├── cache/ # Cached Teal records (24-hour TTL)
├── state/ # Import state for resume functionality
├── logs/ # Import logs (when file logging is enabled)
└── credentials.json # Encrypted credentials (optional, machine-specific)
This keeps your project directory clean and follows standard Unix conventions.
Credential Storage#
Malachite automatically saves your ATProto credentials after a successful login so you don't need to re-enter them on the next run:
Security Features:
- ✅ AES-256-GCM encryption - Military-grade encryption
- ✅ Machine-specific - Credentials are bound to your computer and can't be transferred
- ✅ Secure key derivation - Uses PBKDF2 with 100,000 iterations
- ✅ File permissions - Credentials file is readable only by you (Unix)
How It Works:
- Credentials are encrypted using a key derived from your hostname + username and saved to
~/.malachite/credentials.jsonafter every successful login - On the next run, Malachite loads saved credentials automatically
- In interactive mode, you'll be prompted whether to use the saved credentials or enter new ones
Managing Credentials:
# Clear saved credentials
pnpm start --clear-credentials
# Or through interactive mode (option 7)
pnpm start
Important Notes:
- Credentials are machine-specific and won't work if you copy the file to another computer
- This is a convenience feature - you can always enter credentials manually
- If you change your password, clear and re-save credentials
Project Structure#
malachite/
├── src/
│ ├── lib/
│ │ ├── auth.ts # Authentication & identity resolution
│ │ ├── cli.ts # Command line interface & argument parsing
│ │ ├── csv.ts # CSV parsing & record conversion
│ │ ├── publisher.ts # Batch publishing with rate limiting
│ │ ├── spotify.ts # Spotify JSON parsing
│ │ ├── merge.ts # Combined import deduplication
│ │ └── sync.ts # Re-sync mode & duplicate detection
│ ├── utils/
│ │ ├── car-fetch.ts # CAR export fetcher (com.atproto.sync.getRepo)
│ │ ├── logger.ts # Structured logging system
│ │ ├── helpers.ts # Utility functions (timing, formatting)
│ │ ├── input.ts # User input handling (prompts, passwords)
│ │ ├── rate-limiter.ts # Rate limiting with server-learned quota
│ │ ├── killswitch.ts # Graceful shutdown handling
│ │ ├── tid.ts # TID generation from timestamps
│ │ └── ui.ts # UI elements (spinners, progress bars)
│ ├── config.ts # Configuration constants & version
│ └── types.ts # TypeScript type definitions
├── web/ # SvelteKit web app
│ └── src/lib/
│ ├── core/ # Browser-safe equivalents of src/lib & src/utils
│ │ ├── auth.ts # Password-based ATProto login
│ │ ├── car-fetch.ts# CAR export fetcher (browser-safe)
│ │ ├── csv.ts # CSV parser (no csv-parse dep)
│ │ ├── import.ts # Import orchestration
│ │ ├── merge.ts # Combined import deduplication
│ │ ├── oauth.ts # ATProto OAuth client
│ │ ├── publisher.ts# Batch publisher with progress callbacks
│ │ ├── rate-limiter.ts # In-memory rate limiter
│ │ ├── spotify.ts # Spotify JSON parser
│ │ ├── sync.ts # CAR-based sync & dedup
│ │ └── tid.ts # TID generation (Web Crypto API)
│ ├── config.ts # Shared constants (version injected by Vite)
│ ├── modes.ts # Import mode definitions
│ └── types.ts # TypeScript type definitions
├── lexicons/ # fm.teal.alpha lexicon definitions
│ └── fm.teal.alpha/
│ └── feed/
│ └── play.json # Play record schema
├── package.json
├── tsconfig.json
└── README.md
Technical Details#
Authentication#
- Uses Slingshot resolver to discover your PDS from your handle/DID
- Requires an ATProto app password (not your main password) for the CLI
- Web app supports ATProto OAuth — no app password needed
- Automatically configures the agent for your personal PDS
Batch Publishing#
- Uses
com.atproto.repo.applyWritesfor efficiency (up to 20x faster than individual calls) - Batches up to 200 records per API call (PDS maximum)
- Dynamic batch sizing (1-200 records) based on real-time rate limit quota
- Intelligent quota monitoring with 15% headroom buffer
- Automatic adjustment - scales down as quota depletes, scales up after reset
- Enforces minimum delays between batches for rate limit safety
CAR Export Sync#
All read paths (duplicate checks, sync, deduplicate) use com.atproto.sync.getRepo to download the user's entire repo as a CARv1 file. The CAR is parsed locally using @ipld/car and @ipld/dag-cbor — no AppView XRPC calls are made for reads, so the sync check costs zero write-quota points.
Data Mapping#
Last.fm:
- Direct mapping from CSV columns
- Converts Unix timestamps to ISO 8601
- Preserves MusicBrainz IDs when present
- Generates URLs from artist/track names
- Wraps artists in array format with optional MBID
Spotify:
- Extracts data from JSON fields
- Already in ISO 8601 format (
tsfield) - Generates URLs from
spotify_track_uri - Automatically filters non-music content
- Extracts artist and album from metadata fields
Lexicon Reference#
This importer follows the official fm.teal.alpha lexicon defined in /lexicons/fm.teal.alpha/feed/play.json.
The lexicon defines required and optional field types, string length constraints, array formats, timestamp formatting, and URL validation.
Contributing#
Contributions are welcome — see CONTRIBUTING.md for setup instructions, architecture notes, and PR guidelines.
License#
AGPL-3.0-only - See LICENCE file for details
☕ Support#
If you found this useful, consider buying me a ko-fi!
Credits#
- Uses @atproto/api for ATProto interactions
- CSV parsing via csv-parse
- Identity resolution via Slingshot
- Follows the
fm.teal.alphalexicon standard - Colored output via chalk
- Progress indicators via ora and cli-progress
- Web app built with SvelteKit and Tailwind CSS
- ATProto OAuth via @atproto/oauth-client-browser
Note: This tool is for personal use. Respect the terms of service and rate limits when importing your data.