Skip to content

contributing

plyr.fm is open source. development happens on GitHub (mirrored to tangled.org). contributions welcome — fork the repo and open a PR.

give your assistant the project’s llms.txt. Preview it here, copy the full text with one click, then tell your assistant what you want to work on.

llms.txt
# plyr.fm

> An open-source audio streaming app on ATProto. Discover music and other audio,
> inspect tracks and playlists, and publish with the creator's authorization.
> Public records live on the user's PDS; private playlists stay in plyr.fm.

## Start here

- [Agent guide](https://docs.plyr.fm/developers/agents/): choose a surface, discover capabilities, search, inspect, and verify results.
- [Agent guide as Markdown](https://docs.plyr.fm/agents.md): the same guide without the documentation UI.
- [Developer quickstart](https://docs.plyr.fm/developers/quickstart/): working Python SDK and HTTP examples.
- [OpenAPI JSON](https://api.plyr.fm/openapi.json): current HTTP paths, parameters, response schemas, and authentication requirements.
- [Interactive API reference](https://api.plyr.fm/docs): browse the live schema.

## Choose a surface

- HTTP: `https://api.plyr.fm`. Choose it for other languages, exact response schemas, or API-only features. Public search and track metadata need no token.
- Python SDK: `uv add plyrfm` for typed, composable sync/async workflows. Operations use namespaces: `client.discover.search(...)`, `client.tracks.get(...)`, `client.playlists.list()`.
- CLI: `uvx plyrfm --help` for terminal reads and authorized uploads, edits, and library changes. Use SDK objects or HTTP JSON for programmatic output.
- Hosted MCP: `https://plyrfm.fastmcp.app/mcp`. Discover its tools at connection time. It exposes read-only catalog and library operations; it does not play audio in the user's browser or mutate their library.
- Local MCP: `uvx --prerelease=allow plyrfm-mcp`. Optional `PLYR_TOKEN` enables the user's private library reads; the hosted server accepts `x-plyr-token`.
- App: [plyr.fm](https://plyr.fm) for listening, publishing, and account settings.

## Search, inspect, then act

1. `GET https://api.plyr.fm/search/?q=ambient&type=tracks&limit=5`
2. Read `results` and its `type` discriminator. `counts` describes this response, not the full catalog. Search takes a 2–100 character query and a limit of 1–50 per type; narrow the query rather than inventing an offset.
3. `GET https://api.plyr.fm/tracks/{id}` for a chosen track. Inspect its title, artist, tags, visibility, gating, and labels. Search hits are summaries, not full track records.
4. Share `https://plyr.fm/track/{id}`. HTTP track detail returns its audio URL in `r2_url`; the Python SDK calls it `audio_url`. Use that returned URL, preserving any access requirements. There is no `/tracks/{id}/stream` endpoint.

A metadata response is not proof that audio played. A successful audio fetch is
not proof that the listener heard it. Unknown duration or missing metadata remains
unknown; don't invent a value or describe the sound from its title alone.

## Account actions and visibility

- [Authentication](https://docs.plyr.fm/developers/auth/): developer tokens come from [settings](https://plyr.fm/settings#developer). Browser sessions use HttpOnly cookies. Keep tokens out of URLs, logs, and public files.
- Uploads, likes, comments, playlist edits, and deletions can change the user's library or PDS. Use the SDK, CLI, or HTTP API only with authorization for the action; read the affected resource afterward to verify it.
- Public playlists publish ATProto records; private playlists remain in plyr.fm's database. Experimental private tracks use permissioned data on a compatible PDS. Supporter gating is a separate access model.
- [Moderation](https://docs.plyr.fm/moderation/): discovery filtering differs from direct access. Labels are assertions; a successful metadata lookup does not waive access controls or authorize downloading.

## Contributing to plyr.fm

- [Contributor guide](https://docs.plyr.fm/contributing/): local setup and the contribution workflow.
- [Project status](https://github.com/zzstoatzz/plyr.fm/blob/main/STATUS.md): read first for active work and known issues.
- [Repository instructions](https://github.com/zzstoatzz/plyr.fm/blob/main/AGENTS.md): project conventions, commands, and deployment boundaries.
- [Contribution skill](https://github.com/zzstoatzz/plyr.fm/blob/main/.agents/skills/contribute/SKILL.md): fork, implement, validate, and prepare a pull request. Read the checked-out repository's instructions before editing; use its justfiles for commands.

## Guides and source

- [Listeners](https://docs.plyr.fm/listeners/): player, queue, playlists, downloads, and shortcuts.
- [Creators](https://docs.plyr.fm/artists/): uploads, artwork, embeds, visibility, and export.
- [Sensitive content](https://docs.plyr.fm/sensitive-content/): artwork and audio preferences.
- [Lexicons](https://docs.plyr.fm/lexicons/overview/): environment-aware ATProto records; production uses `fm.plyr`, staging `fm.plyr.stg`, development `fm.plyr.dev`. NSIDs are identifiers, not URLs.
- [Troubleshooting](https://docs.plyr.fm/troubleshooting/): authentication, playback, and indexing.
- [App source](https://github.com/zzstoatzz/plyr.fm)
- [SDK, CLI, and MCP source](https://github.com/zzstoatzz/plyr-python-client)

## publishing access

Portal defaults apply to future uploads; track and album overrides save explicit policies. Listening, original downloads, discovery, and rights metadata are separate. Protected managed audio uses private storage; native Spaces retain their authority boundary. Downloads off does not prevent recording playback or revoke previous copies. See https://docs.plyr.fm/developers/publishing/ for the current contract and client migration.

For repository work, use the repository’s contribute skill for richer agent context. Shared skills live in .agents/skills, which Codex discovers automatically; Claude Code reads the same files through .claude/skills symlinks.

  • uv (Python 3.11+)
  • bun (frontend)
  • just (task runner)
  • docker (for Redis and test databases)
Terminal window
# fork on github, then clone your fork
gh repo fork zzstoatzz/plyr.fm --clone
cd plyr.fm
# install dependencies
uv sync --directory backend
cd frontend && bun install && cd ..
# configure environment
cp backend/.env.example backend/.env
cp frontend/.env.example frontend/.env
# edit backend/.env — see the setup guide linked below for details
# frontend/.env defaults to the local backend on :8001 and works as-is

setup db (if running a local Postgres instance)

Section titled “setup db (if running a local Postgres instance)”
Terminal window
just backend db-init
just backend migrate-up
Terminal window
# start redis (required for background tasks)
just dev-services
# terminal 1 — backend (port 8001, hot reloads)
just backend run
# terminal 2 — frontend (port 5173, hot reloads)
just frontend run

the backend needs a Postgres connection. you can use the Neon dev instance or a local Postgres — set DATABASE_URL in your .env. see backend/.env.example for all configuration options and the local development setup guide for detailed walkthrough.

  1. check STATUS.md for active tasks
  2. open an issue describing the change
  3. fork the repo, branch from main, make your changes
  4. open a PR from your fork
Terminal window
just backend run # start backend
just frontend run # start frontend
just dev-services # start redis
just backend test # run tests (spins up isolated postgres + redis)
just backend lint # type check + ruff
just frontend check # svelte type check
just backend db-init # setup local postgres database
just backend migrate-up # apply database migrations
  • type hints required everywhere (Python and TypeScript)
  • async everywhere — never block the event loop
  • writing: concise, conversational, and usually lowercase; preserve proper names and technical identifiers
  • SvelteKit with Svelte 5 Runes ($state, $derived, $effect)
  • use uv for Python (never pip)
  • add regression tests when fixing bugs

detailed internal documentation (environment setup, deployment, architecture) is in docs/internal/.