You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(sync): replicate history via copy instead of running SQLite live in iCloud
Looper now always reads and writes a fast local DB at the platform data
directory. When a sync folder is configured, it pulls from there at
startup (if the cloud copy has more recent activity by max(last_played_at))
and pushes back on quit via WAL checkpoint + atomic temp+rename.
Previously the cloud folder hosted the live DB, which led to:
- "authorization denied" crashes when macOS TCC blocked iCloud mid-session
- WAL/SHM corruption from iCloud syncing live SQLite sidecars
- Hangs while iCloud rehydrated an evicted database file
iCloud Drive is no longer auto-detected; opt in explicitly via
`looper config set sync-folder`. Failed pulls surface a SyncWarning
banner; failed pushes log a stderr warning instead of crashing on quit.
Drops the now-dead merge_old_db_into + its tests; replaces with three
tests covering pull-skip / pull-replace / push-create semantics. README's
sync section is rewritten to match, including the macOS TCC requirement
for iCloud Drive replication.
Copy file name to clipboardExpand all lines: README.md
+28-58Lines changed: 28 additions & 58 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -141,94 +141,64 @@ Playback history and favorites live in a SQLite database (`looper.sqlite3`). Whe
141
141
142
142
## Cross-Device Sync
143
143
144
-
looper keeps history in sync across your computers by storing`looper.sqlite3` in a shared folder (iCloud Drive, Dropbox, or any folder you choose). No account, no server — just a file in a folder you already sync.
144
+
looper always reads and writes a fast local copy of`looper.sqlite3`. If you point it at a cloud folder (iCloud Drive, Dropbox, anything that syncs files), looper will pull from that folder at startup and push to it on quit. The cloud folder never holds the live database — it's just a passive copy that the cloud provider replicates between your machines on its own.
145
145
146
-
### How it picks where to store the database
146
+
This avoids the long-standing footguns of running SQLite directly on a cloud-synced filesystem (corrupted WAL/SHM sidecars, surprise permission denials, evicted files).
147
147
148
-
On every launch, looper resolves the database location in this order:
148
+
### Where the live database lives
149
149
150
-
1.**Configured sync folder** — if you've run `looper config set sync-folder`, that path wins
151
-
2.**iCloud Drive (macOS only)** — if iCloud Drive is active, looper automatically uses `~/Library/Mobile Documents/com~apple~CloudDocs/looper/looper.sqlite3`
152
-
3.**Platform default** — fallback when neither of the above applies
153
-
154
-
| Platform | Default database path |
155
-
|----------|-----------------------|
156
-
| macOS (no iCloud) |`~/Library/Application Support/sh.kbr.looper/looper.sqlite3`|
The first time looper tries to read or write inside `~/Library/Mobile Documents/...`, macOS will silently deny access until you grant the terminal app that launches looper permission. Open **System Settings → Privacy & Security → Files and Folders** (or **Full Disk Access**) and toggle on **iCloud Drive** for your terminal (Terminal, iTerm, Ghostty, etc.). Restart the terminal so the new entitlement takes effect.
207
183
208
-
### Two-computer upgrade scenario
184
+
If permission isn't granted, looper still runs against the local DB, surfaces a `History sync disabled` banner at startup, and prints a one-line warning to stderr on quit. Nothing crashes.
209
185
210
-
If both computers already have local history from an older version of looper:
186
+
### Sync semantics: last-quitter wins
211
187
212
-
1.**Computer A** upgrades → detects iCloud (or configured folder) → merges its old local history in → archives old file
213
-
2. iCloud syncs to Computer B (the database now has A's history)
214
-
3.**Computer B** upgrades → opens the iCloud file (already has A's data) → merges its own old local history in
188
+
Replication is a file copy in both directions:
215
189
216
-
Result: the shared database has all plays from both computers, tagged with the machine that played each track. No data is lost.
190
+
-**At startup**: if the cloud copy has a more recent `MAX(last_played_at)` than the local copy, looper replaces local with the cloud copy.
191
+
-**At quit**: looper checkpoints the WAL, then atomically replaces the cloud copy with the local one.
217
192
218
-
### Merge rules (when two histories combine)
193
+
This is enough for one-human-at-a-time use across multiple Macs (typical single-user setup). It is **not** a general-purpose multi-master merge: if you play on two machines simultaneously, whichever quits last overwrites the other's session, and your cloud provider may produce conflict copies (e.g. `looper.sqlite3 conflicted-copy 2`). Resolve by closing one, picking the version you want to keep, and deleting the rest.
219
194
220
-
| Field | Result |
221
-
|-------|--------|
222
-
| Play count | Sum of both |
223
-
| Time played | Sum of both |
224
-
| First played | Earliest of both |
225
-
| Last played | Latest of both |
226
-
| Favorite |`true` if either copy is favorited |
227
-
| Last played on | Computer with the more recent play |
195
+
### Disable replication
228
196
229
-
### A note on concurrent access
197
+
```shell
198
+
rm "$HOME/.config/looper/sync_folder"
199
+
```
230
200
231
-
looper enables WAL mode on the database, which makes it safe for one machine to read while another writes. Playing on two machines simultaneously and writing to the same file is unusual and could cause conflicts — for typical single-user use (one active machine at a time) this is not an issue.
201
+
Or just don't set it. Looper falls back to local-only without complaint.
0 commit comments