On this page
- Goals
- Not in the core
- Conventions
- The model
- Items
- Recordings and releases
- Identifiers
- Key
- Notes
- Listens
- Provenance
- Copies
- Sources without track ids
- File identity
- Ownership
- Sources
- Lists
- Entries
- Folders
- Smart lists
- Colours
- Roles
- Dates
- The collection file
- Header
- References
- Source descriptors
- Capabilities
- Extensions and unknown data
- The ext object
- Values of your own
- Extensions and core fields
- Keep what you don't understand
- Where a property goes
- Versioning
- Homes, apps and sources
- via
- Held
- Working with part of a collection
- Combining collections
- Conformance
- Security and privacy
- Registries
- Modules
Open Music Collection Core 1.0
Status: draft for review. Editors: the Open Music Collection editors. License: CC BY 4.0.
Open Music Collection (OMC) is an open standard for a person's music collection: the songs and albums they keep, the lists they organise them into, and where each one lives. Any app can export a collection and any other can import it without losing anything, even data neither understands.
This specification defines the core: four kinds of record, one file format (the OMC file format), a source descriptor, and the rules that let the standard grow without breaking anyone. Everything else (the Access protocol for apps, DJ data, following and feeds, sending music to someone, collectors' details, catalogs and commerce, streaming between apps) is a module built on the core and versioned on its own; see Modules. Changes are proposed as OMC proposals (pull requests to the specification repository). What belongs where says which of these places a new property or endpoint goes in, and the changelog what was decided when.
Goals
- A person can take their whole collection from one app to another, at any time, without asking either one for permission.
- What an app doesn't understand, it keeps and writes back unchanged.
- The core is small: four records. An exporter that knows title, artists and one link per track is already conformant.
- Any source works. A streaming service, a label's shop, a folder of files and a server at home are described the same way.
- New platforms, capabilities and modules are added without a new core version.
Not in the core
- What is for sale, and at what price. The core describes what a person has. Offers, prices, purchases and artists' preferred stores belong to the Catalog and Commerce module.
- Whether something can be played right now. Access depends on subscriptions, regions and licensing at the moment of playback. The core records where a track lives; the source's capabilities and the user's account decide, at play time, what can be done with it.
- Who a person follows, release feeds, sending music to someone, a collector's history of a record and social features. These are modules or app features. A list an app fills for the person is not one of them. It is an ordinary list (see Homes, apps and sources).
- Audio analysis goes on the copy whose audio was measured, in the copy's
ext(the DJ module defines the common part). - Listening history is kept by the home, apart from the collection, in ListenBrainz's format
(see Listens). The collection carries only what the home derives from it:
play_countandlast_played_aton items.
Conventions
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be read as described in RFC 2119 and RFC 8174 when, and only when, they appear in bold capitals.
- Records are JSON objects (RFC 8259), encoded as UTF-8.
- Timestamps are RFC 3339 strings in UTC, for example
2026-10-18T09:30:00Z. - Dates that may be partial (release dates) are
YYYY,YYYY-MMorYYYY-MM-DD. - Field names are
snake_case. A field that is absent and a field that isnullmean the same. - A producer writes OMC data, a consumer reads it, and a host is a collection's home: it keeps the collection and lets people and apps change it. See Homes, apps and sources.
The model
Collection
├── Sources where music lives: a streaming account, a store, a folder, a server
├── Items songs (recordings) and albums (releases) the person keeps
│ └── Copies one per source the item is on, with that source's own id
└── Lists playlists and sets the person organises items into
A song the person has on Spotify and as a FLAC file from Bandcamp is one item with two copies. Lists hold items, so a playlist moves between platforms intact.
Every record has a type and an id. Ids are opaque strings chosen by the producer, unique
within a collection for their type, and SHOULD stay the same across exports of the same
collection from the same host, so that an importer can update instead of duplicate.
Ids are local. An id means something only inside its collection: it is how records in one file point at each other, and how a home recognises its own records when they come back. Nobody compares ids across collections, and consumers MUST NOT parse them. When two collections meet, records are matched by their contents (a shared copy, an ISRC, a UPC), never by id: see Combining collections. The home does the matching.
Items
An item is a song (recording) or an album (release) the person keeps.
| Field | Type | Required | Meaning |
|---|---|---|---|
type | "item" | yes | |
id | string | yes | Unique within the collection |
kind | string | yes | recording or release. Consumers treat unknown kinds as recording for display |
title | string | yes | May be empty when the source has none (a track a platform no longer serves); consumers then show a fallback such as the file name |
artists | string[] | yes | Credited artists in order. May be empty when unknown |
album | string | no | For a recording: the title of the release it appears on |
duration_ms | integer | no | |
release_date | partial date | no | |
label | string | no | |
composers | string[] | no | For a recording: who wrote it, in order (ID3 TCOM) |
album_artists | string[] | no | For a recording: the artists the release it appears on is credited to, as text for display next to album, when there is no release item to say it |
release_type | string | no | For a release: album, single, ep, compilation or another value (open list) |
genres | string[] | no | |
bpm | number | no | Tempo in beats per minute |
key | string | no | Musical key in ID3v2.4 TKEY notation, see Key |
artwork_url | URL | no | |
ids | object | no | Platform-independent identifiers, see below |
rating | integer 1–5 | no | The person's own rating |
note | string | no | The person's own text about the item, any number of lines. See Notes |
catalog_number | string | no | For a release: the label's catalogue number (WARP 123) |
country | string | no | For a release: where this edition came out, as an ISO 3166-1 alpha-2 code (GB), or XW for worldwide |
added_at | timestamp | no | When the item first came into the collection, see Dates |
play_count | integer | no | How many times the person has played it, derived by the home from listens. Read-only |
last_played_at | timestamp | no | When the person last played it, derived the same way. Read-only |
copies | Copy[] | yes | Where the item lives. May be empty (a track the person noted but has nowhere) |
provenance | object | no | Where metadata values came from, see below |
via | object | no | How the item first came into the collection, for people: { "name": "…", "url": "…" }, see via |
ext | object | no | Extensions, see Extensions |
Recordings and releases
A recording is a song as people know it; a release is an album, single, EP or compilation
it came out on. Both are items (kind recording or release), and one recording can come out on
several releases: the single, the album, a compilation years later.
They are linked through copies, because each copy comes from one release: a copy of a recording
names its release in release, with its track_number and disc_number there. The release item
has its own copies, whose ref is the platform's id for the release (a Spotify album id), so an app
can open the release itself. A recording's album stays as text for display.
So when a playlist holds the single's version of a song, the entry names that copy (ref), and
that copy names the single (release), even when the album version is a copy of the same item.
Track and disc numbers stay on copies, since one recording has a different number on each
release. An app that lists an album's songs in order uses the copies that name the release. A
release's length is the sum of its songs' duration_ms. Where a collection has no release items,
album and album_artists on the recordings are what there is to show.
A collection may be all albums. Many people keep albums rather than songs: a record collection,
saved albums, a shop's purchases. A collection may hold only releases, with lists of releases (role: saved for saved albums, wanted for a wantlist). Recordings are optional, and a release item
MUST be complete without them: a consumer that shows songs falls back to the release.
Identifiers
ids holds identifiers that mean the same thing everywhere. Platform ids do not belong here:
they belong on the copy for that platform.
| Key | For | Example |
|---|---|---|
isrc | recording | QZES72600001 |
upc | release | 0123456789012 |
musicbrainz | recording or release (by kind) | a1f2… (MBID, a UUID) |
iswc | recording: the composition, shared by every recording of it | T3452468001 (no punctuation) |
Other keys MAY be used and MUST be kept by consumers that don't know them. New keys are added through the identifier registry.
Key
key is written as ID3v2.4's TKEY frame writes it: a note A to G, then b or # if
needed, then m for minor, or o for off-key. Am, F#, Dbm. At most three characters, plain
ASCII. Rekordbox, Serato and file tags already write it this way. Enharmonic spellings are the same
key: C#m equals Dbm. Camelot (8A) and Open Key (1m) are ways to show a key; a producer that
has one converts it before writing.
Notes
note is the person's own text about an item: an ohr.fm note, a Rekordbox or Serato comment, a
Discogs note. One string, which may have several lines. The same field on a list entry
is about the item in that list ("open the set with this", a blurb in a year-end list).
A file's comment tag often holds text the person never wrote (a key-detection tool's 8A - Energy 7, a shop's "Purchased at …"). An importer that can't tell marks the note
"provenance": { "note": { "origin": "embedded" } }.
Listens
A home MAY keep the person's listens: each time an app played an item, with a timestamp, the track's artists, title and identifiers, and the app that played it. Listens use ListenBrainz's format, are kept apart from the collection, and leave a home in that format. The Access module says how apps send and read them.
The collection carries only what the home derives from them, play_count and last_played_at on
items, and never the listens themselves. These two fields are the home's: apps read them, filter
on them and never write them. A count an app keeps itself, such as Rekordbox's play counter, goes in
that app's namespace.
Provenance
Values such as tempo, key and genre are often disagreed on. provenance records where a field's
value came from, as a map from field name to origin:
"provenance": {
"bpm": { "origin": "analysis", "by": "Rekordbox 7" },
"key": { "origin": "user" },
"genres": { "origin": "platform", "by": "spotify" }
}
origin is one of:
| Origin | The value… |
|---|---|
embedded | was read from the file's tags |
platform | came from a source's metadata |
analysis | was computed from the audio |
catalog | came from a catalog such as MusicBrainz |
user | was set or corrected by the person |
estimated | is a guess where the truth isn't known (a date a source doesn't give) |
inferred | was worked out by an app rather than told (a link read from an email, a match by title) |
by optionally names the tool or source. A consumer that replaces a value SHOULD update its
provenance. Values marked estimated or inferred SHOULD be shown as such, and an app may
ask the person to confirm them.
provenance may also appear on copies and on list entries, for their own
fields ("provenance": { "added_at": { "origin": "estimated" } }). On a copy, ref names the copy itself:
"provenance": { "ref": { "origin": "inferred" } } says an app guessed that this copy is the item
(a link read from an email), so the whole match is a guess.
Copies
A copy is an item on one source.
| Field | Type | Required | Meaning |
|---|---|---|---|
source | string | yes | The id of a source in this collection |
ref | string | yes | The source's own id for this track or album, in the form the platform registry gives for its platform (a Spotify track id, Bandcamp's numeric item id, a Discogs release id, a path relative to a folder) |
url | URL | no | A link a person can open, when the source has one |
ownership | string | no | none (default), digital, physical or file, see below |
release | string | no | On a recording's copy: the id of the release item (album, single, EP) this copy comes from |
track_number | integer | no | The copy's track number on that release |
disc_number | integer | no | The copy's disc number on that release |
position | string | no | The copy's position as printed on the release (A1, B2, 2-03), for releases whose positions aren't plain numbers (vinyl sides) |
file | object | no | For digital and file copies: mime, size_bytes, sample_rate_hz, bit_depth, bitrate_kbps, and audio_hash and fingerprint, see File identity |
format | string | no | For physical copies: vinyl, cd, cassette, minidisc, dvd, reel or another value (open list) |
format_details | string | no | The format as the source describes it: 2×12", LP, Album, 180g |
condition | object | no | For physical copies: { "media": "VG+", "sleeve": "NM" }, in Goldmine grades (M, NM, VG+, VG, G+, G, F, P) where possible |
location | string | no | Where the person keeps a physical copy: "Shelf 3", "Storage box A" |
added_at | timestamp | no | When the item came into the person's collection on that source, see Dates |
provenance | object | no | Where this copy's values came from, as on items |
ext | object | no | Extensions |
Sources without track ids
Some sources have ids only for releases. A Discogs collection lists records, and a copy of a
track there uses the release's ref, a /, and the track's position as printed on the release:
28993732/A1, 116166/2-03. Only where the source prints no position does the producer use the
track's number on the release, #3. So two apps reading the same release write the same refs, and
an edit to the release's tracklist doesn't shift them.
File identity
A file's ref is its path, so a renamed or moved file would look like one copy removed and another
added, and the copy's data (cue points included) would be lost. Two optional members of file
say which audio a file holds:
audio_hash: a hash of the audio data alone, without tags, so renaming, moving and retagging don't change it and re-encoding does.{ "algorithm": "flac-md5", "value": "…" }. FLAC files already carry one (the MD5 inSTREAMINFO). For other formats,algorithmnames what was hashed and how (for MP3, the frames without ID3 and APE tags); an algorithm two apps use joins the registry.fingerprint: an acoustic fingerprint, which survives re-encoding.{ "algorithm": "chromaprint", "value": "…" }.
The same audio_hash on two copies of one source means the file moved: the home updates the
copy's ref and keeps its data. The same audio_hash on two sources is the same audio, so the two
copies belong to one item (see Combining collections). A shared
fingerprint is only a suggestion, since an extended mix and a radio edit can fingerprint alike.
Ownership
Ownership is what the person, or the source on their behalf, says they have. It is a claim, and the only thing the core records about rights:
| Value | Meaning | Examples |
|---|---|---|
none | The person keeps it on that source without owning it | Spotify library, Qobuz favourites, SoundCloud likes |
digital | The person bought a digital copy | Bandcamp or Qobuz purchase, Beatport download |
physical | The person owns a physical copy | Vinyl or CD in a Discogs collection |
file | The person has the audio file; how they got it is not known | A track in a local folder or a DJ library |
Whether a copy can be streamed, previewed or downloaded is not stored: it comes from the source's capabilities and the person's account when they press play.
- Having the audio is a separate copy. A record the person owns on vinyl and has ripped is one
physicalcopy on their shelf source and onefilecopy on their folder. - Sources that keep collections map as follows: a Discogs collection item on vinyl, CD or
cassette is
physical, one in the File format isdigital, and a wantlist item isnone(in a list with thewantedrole). A Bandcamp or Beatport purchase isdigital. - The person's correction wins. When the person changes a copy's ownership (it was a gift, the
source got it wrong), a home keeps their value over the source's from then on, and records it as
"provenance": { "ownership": { "origin": "user" } }on the copy.
Sources
A source is anywhere music lives for this person.
| Field | Type | Required | Meaning |
|---|---|---|---|
type | "source" | yes | |
id | string | yes | Unique within the collection |
platform | string | yes | A name from the platform registry (spotify, bandcamp, local), or the host name of a source that publishes a descriptor |
name | string | yes | What to show people: "Spotify", "My FLAC folder", "Anna's own server" |
account | string | no | The person's account on that source: the platform's stable id for it (a Spotify user id, not a display name), so that two collections of the same account can be matched |
account_name | string | no | What to show people for that account, when it differs from account |
descriptor | URL | no | Where the source's descriptor is. Required when platform is not a registered name |
ext | object | no | Extensions |
A collection can have several sources on the same platform (two Spotify accounts, three folders).
Lists
A list is something the person organises items into.
| Field | Type | Required | Meaning |
|---|---|---|---|
type | "list" | yes | |
id | string | yes | Unique within the collection |
name | string | yes | |
description | string | no | |
ordered | boolean | yes | true for playlists and sets: order matters, and an item may appear more than once where the list's platform allows it. false for tags and folders: each item at most once, or once per copy on a list that lives on a source |
role | string | no | A hint for importers, see below |
source | string | no | The id of the source the list lives on. Absent: the list belongs to the collection itself |
ref | string | no | The source's own id for the list (a Spotify playlist id), so a host can keep it in step with the source |
entries | Entry[] | yes | In order for ordered lists. See Entries |
folder | boolean | no | true for a folder: a list that holds other lists and no entries. See Folders |
parent | string | no | The id of the folder this list is in |
position | number | no | Where this list sorts among the lists in the same folder |
color | string | no | A colour for the list, as #rrggbb. See Colours |
filter | object | no | The rule a smart list is built from. See Smart lists |
created_at | timestamp | no | When the list was made |
via | object | no | How the list first came into the collection, for people, see via |
ext | object | no | Extensions |
Entries
| Field | Type | Required | Meaning |
|---|---|---|---|
item | string | yes | The id of an item in this collection |
ref | string | no | On a list that lives on a source: the ref of the item's copy that this entry is on that source |
added_at | timestamp | no | When the item was added to the list |
note | string | no | The person's text about the item in this list: why it's in the set, a blurb in a ranked list. See Notes |
provenance | object | no | Where this entry's values came from, as on items |
ext | object | no | Extensions |
ref makes a platform's list rebuildable exactly. One item can have several copies on the same
source (a platform's single and album versions of a song are one recording), and the list may hold
one copy or several. Producers SHOULD set ref on every entry of a list that lives on a source
when they know it. An entry without ref means the item has no copy there yet (for example, an
addition the host hasn't sent to the source).
Folders
A folder is a list with folder: true. It holds lists, not items: its entries is [] and its
ordered is false, both still written, so an app that doesn't know folder shows an empty list,
which is true. Any list, folder or not, names its folder in parent. This covers Rekordbox's
playlist folders and My Tag groups, Serato's subcrates, Traktor's and Apple Music's folders, and an
empty folder survives the move.
- A
parentthat doesn't resolve, or names a list that isn't a folder, means the list is at the top level (see References). A folder that is its own ancestor is invalid. - Lists in one folder sort by
position, lowest first; lists without one come after, by name. - A folder on a source matches by source and
ref, like other lists.
Smart lists
A smart list is a saved rule: Rekordbox's Intelligent Playlists, Serato's Smart Crates, iTunes's
Smart Playlists. Any list may carry a filter. Its entries are the results when the list was
written, so an app that doesn't know filter still has the tracks. An app that does recomputes
them, and doesn't edit them by hand.
{"type":"list","id":"l9","name":"Peak time","ordered":true,
"filter":{"match":"all","rules":[
{"field":"genres","op":"contains","value":"Techno"},
{"field":"bpm","op":"between","value":[125,130]},
{"match":"any","rules":[
{"field":"list","op":"in","value":"l3"},
{"field":"rating","op":"gte","value":4}]},
{"field":"added_at","op":"within","value":"P30D"}],
"sort":[{"field":"added_at","order":"desc"}],"limit":25},
"entries":[{"item":"i1"},{"item":"i7"}]}
- A filter is a group:
match(allorany) andrules. A rule is a field, an operator and a value, or another group, to any depth. - Fields are an item's core fields (
play_countandlast_played_atincluded), pluslist(membership: the value is a list id or an array of them) and two facts about copies,platformandownership(true when any copy has the value). Keeping to the fields every app knows keeps the language small. - Operators:
is,is_not,contains,not_contains,starts_with,ends_with,gt,gte,lt,lte,between(a two-value array, both ends included),withinandnot_within(an ISO 8601 duration back from now,P30D),inandnot_in(forlist),exists(trueorfalse). Text compares without regard to case;containson an array field means one of its values does. sort(an array of{ "field": …, "order": "asc" | "desc" }) andlimit(an integer) are optional: "the 25 most recently added". A list withsortis ordered.- A rule an app can't evaluate (an unknown field or operator, a namespaced field, a group deeper
than it supports) means that app shows the stored entries and keeps the filter unchanged. A
source whose rules OMC can't express keeps its own rule in its
extas well, so its app can rebuild it exactly. - Through Access, a smart list answers
can.add: false.
Colours
A colour is a tag: an unordered list with a color. Its name is the list's name, since apps let
people rename their colours ("Peak time"), and what it means is the list's description. A source's
colours sit in a folder in the source's order (position); on the source, the list's ref is the
source's id for the colour. color works on any list: a Finder tag, an Obsidian tag, a playlist.
Rekordbox, Serato and Traktor allow one colour per track. Tags don't enforce that, so a home writing
colours back to such an app uses the one whose list comes first by position.
Roles
Roles exist for one reason: so that the lists a platform builds in keep their meaning when the collection moves to another app. An importer maps them to its own equivalent rather than create a list called "Liked Songs". The person's own lists have no role. So roles stay few:
| Role | Meaning | Examples |
|---|---|---|
library | Everything the person has on a source. Removing an item here removes it from their collection on that source | Bandcamp collection, Discogs collection, a DJ library, a folder |
saved | A favourites list built into the source, of things the person can play or has. Some sources have one per kind of item | Spotify Liked Songs and Saved Albums, SoundCloud likes, Qobuz favourites |
wanted | Things the person doesn't have yet and wants | Discogs wantlist, Bandcamp wishlist |
curated | Lists people make and organise. The default | Playlists, sets, tags, folders, a list an app fills |
history | What was played, in play order: one list per session, named by its date, usually in folders by year and month | Rekordbox, Serato and Traktor history playlists |
A missing role means curated. Consumers MUST treat an unknown role as curated. An app that
needs a role of its own names it in its namespace (com.example.app/backlog). See
Values of your own.
Dates
A collection has three kinds of "added" date, and they fit together:
- An item's
added_atis when it first came into the collection: unless the person added it directly, the earliest of its copies' dates. When a copy is removed and comes back, the item keeps its first date. - A copy's
added_atis when the item came into the person's collection on that source (the date Spotify gives for a like, a Bandcamp purchase date). A copy removed and added again gets the new date. - An entry's
added_atis when the item was added to that list.
Each is when the person added it, wherever they did. A home that sends an addition to a source
keeps the person's date; the moment the source reports it back doesn't change it. When a home first
saw a record is the home's own bookkeeping and has no field. A date that isn't known SHOULD be left out,
and one that is guessed is marked estimated in provenance.
A list's created_at is when the list was made. There is no "updated" date in the core: when
something last changed is the business of the home and its apps (an app that wants to share it puts
it in its namespace), and files are compared by their contents. Files from earlier drafts may carry
updated_at; consumers keep it as unknown data.
The collection file
A collection is exchanged as newline-delimited JSON (one record per line, UTF-8, \n
line endings), so that a collection of any size can be written and read as a stream.
- File extension:
.omc.ndjson. Media type:application/x-ndjsonwith the profilehttps://openmusiccollection.org/core/1.0. The same file, holding part of a collection, is what people send each other (see the Share module). - The first line MUST be the header. After it, records SHOULD appear as all sources, then all items, then all lists, so a streaming reader can resolve references as it goes.
- A consumer MUST accept records in any order after the header.
- Modules MAY define their own record types, named
<namespace>/<name>(for exampleorg.openmusiccollection.feeds/follow). A consumer MUST keep records of types it doesn't know, under the same rules as unknown data.
{"type":"omc","version":"1.0","generated_at":"2026-10-18T09:30:00Z","generator":{"name":"Example Exporter","url":"https://example.com"},"counts":{"sources":1,"items":1,"recordings":1,"releases":0,"lists":1}}
{"type":"source","id":"s1","platform":"spotify","name":"Spotify","account":"31anna0example","account_name":"anna"}
{"type":"item","id":"i1","kind":"recording","title":"Night Drive","artists":["Mira Holt"],"duration_ms":312000,"ids":{"isrc":"QZES72600001"},"copies":[{"source":"s1","ref":"0exampleTrackId000001","url":"https://open.spotify.com/track/0exampleTrackId000001"}]}
{"type":"list","id":"l1","name":"Liked Songs","ordered":true,"role":"saved","source":"s1","entries":[{"item":"i1","added_at":"2026-01-02T10:00:00Z"}]}
Header
| Field | Type | Required | Meaning |
|---|---|---|---|
type | "omc" | yes | |
version | string | yes | Core version, MAJOR.MINOR |
generated_at | timestamp | yes | |
name | string | no | A name for the collection, for people ("Anna's records"). Left out unless the person gives one, so a file carries no personal details by default |
generator | object | no | { "name": "…", "url": "…" }: the software that wrote the file |
counts | object | no | { "sources": n, "items": n, "recordings": n, "releases": n, "lists": n }, so a reader can show progress. items is all items, recordings and releases split them by kind. A hint: readers recompute counts |
modules | object | no | Modules whose data appears in the file, by namespace and version: { "org.openmusiccollection.dj": "0.1" }. Informational: consumers keep data from namespaces not listed here too |
ext | object | no | Extensions |
The header describes the file's contents, nothing else. It MUST NOT carry sync state, such as
how far an app has read a home's changes: that belongs to the connection (the
Access module sends it with the response). A file that holds only part of a
collection says so, since that is a fact about its contents (Access's view), so that a reader
knows the missing records were left out, not deleted.
References
Copies, lists and filters refer to sources, items and lists by id. A consumer MUST NOT reject
a file because a reference doesn't resolve. It SHOULD report it, skip that copy or entry (or
treat that parent as top level, or that list rule as matching nothing), and keep it when writing
the collection back.
Source descriptors
A source that isn't in the platform registry describes itself with a descriptor: a JSON
document, served over HTTPS, conventionally at /.well-known/omc-source.json on the source's
host.
{
"omc": "1.0",
"platform": "music.example.net",
"name": "Example Records",
"homepage": "https://music.example.net",
"icon": "https://music.example.net/icon.png",
"capabilities": ["link", "library.read", "media.stream", "media.download"],
"modules": {
"org.openmusiccollection.access": { "version": "0.1", "base_url": "https://music.example.net/omc" }
}
}
| Field | Required | Meaning |
|---|---|---|
omc | yes | Core version the descriptor follows |
platform | yes | The source's host name. MUST match the host serving the descriptor |
name, homepage | yes | |
icon | no | Square image, at least 128 px |
capabilities | yes | What the source can do, see Capabilities |
modules | no | Modules the source implements and where, by namespace |
ext | no | Extensions |
When an app meets a source it doesn't know, it reads the descriptor and handles the source in the best way it can:
- Native: the app has its own integration for that platform.
- Generic: the app implements the modules the descriptor lists (for example a delivery module for streaming), and uses them.
- Link-out: the app shows the item's metadata and opens the copy's
url.
Every app can do at least the third.
Capabilities
Capabilities are strings. A consumer MUST ignore capabilities it doesn't know.
| Capability | Meaning |
|---|---|
link | Copies have URLs a person can open |
library.read | The person's collection on the source can be read |
saved.read, saved.write | The source's favourites can be read, changed |
lists.read, lists.write, lists.create, lists.reorder | Lists on the source can be read, changed, created, reordered |
search | The source's catalog can be searched |
media.preview | Some or all tracks can be played as previews |
media.stream | Tracks can be streamed in full, for people whose account allows it |
media.download | Owned tracks can be downloaded |
Modules define how an app reads, writes or streams through a source. New capabilities are added through the capability registry.
Extensions and unknown data
Apps keep what they don't understand. Everything else in the standard depends on this.
The ext object
Any record, copy, entry or header MAY carry an ext object. Its keys are namespaces in
reverse domain-name order, owned by whoever owns that domain; its values are any JSON:
"ext": {
"org.openmusiccollection.dj": { "cues": [{ "at_ms": 32000, "label": "Drop" }] },
"com.example.moods": { "mood": "late night" }
}
The namespace org.openmusiccollection and everything below it is reserved for official
modules.
ext holds what the person would miss if they left the app: their cues, their own scores, an app's
settings for a list. An app's cache (a mapping to its own catalog, embeddings, anything it can
compute again) stays in the app's own storage.
Values of your own
Roles, kinds, ownership values, formats and provenance origins are open lists: the core names
the common values, and new ones join through the registries. An app that needs a
value before it is registered, or one that only it needs, writes it in its own namespace, with a
/: "ownership": "com.example.app/borrowed", "role": "com.example.app/backlog".
- Every consumer MUST keep such a value unchanged.
- A consumer that doesn't know it treats it as the nearest core value:
nonefor ownership,curatedfor a role,recordingfor a kind, and no origin for provenance. - A value used by more than one app is a good candidate for an OMC proposal to register it.
Extensions and core fields
Core fields and extensions describe the same records, so they have to agree, and each has to work when the other is missing:
- Core stands alone. A record MUST be complete and correct for a consumer that ignores every extension. An extension adds detail; it is never needed to understand core fields.
- An extension refines, never contradicts. A host's extension may narrow a core value (core
platform: itunes, the extension"com.example.home": { "library": "itunes_file" }), but MUST NOT say something core says otherwise. - Core wins. When they disagree (an app changed a core field and left an extension as it was), consumers follow core, and the host recomputes its extension from core.
- Missing means derive. A host that receives a record without its extension (from another app, a file, a folder) MUST derive what it needs from core fields, using defaults where core says nothing, and MUST NOT refuse the record for the extension's absence.
A host's own namespace (com.example.home for a home at example.com) is the host's. Apps treat
it as read-only. A host SHOULD ignore what an app writes there rather than refuse it, so a
record read earlier, whose host data has since gone stale, can always be sent back whole.
Keep what you don't understand
- A host MUST keep every member it doesn't understand, anywhere in a record (in
ext, or fields added by a later core version), and MUST write them back unchanged when it exports or serves that record, unless the person deleted the record or the data. - A consumer that changes a record MUST NOT drop unknown members from it.
- A host MAY refuse data it can't keep (more than a stated size limit, a copy on a source it can't hold, data on a list entry), and MUST say so with an error rather than drop it silently.
- The header is written fresh for every export: its
generated_at,countsandextdescribe that export. Keeping data applies to records only.
An app that cannot meet this rule for a kind of data (for example because it stores only some fields) MUST say so in its import screen and documentation, as a lossy importer.
Where a property goes
So that apps put the same thing in the same place without asking each other, a property goes in the first of these that fits:
- Nowhere in the collection if nobody would miss it once the app is gone: caches, embeddings, stems separated on the fly, what an app may do with a list right now.
- The app's own namespace if no other app would want to read it (an iPod app's On-The-Go order).
- A module's namespace if some apps share a vocabulary for it but not every app needs it: cues and grids, what was paid. Credits beyond the core's fields (each musician and instrument, producer, engineer) will be a Credits module using DDEX's role names.
- The core if any music app needs it to show, find, match or move a track; at least two independent apps or sources already have it; and it means one obvious thing.
The home's own namespace holds the home's state. A property moves up as more apps need it: from an app's namespace to a module (an OMC proposal), then to the core in a minor version. Readers keep what they don't know, so nothing breaks on the way. A person editing by hand never picks a namespace: in the Folder module, core and module fields are named properties, and anything else they type is kept as their own. What belongs where puts the same question to endpoints and behaviour.
Versioning
- The core is versioned
MAJOR.MINOR. A minor version only adds: new optional fields, new values for open lists (roles, kinds, ownership values, origins, capabilities), new registry entries. - A consumer MUST accept files with a higher minor version than it knows, and handle their new parts through the unknown-data rules.
- A new major version is published only to remove or change meaning, and a file states its
major version in
version. A consumer MAY refuse a major version it doesn't know. - Modules are versioned independently, by namespace, and declare the core version they need.
Homes, apps and sources
Software plays one of three roles for a collection. The same app can be a home for one collection and an app for another, and change roles over time.
| Role | Word for people | What it does |
|---|---|---|
| Host | home | Keeps the collection, settles two changes that collide, and lets apps read and change it (Access module) |
| Client | app | Reads or changes a collection that lives at its home |
| Source | source | A place where music, or lists of it, live with their own copy, which is the authority for them: a streaming service, a store, someone's own server. The home syncs with it |
- A collection has exactly one home at a time, and the home decides when changes collide. An app MAY keep a full copy, for example to work offline, and is still an app. A service's server can be the home of its people's collections while its desktop and phone apps keep copies.
- A person moves home by exporting the collection from the old home and importing it at the new one, or by letting the new home read the old one through Access. Both carry the same data, and the records don't change. Connections stay behind: apps connect again to the new home, the way people sign in to apps again.
- Apps change things in place, and sources keep their own copy. What an app makes for the person
(a list, a tag) belongs to the person and lives at the home, so it has no
source. A list or item has asourceonly when it lives somewhere with its own copy that the home syncs with, such as a Spotify playlist. The same software can be either: an app that collects new releases into a list at the home is an app, and its list is an ordinary list; a service that keeps that list in its own storage, which the home mirrors, is a source, and the list lives on that source. - Connections are the home's own state. Which apps are connected, what each may see and
change, what each may do with a list right now, how far each has synced, who made which change,
and how another collection's ids map to this one's are all the home's own state. They MUST NOT
be written into records (
extincluded) or into collection files. The Access module defines how an app learns what it needs of them. - A home's namespace keeps everything about the person's collection that the home needs to restore it, such as which tracks and lists the person hid, in files and through Access alike. It MUST NOT put state about apps or other homes there. The state of changes not yet sent to a source (an addition the source hasn't taken, a rename it hasn't seen) stays inside the home and is left out of both.
via
via records how an item or a list first came into the collection, for people: "added by
Bandcamp Mail", "saved from Shazam". It is written once, when the record is created, and syncing never
changes it. It says nothing about who may change the record, and it stays true after the app is
gone.
Held
A home MAY keep something an app or a person sends held: outside the collection, waiting for the person to let it in or turn it away. What is held is always music: one or more items (recordings or releases), as records, with whatever the sender said about them. A link the home hasn't identified as music yet is not held; it is the home's own business until it becomes an item.
- Held items are not part of the collection. They are not in its files, and nothing points at them.
When the person lets one in, it is combined like any other addition and
gets its
via; what the sender asked for it (a list to go in) applies then. - How a home shows what is held, and whether it holds anything at all (it may let trusted apps add straight away), is the home's own design. An app MAY ask for what it sends to be held, and a home that holds anything SHOULD do so.
- A held item keeps everything the sender said about it (a shop, a price, a preview link, a catalogue number) until the person decides.
- The Access module answers
heldfor an addition that waits, and the Share module holds what unknown senders send.
Working with part of a collection
No app has to handle everything. An app MAY work with any part of the core: only lists, only the items on the sources it knows, only the one list it fills, only releases. Several apps can share one collection at its home this way, each doing what it is good at, through the Access module or through files.
- An app that changes part of a collection MUST leave everything else as it found it: records it doesn't show, sources it doesn't know, fields it doesn't read. Through Access, it changes only the records it means to; with files, it writes back what it read, with its changes.
- Every copy names its source, so an app can tell which parts of the collection belong to the platforms it handles. And the person decides what each app may see and change: through Access, the home gives each connection a view (some lists, some sources, read only or not), and the app sees only that.
- Some apps only add. They bring music in: one that watches labels' releases, one that reads music links from email. Through Access such an app can be allowed to add items and put them in a list, without seeing anything else in the collection. What it fills is an ordinary list, which moves home with the collection like any other.
- An app that drops data it can't keep (an importer that doesn't take lists, say) is lossy and MUST say so to the person before it does.
Combining collections
A person who brings a collection to a home that already has one (an export from another app, a backup, a piece sent by a friend) wants one collection, without duplicates. Since ids are local, records are matched by what they say. The home combines; an app sends what it has (through a file or the Access module's import) and the home answers with what it did.
The home's own records come first: a file the home exported earlier carries the home's own ids, which are stable across its exports, so the home MAY recognise its records by id and treat the file as an update. Ids from any other collection mean nothing to it.
Sources match by platform and account (or, for a source with a descriptor, by the
descriptor's host and account). Two local sources never match on their own: two folders called
"Music" on two computers are different folders until the person says they are the same.
A source without account matches another without one on the same platform; it matches one with
an account only when the person says so.
Copies match by their source and ref. A copy belongs to one item: two items with the same
copy are the same item, and a producer MUST NOT write one copy on two items. Within one source,
two file copies with the same audio_hash are one file that moved: the home keeps the copy, with
its data, under the new ref.
Items match only items of the same kind (a release never matches a recording, even with a
shared ref: a platform's album and track ids can be equal), in this order:
- a shared copy: the same
refon matching sources; - a shared identifier:
musicbrainz, thenisrcfor recordings,upcfor releases; - for recordings, file copies with the same
audio_hash; - for releases, the same
labelandcatalog_number(compared without regard to case, spaces and punctuation), unless both items have copies on the same source with differentrefs: that is a repress, a separate release with the same number; - otherwise, for recordings, the same title, artists and length (within two seconds), or file
copies with the same
fingerprint, and for releases, the same title, artists, year and number of tracks. This is only a suggestion: the home asks the person or leaves the records apart. A deluxe edition is a different release.
Lists that live on a source match by their source and ref. Lists made in apps (with no
source) stay separate, unless the person merges them.
When records match, the home:
- keeps one record, with the id it already had, and points every reference (copies'
release, entries'item, copies'source) at it; - adds the copies, entries and identifiers the other side has and it lacks, and keeps both sides' unknown data;
- settles a value the two sides disagree on (a title, a rating) by asking the person, or by the rule the person picked for that import ("keep my home's values" or "take the new file's"), and never by comparing timestamps;
- keeps each record's
viaand earliestadded_at.
Records that match nothing are added; the home keeps their ids when they are free in the collection and gives them new ones otherwise. More precisely:
- Entries of a matched ordered list keep the home's order, with the other side's new entries after them.
- Module records are the same when they are identical; otherwise both are kept. A home that knows a module may combine its records, and rewrite ids inside them (a mix's tracklist).
- References to lists (a list's
parent, alistrule in afilter) follow the lists they name, as entries follow items. - References that resolve to nothing in the imported side are kept and reported, as in References.
- The header of the result is the home's, written fresh; a
viewof the imported file doesn't carry over. - A release's number of tracks, for matching, is the number of recordings whose copies name it.
Conformance
| Role | Requirements |
|---|---|
| Producer | Writes files that validate against the JSON Schema, with every required field |
| Consumer | Reads every valid file; ignores unknown capabilities, treats unknown roles as curated and unknown kinds as recording |
| Host (home) | A producer and a consumer that keeps unknown data across import, edits and export, and combines collections as described in Combining collections |
For services, adoption comes in levels, so the first step is small:
| Level | The service… | Module |
|---|---|---|
| 0 · Export | lets people download their collection as an OMC file | Core |
| 1 · Read | publishes a descriptor and lets apps read collections with the person's consent | Access |
| 2 · Write | lets apps change lists and favourites | Access |
| 3 · Media | lets apps stream, preview or download through it | Delivery |
The project publishes a JSON Schema and a validator; a file that passes the validator is a valid core file.
Security and privacy
- Collections are personal data. Account names, local paths and listening choices identify
people. Producers SHOULD let people review what an export contains; a
reffor a local file SHOULD be a path relative to the source folder. A producer that doesn't know the folder MUST at least replace the person's home directory with~(~/Music/a.flac, not/Users/anna/Music/a.flac), the same way every time so refs stay stable. - Don't trust descriptors blindly. An app MUST NOT enable a source from a descriptor without the person's consent, MUST fetch descriptors only over HTTPS, and SHOULD NOT play media from a source the person hasn't enabled.
- URLs are untrusted input. Consumers MUST open only
httpsandhttpURLs fromurl,artwork_urland descriptors, and MUST NOT fetch them automatically from a file the person hasn't chosen to import. - Extensions are untrusted input too. They are kept, and never executed or rendered as markup.
Registries
Short, shared names are kept in registries that grow without a new core version. A name is added by an OMC proposal (a pull request to the specification repository) with one real-world use. Until then, apps use values of their own.
Platforms: apple_music, bandcamp, beatport, deezer, discogs, itunes, local,
qobuz, rekordbox, serato, shelf, soundcloud, spotify, tidal, traktor, youtube.
local is a folder on the person's device; itunes is the library of Apple's Music app (or
iTunes) on a computer; shelf is physical media the person catalogues by hand. Any source not
listed uses its host name and a descriptor.
Each platform's copies write ref in one form, so two apps that meet the same track write the same
copy. A global ref is the same for everyone (a Spotify track id); a personal one means
something only in the person's own account or device (a path).
| Platform | ref of a recording | ref of a release | Scope |
|---|---|---|---|
apple_music | Apple's catalog song id (1440857781) | Apple's catalog album id | global |
bandcamp | Bandcamp's numeric track id (2854920395) | Bandcamp's numeric album id | global |
beatport | Beatport's numeric track id | Beatport's numeric release id | global |
deezer | Deezer's numeric track id | Deezer's numeric album id | global |
discogs | <release id>/<position> (28993732/A1), see Sources without track ids | Discogs's numeric release id | global |
itunes | The track's persistent id in the library | The album's persistent id, if the library has one | personal |
local, rekordbox, serato, traktor | The file's path, relative to the source's folder | — | personal |
qobuz | Qobuz's numeric track id | Qobuz's album id | global |
shelf | <release ref>/<position>, as for Discogs | The person's own id for the record | personal |
soundcloud | SoundCloud's numeric track id | SoundCloud's numeric playlist id | global |
spotify | The base62 track id (4uLU6hMCjMI75M1A2tKUQC) | The base62 album id | global |
tidal | TIDAL's numeric track id | TIDAL's numeric album id | global |
youtube | The video id (dQw4w9WgXcQ) | The playlist id of the album | global |
A producer that has only a link looks the id up (a Bandcamp page carries its numeric id in the page's data) before writing a copy. Lists on a platform use the platform's own list id the same way.
Identifiers: isrc, upc, musicbrainz, iswc.
Roles: library, saved, wanted, curated, history.
Release types: album, single, ep, compilation.
Hash and fingerprint algorithms: flac-md5 (the MD5 of the decoded audio in FLAC's
STREAMINFO), chromaprint.
Cue kinds, filter fields and operators: as listed in this specification and its modules.
Capabilities, kinds, ownership values, formats and provenance origins: as listed in this specification.
Modules
Modules add whole areas on top of the core. Each has its own namespace and version.
| Module | Namespace | Status |
|---|---|---|
| Access: the protocol apps use to read, add to and change a collection with the person's consent | org.openmusiccollection.access | Draft 0.2 |
| DJ: cue points, beat grids, loops and analysis, per file; mixes and their tracklists | org.openmusiccollection.dj | Draft 0.2 |
| Feeds: following artists, labels and people, and their releases, as RSS-style feeds | org.openmusiccollection.feeds | Draft 0.2 |
| Share: sending music to someone, by email or to their share address | org.openmusiccollection.share | Draft 0.1 |
| Collector: a record's history: what was paid, and copies sold, lent or given away | org.openmusiccollection.collector | Draft 0.1 |
| Catalog and Commerce: what exists, where, at what price; artists' own offers | org.openmusiccollection.catalog | Draft 0.1 |
| Delivery: streaming and downloading from any source | org.openmusiccollection.delivery | Draft 0.1 |
| Folder: the collection as a folder of plain-text notes the person owns, opening as an Obsidian vault | org.openmusiccollection.folder | Draft 0.2 |