···11+MIT License
22+33+Permission is hereby granted, free of charge, to any person obtaining a copy
44+of this software and associated documentation files (the "Software"), to deal
55+in the Software without restriction, including without limitation the rights
66+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
77+copies of the Software, and to permit persons to whom the Software is
88+furnished to do so, subject to the following conditions:
99+1010+The above copyright notice and this permission notice shall be included in all
1111+copies or substantial portions of the Software.
1212+1313+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1414+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1515+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1616+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1717+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1818+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1919+SOFTWARE.
+103
README.md
···11+# Lazuli
22+33+Import Last.fm and Spotify listening history to teal.fm.
44+55+## Overview
66+77+Lazuli is a command-line tool that parses listening history exports from Last.fm and Spotify, merges them to remove duplicates, and publishes them to teal.fm as `fm.teal.alpha.feed.play` records.
88+99+Re-written from <https://tangled.org/ewancroft.uk/atproto-lastfm-importer>.
1010+1111+## Usage
1212+1313+### Authentication
1414+1515+Set your Bluesky credentials via environment variables or flags:
1616+1717+```sh
1818+export LAZULI_HANDLE="your-handle.bsky.social"
1919+export LAZULI_PASSWORD="your-app-password"
2020+```
2121+2222+### Commands
2323+2424+#### Export
2525+2626+Parse and merge Last.fm/Spotify exports, output as JSON:
2727+2828+```sh
2929+lazuli export --lastfm=history.csv --spotify=streaming_history.json --output=merged.json
3030+```
3131+3232+#### Import
3333+3434+Import listening history to Bluesky with proper rate limiting:
3535+3636+```sh
3737+lazuli import --lastfm=history.csv --spotify=streaming_history.json
3838+```
3939+4040+Resume is automatic - if interrupted, re-running skips already-imported records:
4141+4242+Import modes:
4343+4444+- `lastfm` - Import only Last.fm data
4545+- `spotify` - Import only Spotify data
4646+- `combined` - Merge both sources (default)
4747+4848+#### Sync
4949+5050+Fetch existing records and show statistics:
5151+5252+```sh
5353+lazuli sync
5454+```
5555+5656+#### Dedupe
5757+5858+Find and remove duplicate records from your Bluesky profile:
5959+6060+```sh
6161+lazuli dedupe --dry-run # Preview without deleting
6262+```
6363+6464+## Environment Variables
6565+6666+| Variable | Description |
6767+| ----------------- | -------------------------------------------- |
6868+| `LAZULI_HANDLE` | Bluesky handle (e.g., `user.bsky.social`) |
6969+| `LAZULI_PASSWORD` | Bluesky app password |
7070+| `LAZULI_LASTFM` | Path to Last.fm CSV file |
7171+| `LAZULI_SPOTIFY` | Path to Spotify JSON file/directory/zip |
7272+| `LAZULI_MODE` | Import mode: `lastfm`, `spotify`, `combined` |
7373+| `LAZULI_DRY_RUN` | Preview without publishing |
7474+| `LAZULI_VERBOSE` | Enable verbose logging |
7575+| `LAZULI_REVERSE` | Process records in reverse order |
7676+7777+## Input Formats
7878+7979+### Last.fm
8080+8181+Export your listening history from Last.fm. The CSV file should have columns:
8282+8383+- UTC timestamp
8484+- Artist name
8585+- Album name
8686+- Track name
8787+- MusicBrainz IDs (optional)
8888+8989+### Spotify
9090+9191+Download your extended streaming history from Spotify (Privacy settings). Lazuli accepts:
9292+9393+- Single JSON files
9494+- Directories containing `Streaming_History_Audio_*.json` files
9595+- ZIP archives of the above
9696+9797+## Features
9898+9999+- **Cross-source deduplication**: Merges Last.fm and Spotify data, removing duplicates within a configurable time tolerance
100100+- **Rate limiting**: Respects ATProto rate limits with configurable batch sizes and delays
101101+- **Automatic resume**: Cache tracks imported records - re-running skips already-imported entries
102102+- **Dry-run mode**: Preview imports without publishing
103103+- **Cache management**: Caches teal records for faster subsequent operations