songlinks

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 27, 2026 License: MIT Imports: 14 Imported by: 0

README

Go Reference ci

Find the songwriter behind a name, an IPI or an ISRC. The official Go SDK for Songlinks: look up a songwriter by name or IPI name number, or find who wrote a recording from its ISRC, and get the person behind every registration, with their works and ISWCs.

One songwriter is rarely one record. They're registered under different spellings, under several IPI name numbers, or with no number at all. Songlinks groups those registrations into one person, shows the evidence for every join, and keeps apart the ones it isn't sure of.

  • Free, with no key and no sign-up.
  • Typed end to end. Every answer is a Go struct, with a description of every field taken from the API's own description of itself.
  • No dependencies. The standard library only, on Go 1.23 and later.
  • Built for real apps. Clear errors, automatic retries when the API is busy, and long lists paged for you.

New to this? Skip to Your first program, step by step: it starts from installing the language and ends with a working answer. The words used here are explained too.

go get github.com/Alexkane/songlinks-go

Thirty seconds

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/Alexkane/songlinks-go"
)

func main() {
	ctx := context.Background()
	client := songlinks.New(songlinks.WithUserAgent("my-app/1.0"))

	answer, err := client.Lookup(ctx, "mark hoppus", nil)
	if err != nil {
		log.Fatal(err)
	}
	if answer.Answer == "rejected" { // for example a one-word name, which matches too many people
		log.Fatalf("no answer: %s", answer.Rejection.Code)
	}
	candidate := answer.Candidates[0]
	fmt.Println(candidate.PersistentID, candidate.Names, candidate.Works)
	// SLP-ZYM7-GD3E-S [MARK HOPPUS] 302

	person, err := client.Person(ctx, "SLP-ZYM7-GD3E-S", &songlinks.PersonOptions{WorksLimit: 3})
	if err != nil {
		log.Fatal(err)
	}
	for _, work := range person.WorksList.Items {
		fmt.Println(work.Title) // I MISS YOU, ALL THE SMALL THINGS, DOWN
	}

	check, err := client.Writers(ctx, []string{"00338980326", "00340247397"})
	if err != nil {
		log.Fatal(err)
	}
	for _, w := range check.Writers {
		fmt.Println(w.IPINameNumber, w.PersonID, w.Name)
		// 00338980326 SLP-ZYM7-GD3E-S MARK HOPPUS
		// 00340247397 SLP-MG23-XM9B-Y TRAVIS L BARKER
	}
}

Every person has a page you can link to, such as songlinks.io/person/SLP-ZYM7-GD3E-S. It shows the registrations behind the person and what each join rests on.

Beta. Unverified machine output, from a dated snapshot of registration data (every answer's Provenance.MLCSnapshot gives its date, and client.Version(ctx) gives the current one). Some answers are wrong and some are incomplete. Not for rights or royalty decisions.

More
// Which of several writers with the same name wrote a song? Pass one of their songs.
answer, err := client.Lookup(ctx, "amy allen", &songlinks.LookupOptions{Song: "espresso"})
// answer.SongMatch.Match == "exact", answer.SongMatch.Candidates == [c1]: the candidate whose Ref is "c1" is the
// Amy Allen credited on that song

// Every work of a person, paged for you. Breaking out of the loop stops the requests.
for work, err := range client.PersonWorks(ctx, "SLP-ZYM7-GD3E-S", 0) {
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(work.Title)
}
Who wrote this recording? Songwriters by ISRC

A released recording usually has an ISRC, a 12-character code such as USMC19959123 (see the words used here). Give Songwriters up to 50 of them and it says, for each one, which songs the recording is registered to, who wrote each song, and each writer's person ID:

// A real ISRC written with dashes, a well-formed code no recording is registered under, and something that is not an
// ISRC at all.
answer, err := client.Songwriters(ctx, []string{"US-MC1-99-59123", "ZZZZZ0000000", "nope"})
if err != nil {
	log.Fatal(err)
}
for _, recording := range answer.Recordings {
	fmt.Println(recording.ISRC, recording.Status)
	for _, work := range recording.Works {
		for _, w := range work.Writers {
			fmt.Println("  ", work.Title, w.Name, w.IPINameNumber, w.Role, w.PersistentID)
		}
	}
}
// USMC19959123 registered
//    ALL THE SMALL THINGS MARK HOPPUS 00338980326 ComposerLyricist SLP-ZYM7-GD3E-S
//    ALL THE SMALL THINGS TRAVIS L BARKER 00340247397 ComposerLyricist SLP-MG23-XM9B-Y
//    ALL THE SMALL THINGS THOMAS DELONGE 00338976704 ComposerLyricist SLP-MEA6-3QQJ-C
// ZZZZZ0000000 unregistered
// nope invalid

Each recording's Status is one of three:

Status What it means Works
registered The recording is registered to at least one song (work). The songs, each with its writers.
unregistered No song in the registration data lists this recording. It is an answer, not an error: the recording may not have been linked to its song yet, or the code may be mistyped. Empty.
invalid The entry is not an ISRC (two letters, three letters or digits, then seven digits). It does not fail the call. ISRC is your text as you sent it. Empty.

Things to know:

  • A writer's PersistentID can be empty, and often is. It is empty when the writer's record isn't part of a stored person yet, for example because the writer was registered with no IPI name number. Nearly half of registered recordings have at least one writer like that. The writer's Name, Role and IPINameNumber (when there is one) are still the answer; PersonURL is empty too, and otherwise is a page such as /person/SLP-ZYM7-GD3E-S on https://songlinks.io.
  • Match answers to your list by ISRC, not by position. Case, dashes and spaces are ignored, so US-MC1-99-59123 and usmc19959123 are one recording and come back as one answer, written USMC19959123. songlinks.NormalizeISRC turns your input into the same form.
  • Only the plain dash is ignored. A word processor sometimes turns - into a longer dash (–); an ISRC written with one comes back invalid.
  • Writers only. Publishers and administrators on the song are left out, and so are ownership shares.
  • One entry, one ISRC. An entry holding a comma, semicolon or line break is refused, because the API would read it as several; more than 50 entries is refused too, so send a long list in batches of songlinks.SongwritersMax.

Your first program, step by step

Never called an API from code before? This takes about five minutes.

Before you start, you need two things:

  • A terminal, where you type commands. On a Mac, open Terminal (in Applications, then Utilities). On Windows, open PowerShell from the Start menu. Type each command below on its own line and press Enter after it.
  • A plain-text editor for code, such as the free Visual Studio Code. A word processor (Word, or TextEdit in its usual mode) adds formatting that stops code from running.
  1. Install Go 1.23 or later from go.dev/dl. Check it worked by typing go version in a terminal; it prints a version such as go version go1.23.4.

  2. Make a folder for your project (a "module", in Go's words) and add the SDK to it:

    mkdir songlinks-test
    cd songlinks-test
    go mod init songlinks-test
    go get github.com/Alexkane/songlinks-go
    

    go get prints the version it added, such as go: added github.com/Alexkane/songlinks-go v0.1.0.

  3. Create a file called main.go in the songlinks-test folder, in your editor, with this in it:

    package main
    
    import (
    	"context"
    	"fmt"
    	"log"
    	"os"
    	"strings"
    
    	"github.com/Alexkane/songlinks-go"
    )
    
    func main() {
    	name := "mark hoppus"
    	if len(os.Args) > 1 {
    		name = os.Args[1]
    	}
    	client := songlinks.New(songlinks.WithUserAgent("my-first-test/1.0"))
    	answer, err := client.Lookup(context.Background(), name, nil)
    	if err != nil {
    		log.Fatal(err)
    	}
    	if answer.Answer == "rejected" {
    		fmt.Println("No answer, because:", answer.Rejection.Code)
    		return
    	}
    	for _, person := range answer.Candidates {
    		id := person.PersistentID
    		if id == "" {
    			id = "(no person ID yet)"
    		}
    		fmt.Printf("%s %s (works: %d)\n", id, strings.Join(person.Names, ", "), person.Works)
    	}
    }
    

    WithUserAgent names your program to the API, so a problem can be traced to it; any short name will do.

  4. Run it, from the same folder, with any songwriter's full name:

    go mod tidy
    go run . "mark hoppus"
    

    go mod tidy records what main.go uses; you only need it once. You should see SLP-ZYM7-GD3E-S MARK HOPPUS (works: 302).

    Keep the quotes around the name: without them, only the first word is sent.

    What you might see instead:

    • Several lines. The name could belong to more than one person, so each possible person gets a line.
    • (no person ID yet). Songlinks found the registrations but hasn't given that person an ID yet. That is normal, and the rest of the line is still the answer.
    • No answer, because: single-token. The name is one word, such as madonna, which matches too many people. Use the full name.
    • No answer, because: no-match. Nothing registered is close to that name. Check the spelling.

From here, the examples do real jobs: copy one from the examples folder on GitHub into your folder as main.go and run it the same way. Reading an answer explains what comes back, and the words used here are explained just below.

The words used here

Music-publishing words, which you'll meet in the answers:

  • Songwriter registration: when a song is written, its publisher registers it so royalties reach the people who wrote it. Each registration names the writers, as typed by whoever filed it. One songwriter can therefore appear under several spellings, and two different songwriters can share a name.
  • Record: one of those registered writer entries: a name as it was typed, with its IPI name number if it has one, and the songs (works) registered under it.
  • IPI name number: an 11-digit number that identifies a songwriter or publisher to collecting societies, such as 00338980326. Many records have none, and one person can have several. More about IPI name numbers.
  • Work: a song as a composition (the melody and lyrics), as opposed to a particular recording of it.
  • ISWC: the international code for a work, such as T0719661581. Not every registered work has one.
  • ISRC: the international code for one recording, such as USMC19959123 for blink-182's recording of "All the Small Things". An ISWC names the song (the composition); an ISRC names one recording of it, so a song that has been recorded many times has one ISWC and many ISRCs. You'll usually find ISRCs in a distributor's or label's catalogue export. They're written with or without dashes (US-MC1-99-59123); Songlinks reads both.
  • Person and person ID: Songlinks groups the records it judges to be one human into a person and gives them a person ID, such as SLP-ZYM7-GD3E-S. Every person has a page, such as songlinks.io/person/SLP-ZYM7-GD3E-S.
  • Candidate: what a lookup by name returns: the records that name reaches, grouped into people for that one question. A candidate usually matches one person ID, but it is worked out fresh each time.
  • Evidence: why two records were grouped: the same IPI name number, or a judgement that they are the same person (for example, the same co-writers and publishers).
  • Rejected: a lookup that gave no candidates, with a reason, such as a one-word name (madonna) that matches too many people to group. It is an answer, not an error.

Programming words:

  • API: a service that programs talk to over the internet. The Songlinks API answers questions such as "who is this songwriter?".
  • SDK: this library. It does the talking to the API for you, so your code calls a function instead of building web requests by hand.
  • Rate limit: the most requests the API accepts from one internet address in a stretch of time: about 100 every 5 minutes, or 20 from VPNs and cloud servers. Over it, the SDK tells you how long to wait.
  • Module: a Go project, with a go.mod file that lists the libraries it uses, such as this one.

Checking a whole roster?

The API answers one name at a time, which suits an app. If you administer a catalogue, send us the writers instead. For each one we send back every registration we can group to them, across spellings, stage names and more than one IPI name number, with the evidence behind each join and the records we deliberately left out. See bulk songwriter lookup, or write to alex@songlinks.to.

Examples

Runnable programs for the jobs people use Songlinks for, each under 70 lines. Copy one and change it:

Program What it does
resolve-name Every person a name could be, with their registrations and what joined them.
which-namesake Two writers share a name: which one wrote this song?
check-roster A list of IPI name numbers in, CSV out: who holds each and their works.
export-catalogue Every work of a person, as CSV.
songwriters-by-isrc A list of ISRCs in, CSV out: the songs each recording is registered to, their writers and person IDs.
go run ./examples/check-roster roster.txt > checked.csv

The examples are vetted and built with the package in CI, so one that misreads a field fails the build.

What it covers

Method API What it answers
Lookup(ctx, name, *LookupOptions) GET /v1/lookup The people a songwriter's name could be. A Song the writer wrote says which of them wrote it.
Person(ctx, id, *PersonOptions) GET /v1/person/{id} One person: their registrations, names, IPI name numbers and a page of works.
PersonWorks(ctx, id, pageSize) GET /v1/person/{id} Every work of a person, as an iter.Seq2[PersonWork, error] over the pages.
Writers(ctx, ipiNameNumbers) GET /v1/writers Up to 50 IPI name numbers checked at once: the stored person behind each.
Songwriters(ctx, isrcs) GET /v1/songwriters Up to 50 recordings checked at once, by ISRC: the songs each is registered to, their writers and the person behind each.
Version(ctx) GET /v1/version What is serving: the release, the model and the date of the registration data.

The answer types are named after the API's schemas (LookupResponse, PersonResponse, Cluster, WritersResponse, SongwritersResponse and the rest), with Go's initialisms (PersistentID, IPINameNumber, ISWC, ISRC). Two are renamed: the API's Record is LookupRecord and its Error is APIError. The full reference is on pkg.go.dev.

Reading an answer

A person and a candidate are different things: see the words used here.

  • A candidate can count more works than its person. "mark hoppus" reaches two registrations, 302 works between them; the person SLP-ZYM7-GD3E-S holds one of them as theirs (294 works) and lists the other in Possible. Person is the authority on what an ID holds.
  • Match a writers check by number, not position. Repeats and two forms of one number come back as one entry, as 11 digits. songlinks.NormalizeIPI turns your input into the same form. A songwriters check is the same, by ISRC, with songlinks.NormalizeISRC.
  • A missing string is "". Many records have no person ID yet, and that is ordinary. A number, boolean or object the API may leave out is a pointer, because there nil and zero say different things: a WriterAnswer's Works is nil unless its Status is "person", and a false Prerelease is a released ID.
  • A lookup candidate is not a stored person. It is the lookup's own grouping, made for the name you asked. PersistentID is the stored person when there is exactly one. A candidate whose records belong to more than one stored person has an empty PersistentID and lists them in StoredPersons. Read each record's own PersistentID.
  • Person IDs can be prerelease. A prerelease ID may stop resolving, or point at a different person, without notice. Check Prerelease before you store one.

Errors and retries

An error answer from the API is an *songlinks.Error with the HTTP StatusCode, the API's Code (the one to branch on), a Message for people, and the response Body. A withdrawn person ID is a *songlinks.PersonWithdrawnError carrying the API's full answer about the ID; it also unwraps to an *Error with Code "withdrawn".

// Continuing inside main above, where client and ctx were created (and with "errors" imported).
personID := "SLP-0000-0000-0" // an ID no one holds, to see the not-found branch
_, err := client.Person(ctx, personID, nil)

var withdrawn *songlinks.PersonWithdrawnError
var apiErr *songlinks.Error
switch {
case errors.As(err, &withdrawn):
	fmt.Println("withdrawn at", withdrawn.Person.WithdrawnAt) // a 410, and a valid answer to keep
case errors.As(err, &apiErr) && apiErr.Code == "rate_limited":
	fmt.Println("retry in", apiErr.RetryAfter)
case errors.As(err, &apiErr) && apiErr.Code == "not_found":
	fmt.Println("no person with that ID") // a 404, such as for SLP-0000-0000-0
case errors.As(err, &apiErr):
	fmt.Println(apiErr.StatusCode, apiErr.Code, apiErr.Message)
case err != nil:
	log.Fatal(err) // a network failure, after the client's retries
}
  • Retried automatically: 429 and 503 answers and network failures, up to WithMaxRetries times (default 2). When the API asks for a wait (in a Retry-After header or the body's retry_after_seconds) of a minute or less, the client waits that long; otherwise it backs off from half a second.
  • Returned at once: a requested wait longer than a minute, in Error.RetryAfter, for you to schedule.
  • Never retried: any other error, and a cancelled or expired context.
  • Merged person IDs are followed to the person they now point at, and the answer's RedirectedFrom names the ID you asked for.

Options

songlinks.New(
	songlinks.WithBaseURL("https://songlinks.io"),  // the default
	songlinks.WithHTTPClient(&http.Client{Timeout: 10 * time.Second}), // default: 30-second timeout
	songlinks.WithMaxRetries(2),                    // 0 turns retrying off
	songlinks.WithUserAgent("my-app/1.2"),          // appended to songlinks-go/<version>
)

A Client is safe for concurrent use; create one and share it.

Fair use

The API is free and open. It allows about 100 requests per 5 minutes from one address, across every endpoint, and 20 from VPNs, proxies and hosting providers, which includes most cloud servers. A Writers or Songwriters call counts as one request however many numbers or ISRCs it holds. Please send a WithUserAgent that names your app, keep to one request at a time per user action, and cache answers you reuse. Over the limit, the SDK returns an *Error with Code "rate_limited" whose RetryAfter is the most you need to wait; for a whole roster, see bulk songwriter lookup.

Development

go vet ./... && go test ./...          # unit tests against a local server, the types against the API's document
SONGLINKS_LIVE=1 go test ./...         # the same calls against songlinks.io

The types are generated: curl -sf https://songlinks.io/v1/openapi.json -o testdata/openapi.json, then python3 scripts/gen_models.py && gofmt -w models.go. CI regenerates them and fails if models.go differs, and models_test.go fails if a type drifts from the document or an exported name loses its doc comment.

Licence

MIT. See LICENSE.

Documentation

Overview

Package songlinks is the official Go SDK for the Songlinks API: find the songwriter behind a name, an IPI name number or a recording's ISRC.

One songwriter is rarely one record. They are registered under different spellings, under several IPI name numbers, or with no number at all. Songlinks groups those registrations into one person, shows the evidence for every join, and keeps apart the ones it isn't sure of. The API is free, with no key and no sign-up.

client := songlinks.New(songlinks.WithUserAgent("my-app/1.0"))
answer, err := client.Lookup(ctx, "mark hoppus", nil)
if err != nil {
	return err
}
if answer.Answer == "rejected" { // for example a one-word name, which matches too many people
	return fmt.Errorf("no answer: %s", answer.Rejection.Code)
}
fmt.Println(answer.Candidates[0].PersistentID) // SLP-ZYM7-GD3E-S

Reading an answer

Two words come up throughout. A person is a songwriter Songlinks has given a person ID (SLP-…), with the registrations it holds as theirs; Client.Person reads one. A candidate (Cluster) is what a lookup returns: the registrations that one name reaches, grouped fresh for that query. A candidate can count more works than its person, because a name can reach registrations the person ID lists only as possible; Client.Person is the authority on what an ID holds.

The answer types are generated from the API's OpenAPI document and named after its schemas, with the API's description of every field as its doc comment. A string the API may leave out or send as null is "" (many records have no person ID yet, and that is ordinary). A number, boolean or object it may leave out is a pointer, because there nil and zero say different things: a nil WriterAnswer.Works is not known (it is set only when Status is "person"), and a false Prerelease is a released ID.

Client.Songwriters starts from recordings instead of names: give it ISRCs, the 12-character codes that identify one recording each (such as USMC19959123), and it returns the works (songs) each recording is registered to and their writers. An ISRC names a recording; an ISWC names the song, which can have many recordings. Each RecordingAnswer has a Status of "registered", "unregistered" (no work lists the recording, an answer rather than an error) or "invalid" (not an ISRC), and a writer's PersistentID is empty when their record is not part of a stored person yet, which is common for writers registered with no IPI name number.

A candidate's PersistentID is the stored person when there is exactly one; when its records belong to more than one, PersistentID is empty and StoredPersons lists them. A Prerelease ID may stop resolving, or point at a different person, without notice: check it before you store one.

Errors and retries

An error answer is returned as an *Error: branch on its Code, the API's own error code. A withdrawn person ID is a *PersonWithdrawnError, which carries the API's full answer about the ID. The public API allows about 100 requests per 5 minutes from one address, across every endpoint, and 20 from VPNs, proxies and hosting providers (which includes most cloud servers). 429 and 503 answers and network failures are retried, up to two times by default, waiting as long as the API asks when that is a minute or less, or backing off from half a second; a longer requested wait is returned at once, in Error.RetryAfter. A cancelled context is never retried.

Beta: answers are unverified machine output from a dated snapshot of registration data (every answer's Provenance.MLCSnapshot gives its date). Some are wrong and some are incomplete. Not for rights or royalty decisions.

Index

Examples

Constants

View Source
const DefaultBaseURL = "https://songlinks.io"

DefaultBaseURL is the API's address.

View Source
const SongwritersMax = 50

SongwritersMax is the most ISRCs Client.Songwriters accepts in one call, the API's own limit.

View Source
const Version = "0.2.0"

Version is this SDK's release.

View Source
const WorksPageMax = 200

WorksPageMax is the most works one Client.Person call returns; Client.PersonWorks pages through the rest.

View Source
const WritersMax = 50

WritersMax is the most IPI name numbers Client.Writers accepts in one call, the API's own limit.

Variables

This section is empty.

Functions

func NormalizeIPI

func NormalizeIPI(ipiNameNumber string) string

NormalizeIPI writes an IPI name number the way Client.Writers answers do, so the answers can be matched to the numbers you sent. Spaces and dashes (including the typographic dashes a spreadsheet substitutes) are dropped, and a run of 1 to 11 digits is padded with leading zeros to 11: "338980326" and "003-389-803 26" both become "00338980326". Anything else is not an IPI name number, and is returned with surrounding spaces removed and cut to 32 characters, as the API echoes it with Status "invalid". A dot is not a separator: "15036818.0" is what a spreadsheet makes of a number read as a float, and dropping the dot would turn it into somebody else's number.

func NormalizeISRC added in v0.2.0

func NormalizeISRC(isrc string) string

NormalizeISRC writes an ISRC the way Client.Songwriters answers do, so the answers can be matched to the codes you sent. Spaces and dashes are dropped and letters are upper-cased: "us-mc1-99-59123" and "USMC19959123" both become "USMC19959123". Anything that is not then an ISRC (two letters, three letters or digits, seven digits) is returned with surrounding spaces removed and cut to 32 characters, as the API echoes it with Status "invalid".

Only the plain dash (-) is dropped, as the API drops only that one: an ISRC whose dashes a word processor has turned into typographic ones (–) is not an ISRC to the API, and comes back "invalid".

Types

type APIError

type APIError struct {
	// `too_many` or `no_numbers` (400), `not_found` (404), `internal` (500), or `not_ready` or `busy` (503). Branch on
	// this.
	Error string `json:"error"`

	// A short sentence for people; its wording may change.
	Message string `json:"message"`
}

APIError is the body of a 400, 404, 500 or 503 response. It never carries detail about what failed. A malformed parameter (an empty name, a threshold out of range) is answered 422 instead, with a `detail` list saying which parameter and why.

type BuildRef

type BuildRef struct {
	// The build's number.
	BuildID int `json:"build_id"`

	// The build's status, such as `applied`.
	Status string `json:"status"`

	// Where the build ran, such as `production`; diagnostic.
	Environment string `json:"environment"`

	// When the build started, as an ISO 8601 timestamp.
	StartedAt string `json:"started_at,omitempty"`

	// When the build finished, as an ISO 8601 timestamp.
	FinishedAt string `json:"finished_at,omitempty"`

	// Identifies the model the build used.
	ModelVersion string `json:"model_version,omitempty"`

	// Identifies the code the build ran.
	CodeCommit string `json:"code_commit"`

	// True if the build ran with uncommitted changes; diagnostic.
	CodeDirty bool `json:"code_dirty"`

	// The date of the registration-data release the build read, YYYY-MM-DD. Empty when that load is undated: never a
	// guess.
	MLCSnapshot string `json:"mlc_snapshot,omitempty"`
}

BuildRef is a build: the automatic run that groups records into people and issues IDs.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client calls the Songlinks API. Create one with New and reuse it; it is safe for concurrent use.

func New

func New(options ...Option) *Client

New creates a client. With no options it calls https://songlinks.io with a 30-second timeout and two retries.

func (*Client) Lookup

func (c *Client) Lookup(ctx context.Context, name string, options *LookupOptions) (*LookupResponse, error)

Lookup looks up a songwriter by name, such as "mark hoppus", and returns the people the name could be, each with the registrations behind them. Check the answer's Answer field first: "rejected" is an answer, not an error, and Rejection says why. A single word, such as "madonna", is rejected with Rejection.Code "single-token", since one word matches too many people to group. Options may be nil.

It returns an error, without calling the API, when the name is empty, and an *Error when the API answers with an error, such as Code "invalid_request" for a Threshold out of range.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/Alexkane/songlinks-go"
)

func main() {
	client := songlinks.New(songlinks.WithUserAgent("my-app/1.0"))
	answer, err := client.Lookup(context.Background(), "mark hoppus", nil)
	if err != nil {
		log.Fatal(err)
	}
	if answer.Answer == "rejected" {
		fmt.Println("no answer:", answer.Rejection.Code)
		return
	}
	for _, person := range answer.Candidates {
		fmt.Println(person.PersistentID, person.Names, person.Works)
	}
	// SLP-ZYM7-GD3E-S [MARK HOPPUS] 302
}
Example (Song)
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/Alexkane/songlinks-go"
)

func main() {
	// Which of several writers with the same name wrote a song? Pass one of their songs.
	client := songlinks.New(songlinks.WithUserAgent("my-app/1.0"))
	answer, err := client.Lookup(context.Background(), "amy allen", &songlinks.LookupOptions{Song: "espresso"})
	if err != nil {
		log.Fatal(err)
	}
	if answer.SongMatch != nil {
		fmt.Println(answer.SongMatch.Match, answer.SongMatch.Candidates)
	}
	// exact [c1]
}

func (*Client) Person

func (c *Client) Person(ctx context.Context, personID string, options *PersonOptions) (*PersonResponse, error)

Person reads a stored person by ID, such as "SLP-ZYM7-GD3E-S", with one page of their works. A merged ID is followed to the person it now points at, and the answer's RedirectedFrom names the ID you asked for. Options may be nil, for the first page.

It returns an error, without calling the API, when the ID is empty; a *PersonWithdrawnError when the ID has been withdrawn; and an *Error for any other error answer, such as Code "not_found" for an unknown ID.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/Alexkane/songlinks-go"
)

func main() {
	client := songlinks.New(songlinks.WithUserAgent("my-app/1.0"))
	person, err := client.Person(context.Background(), "SLP-ZYM7-GD3E-S", &songlinks.PersonOptions{WorksLimit: 3})
	if err != nil {
		log.Fatal(err)
	}
	for _, work := range person.WorksList.Items {
		fmt.Println(work.Title)
	}
	// I MISS YOU
	// ALL THE SMALL THINGS
	// DOWN
}

func (*Client) PersonWorks

func (c *Client) PersonWorks(ctx context.Context, personID string, pageSize int) iter.Seq2[PersonWork, error]

PersonWorks reads every work of a person, in the API's order, pageSize works per request. A pageSize of 0 or less, or above WorksPageMax, means WorksPageMax. Pages are requested only as the loop asks for more, so breaking out of the loop stops the requests. An error ends the sequence: it is yielded once, with a zero PersonWork, and is the same error Client.Person returns, so a withdrawn ID yields a *PersonWithdrawnError.

for work, err := range client.PersonWorks(ctx, "SLP-ZYM7-GD3E-S", 0) {
	if err != nil {
		return err
	}
	fmt.Println(work.Title)
}
Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/Alexkane/songlinks-go"
)

func main() {
	client := songlinks.New(songlinks.WithUserAgent("my-app/1.0"))
	for work, err := range client.PersonWorks(context.Background(), "SLP-ZYM7-GD3E-S", 0) {
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(work.MLCWorkID, work.Title, work.ISWC)
	}
}

func (*Client) Songwriters added in v0.2.0

func (c *Client) Songwriters(ctx context.Context, isrcs []string) (*SongwritersResponse, error)

Songwriters finds who wrote up to SongwritersMax recordings at once, by ISRC, such as "USMC19959123". For each recording it returns the works (songs) the recording is registered to, the writers registered on each work, and the person ID of each writer where Songlinks has one. Case does not matter, and spaces and dashes inside an ISRC are ignored ("US-MC1-99-59123" is "USMC19959123"). The API refuses a longer list rather than shortening it, and so does this method, counting entries: for more, call it once per batch. Each entry is one ISRC: an entry holding a comma, semicolon, line break or the text "%2C" is refused, since the API would read it as several.

Each answer's Status says what was found:

  • "registered": the recording is registered to at least one work, and Works lists them with their writers.
  • "unregistered": no work in the registration data lists the recording. This is an answer, not an error: the recording may not have been linked to its song yet, or the code may be mistyped.
  • "invalid": the entry is not an ISRC. It does not fail the call, and ISRC is the text you sent.

A writer's PersistentID and PersonURL are empty when their record is not part of a stored person yet. That is common, not an error (a writer registered with no IPI name number, for example, is often not grouped into a person), and nearly half of registered recordings have at least one writer like that. The writer's name, role and IPI name number (where there is one) are still the answer. Only writers are listed: publishers and administrators are left out.

The answer has one entry per distinct ISRC, in the order each was first sent. Two spellings of one code ("usmc19959123" and "US-MC1-99-59123") and repeats come back as one entry, written as 12 characters, so match answers to your list by ISRC, not by position: NormalizeISRC turns your input into the form the answer uses.

It returns an error, without calling the API, when the list is empty or too long, and an *Error when the API answers with an error. An entry that is empty or only spaces is skipped by the API, and a list with nothing else in it is answered with an *Error.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/Alexkane/songlinks-go"
)

func main() {
	client := songlinks.New(songlinks.WithUserAgent("my-app/1.0"))
	answer, err := client.Songwriters(context.Background(), []string{"USMC19959123"})
	if err != nil {
		log.Fatal(err)
	}
	for _, recording := range answer.Recordings {
		for _, work := range recording.Works {
			for _, w := range work.Writers {
				fmt.Println(recording.ISRC, work.Title, w.Name, w.PersistentID)
			}
		}
	}
	// USMC19959123 ALL THE SMALL THINGS MARK HOPPUS SLP-ZYM7-GD3E-S
	// USMC19959123 ALL THE SMALL THINGS TRAVIS L BARKER SLP-MG23-XM9B-Y
	// USMC19959123 ALL THE SMALL THINGS THOMAS DELONGE SLP-MEA6-3QQJ-C
}

func (*Client) Version

func (c *Client) Version(ctx context.Context) (*VersionResponse, error)

Version asks what is serving: the API's release, the model and code in use, and the date of the registration data.

It returns an *Error when the API answers with an error.

func (*Client) Writers

func (c *Client) Writers(ctx context.Context, ipiNameNumbers []string) (*WritersResponse, error)

Writers checks up to WritersMax IPI name numbers at once and returns the person who holds each and a status, with totals in Summary. Leading zeros may be left off, and spaces and dashes are ignored. The API refuses a longer list rather than shortening it, and so does this method, counting entries: for more, call it once per batch. Each entry is one number: an entry holding a comma, semicolon, line break or the text "%2C" is refused, since the API would read it as several.

The answer has one entry per distinct number, in the order each was first sent, written as 11 digits. Two forms of one number ("338980326" and "00338980326") and repeats come back as one entry, so match answers to your list by number, not by position: NormalizeIPI turns your input into the form the answer uses.

It returns an error, without calling the API, when the list is empty or too long, and an *Error when the API answers with an error.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/Alexkane/songlinks-go"
)

func main() {
	client := songlinks.New(songlinks.WithUserAgent("my-app/1.0"))
	check, err := client.Writers(context.Background(), []string{"00338980326", "00340247397"})
	if err != nil {
		log.Fatal(err)
	}
	for _, w := range check.Writers {
		fmt.Println(w.IPINameNumber, w.Status, w.PersonID, w.Name)
	}
	// 00338980326 person SLP-ZYM7-GD3E-S MARK HOPPUS
	// 00340247397 person SLP-MG23-XM9B-Y TRAVIS L BARKER
}

type Cluster

type Cluster struct {
	// Names this candidate inside this response only (`c1`, `c2`, ...). Another spelling of the name can group the
	// records differently, so never store it.
	Ref string `json:"ref"`

	// The person ID, `SLP-XXXX-XXXX-C`, or empty when none has been issued, or when the records belong to more than one
	// person (see StoredPersons). Empty is common.
	PersistentID string `json:"persistent_id,omitempty"`

	// True: the ID may stop resolving, or later point at a different person, without notice, so do not store it. False: a
	// released ID. Nil when there is no ID.
	Prerelease *bool `json:"prerelease,omitempty"`

	// The stored person's `state`; empty when there is no ID.
	PersonState string `json:"person_state,omitempty"`

	// When a record's ID has since been merged into this one, that older ID.
	RedirectedFrom string `json:"redirected_from,omitempty"`

	// How many registrations the stored person holds as theirs (its member records); nil when there is no ID.
	PersonRecords *int `json:"person_records,omitempty"`

	// Member records the stored person holds that this lookup did not reach. Read the person to see them.
	RecordsNotInThisAnswer *int `json:"records_not_in_this_answer,omitempty"`

	// Every distinct registered name in Records.
	Names []string `json:"names"`

	// Every distinct IPI name number in Records.
	IPINameNumbers []string `json:"ipi_name_numbers"`

	// Distinct works across Records: a work registered under two of them counts once, so this can be less than their sum.
	// It counts every record this lookup grouped, so it can be more than the stored person's own Works, which counts only
	// the records the person ID holds.
	Works int `json:"works"`

	// The registration records grouped into this person, most works first.
	Records []LookupRecord `json:"records"`

	// One entry per pair of records joined.
	Evidence []Evidence `json:"evidence"`

	// The IPI name number most of this person's works are registered under: the one whose records hold the most works.
	// Empty when no record has one.
	MainIPINameNumber string `json:"main_ipi_name_number,omitempty"`

	// Distinct works on the records carrying MainIPINameNumber. Works minus this is how many works are held only by the
	// person's other registrations, under another number or none. Nil when MainIPINameNumber is.
	WorksOnMainIPI *int `json:"works_on_main_ipi,omitempty"`

	// The person IDs this candidate's records belong to. Usually one, the same as PersistentID. Two or more mean
	// Songlinks' person records hold these registrations as different people, so the candidate has no single
	// PersistentID: read each record's own ID.
	StoredPersons []string `json:"stored_persons,omitempty"`
}

Cluster is one person, as this lookup groups them: the registration records judged to be the same songwriter.

type Error

type Error struct {
	// StatusCode is the HTTP status, such as 404 or 503.
	StatusCode int
	// Code is the API's error code, such as "not_found", "busy", "too_many" or "rate_limited". A 422, the API's
	// answer to a malformed parameter, is "invalid_request".
	Code string
	// Message is the API's message, or a description of the failure when it sent none.
	Message string
	// RetryAfter is how long the API asked the caller to wait before trying again, on a 429 or 503; zero when it
	// did not say. The public API allows about 100 requests per 5 minutes from one address, across every endpoint
	// (20 from VPNs, proxies and hosting providers, which includes most cloud servers).
	// The client waits out a wait of a minute or less and retries by itself, so an error with a short RetryAfter
	// means its retries ran out; a longer wait is returned at once. It is the most you need to wait, not a countdown.
	RetryAfter time.Duration
	// Body is the answer's body, as sent. Decode it into [APIError] for an ordinary error or [RateLimited] for a 429.
	Body json.RawMessage
}

Error is an error answer from the API. Branch on Code, the API's own error code; Message is a sentence meant for people, and its wording may change.

var apiErr *songlinks.Error
if errors.As(err, &apiErr) && apiErr.Code == "not_found" {
	...
}
Example
package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	"github.com/Alexkane/songlinks-go"
)

func main() {
	client := songlinks.New()
	_, err := client.Person(context.Background(), "SLP-0000-0000-0", nil)

	var withdrawn *songlinks.PersonWithdrawnError
	var apiErr *songlinks.Error
	switch {
	case errors.As(err, &withdrawn):
		fmt.Println("withdrawn at", withdrawn.Person.WithdrawnAt) // a 410, and a valid answer to keep
	case errors.As(err, &apiErr):
		fmt.Println(apiErr.StatusCode, apiErr.Code, apiErr.Message)
	case err != nil:
		log.Fatal(err) // a network failure, after the client's retries
	}
}

func (*Error) Error

func (e *Error) Error() string

Error returns the status, the code and the API's message.

type Evidence

type Evidence struct {
	// The two records joined, by `party_record_id`.
	Records []int `json:"records"`

	// `same_ipi_name_number`: both records carry the same IPI name number. `model`: the model judged them to be the same
	// person.
	Rule string `json:"rule"`

	// The model's calibrated probability that the two records are one person, from 0 to 1.
	Probability *float64 `json:"probability,omitempty"`

	// What the two records have in common, under `co_writers`, `publishers` and `administrators`, each `{"count": n,
	// "rarest": [names]}`. `rarest` lists the least common shared names, which say the most; names shared by a very large
	// number of writers are left out. A key may be absent.
	Shared map[string]any `json:"shared,omitempty"`
}

Evidence says why two records in a candidate were joined.

type Link struct {
	// The linked record, by PartyRecordID.
	PartyRecordID int `json:"party_record_id"`

	// The linked record's registered name.
	Name string `json:"name"`

	// The linked record's IPI name number, or empty.
	IPINameNumber string `json:"ipi_name_number,omitempty"`

	// `checked`: the two people share no registered work, so the link is not a co-writer's. `co-writers too`: they share
	// works, as bandmates do, and the link is weaker.
	Grade string `json:"grade,omitempty"`

	// How close together the two records were registered; 1 is adjacent.
	Steps int `json:"steps"`

	// Works registered under the linked record.
	Works int `json:"works"`

	// The record in this lookup the link starts from.
	FromRecord int `json:"from_record"`

	// That record's registered name.
	FromName string `json:"from_name"`

	// `[credited, recorded]`: of the linked record's works that have a recording, how many credit FromName as the artist.
	Forward []int `json:"forward,omitempty"`

	// `[credited, recorded]`: of FromRecord's works that have a recording, how many credit Name as the artist.
	Back []int `json:"back,omitempty"`
}

Link is a record registered under another name next to one of this lookup's records: a stage name and a legal name, for example. A different claim from a candidate: nothing here is joined, and its works are not counted in Works.

type LookupOptions

type LookupOptions struct {
	// Song is the title of a song the writer wrote. When given, the answer's SongMatch says which candidates hold a
	// work with that title.
	Song string
	// Threshold is how sure Songlinks must be before it joins two records into one person, from 0.5 to 0.99. Higher
	// joins fewer records and keeps more apart. Zero leaves the API's default, which [Client.Version] reports as
	// Threshold (0.85 today).
	Threshold float64
	// Routes are extra searches to run beside the name match. The one there is, "catalogue", also finds records
	// registered under another name that share the writer's catalogue, such as a stage name, and returns them in
	// Links. Nil runs it, as the API does by default; an empty, non-nil slice skips it, and the lookup is faster.
	Routes []string
}

LookupOptions are the optional parts of a lookup. A zero field is left to the API.

type LookupRecord

type LookupRecord struct {
	// Identifies this record in the registration data. Not a person ID: one person usually has several records.
	PartyRecordID int `json:"party_record_id"`

	// The name as registered.
	Name string `json:"name"`

	// The part of the name the record is filed under, usually the surname.
	KeyName string `json:"key_name,omitempty"`

	// The rest of the name, usually given names.
	NamesBeforeKeyName string `json:"names_before_key_name,omitempty"`

	// The IPI name number, or empty when the record has none.
	IPINameNumber string `json:"ipi_name_number,omitempty"`

	// Works registered under this record.
	Works int `json:"works"`

	// The person ID this record belongs to, or empty. It can differ from the candidate's PersistentID when this lookup
	// groups a record that the stored person does not hold.
	PersistentID string `json:"persistent_id,omitempty"`

	// True: the ID may stop resolving, or later point at a different person, without notice, so do not store it. False: a
	// released ID. Nil when there is no ID.
	Prerelease *bool `json:"prerelease,omitempty"`
}

LookupRecord is one registration record: a name as it was registered, with its IPI name number if it has one.

type LookupResponse

type LookupResponse struct {
	// The question answered.
	Query Query `json:"query"`

	// Either `candidates` or `rejected`. When it is `rejected`, Rejection says why.
	Answer string `json:"answer"`

	// Why the lookup stopped; nil unless Answer is `rejected`.
	Rejection *Rejection `json:"rejection,omitempty"`

	// The people the name could be, one entry each; empty when Answer is `rejected`.
	Candidates []Cluster `json:"candidates,omitempty"`

	// Pairs of records held apart; never part of a candidate.
	Review []ReviewItem `json:"review,omitempty"`

	// Records registered under another name next to this lookup's; filled only when `routes` includes `catalogue`.
	Links []Link `json:"links,omitempty"`

	// Records grouped into this answer; at most `provenance.max_candidates`.
	RecordsConsidered *int `json:"records_considered,omitempty"`

	// Records compared with the name.
	RecordsSearched *int `json:"records_searched,omitempty"`

	// Records that matched closely enough to be considered.
	RecordsPassed *int `json:"records_passed,omitempty"`

	// True when more records matched than one lookup groups (RecordsPassed > RecordsConsidered). The answer is partial.
	Truncated *bool `json:"truncated,omitempty"`

	// How many candidates carry a person ID.
	PersonsResolved *int `json:"persons_resolved,omitempty"`

	// Which candidates wrote the song; nil unless a `song` was sent and the lookup found candidates.
	SongMatch *SongMatch `json:"song_match,omitempty"`

	// Your remaining allowance; nil when no per-caller limit is in force. The edge's per-address limit (about 100
	// requests per 5 minutes, 20 from VPNs and hosting networks) is separate and not counted here.
	RateLimit *RateLimit `json:"rate_limit,omitempty"`

	// What produced this answer.
	Provenance Provenance `json:"provenance"`
}

LookupResponse is the answer to one lookup. Check Answer first.

type Option

type Option func(*Client)

An Option configures a Client. Pass options to New.

func WithBaseURL

func WithBaseURL(baseURL string) Option

WithBaseURL sets the API's address. The default is DefaultBaseURL.

func WithHTTPClient

func WithHTTPClient(httpClient *http.Client) Option

WithHTTPClient sets the http.Client requests are sent with, for a proxy, custom TLS or tracing. The default has a 30-second timeout. A merged person ID answers with a redirect, so a client that refuses redirects breaks Client.Person for merged IDs.

func WithMaxRetries

func WithMaxRetries(n int) Option

WithMaxRetries sets how many times a request is retried after a 429, a 503 or a network failure. The default is 2; 0 turns retrying off.

func WithUserAgent

func WithUserAgent(userAgent string) Option

WithUserAgent adds your app's name and version, such as "my-app/1.2", to each request's User-Agent after the SDK's own.

type PersonEvent

type PersonEvent struct {
	// When, ISO 8601.
	At string `json:"at"`

	// `issued`, `released` (no longer prerelease), `record_added`, `record_removed`, `record_moved`, `evidence_changed`,
	// `merged`, `split`, `wiped` (a prerelease ID withdrawn) or `flagged`.
	Kind string `json:"kind"`

	// The ID the entry is about; for `merged`, the ID that now redirects.
	PersonID string `json:"person_id"`

	// For `merged`, the ID it redirects to.
	OtherPersonID string `json:"other_person_id,omitempty"`

	// The record concerned, where there is one.
	PartyRecordID *int `json:"party_record_id,omitempty"`

	// The build that made the change, where there is one.
	BuildID *int `json:"build_id,omitempty"`

	// `build` for an automatic build; otherwise who decided.
	DecidedBy string `json:"decided_by,omitempty"`
}

PersonEvent is one entry in the ID's history. History is only ever added to, never edited.

type PersonOptions

type PersonOptions struct {
	// WorksOffset is the position of the first work to return, from 0.
	WorksOffset int
	// WorksLimit is how many works to return, from 1 to 200. Zero leaves the API's default of 100.
	WorksLimit int
}

PersonOptions choose which page of a person's works Client.Person returns. A zero field is left to the API.

type PersonProvenance

type PersonProvenance struct {
	// The build that issued the ID.
	IssuedBy *BuildRef `json:"issued_by,omitempty"`

	// The most recent build to group these records.
	LatestBuild *BuildRef `json:"latest_build,omitempty"`

	// The date of the registration-data release this answer read, YYYY-MM-DD. Empty when that load is undated: never a
	// guess.
	MLCSnapshot string `json:"mlc_snapshot,omitempty"`

	// The model serving lookups; not the model that grouped this person.
	APIModelMD5 string `json:"api_model_md5"`

	// The code that served this response.
	APICodeCommit string `json:"api_code_commit"`
}

PersonProvenance says what grouped these records, and what served this response: two different things. IssuedBy and LatestBuild grouped the records. APIModelMD5 and APICodeCommit only served the response, and decided nothing in it.

type PersonRecord

type PersonRecord struct {
	// Identifies this record in the registration data.
	PartyRecordID int `json:"party_record_id"`

	// The name as registered; empty only if the record is no longer in the registration data.
	Name string `json:"name,omitempty"`

	// The part of the name the record is filed under, usually the surname.
	KeyName string `json:"key_name,omitempty"`

	// The rest of the name, usually given names.
	NamesBeforeKeyName string `json:"names_before_key_name,omitempty"`

	// The IPI name number, or empty.
	IPINameNumber string `json:"ipi_name_number,omitempty"`

	// This record's own works, counted once each.
	Works int `json:"works"`

	// `anchor`: the record the ID was issued on. `asserted`: a member. `possible`: perhaps this person, but not a member.
	Link string `json:"link"`

	// Why the record was linked: `same_ipi` (the same IPI name number as a member), `ipi` (the anchor's own IPI name
	// number), `model` (the model's judgement), `corroborated` (confirmed by an independent outside source) or `verdict`
	// (decided by a person).
	Rule string `json:"rule"`

	// The model's calibrated probability, where the model was involved.
	Score *float64 `json:"score,omitempty"`

	// `build` for an automatic build; otherwise who decided.
	DecidedBy string `json:"decided_by"`

	// The build that linked the record.
	BuildID *int `json:"build_id,omitempty"`

	// When it was linked, ISO 8601.
	LinkedAt string `json:"linked_at,omitempty"`

	// The other person ID that holds this record as a member, or empty; always empty on `records`, whose holder is this
	// person.
	MemberOf string `json:"member_of,omitempty"`

	// Whether that person ID is prerelease.
	MemberOfPrerelease *bool `json:"member_of_prerelease,omitempty"`
}

PersonRecord is one registration record on a stored person, and how it was linked.

type PersonResponse

type PersonResponse struct {
	// `SLP-XXXX-XXXX-C`; the last character is a check character.
	PersonID string `json:"person_id"`

	// True: the ID may stop resolving, or later point at a different person, without notice, so do not store it. False: a
	// released ID.
	Prerelease bool `json:"prerelease"`

	// `active`. `split`: part of the group was separated and this ID kept the anchor. `anchor_missing`: the anchor record
	// is no longer in the registration data; the ID stays.
	State string `json:"state"`

	// What the ID qualifies on now: `same_ipi` (members share a IPI name number), `ipi` (the anchor's IPI name number and
	// catalogue), `corroborated` (a member's IPI name number confirmed by an independent outside source) or `human`
	// (decided by a person). It can change after issue; IssuedEvidence is what it was issued on.
	Evidence string `json:"evidence"`

	// What the ID was issued on, one of the Evidence values, from its history. Empty when the history does not record it.
	IssuedEvidence string `json:"issued_evidence,omitempty"`

	// When the ID was issued, ISO 8601.
	IssuedAt string `json:"issued_at,omitempty"`

	// When it was released; empty while prerelease.
	ReleasedAt string `json:"released_at,omitempty"`

	// The merged ID this request was redirected from, when the history confirms it.
	RedirectedFrom string `json:"redirected_from,omitempty"`

	// IDs merged into this one; each redirects here.
	MergedFrom []string `json:"merged_from,omitempty"`

	// The name to show for this person.
	Name string `json:"name,omitempty"`

	// Every distinct registered name among the members.
	Names []string `json:"names"`

	// Every distinct IPI name number among the members.
	IPINameNumbers []string `json:"ipi_name_numbers"`

	// Distinct works across member records; Possible records are not counted.
	Works int `json:"works"`

	// This person's main IPI name number: the one on the record their ID was created around (the record whose `link` is
	// `anchor`). It never changes. It is usually, but not always, the number most of their works are under. Empty when no
	// member has one.
	MainIPINameNumber string `json:"main_ipi_name_number,omitempty"`

	// Distinct works on the member records carrying MainIPINameNumber. Works minus this is how many works only the
	// person's other registrations hold. Nil when MainIPINameNumber is.
	WorksOnMainIPI *int `json:"works_on_main_ipi,omitempty"`

	// The members: `link` is `anchor` or `asserted`.
	Records []PersonRecord `json:"records"`

	// NOT members: perhaps this person, never counted.
	Possible []PersonRecord `json:"possible,omitempty"`

	// One page of works. For the next page, pass WorksList.NextOffset as PersonOptions.WorksOffset, or read them all with
	// Client.PersonWorks.
	WorksList WorksPage `json:"works_list"`

	// Every change to this ID, oldest first.
	History []PersonEvent `json:"history"`

	// Your remaining allowance; nil when no per-caller limit is in force. The edge's per-address limit (about 100
	// requests per 5 minutes, 20 from VPNs and hosting networks) is separate and not counted here.
	RateLimit *RateLimit `json:"rate_limit,omitempty"`

	// What grouped these records, and what served the response.
	Provenance PersonProvenance `json:"provenance"`
}

PersonResponse is a stored person: what a person ID holds, not a fresh lookup. Records are its members, the registrations the ID holds as this person's. Possible lists registrations that may also be theirs; they are not members, are never counted in Works, and may belong to someone else.

type PersonWithdrawnError

type PersonWithdrawnError struct {
	// Person is the API's answer about the withdrawn ID.
	Person PersonWithdrawnResponse
	// contains filtered or unexported fields
}

PersonWithdrawnError is returned by Client.Person when a person ID has been withdrawn (HTTP 410). The API still answers in full: Person says when the ID was issued and withdrawn, and gives its history. It is a valid answer to keep. errors.As also finds the *Error it wraps, with Code "withdrawn".

func (*PersonWithdrawnError) Error

func (e *PersonWithdrawnError) Error() string

Error says which person ID was withdrawn.

func (*PersonWithdrawnError) Unwrap

func (e *PersonWithdrawnError) Unwrap() error

Unwrap returns the *Error for the 410 answer, so code that handles every API error in one place still sees it.

type PersonWithdrawnResponse

type PersonWithdrawnResponse struct {
	// The withdrawn ID.
	PersonID string `json:"person_id"`

	// Always `withdrawn`.
	State string `json:"state,omitempty"`

	// Whether the ID was prerelease.
	Prerelease *bool `json:"prerelease,omitempty"`

	// When it was issued, ISO 8601.
	IssuedAt string `json:"issued_at,omitempty"`

	// When it was withdrawn, ISO 8601.
	WithdrawnAt string `json:"withdrawn_at,omitempty"`

	// Every change to this ID, oldest first.
	History []PersonEvent `json:"history"`
}

PersonWithdrawnResponse is the body of an HTTP 410 answer: this ID was issued and later withdrawn. This person ID will never be issued to anyone else.

type PersonWork

type PersonWork struct {
	// Identifies the work in the registration data.
	MLCWorkID string `json:"mlc_work_id"`

	// The registered title.
	Title string `json:"title,omitempty"`

	// The ISWC, or empty when the work has none.
	ISWC string `json:"iswc,omitempty"`

	// The member records the work is registered under.
	Records []int `json:"records"`
}

PersonWork is one work registered under one or more of the person's member records (the registrations the ID holds).

type Provenance

type Provenance struct {
	// Identifies the model that grouped the records.
	ModelMD5 string `json:"model_md5"`

	// Identifies the code that produced the answer.
	CodeCommit string `json:"code_commit"`

	// The date of the registration-data release this answer read, YYYY-MM-DD. Empty when that load is undated: never a
	// guess.
	MLCSnapshot string `json:"mlc_snapshot,omitempty"`

	// The threshold this answer used; see the Threshold parameter.
	Threshold float64 `json:"threshold"`

	// The lowest probability at which an unjoined pair is listed in `review`.
	ReviewThreshold float64 `json:"review_threshold"`

	// The most records one lookup groups; see `truncated`.
	MaxCandidates int `json:"max_candidates"`

	// Time taken to produce the answer, in milliseconds.
	LatencyMs int `json:"latency_ms"`
}

Provenance says what produced this answer. Keep ModelMD5 and CodeCommit with any answer you store.

type Query

type Query struct {
	// The name as sent.
	Name string `json:"name"`

	// Which question was answered: `writer_lookup`.
	Question string `json:"question"`

	// The threshold used, as sent or defaulted.
	Threshold float64 `json:"threshold"`

	// The song as sent; empty when none was.
	Song string `json:"song,omitempty"`
}

Query is the question that was answered, echoed back.

type RateLimit

type RateLimit struct {
	// Requests allowed per window.
	Limit int `json:"limit"`

	// Requests left in the current window.
	Remaining int `json:"remaining"`

	// The length of the window, in seconds.
	WindowSeconds int `json:"window_seconds"`
}

RateLimit is your remaining allowance under a per-caller limit.

type RateLimited

type RateLimited struct {
	// Always `rate_limited`.
	Error string `json:"error,omitempty"`

	// A sentence you can show.
	Message string `json:"message"`

	// Requests allowed per window.
	Limit int `json:"limit"`

	// The length of the window, in seconds.
	WindowSeconds int `json:"window_seconds"`

	// The most you need to wait, in seconds. The API's own limit also sends it as the `Retry-After` header; the edge's
	// per-address limit sends it only here.
	RetryAfterSeconds int `json:"retry_after_seconds"`

	// Requests counted in the current window.
	LookupsUsed int `json:"lookups_used"`

	// Where to ask for more.
	Contact string `json:"contact,omitempty"`
}

RateLimited is the body of an HTTP 429 answer: over a per-caller limit. Returned only while a limit is in force; wait RetryAfterSeconds.

type RecordingAnswer added in v0.2.0

type RecordingAnswer struct {
	// The ISRC as 12 characters, upper case, without dashes. For an `invalid` entry, the text as sent, cut to 32
	// characters.
	ISRC string `json:"isrc"`

	// `registered`: the recording is registered to at least one work. `unregistered`: it is registered to no work in this
	// data. `invalid`: not an ISRC (two letters, three letters or digits, seven digits; dashes and spaces are ignored).
	Status string `json:"status"`

	// The works the recording is registered to; empty unless Status is `registered`.
	Works []RecordingWork `json:"works,omitempty"`
}

RecordingAnswer is one ISRC from the list: the works it is registered to and their writers.

type RecordingWork added in v0.2.0

type RecordingWork struct {
	// The work's song code in the registration data.
	SongCode string `json:"song_code"`

	// The registered title.
	Title string `json:"title,omitempty"`

	// The work's ISWC, or empty when the work has none.
	ISWC string `json:"iswc,omitempty"`

	// The writers registered on the work; publishers and administrators are left out.
	Writers []RecordingWriter `json:"writers"`
}

RecordingWork is one work (a song as a composition) the recording is registered to.

type RecordingWriter added in v0.2.0

type RecordingWriter struct {
	// Identifies the writer's registration record: the same number as a LookupRecord's PartyRecordID.
	PartyRecordID int `json:"party_record_id"`

	// The name as registered.
	Name string `json:"name,omitempty"`

	// The record's IPI name number, or empty when it has none.
	IPINameNumber string `json:"ipi_name_number,omitempty"`

	// The writer's role on the work, such as `ComposerLyricist`, `Composer` or `Lyricist`.
	Role string `json:"role"`

	// The person ID this record belongs to, `SLP-XXXX-XXXX-C`; empty when the record is not part of a stored person yet.
	PersistentID string `json:"persistent_id,omitempty"`

	// The person's page, relative to https://songlinks.io; empty when PersistentID is.
	PersonURL string `json:"person_url,omitempty"`
}

RecordingWriter is one writer registered on a work the recording is registered to, and the person whose record it is.

type Rejection

type Rejection struct {
	// `single-token`: the name is one word, which is not searched. `no-match`: nothing matched closely enough.
	// `publishers-only`: every match is registered only as a publisher or administrator, not as a writer.
	Code string `json:"code"`

	// Up to five registered names closest to the one asked for, as spelling suggestions; often empty.
	Nearest []string `json:"nearest,omitempty"`

	// For `publishers-only`, the roles of the records set aside: `publisher`, `administrator`, or both.
	SetAsideRoles []string `json:"set_aside_roles,omitempty"`
}

Rejection says why a lookup stopped without candidates. It comes with HTTP 200: it is an answer, not an error.

type ReviewItem

type ReviewItem struct {
	// The two records, by `party_record_id`.
	Records []int `json:"records"`

	// The candidates the two records are in, by `ref`.
	Refs []string `json:"refs"`

	// `review_band`: the model was unsure, scoring at least `provenance.review_threshold` but below `query.threshold`.
	// `middle_name_conflict`: the records give different middle names, and are never joined on the model alone.
	// `generation_suffix_conflict`: the names carry different generations (JR and SR, II and III), and are never joined
	// on the model alone. `korean_same_name_by_sound`: one Korean name written in different romanisations, held apart
	// rather than joined on how it sounds. `blocked_would_join_conflicting_records`: joining would put two conflicting
	// records into one person through a third, so it was held back whatever the probability.
	Reason string `json:"reason"`

	// The model's calibrated probability that the two records are one person, from 0 to 1.
	Probability *float64 `json:"probability,omitempty"`

	// What the two records have in common, under `co_writers`, `publishers` and `administrators`, each `{"count": n,
	// "rarest": [names]}`. `rarest` lists the least common shared names, which say the most; names shared by a very large
	// number of writers are left out. A key may be absent.
	Shared map[string]any `json:"shared,omitempty"`
}

ReviewItem is a pair of records that was NOT joined. Nothing here is part of any candidate: show it apart from `candidates`.

type SongBeyond

type SongBeyond struct {
	// The record.
	PartyRecordID int `json:"party_record_id"`

	// The name it is registered under.
	Name string `json:"name"`

	// Its IPI name number, if it has one.
	IPINameNumber string `json:"ipi_name_number,omitempty"`

	// The person ID holding this record, if any.
	PersistentID string `json:"persistent_id,omitempty"`

	// `same_name`: the name matches as a lookup's would, beyond the records the lookup compared. `half_name`: only the
	// given name or the surname matches, as with a stage name; offered only for a title few works share.
	Tier string `json:"tier"`

	// Its works with this exact title, as registered; at most three.
	Titles []string `json:"titles"`
}

SongBeyond is a writer the song reached that the name did not. Shown beside the answer, never joined to anyone in it.

type SongMatch

type SongMatch struct {
	// The song as sent.
	Song string `json:"song"`

	// `exact`: a work has this title. `partial`: no exact title, but the song's words appear whole and in order in one
	// (`hurt` in `HURT SO GOOD`), which is weaker evidence. `none`: no one in this answer has a work by that title.
	// `too_short`: the song has no letters or digits to match on.
	Match string `json:"match"`

	// Refs of the candidates holding a match, in the order of Candidates.
	Candidates []string `json:"candidates,omitempty"`

	// The matching records, with their titles.
	Records []SongRecord `json:"records,omitempty"`

	// Writers with a work by this exact title whom the name did not find; empty when BeyondStatus is not `ok`.
	Beyond []SongBeyond `json:"beyond,omitempty"`

	// Empty when this search did not run. `ok`: it ran. `none`: no work has this exact title. `common`: too many works
	// share the title to point at anyone. `unavailable`: not offered here yet.
	BeyondStatus string `json:"beyond_status,omitempty"`
}

SongMatch says which of this answer's people wrote a work with the song's title. Candidates and Records only point at people already in the answer and never change who is grouped with whom. Beyond, when present, lists writers the song reached that the name did not, kept apart from the answer.

type SongRecord

type SongRecord struct {
	// The record, as in `candidates[].records[]`.
	PartyRecordID int `json:"party_record_id"`

	// The candidate holding it, as `candidates[].ref`.
	Ref string `json:"ref"`

	// Its matching work titles, as registered; at most three.
	Titles []string `json:"titles"`
}

SongRecord is a record in this answer with a work by the song's title.

type SongwritersResponse added in v0.2.0

type SongwritersResponse struct {
	// One entry per distinct ISRC, in the order first sent.
	Recordings []RecordingAnswer `json:"recordings"`

	// Your remaining allowance; nil when no per-caller limit is in force.
	RateLimit *RateLimit `json:"rate_limit,omitempty"`
}

SongwritersResponse lists the writers behind a list of recordings, by ISRC, in the order sent.

type VersionResponse

type VersionResponse struct {
	// The release, as in the site's small print. Two deploys of one release can carry different models, which is what
	// ModelMD5 is for.
	Version string `json:"version"`

	// Identifies the model serving lookups.
	ModelMD5 string `json:"model_md5"`

	// Identifies the code serving.
	CodeCommit string `json:"code_commit"`

	// The date of the registration-data release this answer read, YYYY-MM-DD. Empty when that load is undated: never a
	// guess.
	MLCSnapshot string `json:"mlc_snapshot,omitempty"`

	// The default Threshold for a lookup.
	Threshold float64 `json:"threshold"`

	// The lowest probability at which an unjoined pair is listed in `review`.
	ReviewThreshold float64 `json:"review_threshold"`

	// The most records one lookup groups.
	MaxCandidates int `json:"max_candidates"`

	// Always 0: no lookup was run.
	LatencyMs int `json:"latency_ms"`
}

VersionResponse says what is serving: the release, the model and the settings every lookup uses.

type WorksPage

type WorksPage struct {
	// All the person's works: distinct works across member records, a work under two of them counted once.
	Total int `json:"total"`

	// The position of the first work on this page.
	Offset int `json:"offset"`

	// The page size asked for.
	Limit int `json:"limit"`

	// This page's works.
	Items []PersonWork `json:"items"`

	// The offset to ask for next, as PersonOptions.WorksOffset, or nil on the last page. Client.PersonWorks reads every
	// page for you.
	NextOffset *int `json:"next_offset,omitempty"`

	// `recordings` when these works are ordered by how many recordings each has, `coded` when that count is not available
	// and the page falls back to works with an ISWC first.
	OrderedBy string `json:"ordered_by,omitempty"`
}

WorksPage is one page of the person's works: most-recorded first when recording counts are available (see OrderedBy).

type WriterAnswer

type WriterAnswer struct {
	// The number, as 11 digits with its leading zeros. For an `invalid` entry, the text as sent, cut to 32 characters.
	IPINameNumber string `json:"ipi_name_number"`

	// `person`: one person holds this number. `several`: more than one person holds it, and none is chosen. `none`: no
	// person here holds it; it may belong to a publisher, or its records have not been grouped. `invalid`: not an IPI
	// name number (up to 11 digits; spaces and dashes are ignored).
	Status string `json:"status"`

	// The person's ID, `SLP-XXXX-XXXX-C`; empty unless Status is `person`.
	PersonID string `json:"person_id,omitempty"`

	// The name to show for the person; empty unless Status is `person`.
	Name string `json:"name,omitempty"`

	// Every distinct name the person's records are registered under, Name first. Compare a spelling you hold against it:
	// a spelling not in this list is not one this person is registered under. Empty unless Status is `person`.
	Names []string `json:"names,omitempty"`

	// Distinct works across the person's records; nil unless Status is `person`.
	Works *int `json:"works,omitempty"`

	// The person's main number: the one on the record their ID was created around. It can differ from IPINameNumber: the
	// number sent may be a secondary one.
	MainIPINameNumber string `json:"main_ipi_name_number,omitempty"`

	// Distinct works on the records carrying MainIPINameNumber.
	WorksOnMainIPI *int `json:"works_on_main_ipi,omitempty"`

	// Works minus WorksOnMainIPI: works registered only under another spelling of the name or another IPI name number.
	// Nil when WorksOnMainIPI is.
	WorksElsewhere *int `json:"works_elsewhere,omitempty"`

	// How many persons hold this number: 1 for `person`, 2 or more for `several`, 0 otherwise.
	Persons *int `json:"persons,omitempty"`

	// The page to open, relative to https://songlinks.io: the person's page, or for `several` the page listing everyone
	// who holds the number.
	PersonURL string `json:"person_url,omitempty"`
}

WriterAnswer is one IPI name number from the list, and the person who holds it.

type WritersResponse

type WritersResponse struct {
	// One entry per distinct number, in the order first sent.
	Writers []WriterAnswer `json:"writers"`

	// Totals over Writers.
	Summary WritersSummary `json:"summary"`

	// Your remaining allowance; nil when no per-caller limit is in force.
	RateLimit *RateLimit `json:"rate_limit,omitempty"`
}

WritersResponse lists the writers behind a list of IPI name numbers, in the order sent, and how each one's works divide between their main number and the rest.

type WritersSummary

type WritersSummary struct {
	// Distinct entries checked, invalid ones included.
	Numbers int `json:"numbers"`

	// Numbers held by one person.
	Person int `json:"person"`

	// Numbers held by more than one person.
	Several int `json:"several"`

	// Numbers no person here holds.
	None int `json:"none"`

	// Entries that are not IPI name numbers.
	Invalid int `json:"invalid"`

	// Distinct persons found. Two numbers of one writer count once.
	Persons int `json:"persons"`

	// Of Persons, how many have `works_elsewhere` above zero: writers with works registered under another spelling or
	// number.
	WithWorksElsewhere int `json:"with_works_elsewhere"`
}

WritersSummary holds totals over the list. Each number is counted once, however many times it was sent.

Directories

Path Synopsis
examples
check-roster command
Command check-roster checks a roster of IPI name numbers: who holds each, and how many works they have.
Command check-roster checks a roster of IPI name numbers: who holds each, and how many works they have.
export-catalogue command
Command export-catalogue writes every work of a person as CSV.
Command export-catalogue writes every work of a person as CSV.
resolve-name command
Command resolve-name prints each person a songwriter's name could be, with their registrations and what joined them.
Command resolve-name prints each person a songwriter's name could be, with their registrations and what joined them.
songwriters-by-isrc command
Command songwriters-by-isrc finds who wrote each recording in a list of ISRCs: the songs each recording is registered to, their writers, and each writer's person ID where Songlinks has one.
Command songwriters-by-isrc finds who wrote each recording in a list of ISRCs: the songs each recording is registered to, their writers, and each writer's person ID where Songlinks has one.
which-namesake command
Command which-namesake answers: two writers share a name, which one wrote this song?
Command which-namesake answers: two writers share a name, which one wrote this song?

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL