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`