like malachite (atproto-lastfm-importer) but in go and bluer
go
spotify
tealfm
lastfm
atproto
1# Lazuli
2
3Import Last.fm and Spotify listening history to teal.fm.
4
5## Overview
6
7Lazuli 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.
8
9Re-written from <https://tangled.org/ewancroft.uk/atproto-lastfm-importer>.
10
11## Usage
12
13### Authentication
14
15Set your Bluesky credentials via environment variables or flags:
16
17```sh
18export LAZULI_HANDLE="your-handle.bsky.social"
19export LAZULI_PASSWORD="your-app-password"
20```
21
22### Commands
23
24| Command | Usage |
25| :------- | :------------------------------------------------------- |
26| `export` | Parse and merge Last.fm/Spotify exports into a JSON file |
27| `import` | Import new records to Bluesky (auto-skips existing) |
28| `sync` | Refresh the local cache with records from Bluesky |
29| `stats` | Show database status and daily rate limit consumption |
30| `failed` | List records that failed to import |
31| `retry` | Attempt to re-import failed records |
32| `dedupe` | Remove duplicate records from your Bluesky profile |
33| `debug` | Dump raw records from Bluesky for troubleshooting |
34
35### Advanced Options
36
37- **Rate Limiting**: Lazuli automatically respects Bluesky/ATProto rate limits (default 9,000 writes/day). Use `lazuli stats` to see your remaining quota.
38- **Automatic Resume**: The local cache tracks which records were successfully imported. If a process is interrupted, re-running the same command will skip already-published entries.
39- **Output Formats**: Most commands support `--output-format=json` for machine-readable output.
40- **Fresh Sync**: Use `--fresh` to bypass the local cache and fetch everything directly from the server.
41- **CAR Export**: Use `--car` with `sync` and `dedupe` commands to fetch the entire repository via `com.atproto.sync.getRepo` (much faster for large repos, uses a single API call).
42
43## Environment Variables
44
45| Variable | Description |
46| ----------------- | ----------------------------------------- |
47| `LAZULI_HANDLE` | Bluesky handle (e.g., `user.bsky.social`) |
48| `LAZULI_PASSWORD` | Bluesky app password |
49| `LAZULI_LASTFM` | Path to Last.fm CSV file |
50| `LAZULI_SPOTIFY` | Path to Spotify JSON file/directory/zip |
51| `LAZULI_DRY_RUN` | Preview without publishing |
52| `LAZULI_VERBOSE` | Enable verbose logging |
53| `LAZULI_REVERSE` | Process records in reverse order |
54| `LAZULI_USE_CAR` | Use CAR export for sync/dedupe (1/true) |
55
56## Input Formats
57
58### Last.fm
59
60Export your listening history from Last.fm. The CSV file should have columns:
61
62- UTC timestamp
63- Artist name
64- Album name
65- Track name
66- MusicBrainz IDs (optional)
67
68### Spotify
69
70Lazuli is designed to work with your **Extended Streaming History** from Spotify. You can request this from your [Spotify Privacy Settings](https://www.spotify.com/account/privacy/).
71
72The recommended way to use Spotify data is by passing the **ZIP archive** you receive from Spotify directly. Lazuli will automatically find and parse all streaming history files within it.
73
74Lazuli accepts:
75
76- **ZIP archives** containing extended history (Recommended)
77- Directories containing `Streaming_History_Audio_*.json` files
78- Single `Streaming_History_Audio_*.json` files
79
80> [!IMPORTANT]
81> Make sure to request "Extended streaming history", as the standard "Account data" export does not contain your full listening history.
82
83## Features
84
85- **Cross-source deduplication**: Merges Last.fm and Spotify data, removing duplicates within a configurable time tolerance
86- **Rate limiting**: Respects ATProto rate limits with configurable batch sizes and delays
87- **Automatic resume**: Cache tracks imported records - re-running skips already-imported entries
88- **Dry-run mode**: Preview imports without publishing
89- **Cache management**: Caches teal records for faster subsequent operations