music on atproto
plyr.fm
1# pdsx guide for plyr.fm
2
3[pdsx](https://github.com/zzstoatzz/pdsx) is a CLI tool for ATProto record operations. this guide covers how to use it for inspecting and managing plyr.fm track records.
4
5## installation
6
7```bash
8# use uvx for one-off commands (auto-updates)
9uvx pdsx --version
10
11# or install globally
12uv tool install pdsx
13```
14
15## authentication vs unauthenticated reads
16
17### unauthenticated reads (public data)
18
19use `-r` flag with handle or DID:
20
21```bash
22# read from bluesky PDS (default)
23uvx pdsx -r zzstoatzzdevlog.bsky.social ls fm.plyr.track
24
25# read from custom PDS (requires --pds flag)
26uvx pdsx --pds https://pds.zzstoatzz.io -r zzstoatzz.io ls fm.plyr.track
27```
28
29**NOTE** pds.zzstoatzz.io is JUST AN EXAMPLE of a custom PDS for the specific case of zzstoatzz.io. each user has their own PDS URL, whether bsky.social or custom.
30
31**important**: unauthenticated reads assume bsky.social PDS by default. for custom PDS instances (like zzstoatzz.io), you **must** provide `--pds` explicitly.
32
33### authenticated operations (write access)
34
35use `--handle` and `--password` flags:
36
37```bash
38# for bluesky users (auto-discovers PDS)
39uvx pdsx --handle you.bsky.social --password xxxx-xxxx ls fm.plyr.track
40
41# creates records, updates, etc.
42uvx pdsx --handle you.bsky.social --password xxxx-xxxx create fm.plyr.track title='test'
43```
44
45**note**: authenticated operations auto-discover PDS from handle, so you don't need `--pds` flag when using `--handle` and `--password`.
46
47## common operations
48
49### list all tracks for a user
50
51```bash
52# unauthenticated read (public)
53uvx pdsx --pds https://pds.zzstoatzz.io -r zzstoatzz.io ls fm.plyr.track
54
55# authenticated (shows your own tracks)
56uvx pdsx --handle zzstoatzzdevlog.bsky.social --password "$ATPROTO_PASSWORD" ls fm.plyr.track
57```
58
59### inspect a specific track
60
61track URIs have the format: `at://did/collection/rkey`
62
63```bash
64# get full record details
65uvx pdsx --pds https://pds.zzstoatzz.io cat at://did:plc:xbtmt2zjwlrfegqvch7fboei/fm.plyr.track/3m5a4wg7i352p
66```
67
68### find tracks with specific criteria
69
70use shell tools to filter:
71
72```bash
73# find tracks with images
74uvx pdsx --pds https://pds.zzstoatzz.io -r zzstoatzz.io ls fm.plyr.track | grep imageUrl
75
76# find tracks with features
77uvx pdsx --pds https://pds.zzstoatzz.io -r zzstoatzz.io ls fm.plyr.track | grep features
78
79# count total tracks
80uvx pdsx --pds https://pds.zzstoatzz.io -r zzstoatzz.io ls fm.plyr.track | head -1
81```
82
83## debugging orphaned/stale records
84
85### scenario 1: tracks in database but no ATProto records
86
87```bash
88# 1. check what records exist on PDS
89uvx pdsx --pds https://pds.zzstoatzz.io -r zzstoatzz.io ls fm.plyr.track
90
91# 2. query database for tracks with that artist_did
92# (use neon MCP or direct psql)
93
94# 3. compare - any tracks in DB without atproto_record_uri are orphaned
95```
96
97### scenario 2: stale URIs pointing to old namespace
98
99```bash
100# check for old namespace records (should be none in fm.plyr.track)
101uvx pdsx --pds https://pds.zzstoatzz.io -r zzstoatzz.io ls app.relay.track
102
103# if you find any, those are stale and should be migrated
104```
105
106### scenario 3: verify record contents match database
107
108```bash
109# get a specific record
110uvx pdsx --pds https://pds.zzstoatzz.io cat at://did:plc:xbtmt2zjwlrfegqvch7fboei/fm.plyr.track/3m5a4wg7i352p
111
112# compare imageUrl, features, album, etc. with database values
113# mismatches indicate failed updates
114```
115
116## atproto namespace
117
118plyr.fm uses environment-specific namespaces configured via `ATPROTO_APP_NAMESPACE`:
119- **dev**: `fm.plyr.dev` → track collection: `fm.plyr.dev.track`
120- **staging**: `fm.plyr.stg` → track collection: `fm.plyr.stg.track`
121- **prod**: `fm.plyr` → track collection: `fm.plyr.track`
122
123**critical**: never use bluesky lexicons (app.bsky.*) for plyr.fm records. always use fm.plyr.* namespace.
124
125when using pdsx with dev environment, query `fm.plyr.dev.track`, not `fm.plyr.track`.
126
127## credential management
128
129store credentials in `.env`:
130
131```bash
132# dev log account (test operations)
133ATPROTO_HANDLE=zzstoatzzdevlog.bsky.social
134ATPROTO_PASSWORD=your-app-password
135
136# main account (backfills, migrations)
137ATPROTO_MAIN_HANDLE=zzstoatzz.io
138ATPROTO_MAIN_PASSWORD=your-app-password
139```
140
141use in scripts:
142
143```python
144from pydantic_settings import BaseSettings, SettingsConfigDict
145
146class Settings(BaseSettings):
147 model_config = SettingsConfigDict(
148 env_file=".env",
149 extra="ignore",
150 )
151
152 handle: str = Field(validation_alias="ATPROTO_HANDLE")
153 password: str = Field(validation_alias="ATPROTO_PASSWORD")
154```
155
156## workflow examples
157
158### verify backfill success
159
160after running `scripts/backfill_atproto_records.py`:
161
162```bash
163# 1. check how many records were created
164uvx pdsx --pds https://pds.zzstoatzz.io -r zzstoatzz.io ls fm.plyr.track | head -1
165
166# 2. verify specific tracks have correct data
167uvx pdsx --pds https://pds.zzstoatzz.io -r zzstoatzz.io ls fm.plyr.track | grep -E "webhook|geese"
168
169# 3. confirm imageUrl present for tracks that should have it
170uvx pdsx --pds https://pds.zzstoatzz.io cat at://did:plc:xbtmt2zjwlrfegqvch7fboei/fm.plyr.track/3m5a4wg7i352p | grep imageUrl
171```
172
173### clean up test records
174
175```bash
176# list test records to get their rkeys
177uvx pdsx --handle zzstoatzzdevlog.bsky.social --password "$ATPROTO_PASSWORD" ls fm.plyr.track | grep test
178
179# delete by URI
180uvx pdsx --handle zzstoatzzdevlog.bsky.social --password "$ATPROTO_PASSWORD" rm at://did:plc:pmz4rx66ijxzke6ka5o3owmg/fm.plyr.track/3m57zgph47z2w
181```
182
183### compare database vs atproto records
184
185when debugging sync issues, you need to compare both sources:
186
187```bash
188# 1. get record count from PDS
189uvx pdsx --pds https://pds.zzstoatzz.io -r zzstoatzz.io ls fm.plyr.track | head -1
190# output: "found 15 records"
191
192# 2. get record count from database (use neon MCP)
193# SELECT COUNT(*) FROM tracks WHERE artist_did = 'did:plc:xbtmt2zjwlrfegqvch7fboei'
194
195# 3. if counts don't match, list all records to find missing ones
196uvx pdsx --pds https://pds.zzstoatzz.io -r zzstoatzz.io ls fm.plyr.track | grep -E "rkey|title"
197```
198
199## known limitations
200
2011. **custom PDS requires explicit flag for unauthenticated reads** ([#30](https://github.com/zzstoatzz/pdsx/issues/30)):
202 ```bash
203 # currently won't work - defaults to bsky.social
204 uvx pdsx -r zzstoatzz.io ls fm.plyr.track
205
206 # workaround: use explicit --pds flag
207 uvx pdsx --pds https://pds.zzstoatzz.io -r zzstoatzz.io ls fm.plyr.track
208 ```
209
2102. **cat command requires full AT-URI format** ([#31](https://github.com/zzstoatzz/pdsx/issues/31)):
211 ```bash
212 # currently required
213 uvx pdsx cat at://did:plc:abc/fm.plyr.track/xyz
214
215 # shorthand not yet supported
216 uvx pdsx cat fm.plyr.track/xyz
217 ```
218
2193. **flag order matters**: `-r`, `--handle`, `--password`, `--pds` must come BEFORE the command (ls, cat, etc.)
220 ```bash
221 # correct
222 uvx pdsx -r zzstoatzz.io ls fm.plyr.track
223
224 # wrong
225 uvx pdsx ls -r zzstoatzz.io fm.plyr.track
226 ```
227
228## troubleshooting
229
230### "BadJwtSignature" errors
231
232this usually means you're querying the wrong PDS for the user's DID.
233
234**root cause**: each user's ATProto identity (DID) is hosted on a specific PDS. trying to read records from the wrong PDS results in signature errors.
235
236**solution**: use the --help flag, and if it doesn't explain that pdsx can resolve the users PDS from their DID, then open an upstream issue but you can resolve the user's PDS URL from their DID using the [PLC directory](../backend/atproto-identity.md).
237
238```bash
239# resolve PDS for a DID
240curl -s "https://plc.directory/did:plc:xbtmt2zjwlrfegqvch7fboei" | jq -r '.service[] | select(.type == "AtprotoPersonalDataServer") | .serviceEndpoint'
241# output: https://pds.zzstoatzz.io
242
243# then use that PDS with pdsx
244uvx pdsx --pds https://pds.zzstoatzz.io -r zzstoatzz.io ls fm.plyr.track
245```
246
247**quick reference**:
248- bluesky users: usually `https://bsky.social` (default, no flag needed)
249- custom PDS users: must resolve via PLC directory and provide `--pds` flag
250
251### "could not find repo" errors
252
253this means:
254- DID/handle doesn't exist on the queried PDS
255- using wrong PDS (bsky.social vs custom)
256
257solution: verify handle and add correct `--pds` flag.
258
259### empty results when you expect records
260
261check:
2621. are you querying the right PDS? (`--pds` flag)
2632. are you querying the right collection? (`fm.plyr.track` not `app.relay.track`)
2643. does the user actually have records? (check database)
265
266## references
267
268- pdsx releases: https://github.com/zzstoatzz/pdsx/releases
269- ATProto specs: https://atproto.com
270- plyr.fm track schema: `src/backend/_internal/atproto/records.py:build_track_record`