@emelymusic/identifiers
v0.1.0
Published
Validation, normalization and check-digit arithmetic for music industry identifiers: ISWC, ISNI, IPI name/base, ISRC, UPC/EAN, GRid, DPID, LC, IPN. Structured failure reasons, zero dependencies, runs in Node and the browser.
Maintainers
Readme
@emelymusic/identifiers
Validation, normalization and check-digit arithmetic for the identifiers the music industry actually files: ISWC, ISNI, IPI name and base numbers, ISRC, UPC/EAN, GRid, DPID, label code (LC) and IPN. emely. uses this one implementation on both sides of the wire - in the browser forms and in the backend write handlers that refuse a bad value - because the same rule written twice is the same rule drifting twice. It is pure TypeScript ESM with zero runtime dependencies, so the identical check runs in Node and in a browser bundle with no loading state.
A passing check means an identifier is well-formed. It says nothing about whether the number belongs to anybody; it is not a lookup and never claims to be one.
Install
npm install @emelymusic/identifiers
# or
pnpm add @emelymusic/identifiersQuick start
Works identically in Node (>= 22) and in a bundler for the browser:
import {
inspectIdentifier,
inspectIsni,
validateIpi,
normalizeIpiBase,
} from '@emelymusic/identifiers';
// Kind-dispatched: one entry point for every identifier.
inspectIdentifier('isni', '0000 0001 2150 062X');
// {
// valid: false,
// normalized: '000000012150062X',
// failure: {reason: 'checkDigit', expected: '7', actual: 'X'},
// }
// Per-identifier inspectors, validators and normalizers.
validateIpi('28476368');
// {valid: true, normalized: '00028476368'}
// (a short all-digit IPI name number is left-padded: leading zeros are
// routinely lost in spreadsheets, and the check digits still have to pass)
normalizeIpiBase('i-001.068.130-6'); // 'I-001068130-6' (Picasso)The identifiers
| Kind | Format | Check rule | Standard | normalized returns |
|---|---|---|---|---|
| isni | 15 digits + check char (digit or X), often grouped in fours | ISO 7064 MOD 11-2 | ISO 27729 | 16 chars, separators stripped, x upper-cased |
| ipiName | 9-digit body + 2 check digits, 11 digits total | weighted sum (weights 10..2) mod 101 | CISAC IPI | 11 digits; a short all-digit input is left-padded |
| ipiBase | I-NNNNNNNNN-C (letter I, 9 digits, check digit) | weighted mod-10, initial weight 2 | CISAC IPI (per CWR spec) | printed form I-NNNNNNNNN-C |
| iswc | T-DDD.DDD.DDD-C | weighted mod-10 over the 9 digits, +1 constant | ISO 15707 | T + 10 digits (dots/hyphens stripped) |
| isrc | CC-XXX-YY-NNNNN | none; shape only, no check digit exists | ISO 3901 | 12 chars, upper-cased |
| upc | 12 (UPC-A) or 13 (EAN-13) digits | GS1 mod-10 | GS1 | the digits as entered |
| grid | A1 + 5-char issuer + 10-char release + check char, 18 total | ISO 7064 MOD 37,36 | IFPI GRid standard | the 18-char string; separators are refused, not repaired |
| dpid | PADPIDA + 10 digits + check char | ISO 7064 MOD 37,36 (inferred, see below) | DDEX | 18 chars, upper-cased |
| lc | LC + 4-6 digits | none; shape only | GVL label code | LC + digits |
| ipn | 4-15 digits | none published; digit shape only | SCAPR IPD | the digit run |
API layers
inspect*(raw) -> IdentifierInspectionis the primary entry point:{valid, normalized, failure?}.normalizedis the canonical form and is returned whenever the structure parses, even when the check digit fails, so a caller can echo back what was actually checked.validate*(raw) -> {valid, normalized}is the same verdict without the failure detail.isValid*(value)are strict boolean gates for re-checking an already stored value (e.g. on export). Heads-up:isValidUpc,isValidGridandisValidIsrcdo not strip whitespace while theirinspect*counterparts do; that asymmetry is deliberate and documented in the module headers.normalize*/format*are canonical-form helpers (normalizeIpiBaseto the printedI-NNNNNNNNN-Cform,formatIsnito four-digit groups,formatIswcForCwrtoT-DDD.DDD.DDD-C), exported separately from the validators: the caller opts in to transforming.*CheckDigit/*CheckCharexpose the check arithmetic for tests and for thenextGtin13/nextGridconstructors.
Structured failures
An inspect* call never just says "invalid". The failure field carries a
machine-readable reason so the caller can render its own wording (no UI
strings or i18n keys ship in this package):
type IdentifierFailure = {
reason: 'length' | 'character' | 'checkDigit' | 'prefix' | 'shape';
expected?: string; // expected check char(s) or length
actual?: string; // entered check char(s) or actual length
char?: string; // the offending character (reason 'character')
position?: number; // its 1-based position in the cleaned input
suggestion?: string; // a plausible fix, e.g. '0' for the letter O
};inspectIdentifier('isni', '0000-0001-2150-062X');
// failure: {reason: 'checkDigit', expected: '7', actual: 'X'}
// -> "the check character should be 7, you entered X"
inspectIdentifier('isni', '000O000121500627');
// failure: {reason: 'character', char: 'O', position: 4, suggestion: '0'}
// -> "the character O at position 4 is not allowed there; did you mean 0?"How the ISWC check digit works
An ISWC is T + 9 digits + 1 check digit (ISO 15707). Take the standard's
own printed example, T-034.524.680-1. Body digits 0 3 4 5 2 4 6 8 0,
weights 1..9, plus a constant of 1:
sum = 1 + (1*0 + 2*3 + 3*4 + 4*5 + 5*2 + 6*4 + 7*6 + 8*8 + 9*0)
= 1 + 178 = 179
check = (10 - (179 mod 10)) mod 10 = (10 - 9) mod 10 = 1So the check digit is 1, and T-034.524.680-1 is well-formed while
T-034.524.680-2 reports {reason: 'checkDigit', expected: '1', actual: '2'}.
The ISNI check is ISO 7064 MOD 11-2 over the first 15 digits: start with
total = 0, then for each digit total = (total + digit) * 2 mod 11. The
check is (12 - total) mod 11, and a result of 10 is written X
(0000 0001 2281 955X is valid; the same body ending 3 is not).
Sources
- ISWC: ISO 15707 - iswc.org/iswc,
Wikipedia.
Anchored on the standard's printed example
T-034.524.680-1. - ISNI: ISO 27729 with ISO 7064 MOD 11-2 - Wikipedia.
- IPI name and base: CISAC's Interested Parties Information system; the
checks are per the CWR specification and django-music-publisher's
implementation, anchored on Picasso's
I-001068130-6- Wikipedia. - ISRC: ISO 3901 - Wikipedia.
- UPC/EAN: GS1 GTIN mod-10 - Wikipedia.
- GRid: IFPI GRid standard, ISO 7064 MOD 37,36, anchored on the
standard's worked example
A12425GABC1234002M- Wikipedia. - DPID: DDEX. DDEX does not publish the check rule normatively; our MOD 37,36 implementation is inferred from python-stdnum and confirmed against real allocated DPIDs. If DDEX ever publishes a spec, reconcile against it.
- LC: the GVL label code,
LC+ 4-6 digits - Wikipedia. - IPN: society-issued performer number (SCAPR IPD). No published check rule exists; the shape is all we check.
The contract vectors
identifier-vectors.json at the package root is the test contract: real
published identifiers as anchors (the ISWC example above, Picasso's IPI base,
the IFPI GRid worked example, Shakespeare's ISNI and IPI) plus the refusal
cases for every kind. It is importable as
@emelymusic/identifiers/identifier-vectors.json and drives most of the test
suite. To add a case, append an entry:
{
"identifier": "isni",
"input": "0000 0001 2103 2683",
"valid": true,
"normalized": "0000000121032683",
"clientAccepts": true,
"note": "William Shakespeare's ISNI"
}valid/normalized are the canonical contract. clientAccepts records a
deliberately stricter client-side contract used by emely-web's own suite
(refine, never transform: a short IPI the canonical rule left-pads is still
rejected there). New cases go into this file, never inline in a test.
Development
Requires Node >= 22 and pnpm.
pnpm install
pnpm test # vitest run
pnpm typecheck # tsc --noEmit over src + test
pnpm build # tsc -> dist/ with .d.tsLicense
MIT, see LICENSE.
Maintained by Emely GmbH. Contact: [email protected].
