···11# Last.fm to ATProto Importer
2233-Import your Last.fm listening history to the AT Protocol network using the `fm.teal.alpha.feed.play` lexicon.
33+Import your Last.fm and Spotify listening history to the AT Protocol network using the `fm.teal.alpha.feed.play` lexicon.
4455-(Also [on Tangled!](https://tangled.org/@did:plc:ofrbh253gwicbkc5nktqepol/atproto-lastfm-importer))
55+[Also available on Tangled](https://tangled.org/@did:plc:ofrbh253gwicbkc5nktqepol/atproto-lastfm-importer)
6677-## Features
77+## ⚠️ Important: Rate Limits
8899-- ✅ **Structured Logging**: Color-coded output with debug/verbose modes
1010-- ✅ **Batch Operations**: Uses `com.atproto.repo.applyWrites` for efficient batch publishing (up to 200 records per call)
1111-- ✅ **Spotify Support**: Import from Spotify Extended Streaming History (JSON format)
1212-- ✅ **Combined Import**: Merge Last.fm and Spotify exports, automatically deduplicating overlapping plays
1313-- ✅ **TID-based Record Keys**: Records use timestamp-based identifiers for chronological ordering
1414-- ✅ **Re-Sync Mode**: Check existing Teal records and only import new scrobbles (no duplicates!)
1515-- ✅ **Rate Limiting**: Automatically limits imports to 1K records per day to prevent rate limiting your entire PDS
1616-- ✅ **Multi-Day Imports**: Large imports (>1K records) automatically span multiple days with 24-hour pauses
1717-- ✅ **Resume Support**: Safe to stop (Ctrl+C) and restart - continues from where it left off
1818-- ✅ **Graceful Cancellation**: Press Ctrl+C to stop after the current batch completes
1919-- ✅ **Identity Resolution**: Resolves ATProto handles/DIDs using Slingshot
2020-- ✅ **PDS Auto-Discovery**: Automatically connects to your personal PDS
2121-- ✅ **Dry Run Mode**: Preview records without publishing
2222-- ✅ **Batch Processing**: Configurable batching with rate limit safety
2323-- ✅ **Progress Tracking**: Real-time progress with time estimates
2424-- ✅ **Error Handling**: Continues on errors with detailed reporting
2525-- ✅ **MusicBrainz Support**: Preserves MusicBrainz IDs when available (Last.fm only)
2626-- ✅ **Chronological Ordering**: Processes oldest first (or newest with `-r` flag)
99+**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.
27102828-## Important: Rate Limits
2929-3030-⚠️ **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!
3131-3232-This importer automatically:
3333-- Limits imports to **1,000 records per day** (90% of safe limit)
3434-- Calculates optimal batch sizes and delays
3535-- Pauses 24 hours between days for large imports
3636-- Shows clear progress and time estimates
1111+This importer automatically protects your PDS by:
1212+- Limiting imports to **1,000 records per day** (with 75% safety margin)
1313+- Calculating optimal batch sizes and delays
1414+- Pausing 24 hours between days for large imports
1515+- Providing clear progress tracking and time estimates
37163838-See: [Bluesky Rate Limits Documentation](https://docs.bsky.app/blog/rate-limits-pds-v3)
1717+For more details, see the [Bluesky Rate Limits Documentation](https://docs.bsky.app/blog/rate-limits-pds-v3).
39184040-## Setup
1919+## Quick Start
41204221```bash
2222+# Install dependencies
4323npm install
2424+2525+# Build the project
4426npm run build
2727+2828+# Run with interactive prompts
2929+npm start
3030+3131+# Or run with command line arguments
3232+npm start -- -i lastfm.csv -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y
4533```
46344747-## Usage
3535+## Features
3636+3737+### Import Capabilities
3838+- ✅ **Last.fm Import**: Full support for Last.fm CSV exports with MusicBrainz IDs
3939+- ✅ **Spotify Import**: Import Extended Streaming History JSON files
4040+- ✅ **Combined Import**: Merge Last.fm and Spotify exports with intelligent deduplication
4141+- ✅ **Re-Sync Mode**: Import only new scrobbles without creating duplicates
4242+- ✅ **Duplicate Removal**: Clean up accidentally imported duplicate records
4343+4444+### Performance & Safety
4545+- ✅ **Batch Operations**: Uses `com.atproto.repo.applyWrites` for efficient batch publishing (up to 200 records per call)
4646+- ✅ **Rate Limiting**: Automatic daily limits prevent PDS rate limiting
4747+- ✅ **Multi-Day Imports**: Large imports automatically span multiple days with 24-hour pauses
4848+- ✅ **Resume Support**: Safe to stop (Ctrl+C) and restart - continues from where it left off
4949+- ✅ **Graceful Cancellation**: Press Ctrl+C to stop after the current batch completes
5050+5151+### User Experience
5252+- ✅ **Structured Logging**: Color-coded output with debug/verbose modes
5353+- ✅ **Progress Tracking**: Real-time progress with time estimates
5454+- ✅ **Dry Run Mode**: Preview records without publishing
5555+- ✅ **Interactive Mode**: Simple prompts guide you through the process
5656+- ✅ **Command Line Mode**: Full automation support for scripting
48574949-### Combined Import Mode
5858+### Technical Features
5959+- ✅ **TID-based Record Keys**: Timestamp-based identifiers for chronological ordering
6060+- ✅ **Identity Resolution**: Resolves ATProto handles/DIDs using Slingshot
6161+- ✅ **PDS Auto-Discovery**: Automatically connects to your personal PDS
6262+- ✅ **MusicBrainz Support**: Preserves MusicBrainz IDs when available (Last.fm)
6363+- ✅ **Chronological Ordering**: Processes oldest first (or newest with `-r` flag)
6464+- ✅ **Error Handling**: Continues on errors with detailed reporting
6565+6666+## Usage Examples
6767+6868+### Combined Import (Last.fm + Spotify)
50695170Merge your Last.fm and Spotify listening history into a single, deduplicated import:
5271···5877npm start -- -i lastfm.csv --spotify-input spotify-export/ -m combined -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y
5978```
60796161-Combined mode will:
6262-1. Parse both Last.fm CSV and Spotify JSON exports
6363-2. Normalize track names and artist names for comparison
6464-3. Identify duplicate plays (same track within 5 minutes)
6565-4. Choose the best version of each play:
6666- - Prefers Last.fm records with MusicBrainz IDs
6767- - Otherwise prefers Spotify for better metadata quality
6868-5. Merge into a single chronological timeline
6969-6. Show detailed statistics about the merge
8080+**What combined mode does:**
8181+1. Parses both Last.fm CSV and Spotify JSON exports
8282+2. Normalizes track names and artist names for comparison
8383+3. Identifies duplicate plays (same track within 5 minutes)
8484+4. Chooses the best version of each play (prefers Last.fm with MusicBrainz IDs)
8585+5. Merges into a single chronological timeline
8686+6. Shows detailed statistics about the merge
70877171-This is perfect for:
7272-- Getting complete listening history from both services
7373-- Filling gaps where one service was used more than the other
7474-- Ensuring the best metadata quality for each play
7575-- Avoiding duplicate entries when both services tracked the same play
7676-7777-**Example Output:**
8888+**Example output:**
7889```
7990📊 Merge Statistics
8091═══════════════════════════════════════════
···9610797108### Re-Sync Mode
981099999-If you've already imported scrobbles before and want to sync your Last.fm export with Teal without creating duplicates:
110110+Sync your Last.fm export with Teal without creating duplicates:
100111101112```bash
102113# Preview what will be synced
···106117npm start -- -i lastfm.csv -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -m sync -y
107118```
108119109109-Sync mode will:
110110-1. Fetch all existing play records from your Teal feed
111111-2. Compare them against your Last.fm export
112112-3. Identify gaps (scrobbles in Last.fm that aren't in Teal)
113113-4. Only import the missing records
114114-5. Show detailed statistics about duplicates and new records
115115-116116-This is perfect for:
120120+**Perfect for:**
117121- Re-running imports with updated Last.fm exports
118122- Recovering from interrupted imports
119123- Adding recent scrobbles without duplicating old ones
120124121125**Note:** Sync mode requires authentication even in dry-run mode to fetch existing records.
122126123123-### Remove Duplicates Mode
127127+### Remove Duplicates
124128125125-If you accidentally imported duplicate records, you can clean them up:
129129+Clean up accidentally imported duplicate records:
126130127131```bash
128132# Preview duplicates (dry run)
···132136npm start -- -m deduplicate -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx
133137```
134138135135-This will:
136136-1. Fetch all existing records from Teal
137137-2. Identify duplicate plays (same track, artist, and timestamp)
138138-3. Keep the first occurrence of each duplicate
139139-4. Delete the rest
139139+### Import from Spotify
140140141141-### Interactive Mode
141141+```bash
142142+# Import single Spotify JSON file
143143+npm start -- -i Streaming_History_Audio_2021-2023_0.json -m spotify -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y
142144143143-The simplest way to use the importer - just run it and follow the prompts:
144144-145145-```bash
146146-npm start
145145+# Import directory with multiple Spotify files (recommended)
146146+npm start -- -i '/path/to/Spotify Extended Streaming History' -m spotify -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y
147147```
148148149149-### Command Line Mode
150150-151151-For automation or scripting, provide all parameters via flags:
149149+### Import from Last.fm
152150153151```bash
154154-# Full automation (Last.fm)
152152+# Standard Last.fm import
155153npm start -- -i lastfm.csv -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y
156156-157157-# Import from Spotify (single file)
158158-npm start -- -i Streaming_History_Audio_2021-2023_0.json -m spotify -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y
159159-160160-# Import from Spotify (directory with multiple files - recommended)
161161-npm start -- -i '/path/to/Spotify Extended Streaming History' -m spotify -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y
162162-163163-# Combined import (merge Last.fm and Spotify)
164164-npm start -- -i lastfm.csv --spotify-input '/path/to/Spotify Extended Streaming History' -m combined -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y
165154166155# Preview without publishing
167156npm start -- -i lastfm.csv --dry-run
168157169169-# Preview with verbose debug output
158158+# Process newest tracks first
159159+npm start -- -i lastfm.csv -h alice.bsky.social -r -y
160160+161161+# Verbose debug output
170162npm start -- -i lastfm.csv --dry-run -v
171163172164# Quiet mode (only warnings and errors)
173165npm start -- -i lastfm.csv -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -q -y
166166+```
174167175175-# Custom batch settings (advanced users)
168168+### Advanced Options
169169+170170+```bash
171171+# Custom batch settings (advanced users only)
176172npm start -- -i lastfm.csv -h alice.bsky.social -b 20 -d 3000
177173178178-# Process newest tracks first
179179-npm start -- -i lastfm.csv -h alice.bsky.social -r -y
174174+# Full automation with all flags
175175+npm start -- -i lastfm.csv -h alice.bsky.social -p xxxx-xxxx-xxxx-xxxx -y -q
180176```
181177182178## Command Line Options
183179184184-### Authentication
185185-| Option | Short | Description |
186186-|--------|-------|-------------|
187187-| `--handle <handle>` | `-h` | ATProto handle or DID (e.g., alice.bsky.social) |
188188-| `--password <pass>` | `-p` | ATProto app password |
180180+### Required Options
181181+182182+| Option | Short | Description | Example |
183183+|--------|-------|-------------|---------|
184184+| `--input <path>` | `-i` | Path to Last.fm CSV or Spotify JSON file/directory | `-i lastfm.csv` |
185185+| `--handle <handle>` | `-h` | ATProto handle or DID | `-h alice.bsky.social` |
186186+| `--password <pass>` | `-p` | ATProto app password | `-p xxxx-xxxx-xxxx-xxxx` |
189187190190-### Input
191191-| Option | Short | Description |
192192-|--------|-------|-------------|
193193-| `--input <path>` | `-i` | Path to Last.fm CSV or Spotify JSON file/directory |
194194-| `--spotify-input <path>` | | Path to Spotify export (for combined mode) |
188188+### Import Mode
195189196196-### Mode
197190| Option | Short | Description | Default |
198191|--------|-------|-------------|---------|
199192| `--mode <mode>` | `-m` | Import mode | `lastfm` |
200193201194**Available modes:**
202195- `lastfm` - Import Last.fm export only
203203-- `spotify` - Import Spotify export only
196196+- `spotify` - Import Spotify export only
204197- `combined` - Merge Last.fm + Spotify exports
205198- `sync` - Skip existing records (sync mode)
206199- `deduplicate` - Remove duplicate records
207200208208-### Batch Configuration
209209-| Option | Short | Description | Default |
210210-|--------|-------|-------------|---------|
211211-| `--batch-size <num>` | `-b` | Records per batch | Auto-calculated |
212212-| `--batch-delay <ms>` | `-d` | Delay between batches in ms | 500 (min: 500) |
201201+### Additional Options
213202214214-### Import Options
215203| Option | Short | Description | Default |
216204|--------|-------|-------------|---------|
217217-| `--reverse` | `-r` | Process newest first | false (oldest first) |
218218-| `--yes` | `-y` | Skip confirmation prompts | false |
219219-| `--dry-run` | | Preview without importing | false |
220220-221221-### Output
222222-| Option | Short | Description | Default |
223223-|--------|-------|-------------|---------|
224224-| `--verbose` | `-v` | Enable debug logging | false |
225225-| `--quiet` | `-q` | Suppress non-essential output | false |
205205+| `--spotify-input <path>` | | Path to Spotify export (for combined mode) | - |
206206+| `--reverse` | `-r` | Process newest first | `false` |
207207+| `--yes` | `-y` | Skip confirmation prompts | `false` |
208208+| `--dry-run` | | Preview without importing | `false` |
209209+| `--verbose` | `-v` | Enable debug logging | `false` |
210210+| `--quiet` | `-q` | Suppress non-essential output | `false` |
211211+| `--batch-size <num>` | `-b` | Records per batch (1-200) | Auto-calculated |
212212+| `--batch-delay <ms>` | `-d` | Delay between batches in ms | `500` (min) |
226213| `--help` | | Show help message | - |
227214228215### Legacy Flags (Backwards Compatible)
229216230230-For backwards compatibility, the following old flags still work:
231231-- `--file` → Use `--input` instead
232232-- `--identifier` → Use `--handle` instead
233233-- `--spotify-file` → Use `--spotify-input` instead
234234-- `--reverse-chronological` → Use `--reverse` instead
235235-- `--spotify` → Use `--mode spotify` instead
236236-- `--combined` → Use `--mode combined` instead
237237-- `--sync` → Use `--mode sync` instead
238238-- `--remove-duplicates` → Use `--mode deduplicate` instead
239239-240240-### Batch Settings
241241-242242-The importer automatically calculates optimal batch settings based on your total record count and rate limits. You generally **don't need** to specify batch settings unless you have specific requirements.
243243-244244-**Automatic behavior:**
245245-- For imports < 1K records: Uses default settings (200 records/batch, 500ms delay)
246246-- For imports > 1K records: Automatically calculates settings to spread across multiple days
247247-248248-**Manual override** (advanced):
249249-- `--batch-size`: Number of records processed per batch (1-200, PDS maximum)
250250-- `--batch-delay`: Milliseconds to wait between batches (min: 500)
251251-252252-⚠️ Lower delays increase speed but risk hitting rate limits. The automatic calculation is recommended.
253253-254254-## Logging and Output
255255-256256-The importer includes a structured logging system with color-coded output:
257257-258258-- **Green (✓)**: Success messages
259259-- **Cyan (→)**: Progress updates
260260-- **Yellow (⚠️)**: Warnings
261261-- **Red (✗)**: Errors
262262-- **Bold Red (🛑)**: Fatal errors
263263-264264-### Verbosity Levels
265265-266266-**Default Mode**: Shows standard operational messages
267267-```bash
268268-npm start -- -i lastfm.csv -h alice.bsky.social -p pass
269269-```
270270-271271-**Verbose Mode** (`-v`): Shows detailed debug information including batch timing, API calls, etc.
272272-```bash
273273-npm start -- -i lastfm.csv -h alice.bsky.social -p pass -v
274274-```
275275-276276-**Quiet Mode** (`-q`): Only shows warnings and errors
277277-```bash
278278-npm start -- -i lastfm.csv -h alice.bsky.social -p pass -q
279279-```
217217+These old flags still work but are deprecated:
218218+- `--file` → Use `--input`
219219+- `--identifier` → Use `--handle`
220220+- `--spotify-file` → Use `--spotify-input`
221221+- `--reverse-chronological` → Use `--reverse`
222222+- `--spotify` → Use `--mode spotify`
223223+- `--combined` → Use `--mode combined`
224224+- `--sync` → Use `--mode sync`
225225+- `--remove-duplicates` → Use `--mode deduplicate`
280226281227## Getting Your Data
282228283229### Last.fm Export
284230285285-1. Go to <https://lastfm.ghan.nl/export/>
231231+1. Visit [Last.fm Export Tool](https://lastfm.ghan.nl/export/)
2862322. Request your data export in CSV format
2872333. Download the CSV file when ready
288288-4. Use the CSV file path with this script
234234+4. Use the CSV file path with this importer
289235290236### Spotify Export
291237292292-1. Go to your [Spotify Privacy Settings](https://www.spotify.com/account/privacy/)
293293-2. Scroll down to "Download your data" and request your data
294294-3. Select "Extended streaming history" (this can take up to 30 days)
238238+1. Go to [Spotify Privacy Settings](https://www.spotify.com/account/privacy/)
239239+2. Scroll to "Download your data" and request your data
240240+3. Select "Extended streaming history" (can take up to 30 days)
2952414. When ready, download and extract the ZIP file
2962425. Use either:
297243 - A single JSON file: `Streaming_History_Audio_2021-2023_0.json`
298244 - The entire extracted directory (recommended)
299245300300-**Note**: Spotify exports include multiple JSON files. The importer automatically:
246246+**Note:** The importer automatically:
301247- Reads all `Streaming_History_Audio_*.json` files in a directory
302302-- Filters out podcasts, audiobooks, and other non-music content
248248+- Filters out podcasts, audiobooks, and non-music content
303249- Combines all music tracks into a single import
304250305305-## What Gets Imported
251251+## Data Format
306252307307-Each scrobble (from Last.fm or Spotify) becomes an `fm.teal.alpha.feed.play` record with:
253253+Each scrobble becomes an `fm.teal.alpha.feed.play` record with:
308254309255### Required Fields
310256- **trackName**: The name of the track
311257- **artists**: Array of artist objects (requires `artistName`, optional `artistMbId` for Last.fm)
312258- **playedTime**: ISO 8601 timestamp of when you listened
313259- **submissionClientAgent**: Identifies this importer (`lastfm-importer/v0.6.0`)
314314-- **musicServiceBaseDomain**: Set to `last.fm` or `spotify.com` depending on source
260260+- **musicServiceBaseDomain**: Set to `last.fm` or `spotify.com`
315261316316-### Optional Fields (when available)
262262+### Optional Fields
317263- **releaseName**: Album/release name
318264- **releaseMbId**: MusicBrainz release ID (Last.fm only)
319265- **recordingMbId**: MusicBrainz recording/track ID (Last.fm only)
···360306}
361307```
362308363363-## Processing Order
309309+## How It Works
364310365365-By default, records are processed **oldest first** (chronological order). This means your earliest scrobbles will appear first in your ATProto feed.
366366-367367-Use the `--reverse` or `-r` flag to process **newest first** instead.
311311+### Processing Flow
312312+1. **Parses input file(s)**:
313313+ - Last.fm: CSV using `csv-parse` library
314314+ - Spotify: JSON files (single or multiple in directory)
315315+2. **Filters data**:
316316+ - Spotify: Automatically removes podcasts, audiobooks, and non-music content
317317+3. **Converts to schema**: Maps to `fm.teal.alpha.feed.play` format
318318+4. **Sorts records**: Chronologically (oldest first) or reverse with `-r` flag
319319+5. **Generates TID-based keys**: From `playedTime` for chronological ordering
320320+6. **Validates fields**: Ensures required fields are present
321321+7. **Publishes in batches**: Uses `com.atproto.repo.applyWrites` (up to 200 records per call)
368322369369-## Multi-Day Imports
323323+### Rate Limiting Algorithm
324324+1. Calculates safe daily limit (75% of 10K = 7,500 records/day by default)
325325+2. Determines how many days needed for your import
326326+3. Calculates optimal batch size and delay to spread records evenly
327327+4. Enforces minimum delay between batches
328328+5. Shows clear schedule before starting
370329371371-For imports exceeding 1,000 records (after applying the 90% safety margin), the importer automatically:
330330+### Multi-Day Imports
372331332332+For imports exceeding the daily limit, the importer automatically:
3733331. **Calculates a schedule**: Splits your import across multiple days
3743342. **Shows the plan**: Displays which records will be imported each day
3753353. **Processes Day 1**: Imports the first batch of records
3763364. **Pauses 24 hours**: Waits a full day before continuing
3773375. **Repeats**: Continues until all records are imported
378338339339+**Example output for a 20,000 record import:**
340340+```
341341+📊 Rate Limiting Information:
342342+ Total records: 20,000
343343+ Daily limit: 7,500 records/day
344344+ Estimated duration: 3 days
345345+ Batch size: 200 records
346346+ Batch delay: 11.52s
347347+```
348348+379349**Important notes:**
380380-- You can safely stop (Ctrl+C) and restart the importer
381381-- Progress is preserved - it continues where it left off
350350+- You can safely stop (Ctrl+C) and restart
351351+- Progress is preserved - continues where it left off
382352- Each day's progress is clearly displayed
383353- Time estimates account for multi-day duration
384354385385-Example output for a 5,000 record import:
386386-```
387387-📊 Rate Limiting Information:
388388- Total records: 5,000
389389- Daily limit: 900 records/day
390390- Estimated duration: 6 days
391391- Batch size: 50 records
392392- Batch delay: 1920.0s
393393-```
355355+## Logging and Output
356356+357357+The importer uses color-coded output for clarity:
358358+359359+- **Green (✓)**: Success messages
360360+- **Cyan (→)**: Progress updates
361361+- **Yellow (⚠️)**: Warnings
362362+- **Red (✗)**: Errors
363363+- **Bold Red (🛑)**: Fatal errors
394364395395-## Dry Run Mode
365365+### Verbosity Levels
396366397397-Preview what will be imported without actually publishing:
367367+**Default Mode**: Standard operational messages
368368+```bash
369369+npm start -- -i lastfm.csv -h alice.bsky.social -p pass
370370+```
398371372372+**Verbose Mode** (`-v`): Detailed debug information including batch timing and API calls
399373```bash
400400-npm start -- -i lastfm.csv --dry-run
374374+npm start -- -i lastfm.csv -h alice.bsky.social -p pass -v
401375```
402376403403-Dry run shows:
404404-- Total record count
405405-- Rate limiting schedule (if applicable)
406406-- Multi-day import plan (if needed)
407407-- Preview of first 5 records with full details
408408-- MusicBrainz IDs when available
377377+**Quiet Mode** (`-q`): Only warnings and errors
378378+```bash
379379+npm start -- -i lastfm.csv -h alice.bsky.social -p pass -q
380380+```
409381410382## Error Handling
411383412384The importer is designed to be resilient:
413385414414-- **Network errors**: Records that fail are logged but don't stop the import
386386+- **Network errors**: Failed records are logged but don't stop the import
415387- **Invalid data**: Skipped with error messages
416388- **Authentication issues**: Clear error messages with suggested fixes
417389- **Rate limit hits**: Automatic adjustment and retry logic
418390- **Ctrl+C handling**: Gracefully stops after current batch
419391420420-Failed records are logged but don't prevent the rest of your import from completing.
392392+## Troubleshooting
393393+394394+### Authentication Issues
395395+396396+**"Handle not found"**
397397+- Verify your ATProto handle is correct (e.g., `alice.bsky.social`)
398398+- Ensure you're using a valid DID or handle
399399+400400+**"Invalid credentials"**
401401+- Use an **app password**, not your main account password
402402+- Generate app passwords in your account settings
403403+404404+### Performance Issues
405405+406406+**"Rate limit exceeded"**
407407+- The importer should prevent this automatically
408408+- If you see this, wait 24 hours before retrying
409409+- Consider reducing batch size with `-b` flag
410410+411411+**Import seems stuck**
412412+- Check progress messages - large imports take time
413413+- Multi-day imports pause for 24 hours between days
414414+- You can safely stop (Ctrl+C) and resume later
415415+- Use `--verbose` flag to see detailed progress
416416+417417+### Connection Issues
418418+419419+**"Connection refused"**
420420+- Check your internet connection
421421+- Verify your PDS is accessible
422422+- Some PDSs may have firewall rules
423423+424424+### Output Control
425425+426426+**Too much output**
427427+- Use `--quiet` flag to suppress non-essential messages
428428+- Only warnings and errors will be shown
429429+430430+**Need more details**
431431+- Use `--verbose` flag to see debug-level information
432432+- Shows batch timing, API calls, and detailed progress
433433+434434+## Development
435435+436436+```bash
437437+# Type checking
438438+npm run type-check
439439+440440+# Build
441441+npm run build
442442+443443+# Development mode (rebuild + run)
444444+npm run dev
445445+446446+# Run tests
447447+npm run test
448448+449449+# Clean build artifacts
450450+npm run clean
451451+```
421452422453## Project Structure
423454···433464│ │ ├── merge.ts # Combined import deduplication
434465│ │ └── sync.ts # Re-sync mode & duplicate detection
435466│ ├── utils/
436436-│ │ ├── logger.ts # Structured logging system (NEW!)
467467+│ │ ├── logger.ts # Structured logging system
437468│ │ ├── helpers.ts # Utility functions (timing, formatting)
438469│ │ ├── input.ts # User input handling (prompts, passwords)
439470│ │ ├── rate-limiter.ts # Rate limiting calculations
···448479│ └── play.json # Play record schema
449480├── package.json
450481├── tsconfig.json
451451-├── CLI_IMPROVEMENTS.md # Detailed CLI documentation
452482└── README.md
453483```
454484455455-## Development
456456-457457-```bash
458458-# Type checking
459459-npm run type-check
460460-461461-# Build
462462-npm run build
463463-464464-# Development mode (rebuild + run)
465465-npm run dev
466466-467467-# Clean build artifacts
468468-npm run clean
469469-```
470470-471485## Technical Details
472486473487### Authentication
···475489- Requires an ATProto app password (not your main password)
476490- Automatically configures the agent for your personal PDS
477491478478-### Rate Limiting Algorithm
479479-1. Calculates safe daily limit (90% of 1K = 900 records/day)
480480-2. Determines how many days needed for your import
481481-3. Calculates optimal batch size and delay to spread records evenly
482482-4. Enforces minimum 500ms delay between batches
483483-5. Shows clear schedule before starting
484484-485485-### Record Processing
486486-1. Parses input file(s):
487487- - **Last.fm**: CSV using `csv-parse` library
488488- - **Spotify**: JSON files (single or multiple in directory)
489489-2. Filters data:
490490- - **Spotify**: Automatically removes podcasts, audiobooks, and non-music content
491491-3. Converts to `fm.teal.alpha.feed.play` schema
492492-4. Sorts records chronologically (or reverse if `-r` flag)
493493-5. Generates TID-based record keys from `playedTime` for chronological ordering
494494-6. Validates required fields
495495-7. Publishes in batches using `com.atproto.repo.applyWrites` (up to 200 records per call, the PDS maximum)
496496-497497-**Note:** The batch publishing uses `applyWrites` instead of individual `createRecord` calls for dramatically improved performance (up to 20x faster).
492492+### Batch Publishing
493493+- Uses `com.atproto.repo.applyWrites` for efficiency (up to 20x faster than individual calls)
494494+- Batches up to 200 records per API call (PDS maximum)
495495+- Automatically adjusts batch size based on total record count
496496+- Enforces minimum delays between batches for rate limit safety
498497499498### Data Mapping
500499501500**Last.fm:**
502502-- **Track info**: Direct mapping from CSV columns
503503-- **Timestamps**: Converts Unix timestamps to ISO 8601
504504-- **MusicBrainz IDs**: Preserved when present in CSV
505505-- **URLs**: Generated from artist/track names
506506-- **Artists**: Wrapped in array format with optional MBID
501501+- Direct mapping from CSV columns
502502+- Converts Unix timestamps to ISO 8601
503503+- Preserves MusicBrainz IDs when present
504504+- Generates URLs from artist/track names
505505+- Wraps artists in array format with optional MBID
507506508507**Spotify:**
509509-- **Track info**: Extracted from JSON fields
510510-- **Timestamps**: Already in ISO 8601 format (`ts` field)
511511-- **URLs**: Generated from `spotify_track_uri` field
512512-- **Artists**: Extracted from `master_metadata_album_artist_name`
513513-- **Albums**: Extracted from `master_metadata_album_album_name`
514514-- **Filtering**: Non-music content automatically excluded
508508+- Extracts data from JSON fields
509509+- Already in ISO 8601 format (`ts` field)
510510+- Generates URLs from `spotify_track_uri`
511511+- Automatically filters non-music content
512512+- Extracts artist and album from metadata fields
515513516516-## Lexicon Reference
514514+### Lexicon Reference
517515518516This importer follows the official `fm.teal.alpha` lexicon defined in `/lexicons/fm.teal.alpha/feed/play.json`.
519517520520-The lexicon defines:
521521-- Required and optional field types
522522-- String length constraints
523523-- Array formats
524524-- Timestamp formatting
525525-- URL validation
526526-527527-## Troubleshooting
528528-529529-### "Handle not found"
530530-- Verify your ATProto handle is correct (e.g., `alice.bsky.social`)
531531-- Make sure you're using a valid DID or handle
532532-533533-### "Invalid credentials"
534534-- Use an **app password**, not your main account password
535535-- Generate app passwords in your account settings
536536-537537-### "Rate limit exceeded"
538538-- The importer should prevent this automatically
539539-- If you see this, wait 24 hours before retrying
540540-- Consider reducing batch size or increasing delay
541541-542542-### "Connection refused"
543543-- Check your internet connection
544544-- Verify your PDS is accessible
545545-- Some PDSs may have firewall rules
546546-547547-### Import seems stuck
548548-- Check progress messages - large imports take time
549549-- Multi-day imports pause for 24 hours between days
550550-- You can safely stop (Ctrl+C) and resume later
551551-- Use `--verbose` flag to see detailed progress
552552-553553-### Too much output
554554-- Use `--quiet` flag to suppress non-essential messages
555555-- Only warnings and errors will be shown
556556-557557-### Need more details
558558-- Use `--verbose` flag to see debug-level information
559559-- Shows batch timing, API calls, and detailed progress
518518+The lexicon defines required and optional field types, string length constraints, array formats, timestamp formatting, and URL validation.
560519561520## Contributing
562521563563-Contributions welcome! Please:
522522+Contributions are welcome! Please:
5645231. Fork the repository
5655242. Create a feature branch
5665253. Make your changes with tests
5675264. Submit a pull request
568527569569-See `CLI_IMPROVEMENTS.md` for developer documentation on the logging system and CLI structure.
570570-571528## License
572529573530AGPL-3.0-only - See LICENCE file for details
···579536- Identity resolution via [Slingshot](https://slingshot.danner.cloud)
580537- Follows the `fm.teal.alpha` lexicon standard
581538- Colored output via [chalk](https://www.npmjs.com/package/chalk)
539539+- Progress indicators via [ora](https://www.npmjs.com/package/ora) and [cli-progress](https://www.npmjs.com/package/cli-progress)
582540583541---
584542585585-**Note**: This tool is for personal use. Respect the terms of service and rate limits when exporting your data.543543+**Note**: This tool is for personal use. Respect the terms of service and rate limits when importing your data.