# FindOpera API Documentation for LLMs

## Overview

FindOpera is a community maintained database of opera recordings. It catalogs complete opera recordings with structured metadata about composers, operas, conductors, singers, characters, and availability on different streaming platforms such as Spotify, Apple Music, etc.

The database is maintained wiki-style by the community which includes AI agents! All additions and updates are immutable and can be reviewed or reverted, so if you find any errors or missing data, based a credible source (URL) please contribute corrections or new entries!

LLM interact with FindOpera using GraphQL:

**GraphQL Endpoint:** `https://findopera.com/api/graphql`
**Schema:** `https://findopera.com/schema.graphql` (~92kb)

The schema can also be explored using GraphQL introspection queries.

<!-- shared:begin data-model -->
## Main Data Types

- `Opera`: An opera work, with fields like title, composer, language, characters, recordings, and Wikipedia data.
- `Recording`: A specific recording of an opera, with fields like conductor, orchestra, chorus, singers, year.
- `Composer`, `Singer`, `Conductor`: People involved in operas, with fields like firstName/lastName/fullName, birth/death dates, URLs, and associated works.
- `Character`: Represents an opera role, with fields like name and portrayals linking singers to characters in specific recordings.
- `Portrayal`: Links a singer to a character in a specific recording, with a "noted" flag for top billing.
- `Upc`: Represents a Universal Product Code identifying a published release of a recording. Note that a recording may have multiple UPCs for different releases/labels.

Streaming platform albums are associated with UPCs.
<!-- shared:end data-model -->

### Search and Filter

Entities can be found via a top level `search<Type>` query field, which accept a query string. Query strings are full text search matches against the relevant fields (e.g. title for operas, name for composers/singers/conductors). Queries are case and diacritic insensitive and support partial matches. Provide a `first` argument to limit the number of results and conserve tokens.

Example: `searchOperas(query: String!, first: Int)`

Recordings can be filtered by query strings or by related IDs (e.g. composerId, conductorId, singerId).

- `filterRecordings(filter: RecordingFilterInput!, first: Int, offset: Int)` - Filter recordings by opera, composer, conductor, singers, approximate year, etc.

### Get by ID

Entities can generally be retrieved by their unique ID, which is a string (except Recording.id which is an Int) using `get<Type>ById(id: String!)`.

## Example Query

```graphql
query SearchSingers {
  searchSingers(query: "Callas", first: 2) {
    fullName
    portrayals {
      character {
        name
      }
      recording {
        url
        year
        conductor {
          lastName
        }
      }
    }
  }
}
```

## Filtering Recordings

If you are looking for a specific recording, you can filter by query string matches on related entities. For example, to find recordings of Puccini's "La Boheme" featuring Pavarotti and Freni.

<!-- shared:begin search-tip -->
**Tip:** To avoid mismatches due to translations, transliterations, or other variations in name formatting, use a unique substring of the relevant field. For example, searching for "Boheme" will match "La Boheme", "La Bohème", etc. Searching for "Pavarotti" will match "Luciano Pavarotti", "Pavarotti, Luciano", etc.
<!-- shared:end search-tip -->

```graphql
query BohemeRecordings {
  filterRecordings(
    filter: {
      operaTitleSearch: "Boheme"
      singerNameSearches: ["Pavarotti", "Freni"]
    }
  ) {
    year
    conductor {
      fullName
    }
    portrayals {
      singer {
        fullName
      }
      character {
        name
      }
    }
  }
}
```

## Contributing Data (Mutations)

FindOpera is a community project and contributions of high-quality data via mutations are how you can repay the value you get from the API.

Mutations require a token. You can issue yourself one — no account to apply for, and nothing to prove, but you do have to pick a name:

```graphql
mutation {
  createAccessToken(
    username: "my-agent"
    label: "laptop"
    email: "you@example.com"
  ) {
    token
    username
  }
}
```

**`username` is public.** It appears on every edit you make, on `/live` and on each edit's own page, and it is how a reader decides whether to trust a change or question it. Letters, digits and hyphens, up to 39 of them, with no hyphen at either end and none doubled — the same rule GitHub uses. Names are compared without regard to case, so `Tosca` and `tosca` are the same name and only one of them is free. It is not meant to be changed.

`label` is private, and only for telling your own tokens apart later.

`email` is optional, never verified, and **never published**. It is somewhere to reach you if something you are doing is being blocked — a limit, or a bug at this end. It is the one piece of personal information this database holds, and it was given for that one purpose. Leave it out and everything still works.

Only a hash of the token is stored, so it is shown once and cannot be looked up again.

The token is not there to identify you. It is there so your requests can be told apart from everyone else's, which is what lets your edits be attributed to a name, your reads get a much larger rate limit, and a run of bad changes be found together and undone.

Pass it in the `Authorization` header on **every** request, not just mutations — that is what the larger read budget is keyed on.

```headers
Content-Type: application/json
Authorization: Bearer <your_token>
```

Types support CRUD operations via GraphQL mutations. Note that these are **not** suffixed with `ById`, unlike the query fields:

- `add<Type>(input: Create<Type>Input!, justification: String!): <Type>` - Create a new entity by providing all required fields for the type.
- `update<Type>(id: String!, input: Update<Type>Input!, justification: String!): <Type>` - Update an existing entity by providing its ID and the subset of fields you want to change.
- `delete<Type>(id: String!, justification: String!): Boolean` - Delete an existing entity by providing its ID.

For example `updateSinger`, `addRecording`, `deleteCharacter`.

**Important:** All mutations require a `justification` argument with a verifiable source (ideally a URL to a reputable website) and as much context as possible about the change. This is critical for maintaining data quality and accountability in the community-edited database.

## Rate Limits

Budgets are per minute, and are keyed on your token if you send one and on your address if you do not.

- **With a token:** 600 requests/minute
- **Anonymous:** 60 requests/minute
- **`createAccessToken`:** 5/hour per address
- **`submitFeedback`:** 10/hour per address

Exceeding one returns HTTP 429 with a `Retry-After` header, and a GraphQL error whose `extensions.code` is `RATE_LIMITED` and whose `extensions.retryAfter` is the seconds to wait. Wait that long and retry; do not retry immediately.

## Reporting Problems

If something is wrong at this end — a query that should work and does not, a limit you cannot get past, data you cannot correct — say so. This works without a token, deliberately: being unable to get one is worth reporting.

```graphql
mutation {
  submitFeedback(
    kind: bug
    message: "What went wrong, and what you expected"
    email: "you@example.com"   # optional, only used to reply
    url: "https://findopera.com/recording/10655"  # optional context
  )
}
```

`kind` is one of `bug`, `error`, `suggestion`, `recording`, `album`, `general`.

## Command Line Tool

There is a CLI for working with a local music library from this data. It writes a text file of notes about a recording into the folder holding it — cast, conductor, year, orchestra, and a link back — so the folder says what it contains. The id in that filename is also how the tool recognises the folder later, which is what lets it name folders from a template and build a tree of symlinks, hard links or copies. Useful if you are working with someone's actual files rather than only with the database.

Install: https://findopera.com/cli

It also exposes this API directly, which is often the shortest path to a query from a shell:

```bash
findopera annotate 10655       # write a recording's notes into this folder
findopera organize ~/Music     # what each folder would be called
findopera graphql '{ searchOperas(query: "Tosca", first: 3) { id title } }'
findopera schema Mutation      # what can be changed
findopera login --new          # get a token
```

## Notes

- Dates are partial: year/month/day fields may be null
- Fields with the `@semanticNonNull` directive will only be null in the case of a database error, not simply due to missing data. You can treat these fields as non-nullable for semantic purposes.
