Core 1.0Draft for review

On this page
  1. Goals
  2. Not in the core
  3. Conventions
  4. The model
  5. Items
  6. Recordings and releases
  7. Identifiers
  8. Key
  9. Notes
  10. Listens
  11. Provenance
  12. Copies
  13. Sources without track ids
  14. File identity
  15. Ownership
  16. Sources
  17. Lists
  18. Entries
  19. Folders
  20. Smart lists
  21. Colours
  22. Roles
  23. Dates
  24. The collection file
  25. Header
  26. References
  27. Source descriptors
  28. Capabilities
  29. Extensions and unknown data
  30. The ext object
  31. Values of your own
  32. Extensions and core fields
  33. Keep what you don't understand
  34. Where a property goes
  35. Versioning
  36. Homes, apps and sources
  37. via
  38. Held
  39. Working with part of a collection
  40. Combining collections
  41. Conformance
  42. Security and privacy
  43. Registries
  44. 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_count and last_played_at on 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-MM or YYYY-MM-DD.
  • Field names are snake_case. A field that is absent and a field that is null mean 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.

FieldTypeRequiredMeaning
type"item"yes
idstringyesUnique within the collection
kindstringyesrecording or release. Consumers treat unknown kinds as recording for display
titlestringyesMay be empty when the source has none (a track a platform no longer serves); consumers then show a fallback such as the file name
artistsstring[]yesCredited artists in order. May be empty when unknown
albumstringnoFor a recording: the title of the release it appears on
duration_msintegerno
release_datepartial dateno
labelstringno
composersstring[]noFor a recording: who wrote it, in order (ID3 TCOM)
album_artistsstring[]noFor 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_typestringnoFor a release: album, single, ep, compilation or another value (open list)
genresstring[]no
bpmnumbernoTempo in beats per minute
keystringnoMusical key in ID3v2.4 TKEY notation, see Key
artwork_urlURLno
idsobjectnoPlatform-independent identifiers, see below
ratinginteger 1–5noThe person's own rating
notestringnoThe person's own text about the item, any number of lines. See Notes
catalog_numberstringnoFor a release: the label's catalogue number (WARP 123)
countrystringnoFor a release: where this edition came out, as an ISO 3166-1 alpha-2 code (GB), or XW for worldwide
added_attimestampnoWhen the item first came into the collection, see Dates
play_countintegernoHow many times the person has played it, derived by the home from listens. Read-only
last_played_attimestampnoWhen the person last played it, derived the same way. Read-only
copiesCopy[]yesWhere the item lives. May be empty (a track the person noted but has nowhere)
provenanceobjectnoWhere metadata values came from, see below
viaobjectnoHow the item first came into the collection, for people: { "name": "…", "url": "…" }, see via
extobjectnoExtensions, 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.

KeyForExample
isrcrecordingQZES72600001
upcrelease0123456789012
musicbrainzrecording or release (by kind)a1f2… (MBID, a UUID)
iswcrecording: the composition, shared by every recording of itT3452468001 (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:

OriginThe value…
embeddedwas read from the file's tags
platformcame from a source's metadata
analysiswas computed from the audio
catalogcame from a catalog such as MusicBrainz
userwas set or corrected by the person
estimatedis a guess where the truth isn't known (a date a source doesn't give)
inferredwas 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.

FieldTypeRequiredMeaning
sourcestringyesThe id of a source in this collection
refstringyesThe 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)
urlURLnoA link a person can open, when the source has one
ownershipstringnonone (default), digital, physical or file, see below
releasestringnoOn a recording's copy: the id of the release item (album, single, EP) this copy comes from
track_numberintegernoThe copy's track number on that release
disc_numberintegernoThe copy's disc number on that release
positionstringnoThe copy's position as printed on the release (A1, B2, 2-03), for releases whose positions aren't plain numbers (vinyl sides)
fileobjectnoFor digital and file copies: mime, size_bytes, sample_rate_hz, bit_depth, bitrate_kbps, and audio_hash and fingerprint, see File identity
formatstringnoFor physical copies: vinyl, cd, cassette, minidisc, dvd, reel or another value (open list)
format_detailsstringnoThe format as the source describes it: 2×12", LP, Album, 180g
conditionobjectnoFor physical copies: { "media": "VG+", "sleeve": "NM" }, in Goldmine grades (M, NM, VG+, VG, G+, G, F, P) where possible
locationstringnoWhere the person keeps a physical copy: "Shelf 3", "Storage box A"
added_attimestampnoWhen the item came into the person's collection on that source, see Dates
provenanceobjectnoWhere this copy's values came from, as on items
extobjectnoExtensions

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 in STREAMINFO). For other formats, algorithm names 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:

ValueMeaningExamples
noneThe person keeps it on that source without owning itSpotify library, Qobuz favourites, SoundCloud likes
digitalThe person bought a digital copyBandcamp or Qobuz purchase, Beatport download
physicalThe person owns a physical copyVinyl or CD in a Discogs collection
fileThe person has the audio file; how they got it is not knownA 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 physical copy on their shelf source and one file copy 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 is digital, and a wantlist item is none (in a list with the wanted role). A Bandcamp or Beatport purchase is digital.
  • 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.

FieldTypeRequiredMeaning
type"source"yes
idstringyesUnique within the collection
platformstringyesA name from the platform registry (spotify, bandcamp, local), or the host name of a source that publishes a descriptor
namestringyesWhat to show people: "Spotify", "My FLAC folder", "Anna's own server"
accountstringnoThe 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_namestringnoWhat to show people for that account, when it differs from account
descriptorURLnoWhere the source's descriptor is. Required when platform is not a registered name
extobjectnoExtensions

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.

FieldTypeRequiredMeaning
type"list"yes
idstringyesUnique within the collection
namestringyes
descriptionstringno
orderedbooleanyestrue 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
rolestringnoA hint for importers, see below
sourcestringnoThe id of the source the list lives on. Absent: the list belongs to the collection itself
refstringnoThe source's own id for the list (a Spotify playlist id), so a host can keep it in step with the source
entriesEntry[]yesIn order for ordered lists. See Entries
folderbooleannotrue for a folder: a list that holds other lists and no entries. See Folders
parentstringnoThe id of the folder this list is in
positionnumbernoWhere this list sorts among the lists in the same folder
colorstringnoA colour for the list, as #rrggbb. See Colours
filterobjectnoThe rule a smart list is built from. See Smart lists
created_attimestampnoWhen the list was made
viaobjectnoHow the list first came into the collection, for people, see via
extobjectnoExtensions

Entries

FieldTypeRequiredMeaning
itemstringyesThe id of an item in this collection
refstringnoOn a list that lives on a source: the ref of the item's copy that this entry is on that source
added_attimestampnoWhen the item was added to the list
notestringnoThe person's text about the item in this list: why it's in the set, a blurb in a ranked list. See Notes
provenanceobjectnoWhere this entry's values came from, as on items
extobjectnoExtensions

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 parent that 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 (all or any) and rules. A rule is a field, an operator and a value, or another group, to any depth.
  • Fields are an item's core fields (play_count and last_played_at included), plus list (membership: the value is a list id or an array of them) and two facts about copies, platform and ownership (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), within and not_within (an ISO 8601 duration back from now, P30D), in and not_in (for list), exists (true or false). Text compares without regard to case; contains on an array field means one of its values does.
  • sort (an array of { "field": …, "order": "asc" | "desc" }) and limit (an integer) are optional: "the 25 most recently added". A list with sort is 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 ext as 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:

RoleMeaningExamples
libraryEverything the person has on a source. Removing an item here removes it from their collection on that sourceBandcamp collection, Discogs collection, a DJ library, a folder
savedA favourites list built into the source, of things the person can play or has. Some sources have one per kind of itemSpotify Liked Songs and Saved Albums, SoundCloud likes, Qobuz favourites
wantedThings the person doesn't have yet and wantsDiscogs wantlist, Bandcamp wishlist
curatedLists people make and organise. The defaultPlaylists, sets, tags, folders, a list an app fills
historyWhat was played, in play order: one list per session, named by its date, usually in folders by year and monthRekordbox, 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_at is 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_at is 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_at is 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-ndjson with the profile https://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 example org.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"}]}
FieldTypeRequiredMeaning
type"omc"yes
versionstringyesCore version, MAJOR.MINOR
generated_attimestampyes
namestringnoA 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
generatorobjectno{ "name": "…", "url": "…" }: the software that wrote the file
countsobjectno{ "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
modulesobjectnoModules 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
extobjectnoExtensions

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" }
  }
}
FieldRequiredMeaning
omcyesCore version the descriptor follows
platformyesThe source's host name. MUST match the host serving the descriptor
name, homepageyes
iconnoSquare image, at least 128 px
capabilitiesyesWhat the source can do, see Capabilities
modulesnoModules the source implements and where, by namespace
extnoExtensions

When an app meets a source it doesn't know, it reads the descriptor and handles the source in the best way it can:

  1. Native: the app has its own integration for that platform.
  2. Generic: the app implements the modules the descriptor lists (for example a delivery module for streaming), and uses them.
  3. 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.

CapabilityMeaning
linkCopies have URLs a person can open
library.readThe person's collection on the source can be read
saved.read, saved.writeThe source's favourites can be read, changed
lists.read, lists.write, lists.create, lists.reorderLists on the source can be read, changed, created, reordered
searchThe source's catalog can be searched
media.previewSome or all tracks can be played as previews
media.streamTracks can be streamed in full, for people whose account allows it
media.downloadOwned 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: none for ownership, curated for a role, recording for 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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, counts and ext describe 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:

  1. 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.
  2. The app's own namespace if no other app would want to read it (an iPod app's On-The-Go order).
  3. 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.
  4. 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.

RoleWord for peopleWhat it does
HosthomeKeeps the collection, settles two changes that collide, and lets apps read and change it (Access module)
ClientappReads or changes a collection that lives at its home
SourcesourceA 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 a source only 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 (ext included) 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 held for 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:

  1. a shared copy: the same ref on matching sources;
  2. a shared identifier: musicbrainz, then isrc for recordings, upc for releases;
  3. for recordings, file copies with the same audio_hash;
  4. for releases, the same label and catalog_number (compared without regard to case, spaces and punctuation), unless both items have copies on the same source with different refs: that is a repress, a separate release with the same number;
  5. 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 via and earliest added_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, a list rule in a filter) 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 view of 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

RoleRequirements
ProducerWrites files that validate against the JSON Schema, with every required field
ConsumerReads 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:

LevelThe service…Module
0 · Exportlets people download their collection as an OMC fileCore
1 · Readpublishes a descriptor and lets apps read collections with the person's consentAccess
2 · Writelets apps change lists and favouritesAccess
3 · Medialets apps stream, preview or download through itDelivery

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 ref for 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 https and http URLs from url, artwork_url and 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).

Platformref of a recordingref of a releaseScope
apple_musicApple's catalog song id (1440857781)Apple's catalog album idglobal
bandcampBandcamp's numeric track id (2854920395)Bandcamp's numeric album idglobal
beatportBeatport's numeric track idBeatport's numeric release idglobal
deezerDeezer's numeric track idDeezer's numeric album idglobal
discogs<release id>/<position> (28993732/A1), see Sources without track idsDiscogs's numeric release idglobal
itunesThe track's persistent id in the libraryThe album's persistent id, if the library has onepersonal
local, rekordbox, serato, traktorThe file's path, relative to the source's folder—personal
qobuzQobuz's numeric track idQobuz's album idglobal
shelf<release ref>/<position>, as for DiscogsThe person's own id for the recordpersonal
soundcloudSoundCloud's numeric track idSoundCloud's numeric playlist idglobal
spotifyThe base62 track id (4uLU6hMCjMI75M1A2tKUQC)The base62 album idglobal
tidalTIDAL's numeric track idTIDAL's numeric album idglobal
youtubeThe video id (dQw4w9WgXcQ)The playlist id of the albumglobal

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.

ModuleNamespaceStatus
Access: the protocol apps use to read, add to and change a collection with the person's consentorg.openmusiccollection.accessDraft 0.2
DJ: cue points, beat grids, loops and analysis, per file; mixes and their tracklistsorg.openmusiccollection.djDraft 0.2
Feeds: following artists, labels and people, and their releases, as RSS-style feedsorg.openmusiccollection.feedsDraft 0.2
Share: sending music to someone, by email or to their share addressorg.openmusiccollection.shareDraft 0.1
Collector: a record's history: what was paid, and copies sold, lent or given awayorg.openmusiccollection.collectorDraft 0.1
Catalog and Commerce: what exists, where, at what price; artists' own offersorg.openmusiccollection.catalogDraft 0.1
Delivery: streaming and downloading from any sourceorg.openmusiccollection.deliveryDraft 0.1
Folder: the collection as a folder of plain-text notes the person owns, opening as an Obsidian vaultorg.openmusiccollection.folderDraft 0.2