# Open Music Collection: instructions for agents

> You are helping someone export, import, store or serve a music collection in the **Open Music Collection (OMC)** file format, or build an app that reads and changes a collection through the OMC Access protocol.
> Read this whole file before you write code: it is short. The normative specification is https://www.tangle.music/omc/spec/ and the JSON Schema is https://www.tangle.music/omc/schema/core-1.0.schema.json.

## What OMC is

An open standard for a person's music collection: the songs and albums they keep (**items**), where each one lives (**copies** on **sources**), and the **lists** they organise them into. The OMC file format (`.omc.ndjson`) is newline-delimited JSON: a header line, then one record per line. A collection has one **home** (the service that keeps it); **apps** connect to it; **sources** (Spotify, Bandcamp, a folder) hold their own copy, which the home keeps in step with.

## The model in one screen

| Record | Required fields | Notes |
|---|---|---|
| header | `type: "omc"`, `version: "1.0"`, `generated_at` | Always the first line. Optional `name`, `counts` (`items`, `recordings`, `releases`, …), `modules` (only modules whose data the file holds) |
| source | `type`, `id`, `platform`, `name` | `platform` is a registered name (`spotify`, `bandcamp`, `local`, …) or a host name with a `descriptor` URL. `account` is the platform's stable account id, `account_name` the name to show |
| item | `type`, `id`, `kind` (`recording` or `release`), `title`, `artists`, `copies` | Platform-independent ids (`isrc`, `upc`, `musicbrainz`, `iswc`) go in `ids`. `note` is the person's own text. `key` is ID3 `TKEY` (`Am`, `F#`, `Dbm`). Also `composers`, `album_artists`, `release_type` (releases). `play_count` and `last_played_at` are the home's, read-only |
| copy (inside an item) | `source`, `ref` | `ref` is the source's own id, in the form the platform registry gives (Bandcamp: the numeric id; Spotify: the base62 id). `ownership`: `none`, `digital`, `physical`, `file`. A recording's copy names its `release` item. `file` may carry `audio_hash` and `fingerprint` |
| list | `type`, `id`, `name`, `ordered`, `entries` | `entries: [{ "item": "<item id>", "note": "…" }]`. Optional `role`: `library`, `saved`, `wanted`, `curated`, `history`. A folder is `folder: true` with no entries; lists name it in `parent` and sort by `position`. `color` makes a tag a colour; `filter` makes a smart list |

Write sources first, then items, then lists. Readers must accept any order.

## Rules for the file

- **Keep what you don't understand.** When you read a record and write it back, keep every field you don't know, anywhere in it (including `ext`), and records of types you don't know. Never "clean up" unknown fields. See https://www.tangle.music/omc/spec/#extensions-and-unknown-data.
- **Ids are local.** An id means something only inside its collection. Keep ids stable across exports from the same home, never parse them, and never compare them across collections.
- **Combining collections matches by content.** Sources by `platform` and `account` (two `local` sources never match on their own), copies by source and `ref`, items by a shared copy, then `musicbrainz`, `isrc` or `upc`, then the same `audio_hash` (recordings) or `label` and `catalog_number` (releases, except represses), and by fingerprint or title and artists only as a suggestion the person confirms. Settle conflicting values by asking the person or by the rule they picked, never by comparing timestamps. See https://www.tangle.music/omc/spec/#combining-collections.
- **Platform ids go on copies.** A Spotify track id is a copy's `ref`; `ids` is only for identifiers that mean the same everywhere. On sources with no track ids (Discogs), a track's `ref` is `<release ref>/<position>` (`28993732/A1`).
- **A collection may be all releases.** Recordings are optional; a release must make sense without them. Saved albums are a list of releases with `role: "saved"`, a wantlist `role: "wanted"`.
- **Don't store access.** Never write "streamable" or "available" into a collection. Store ownership, which is a claim; ask the source at play time.
- **Don't put prices in a collection.** Prices and offers belong to the Catalog and Commerce module; what the person paid belongs to the Collector module.
- **Roles are only for a platform's built-in lists.** Liked songs or favourites: `saved`. A platform's whole collection: `library`. A wantlist: `wanted`. The person's own lists: no role.
- **Values of your own are namespaced.** Roles, kinds, ownership values, formats and provenance origins are open lists. A value that isn't in the specification is written in your namespace (`"role": "com.yourapp/backlog"`); everyone keeps it, and apps that don't know it show the nearest core value (`none`, `curated`, `recording`).
- **Ordered or not.** Playlists and DJ sets are `ordered: true` (repeats allowed where the platform allows them; elsewhere a repeat comes back in `unchanged`). Tags and folders are `ordered: false` (each item once; on a list that lives on a source, once per copy, with the copy's `ref` on the entry).
- **Smart lists** carry a `filter` (`match` `all` or `any`, `rules` of `field`, `op`, `value`, nested to any depth, optional `sort` and `limit`). Their `entries` are the results when written. If you can't evaluate a rule, show the stored entries and keep the filter. See https://www.tangle.music/omc/spec/#smart-lists.
- **Colours are tags:** an unordered list with `color` (`#rrggbb`). There is no colour field on items.
- **Dates:** `added_at` is when the person added it: an item's is when it first came into the collection, a copy's when it came in on that source, an entry's when it was added to that list. Leave out what you don't know; mark a guess with `"provenance": { "added_at": { "origin": "estimated" } }`, and data your app worked out with origin `inferred`. There is no `updated_at`: don't write one.
- **No sync state in records or files.** Which apps are connected, what each may see or do (`can`), a list's `version`, how far each has read (the cursor) and who changed what belong to the home and the connection, never in records, `ext` or files. `via` (how an item or list first came in) is written once.
- **Where a property goes.** Nowhere, if nobody would miss it when your app is gone (caches, embeddings, mappings to your catalog). In your own namespace if no other app would read it. In a module's namespace if some apps share it (DJ cues and grids). In the core only through an OMC proposal. Measured values (BPM, key, energy) go on the copy whose audio you measured, in the DJ module's `analysis`. See https://www.tangle.music/omc/spec/where/.
- **Local files:** `ref` is a path relative to the source folder. If you don't know the folder, replace the home directory with `~` (`~/Music/a.flac`), the same way every time. Never write a path that contains a username.
- **Extensions** go in `ext`, under a reverse-domain namespace you own: `"ext": { "com.yourapp": { … } }`. `org.openmusiccollection.*` is reserved; a home's own namespace is read-only to apps.
- **Validate** before you ship: the project's validator checks every line and, with `roundtrip`, that an app keeps unknown data.

## Rules for apps using Access

Access is the protocol apps use to reach a collection at its home (https://www.tangle.music/omc/modules/access/): OAuth 2.1 with PKCE, core records as JSON over HTTP.

- **Ask for the least.** Scopes: `library:read`, `lists:read`, `lists:write`, `library:write`, `library:add` for apps that only bring music in, and `listens:write` / `listens:read`. With `library:add` an app adds items (`POST /items`, optionally `?list=`, `?hold=true` to ask the home to hold it) and fills lists it made or the person picked, and learns only each item's id and whether it was `created`, `existing` or `held` (waiting for the person to let it in; always music items, never bare links). A link on a registered platform (or a source with a descriptor) is a copy with `ownership: none`, and the home adds that source if the person has none; a link to a shop page goes in `via`. Don't call anything an inbox: an app's list is a list, and Share's addresses are share addresses.
- **Identify your app by an address you own** where the home supports it: your `client_id` is an `https` URL serving a Client ID Metadata Document (`client_name`, `logo_uri`, `redirect_uris`), so you needn't register with every home.
- **Keep up with the cursor.** Download `GET /collection` once, keep the `OMC-Cursor` response header, then read `GET /changes?cursor=…` (or listen on `/events`). Every write also answers with `OMC-Cursor`. Never store the cursor in the collection.
- **Ask the list what you may do.** Every list Access returns carries `can` (`add`, `remove`, `reorder`, `rename`, `delete`) for your app at that moment; `GET /lists/can` gives it for every list at once. Read it each time; never write it back or into a file.
- **Address ordered lists by position.** `GET /lists/{id}` returns a `version` (and `ETag`). Removing, moving (`POST /lists/{id}/entries/move` with `from`, `before`) and changing an entry's `note` name 0-based positions and that `version`; if the list changed, you get `409 version_conflict`: read it again and retry. Unordered lists use item ids.
- **Find records by content.** `GET /items?source=…&ref=…`, `?isrc=`, `?upc=`, `?musicbrainz=` find the home's item for something you know from elsewhere. Don't keep your own ids as if they were the home's.
- **Let the home combine.** Send what you have (`POST /items`, or `POST /import` for a piece of a collection file with your own ids) and use the ids the home answers with.
- **PATCH is a merge patch.** Send back whole records you read; the home's own fields must be unchanged (`422 field_not_writable`), and its namespace is ignored. You may write `rating` as the person's own rating. Write only your own namespace or a module's: another app's may be refused (`403 namespace_not_yours`). `data_bytes` in `/me` limits your own namespace per record; modules set their own limits.
- **Send plays as listens.** `POST /listens` takes ListenBrainz's submit format; the home derives `play_count` and `last_played_at`. Never keep play counts in `ext`.
- **Stay in your view.** The person may limit a connection to some lists, sources or record types; changes outside it are refused with `403 out_of_view`.

## Minimal valid file

```json
{"type":"omc","version":"1.0","generated_at":"2026-10-18T09:30:00Z"}
{"type":"source","id":"s1","platform":"spotify","name":"Spotify","account":"31anna0example"}
{"type":"item","id":"i1","kind":"recording","title":"Night Drive","artists":["Mira Holt"],"copies":[{"source":"s1","ref":"0exampleTrackId000001"}]}
{"type":"list","id":"l1","name":"Liked Songs","ordered":true,"role":"saved","source":"s1","entries":[{"item":"i1"}]}
```

## Modules

The core leaves these to modules, each a draft: Access (the protocol for apps), DJ (cues and beat grids on a copy, mixes), Feeds (following, as JSON Feed or RSS with OMC records inside), Share (sending music to someone), Collector (a copy's history), Catalog and Commerce (offers and prices), Delivery (streaming from any source), Folder (the collection as Obsidian notes). See https://www.tangle.music/omc/modules/.
