mirror of
https://github.com/BradNut/graphbrainz
synced 2025-09-08 17:40:32 +00:00
Compare commits
11 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94cf657f30 | ||
|
|
c020795b58 | ||
|
|
42f237d068 | ||
|
|
740a499377 | ||
|
|
f095cd4de7 | ||
|
|
24d53d4687 | ||
|
|
24ebfa1b51 | ||
|
|
cb2d2a1a3f | ||
|
|
a6693ed5a4 | ||
|
|
6d3d5a595b | ||
|
|
50d3268366 |
718 changed files with 29194 additions and 19075 deletions
19
.babelrc
19
.babelrc
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"presets": [
|
|
||||||
[
|
|
||||||
"env",
|
|
||||||
{
|
|
||||||
"targets": {
|
|
||||||
"node": "8.10"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"plugins": ["transform-runtime", "syntax-object-rest-spread"],
|
|
||||||
"only": ["scripts/**", "src/**", "test/helpers/**"],
|
|
||||||
"env": {
|
|
||||||
"test": {
|
|
||||||
"plugins": ["istanbul"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/.nyc_output
|
/.nyc_output
|
||||||
/coverage
|
/coverage
|
||||||
/lib
|
/lib
|
||||||
!.eslintrc.js
|
!.eslintrc.cjs
|
||||||
|
|
|
||||||
45
.eslintrc.cjs
Normal file
45
.eslintrc.cjs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
es2020: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:promise/recommended',
|
||||||
|
'plugin:node/recommended',
|
||||||
|
'plugin:import/errors',
|
||||||
|
'plugin:import/warnings',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{ vars: 'all', args: 'none', ignoreRestSiblings: false },
|
||||||
|
],
|
||||||
|
'import/default': 'off',
|
||||||
|
'import/no-named-as-default': 'off',
|
||||||
|
'node/no-unsupported-features/es-syntax': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
ignores: ['dynamicImport', 'modules'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'prettier/prettier': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
singleQuote: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'promise/always-return': 'off',
|
||||||
|
'promise/catch-or-return': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
allowThen: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
20
.eslintrc.js
20
.eslintrc.js
|
|
@ -1,20 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
extends: ['standard', 'prettier', 'prettier/standard'],
|
|
||||||
env: {
|
|
||||||
es6: true,
|
|
||||||
node: true
|
|
||||||
},
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 6
|
|
||||||
},
|
|
||||||
plugins: ['prettier'],
|
|
||||||
rules: {
|
|
||||||
'prettier/prettier': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
singleQuote: true,
|
|
||||||
semi: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
/.nyc_output
|
||||||
|
/coverage
|
||||||
|
/lib
|
||||||
10
.travis.yml
10
.travis.yml
|
|
@ -1,9 +1,9 @@
|
||||||
language: node_js
|
language: node_js
|
||||||
|
|
||||||
node_js:
|
node_js:
|
||||||
- '8'
|
- '12'
|
||||||
- '9'
|
- '14'
|
||||||
- '10'
|
- '15'
|
||||||
|
|
||||||
# Use container-based Travis infrastructure.
|
# Use container-based Travis infrastructure.
|
||||||
sudo: false
|
sudo: false
|
||||||
|
|
@ -11,7 +11,7 @@ sudo: false
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
- "/^greenkeeper/.*$/"
|
- '/^greenkeeper/.*$/'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
|
|
@ -22,5 +22,5 @@ script:
|
||||||
- yarn test
|
- yarn test
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- $(yarn bin)/nyc report --reporter=text-lcov | $(yarn bin)/coveralls
|
- $(yarn bin)/c8 report --reporter=text-lcov | $(yarn bin)/coveralls
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
- bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
|
||||||
81
README.md
81
README.md
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
[](https://travis-ci.org/exogen/graphbrainz)
|
[](https://travis-ci.org/exogen/graphbrainz)
|
||||||
[](https://codecov.io/gh/exogen/graphbrainz)
|
[](https://codecov.io/gh/exogen/graphbrainz)
|
||||||
[](https://greenkeeper.io/)
|
|
||||||
[](https://www.npmjs.com/package/graphbrainz)
|
[](https://www.npmjs.com/package/graphbrainz)
|
||||||
[](https://github.com/exogen/graphbrainz/blob/master/LICENSE)
|
[](https://github.com/exogen/graphbrainz/blob/master/LICENSE)
|
||||||
|
|
||||||
|
|
@ -10,12 +9,25 @@ A [GraphQL][] schema, [Express][] server, and middleware for querying the
|
||||||
[MusicBrainz][] API. It features an [extensible](./docs/extensions) schema to
|
[MusicBrainz][] API. It features an [extensible](./docs/extensions) schema to
|
||||||
add integration with Discogs, Spotify, Last.fm, fanart.tv, and more!
|
add integration with Discogs, Spotify, Last.fm, fanart.tv, and more!
|
||||||
|
|
||||||
|
**[Try out the live demo!][demo]** :bulb: Use the “Docs” sidebar, the
|
||||||
|
[schema][], or the [types][] docs to help construct your query.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Install with npm:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm install graphbrainz --save
|
npm install graphbrainz --save
|
||||||
```
|
```
|
||||||
|
|
||||||
**[Try out the live demo!][demo]** :bulb: Use the “Docs” sidebar, the
|
Install with Yarn:
|
||||||
[schema][], or the [types][] docs to help construct your query.
|
|
||||||
|
```sh
|
||||||
|
yarn add graphbrainz
|
||||||
|
```
|
||||||
|
|
||||||
|
_GraphBrainz is written and distributed as native ECMAScript modules
|
||||||
|
(ESM) and requires a compatible version of Node.js_
|
||||||
|
|
||||||
## Contents
|
## Contents
|
||||||
|
|
||||||
|
|
@ -43,8 +55,8 @@ middleware supplying a GraphQL endpoint.
|
||||||
|
|
||||||
### As a standalone server
|
### As a standalone server
|
||||||
|
|
||||||
Run the included `graphbrainz` executable to start the server. The server
|
Run the included `graphbrainz` executable to start the server. The server is
|
||||||
is configured using [environment variables](#environment-variables).
|
configured using [environment variables](#environment-variables).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ graphbrainz
|
$ graphbrainz
|
||||||
|
|
@ -69,7 +81,7 @@ an endpoint, or you just want more customization, use the middleware.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import graphbrainz from 'graphbrainz';
|
import { middleware as graphbrainz } from 'graphbrainz';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
|
@ -89,11 +101,11 @@ app.listen(3000);
|
||||||
The `graphbrainz` middleware function accepts the following options:
|
The `graphbrainz` middleware function accepts the following options:
|
||||||
|
|
||||||
- **`client`**: A custom API client instance to use. See the
|
- **`client`**: A custom API client instance to use. See the
|
||||||
[client submodule](src/api/client.js) for help with creating a custom instance.
|
[client submodule](src/api/client.js) for help with creating a custom
|
||||||
You probably only need to do this if you want to adjust the rate limit and retry
|
instance. You probably only need to do this if you want to adjust the rate
|
||||||
behavior.
|
limit and retry behavior.
|
||||||
- Any remaining options are passed along to the standard GraphQL middleware.
|
- Any remaining options are passed along to the standard GraphQL middleware. See
|
||||||
See the [express-graphql][] documentation for more information.
|
the [express-graphql][] documentation for more information.
|
||||||
|
|
||||||
### As a client
|
### As a client
|
||||||
|
|
||||||
|
|
@ -103,13 +115,11 @@ or exposing a GraphQL endpoint, use the GraphBrainz schema with a library like
|
||||||
GraphBrainz resolvers expect, like so:
|
GraphBrainz resolvers expect, like so:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { graphql } from 'graphql'
|
import { graphql } from 'graphql';
|
||||||
import { MusicBrainz } from 'graphbrainz/lib/api'
|
import { MusicBrainz, createContext, baseSchema } from 'graphbrainz';
|
||||||
import createContext from 'graphbrainz/lib/context'
|
|
||||||
import schema from 'graphbrainz/lib/schema'
|
|
||||||
|
|
||||||
const client = new MusicBrainz()
|
const client = new MusicBrainz();
|
||||||
const context = createContext({ client })
|
const context = createContext({ client });
|
||||||
|
|
||||||
graphql(
|
graphql(
|
||||||
schema,
|
schema,
|
||||||
|
|
@ -125,19 +135,20 @@ graphql(
|
||||||
null,
|
null,
|
||||||
context
|
context
|
||||||
)
|
)
|
||||||
.then(result => {
|
.then((result) => {
|
||||||
const { releaseGroup } = result.data.lookup
|
const { releaseGroup } = result.data.lookup;
|
||||||
console.log(`The album title is “${releaseGroup.title}”.`)
|
console.log(`The album title is “${releaseGroup.title}”.`);
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error(err)
|
|
||||||
})
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
- **`MUSICBRAINZ_BASE_URL`**: The base MusicBrainz API URL to use. Change this
|
- **`MUSICBRAINZ_BASE_URL`**: The base MusicBrainz API URL to use. Change this
|
||||||
if you are running your own MusicBrainz mirror. Defaults to `http://musicbrainz.org/ws/2/`.
|
if you are running your own MusicBrainz mirror. Defaults to
|
||||||
|
`http://musicbrainz.org/ws/2/`.
|
||||||
- **`GRAPHBRAINZ_PATH`**: The URL route at which to expose the GraphQL endpoint,
|
- **`GRAPHBRAINZ_PATH`**: The URL route at which to expose the GraphQL endpoint,
|
||||||
if running the standalone server. Defaults to `/`.
|
if running the standalone server. Defaults to `/`.
|
||||||
- **`GRAPHBRAINZ_CORS_ORIGIN`**: The value of the `origin` option to pass to the
|
- **`GRAPHBRAINZ_CORS_ORIGIN`**: The value of the `origin` option to pass to the
|
||||||
|
|
@ -176,7 +187,8 @@ See the [debug][] package for more information.
|
||||||
|
|
||||||
## Example Queries
|
## Example Queries
|
||||||
|
|
||||||
Nirvana albums and each album’s singles ([try it](<https://graphbrainz.herokuapp.com/?query=query%20NirvanaAlbumSingles%20%7B%0A%20%20lookup%20%7B%0A%20%20%20%20artist(mbid%3A%20%225b11f4ce-a62d-471e-81fc-a69a8278c7da%22)%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20releaseGroups(type%3A%20ALBUM)%20%7B%0A%20%20%20%20%20%20%20%20edges%20%7B%0A%20%20%20%20%20%20%20%20%20%20node%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20title%0A%20%20%20%20%20%20%20%20%20%20%20%20firstReleaseDate%0A%20%20%20%20%20%20%20%20%20%20%20%20relationships%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20releaseGroups(type%3A%20%22single%20from%22)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20edges%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20node%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20target%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20...%20on%20ReleaseGroup%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20title%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20firstReleaseDate%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A&operationName=NirvanaAlbumSingles>)):
|
Nirvana albums and each album’s singles
|
||||||
|
([try it](<https://graphbrainz.herokuapp.com/?query=query%20NirvanaAlbumSingles%20%7B%0A%20%20lookup%20%7B%0A%20%20%20%20artist(mbid%3A%20%225b11f4ce-a62d-471e-81fc-a69a8278c7da%22)%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20releaseGroups(type%3A%20ALBUM)%20%7B%0A%20%20%20%20%20%20%20%20edges%20%7B%0A%20%20%20%20%20%20%20%20%20%20node%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20title%0A%20%20%20%20%20%20%20%20%20%20%20%20firstReleaseDate%0A%20%20%20%20%20%20%20%20%20%20%20%20relationships%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20releaseGroups(type%3A%20%22single%20from%22)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20edges%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20node%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20target%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20...%20on%20ReleaseGroup%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20title%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20firstReleaseDate%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A&operationName=NirvanaAlbumSingles>)):
|
||||||
|
|
||||||
```graphql
|
```graphql
|
||||||
query NirvanaAlbumSingles {
|
query NirvanaAlbumSingles {
|
||||||
|
|
@ -212,7 +224,8 @@ query NirvanaAlbumSingles {
|
||||||
|
|
||||||
### Pagination
|
### Pagination
|
||||||
|
|
||||||
The first five labels with “Apple” in the name ([try it](<https://graphbrainz.herokuapp.com/?query=query%20AppleLabels%20%7B%0A%20%20search%20%7B%0A%20%20%20%20labels(query%3A%20%22Apple%22%2C%20first%3A%205)%20%7B%0A%20%20%20%20%20%20...labelResults%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A%0Afragment%20labelResults%20on%20LabelConnection%20%7B%0A%20%20pageInfo%20%7B%0A%20%20%20%20endCursor%0A%20%20%7D%0A%20%20edges%20%7B%0A%20%20%20%20cursor%0A%20%20%20%20node%20%7B%0A%20%20%20%20%20%20mbid%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20type%0A%20%20%20%20%20%20area%20%7B%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A&operationName=AppleLabels>)):
|
The first five labels with “Apple” in the name
|
||||||
|
([try it](<https://graphbrainz.herokuapp.com/?query=query%20AppleLabels%20%7B%0A%20%20search%20%7B%0A%20%20%20%20labels(query%3A%20%22Apple%22%2C%20first%3A%205)%20%7B%0A%20%20%20%20%20%20...labelResults%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A%0Afragment%20labelResults%20on%20LabelConnection%20%7B%0A%20%20pageInfo%20%7B%0A%20%20%20%20endCursor%0A%20%20%7D%0A%20%20edges%20%7B%0A%20%20%20%20cursor%0A%20%20%20%20node%20%7B%0A%20%20%20%20%20%20mbid%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20type%0A%20%20%20%20%20%20area%20%7B%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A&operationName=AppleLabels>)):
|
||||||
|
|
||||||
```graphql
|
```graphql
|
||||||
query AppleLabels {
|
query AppleLabels {
|
||||||
|
|
@ -241,7 +254,8 @@ fragment labelResults on LabelConnection {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
…and the next five, using the `endCursor` from the previous result ([try it](<https://graphbrainz.herokuapp.com/?query=query%20AppleLabels%20%7B%0A%20%20search%20%7B%0A%20%20%20%20labels(query%3A%20%22Apple%22%2C%20first%3A%205%2C%20after%3A%20%22YXJyYXljb25uZWN0aW9uOjQ%3D%22)%20%7B%0A%20%20%20%20%20%20...labelResults%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A%0Afragment%20labelResults%20on%20LabelConnection%20%7B%0A%20%20pageInfo%20%7B%0A%20%20%20%20endCursor%0A%20%20%7D%0A%20%20edges%20%7B%0A%20%20%20%20cursor%0A%20%20%20%20node%20%7B%0A%20%20%20%20%20%20mbid%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20type%0A%20%20%20%20%20%20area%20%7B%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A&operationName=AppleLabels>)):
|
…and the next five, using the `endCursor` from the previous result
|
||||||
|
([try it](<https://graphbrainz.herokuapp.com/?query=query%20AppleLabels%20%7B%0A%20%20search%20%7B%0A%20%20%20%20labels(query%3A%20%22Apple%22%2C%20first%3A%205%2C%20after%3A%20%22YXJyYXljb25uZWN0aW9uOjQ%3D%22)%20%7B%0A%20%20%20%20%20%20...labelResults%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A%0Afragment%20labelResults%20on%20LabelConnection%20%7B%0A%20%20pageInfo%20%7B%0A%20%20%20%20endCursor%0A%20%20%7D%0A%20%20edges%20%7B%0A%20%20%20%20cursor%0A%20%20%20%20node%20%7B%0A%20%20%20%20%20%20mbid%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20type%0A%20%20%20%20%20%20area%20%7B%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A&operationName=AppleLabels>)):
|
||||||
|
|
||||||
```graphql
|
```graphql
|
||||||
query AppleLabels {
|
query AppleLabels {
|
||||||
|
|
@ -326,7 +340,8 @@ fragment marriages on Artist {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Images of Tom Petty provided by various extensions ([try it](<https://graphbrainz.herokuapp.com/?query=query%20TomPettyImages%20%7B%0A%20%20lookup%20%7B%0A%20%20%20%20artist(mbid%3A%20%225ca3f318-d028-4151-ac73-78e2b2d6cdcc%22)%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20mediaWikiImages%20%7B%0A%20%20%20%20%20%20%20%20url%0A%20%20%20%20%20%20%20%20objectName%0A%20%20%20%20%20%20%20%20descriptionHTML%0A%20%20%20%20%20%20%20%20licenseShortName%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20fanArt%20%7B%0A%20%20%20%20%20%20%20%20thumbnails%20%7B%0A%20%20%20%20%20%20%20%20%20%20url%0A%20%20%20%20%20%20%20%20%20%20likeCount%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20theAudioDB%20%7B%0A%20%20%20%20%20%20%20%20logo%0A%20%20%20%20%20%20%20%20biography%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A&operationName=TomPettyImages>)):
|
Images of Tom Petty provided by various extensions
|
||||||
|
([try it](<https://graphbrainz.herokuapp.com/?query=query%20TomPettyImages%20%7B%0A%20%20lookup%20%7B%0A%20%20%20%20artist(mbid%3A%20%225ca3f318-d028-4151-ac73-78e2b2d6cdcc%22)%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20mediaWikiImages%20%7B%0A%20%20%20%20%20%20%20%20url%0A%20%20%20%20%20%20%20%20objectName%0A%20%20%20%20%20%20%20%20descriptionHTML%0A%20%20%20%20%20%20%20%20licenseShortName%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20fanArt%20%7B%0A%20%20%20%20%20%20%20%20thumbnails%20%7B%0A%20%20%20%20%20%20%20%20%20%20url%0A%20%20%20%20%20%20%20%20%20%20likeCount%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20theAudioDB%20%7B%0A%20%20%20%20%20%20%20%20logo%0A%20%20%20%20%20%20%20%20biography%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A&operationName=TomPettyImages>)):
|
||||||
|
|
||||||
```graphql
|
```graphql
|
||||||
query TomPettyImages {
|
query TomPettyImages {
|
||||||
|
|
@ -358,8 +373,8 @@ You can find more example queries in the [schema tests][].
|
||||||
|
|
||||||
## Questions
|
## Questions
|
||||||
|
|
||||||
**What’s with the cumbersome `edges`/`node` nesting? Why `first`/`after`
|
**What’s with the cumbersome `edges`/`node` nesting? Why `first`/`after` instead
|
||||||
instead of `limit`/`offset`? Why `mbid` instead of `id`?**
|
of `limit`/`offset`? Why `mbid` instead of `id`?**
|
||||||
|
|
||||||
You can thank [Relay][] for that; these are properties of a Relay-compliant
|
You can thank [Relay][] for that; these are properties of a Relay-compliant
|
||||||
schema. The schema was originally designed to be more user-friendly, but in the
|
schema. The schema was originally designed to be more user-friendly, but in the
|
||||||
|
|
@ -393,9 +408,9 @@ query ChristmasAlbums {
|
||||||
|
|
||||||
It’s likely that your query requires multiple round trips to the MusicBrainz
|
It’s likely that your query requires multiple round trips to the MusicBrainz
|
||||||
REST API, which is subject to [rate limiting][]. While the query resolver tries
|
REST API, which is subject to [rate limiting][]. While the query resolver tries
|
||||||
very hard to fetch only the data necessary, and with the smallest number of
|
very hard to fetch only the data necessary, and with the smallest number of API
|
||||||
API requests, it is not 100% optimal (yet). Make sure you are only requesting
|
requests, it is not 100% optimal (yet). Make sure you are only requesting the
|
||||||
the fields you need and a reasonable level of nested entities – unless you are
|
fields you need and a reasonable level of nested entities – unless you are
|
||||||
willing to wait.
|
willing to wait.
|
||||||
|
|
||||||
You can also set up a [local MusicBrainz mirror][mirror] and configure
|
You can also set up a [local MusicBrainz mirror][mirror] and configure
|
||||||
|
|
|
||||||
4
cli.js
4
cli.js
|
|
@ -1,2 +1,4 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
require('./lib/index').start()
|
import { start } from './src/index.js';
|
||||||
|
|
||||||
|
start();
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,14 @@ Extensions can define new GraphQL types and use the `extend type` syntax to add
|
||||||
new fields to any existing GraphBrainz type, including the root query.
|
new fields to any existing GraphBrainz type, including the root query.
|
||||||
|
|
||||||
Several extensions are included by default, and you can install any number of
|
Several extensions are included by default, and you can install any number of
|
||||||
additional extensions from a package manager or [write your own](#extension-api).
|
additional extensions from a package manager or
|
||||||
|
[write your own](#extension-api).
|
||||||
|
|
||||||
## Contents
|
## Contents
|
||||||
|
|
||||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||||
|
|
||||||
|
|
||||||
- [Loading Extensions](#loading-extensions)
|
- [Loading Extensions](#loading-extensions)
|
||||||
- [Built-in Extensions](#built-in-extensions)
|
- [Built-in Extensions](#built-in-extensions)
|
||||||
- [More Extensions](#more-extensions)
|
- [More Extensions](#more-extensions)
|
||||||
|
|
@ -31,9 +31,9 @@ additional extensions from a package manager or [write your own](#extension-api)
|
||||||
## Loading Extensions
|
## Loading Extensions
|
||||||
|
|
||||||
The extensions to load are specified using the `extensions` option to the
|
The extensions to load are specified using the `extensions` option to the
|
||||||
exported `graphbrainz()` middleware function. Each extension must be an object
|
exported `middleware()` function. Each extension must be an object conforming to
|
||||||
conforming to the [Extension API](#extension-api), or the path to a module to
|
the [Extension API](#extension-api), or the path to a module to load via
|
||||||
load via `require()` that exports such an object.
|
`require()` that exports such an object.
|
||||||
|
|
||||||
If you are running GraphBrainz as a standalone server, you may specify
|
If you are running GraphBrainz as a standalone server, you may specify
|
||||||
extensions via the `GRAPHBRAINZ_EXTENSIONS` environment variable, which will be
|
extensions via the `GRAPHBRAINZ_EXTENSIONS` environment variable, which will be
|
||||||
|
|
@ -50,14 +50,14 @@ or environment variables. Check the documentation for each extension you use.
|
||||||
The default extensions configuration looks like this:
|
The default extensions configuration looks like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
graphbrainz({
|
middleware({
|
||||||
extensions: [
|
extensions: [
|
||||||
'graphbrainz/extensions/cover-art-archive',
|
'graphbrainz/extensions/cover-art-archive',
|
||||||
'graphbrainz/extensions/fanart-tv',
|
'graphbrainz/extensions/fanart-tv',
|
||||||
'graphbrainz/extensions/mediawiki',
|
'graphbrainz/extensions/mediawiki',
|
||||||
'graphbrainz/extensions/the-audio-db'
|
'graphbrainz/extensions/the-audio-db',
|
||||||
]
|
],
|
||||||
})
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Built-in Extensions
|
## Built-in Extensions
|
||||||
|
|
@ -71,8 +71,8 @@ See their respective documentation pages for schema info and config options.
|
||||||
releases, and labels from fanart.tv.
|
releases, and labels from fanart.tv.
|
||||||
- [MediaWiki](./mediawiki.md): Retrieve information from MediaWiki image pages,
|
- [MediaWiki](./mediawiki.md): Retrieve information from MediaWiki image pages,
|
||||||
like the actual image file URL and EXIF metadata.
|
like the actual image file URL and EXIF metadata.
|
||||||
- [TheAudioDB](./the-audio-db.md): Retrieve images and information about artists,
|
- [TheAudioDB](./the-audio-db.md): Retrieve images and information about
|
||||||
releases, and recordings from TheAudioDB.com.
|
artists, releases, and recordings from TheAudioDB.com.
|
||||||
|
|
||||||
## More Extensions
|
## More Extensions
|
||||||
|
|
||||||
|
|
@ -80,19 +80,21 @@ The following extensions are published separately, but can easily be added to
|
||||||
GraphBrainz by installing them:
|
GraphBrainz by installing them:
|
||||||
|
|
||||||
- [Last.fm](https://github.com/exogen/graphbrainz-extension-lastfm): Retrieve
|
- [Last.fm](https://github.com/exogen/graphbrainz-extension-lastfm): Retrieve
|
||||||
artist, release, and recording information from [Last.fm](https://www.last.fm/).
|
artist, release, and recording information from
|
||||||
|
[Last.fm](https://www.last.fm/).
|
||||||
- [Discogs](https://github.com/exogen/graphbrainz-extension-discogs): Retrieve
|
- [Discogs](https://github.com/exogen/graphbrainz-extension-discogs): Retrieve
|
||||||
artist, label, release, and release group information from
|
artist, label, release, and release group information from
|
||||||
[Discogs](https://www.discogs.com/).
|
[Discogs](https://www.discogs.com/).
|
||||||
- [Spotify](https://github.com/exogen/graphbrainz-extension-spotify): Retrieve
|
- [Spotify](https://github.com/exogen/graphbrainz-extension-spotify): Retrieve
|
||||||
artist, release, and recording information from [Spotify](https://www.spotify.com/).
|
artist, release, and recording information from
|
||||||
|
[Spotify](https://www.spotify.com/).
|
||||||
|
|
||||||
## Extension API
|
## Extension API
|
||||||
|
|
||||||
The core idea behind extensions comes from the [schema stitching][] feature
|
The core idea behind extensions comes from the [schema stitching][] feature from
|
||||||
from [graphql-tools][], although GraphBrainz does not currently use the exact
|
[graphql-tools][], although GraphBrainz does not currently use the exact
|
||||||
technique documented there. Instead, we call `parse` and `extendSchema` from
|
technique documented there. Instead, we call `parse` and `extendSchema` from
|
||||||
[GraphQL.js][], followed by [addResolveFunctionsToSchema][].
|
[GraphQL.js][], followed by [addResolversToSchema][].
|
||||||
|
|
||||||
Extensions must export an object shaped like so:
|
Extensions must export an object shaped like so:
|
||||||
|
|
||||||
|
|
@ -103,8 +105,8 @@ type Extension = {
|
||||||
extendContext?: (context: Context, options: Options) => Context,
|
extendContext?: (context: Context, options: Options) => Context,
|
||||||
extendSchema?:
|
extendSchema?:
|
||||||
| { schemas: Array<string | DocumentNode>, resolvers: ResolverMap }
|
| { schemas: Array<string | DocumentNode>, resolvers: ResolverMap }
|
||||||
| ((schema: GraphQLSchema, options: Options) => GraphQLSchema)
|
| ((schema: GraphQLSchema, options: Options) => GraphQLSchema),
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### Properties
|
### Properties
|
||||||
|
|
@ -133,7 +135,7 @@ If it is an object, it should have a `schemas` array and a `resolvers` object.
|
||||||
Each schema must be a string (containing type definitions in GraphQL schema
|
Each schema must be a string (containing type definitions in GraphQL schema
|
||||||
language) or a `DocumentNode` (if the type definitions have already been
|
language) or a `DocumentNode` (if the type definitions have already been
|
||||||
parsed). The `resolvers` object should contain a mapping of type fields to new
|
parsed). The `resolvers` object should contain a mapping of type fields to new
|
||||||
resolver functions for those fields. See [addResolveFunctionsToSchema][].
|
resolver functions for those fields. See [addResolversToSchema][].
|
||||||
|
|
||||||
If it is a function, it should accept `schema` and `options` arguments and
|
If it is a function, it should accept `schema` and `options` arguments and
|
||||||
return a new schema. Use this if you’d like to perform custom schema extension
|
return a new schema. Use this if you’d like to perform custom schema extension
|
||||||
|
|
@ -153,17 +155,17 @@ module.exports = {
|
||||||
extend type Query {
|
extend type Query {
|
||||||
helloWorld: String!
|
helloWorld: String!
|
||||||
}
|
}
|
||||||
`
|
`,
|
||||||
],
|
],
|
||||||
resolvers: {
|
resolvers: {
|
||||||
Query: {
|
Query: {
|
||||||
helloWorld: {
|
helloWorld: {
|
||||||
resolve: () => 'It worked!'
|
resolve: () => 'It worked!',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
This will allow the following query to be made:
|
This will allow the following query to be made:
|
||||||
|
|
@ -181,24 +183,24 @@ See the code for the [built-in extensions][] for more examples.
|
||||||
Extensions can load and resolve data in any manner they please, and you can
|
Extensions can load and resolve data in any manner they please, and you can
|
||||||
write them in any way that conforms to the API. But if you want an extra feather
|
write them in any way that conforms to the API. But if you want an extra feather
|
||||||
in your cap, there are a few guidelines you should follow in order to maintain
|
in your cap, there are a few guidelines you should follow in order to maintain
|
||||||
consistency with GraphBrainz and the built-in extensions. Here are some tips
|
consistency with GraphBrainz and the built-in extensions. Here are some tips for
|
||||||
for writing a good extension:
|
writing a good extension:
|
||||||
|
|
||||||
- If you need to make HTTP requests, using a [Client][] subclass will get you
|
- If you need to make HTTP requests, using a [Client][] subclass will get you
|
||||||
rate limiting, error handling, retrying, and a Promise-based API for free.
|
rate limiting, error handling, retrying, and a Promise-based API for free.
|
||||||
- Default to following the rate limiting rules of any APIs you use. If there
|
- Default to following the rate limiting rules of any APIs you use. If there are
|
||||||
are no guidelines on rate limiting, consider playing nice anyway and limiting
|
no guidelines on rate limiting, consider playing nice anyway and limiting your
|
||||||
your client to around 1 to 10 requests per second.
|
client to around 1 to 10 requests per second.
|
||||||
- Use a [DataLoader][dataloader] instance to batch and cache requests. Even if
|
- Use a [DataLoader][dataloader] instance to batch and cache requests. Even if
|
||||||
the data source doesn’t support batching, DataLoader will help by deduping
|
the data source doesn’t support batching, DataLoader will help by deduping
|
||||||
in-flight requests for the same key, preventing unnecessary requests.
|
in-flight requests for the same key, preventing unnecessary requests.
|
||||||
- Use a configurable cache and make sure you aren’t caching everything
|
- Use a configurable cache and make sure you aren’t caching everything
|
||||||
indefinitely by accident. The `cacheMap` option to DataLoader is a good place
|
indefinitely by accident. The `cacheMap` option to DataLoader is a good place
|
||||||
to put it.
|
to put it.
|
||||||
- Get as much configuration from environment variables as possible so that
|
- Get as much configuration from environment variables as possible so that users
|
||||||
users can just run the standalone server instead of writing any code. If you
|
can just run the standalone server instead of writing any code. If you need
|
||||||
need more complex configuration, use a single field on the `options` object
|
more complex configuration, use a single field on the `options` object as a
|
||||||
as a namespace for your extension’s options.
|
namespace for your extension’s options.
|
||||||
- Don’t hesitate to rename fields returned by third-party APIs when translating
|
- Don’t hesitate to rename fields returned by third-party APIs when translating
|
||||||
them to the GraphQL schema. Consistency with GraphQL conventions and the
|
them to the GraphQL schema. Consistency with GraphQL conventions and the
|
||||||
GraphBrainz schema is more desirable than consistency with the original API
|
GraphBrainz schema is more desirable than consistency with the original API
|
||||||
|
|
@ -236,14 +238,18 @@ for writing a good extension:
|
||||||
with the built-in extensions.
|
with the built-in extensions.
|
||||||
|
|
||||||
[graphql-tools]: http://dev.apollodata.com/tools/graphql-tools/index.html
|
[graphql-tools]: http://dev.apollodata.com/tools/graphql-tools/index.html
|
||||||
[schema stitching]: http://dev.apollodata.com/tools/graphql-tools/schema-stitching.html
|
[schema stitching]:
|
||||||
[mergeschemas]: http://dev.apollodata.com/tools/graphql-tools/schema-stitching.html#mergeSchemas
|
http://dev.apollodata.com/tools/graphql-tools/schema-stitching.html
|
||||||
|
[mergeschemas]:
|
||||||
|
http://dev.apollodata.com/tools/graphql-tools/schema-stitching.html#mergeSchemas
|
||||||
[dataloader]: https://github.com/facebook/dataloader
|
[dataloader]: https://github.com/facebook/dataloader
|
||||||
[built-in extensions]: ../../src/extensions
|
[built-in extensions]: ../../src/extensions
|
||||||
[client]: ../../src/api/client.js
|
[client]: ../../src/api/client.js
|
||||||
[graphql-markdown]: https://github.com/exogen/graphql-markdown
|
[graphql-markdown]: https://github.com/exogen/graphql-markdown
|
||||||
[diffschema]: https://github.com/exogen/graphql-markdown#diffschemaoldschema-object-newschema-object-options-object
|
[diffschema]:
|
||||||
|
https://github.com/exogen/graphql-markdown#diffschemaoldschema-object-newschema-object-options-object
|
||||||
[build-extension-docs]: ../../scripts/build-extension-docs.js
|
[build-extension-docs]: ../../scripts/build-extension-docs.js
|
||||||
[relay]: https://facebook.github.io/relay/
|
[relay]: https://facebook.github.io/relay/
|
||||||
[graphql.js]: http://graphql.org/graphql-js/
|
[graphql.js]: http://graphql.org/graphql-js/
|
||||||
[addresolvefunctionstoschema]: http://dev.apollodata.com/tools/graphql-tools/resolvers.html#addResolveFunctionsToSchema
|
[addresolverstoschema]:
|
||||||
|
http://dev.apollodata.com/tools/graphql-tools/resolvers.html#addResolversToSchema
|
||||||
|
|
|
||||||
3112
docs/schema.md
3112
docs/schema.md
File diff suppressed because it is too large
Load diff
|
|
@ -62,6 +62,7 @@ or the schemas provided by the [built-in extensions](extensions).
|
||||||
* [Tag](#tag)
|
* [Tag](#tag)
|
||||||
* [TagConnection](#tagconnection)
|
* [TagConnection](#tagconnection)
|
||||||
* [TagEdge](#tagedge)
|
* [TagEdge](#tagedge)
|
||||||
|
* [Track](#track)
|
||||||
* [URL](#url)
|
* [URL](#url)
|
||||||
* [Work](#work)
|
* [Work](#work)
|
||||||
* [WorkConnection](#workconnection)
|
* [WorkConnection](#workconnection)
|
||||||
|
|
@ -3544,6 +3545,15 @@ The number of audio tracks on this medium.
|
||||||
|
|
||||||
A list of physical discs and their disc IDs for this medium.
|
A list of physical discs and their disc IDs for this medium.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" valign="top"><strong>tracks</strong></td>
|
||||||
|
<td valign="top">[<a href="#track">Track</a>]</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
The list of tracks on the given media.
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -6719,6 +6729,81 @@ these results were found through a search.
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
### Track
|
||||||
|
|
||||||
|
A track is the way a recording is represented on a particular
|
||||||
|
release (or, more exactly, on a particular medium). Every track has a title
|
||||||
|
(see the guidelines for titles) and is credited to one or more artists.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="left">Field</th>
|
||||||
|
<th align="right">Argument</th>
|
||||||
|
<th align="left">Type</th>
|
||||||
|
<th align="left">Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" valign="top"><strong>mbid</strong></td>
|
||||||
|
<td valign="top"><a href="#mbid">MBID</a>!</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
The MBID of the entity.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" valign="top"><strong>title</strong></td>
|
||||||
|
<td valign="top"><a href="#string">String</a></td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
The official title of the entity.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" valign="top"><strong>position</strong></td>
|
||||||
|
<td valign="top"><a href="#int">Int</a></td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
The track’s position on the overall release (including all
|
||||||
|
tracks from all discs).
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" valign="top"><strong>number</strong></td>
|
||||||
|
<td valign="top"><a href="#string">String</a></td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
The track number, which may include information about the
|
||||||
|
disc or side it appears on, e.g. “A1” or “B3”.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" valign="top"><strong>length</strong></td>
|
||||||
|
<td valign="top"><a href="#duration">Duration</a></td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
The length of the track.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" valign="top"><strong>recording</strong></td>
|
||||||
|
<td valign="top"><a href="#recording">Recording</a></td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
The recording that appears on the track.
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
### URL
|
### URL
|
||||||
|
|
||||||
A [URL](https://musicbrainz.org/doc/URL) pointing to a resource
|
A [URL](https://musicbrainz.org/doc/URL) pointing to a resource
|
||||||
|
|
@ -7320,7 +7405,7 @@ A length of time, in milliseconds.
|
||||||
|
|
||||||
### Float
|
### Float
|
||||||
|
|
||||||
The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).
|
The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).
|
||||||
|
|
||||||
### ID
|
### ID
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
module.exports = require('../lib/extensions/cover-art-archive')
|
export { default } from '../src/extensions/cover-art-archive/index.js';
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
module.exports = require('../lib/extensions/fanart-tv')
|
export { default } from '../src/extensions/fanart-tv/index.js';
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
module.exports = require('../lib/extensions/mediawiki')
|
export { default } from '../src/extensions/mediawiki/index.js';
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
module.exports = require('../lib/extensions/the-audio-db')
|
export { default } from '../src/extensions/the-audio-db/index.js';
|
||||||
|
|
|
||||||
203
package.json
203
package.json
|
|
@ -1,54 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "graphbrainz",
|
"name": "graphbrainz",
|
||||||
"version": "8.0.3",
|
"version": "9.0.0",
|
||||||
"description": "A GraphQL schema, Express server, and middleware for querying the MusicBrainz.",
|
"description": "A GraphQL schema, Express server, and middleware for querying the MusicBrainz.",
|
||||||
"main": "lib/index.js",
|
|
||||||
"bin": "cli.js",
|
|
||||||
"files": [
|
|
||||||
"extensions",
|
|
||||||
"lib",
|
|
||||||
"scripts",
|
|
||||||
"src",
|
|
||||||
".babelrc",
|
|
||||||
"cli.js",
|
|
||||||
"Procfile",
|
|
||||||
"schema.json",
|
|
||||||
"yarn.lock"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.10.0",
|
|
||||||
"npm": ">=5.2.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "npm run build:lib && npm run update-schema && npm run build:docs",
|
|
||||||
"build:docs": "npm run build:docs:readme && npm run build:docs:schema && npm run build:docs:types && npm run build:docs:extensions",
|
|
||||||
"build:docs:extensions": "babel-node scripts/build-extension-docs.js",
|
|
||||||
"build:docs:readme": "doctoc --notitle README.md docs/extensions/README.md",
|
|
||||||
"build:docs:schema": "printf '# GraphQL Schema\\n\\n%s\n' \"$(npm run -s print-schema:md)\" > docs/schema.md",
|
|
||||||
"build:docs:types": "graphql-markdown ./src/schema.js --require babel-register --no-title --update-file docs/types.md",
|
|
||||||
"build:lib": "babel --out-dir lib src",
|
|
||||||
"clean": "npm run clean:lib",
|
|
||||||
"clean:lib": "rm -rf lib",
|
|
||||||
"deploy": "./scripts/deploy.sh",
|
|
||||||
"format": "npm run lint:fix",
|
|
||||||
"lint": "eslint .",
|
|
||||||
"lint:fix": "eslint --fix .",
|
|
||||||
"postinstall": "postinstall-build lib --script build:lib",
|
|
||||||
"prepublish": "npm run clean:lib && npm run build:lib",
|
|
||||||
"preversion": "npm run update-schema && npm run build:docs && git add schema.json docs",
|
|
||||||
"print-schema": "babel-node scripts/print-schema.js",
|
|
||||||
"print-schema:json": "npm run print-schema -- --json",
|
|
||||||
"print-schema:md": "printf '```graphql\\n%s\\n```' \"$(npm run -s print-schema)\"",
|
|
||||||
"start": "node lib/index.js",
|
|
||||||
"start:dev": "nodemon --exec babel-node src/index.js",
|
|
||||||
"test": "npm run lint && npm run test:coverage",
|
|
||||||
"test:coverage": "cross-env NODE_ENV=test nyc npm run test:only",
|
|
||||||
"test:only": "cross-env VCR_MODE=playback ava",
|
|
||||||
"test:record": "cross-env VCR_MODE=record ava",
|
|
||||||
"test:record-new": "cross-env VCR_MODE=cache ava --concurrency 1",
|
|
||||||
"test:watch": "npm run test:only -- --watch",
|
|
||||||
"update-schema": "npm run -s print-schema:json > schema.json"
|
|
||||||
},
|
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"musicbrainz",
|
"musicbrainz",
|
||||||
"graphql",
|
"graphql",
|
||||||
|
|
@ -61,83 +14,107 @@
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Brian Beck",
|
"name": "Brian Beck",
|
||||||
"email": "exogen@gmail.com",
|
"email": "exogen@gmail.com",
|
||||||
"url": "http://brianbeck.com/"
|
"url": "https://brianbeck.com/"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/exogen/graphbrainz.git"
|
"url": "https://github.com/exogen/graphbrainz.git"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"engines": {
|
||||||
"babel-runtime": "^6.25.0",
|
"node": ">=12.18.0",
|
||||||
"compression": "^1.7.3",
|
"npm": ">=6.0.0"
|
||||||
"cors": "^2.8.4",
|
|
||||||
"dashify": "^1.0.0",
|
|
||||||
"dataloader": "^1.4.0",
|
|
||||||
"debug": "^3.0.0",
|
|
||||||
"dotenv": "^6.0.0",
|
|
||||||
"es6-error": "^4.1.1",
|
|
||||||
"express": "^4.16.3",
|
|
||||||
"express-graphql": "^0.6.12",
|
|
||||||
"graphql": "^0.13.2",
|
|
||||||
"graphql-relay": "^0.5.5",
|
|
||||||
"graphql-tools": "^3.1.1",
|
|
||||||
"lru-cache": "^4.1.3",
|
|
||||||
"pascalcase": "^0.1.1",
|
|
||||||
"postinstall-build": "^5.0.1",
|
|
||||||
"qs": "^6.5.2",
|
|
||||||
"request": "^2.87.0",
|
|
||||||
"retry": "^0.12.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"type": "module",
|
||||||
"ava": "^0.25.0",
|
"main": "./src/index.js",
|
||||||
"babel-cli": "^6.24.1",
|
"exports": {
|
||||||
"babel-eslint": "^8.2.6",
|
".": "./src/index.js",
|
||||||
"babel-plugin-istanbul": "^4.1.6",
|
"./extensions/cover-art-archive": "./extensions/cover-art-archive.js",
|
||||||
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
|
"./extensions/fanart-tv": "./extensions/fanart-tv.js",
|
||||||
"babel-plugin-transform-runtime": "^6.23.0",
|
"./extensions/mediawiki": "./extensions/mediawiki.js",
|
||||||
"babel-preset-env": "^1.7.0",
|
"./extensions/the-audio-db": "./extensions/the-audio-db.js",
|
||||||
"babel-register": "^6.24.1",
|
"./package.json": "./package.json",
|
||||||
"coveralls": "^3.0.2",
|
"./schema.json": "./schema.json"
|
||||||
"cross-env": "^5.2.0",
|
|
||||||
"doctoc": "^1.3.1",
|
|
||||||
"eslint": "^5.3.0",
|
|
||||||
"eslint-config-prettier": "^2.9.0",
|
|
||||||
"eslint-config-standard": "^11.0.0",
|
|
||||||
"eslint-plugin-import": "^2.13.0",
|
|
||||||
"eslint-plugin-markdown": "^1.0.0-beta.6",
|
|
||||||
"eslint-plugin-node": "^7.0.1",
|
|
||||||
"eslint-plugin-prettier": "^2.6.2",
|
|
||||||
"eslint-plugin-promise": "^3.8.0",
|
|
||||||
"eslint-plugin-standard": "^3.1.0",
|
|
||||||
"graphql-markdown": "^4.0.0",
|
|
||||||
"nodemon": "^1.18.3",
|
|
||||||
"nyc": "^12.0.2",
|
|
||||||
"prettier": "^1.14.0",
|
|
||||||
"replayer": "^2.2.3",
|
|
||||||
"rimraf": "^2.6.1",
|
|
||||||
"sinon": "^6.1.4"
|
|
||||||
},
|
},
|
||||||
"standard": {
|
"bin": "cli.js",
|
||||||
"parser": "babel-eslint"
|
"files": [
|
||||||
|
"extensions",
|
||||||
|
"src",
|
||||||
|
"cli.js",
|
||||||
|
"schema.json"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "npm run update-schema && npm run build:docs",
|
||||||
|
"build:docs": "npm run build:docs:readme && npm run build:docs:schema && npm run build:docs:types && npm run build:docs:extensions",
|
||||||
|
"build:docs:extensions": "node scripts/build-extension-docs.js",
|
||||||
|
"build:docs:readme": "doctoc --notitle README.md docs/extensions/README.md",
|
||||||
|
"build:docs:schema": "printf '# GraphQL Schema\\n\\n%s\n' \"$(npm run -s print-schema:md)\" > docs/schema.md",
|
||||||
|
"build:docs:types": "graphql-markdown ./schema.json --no-title --update-file docs/types.md",
|
||||||
|
"deploy": "./scripts/deploy.sh",
|
||||||
|
"format": "npm run lint:fix",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint --fix .",
|
||||||
|
"preversion": "npm run update-schema && npm run build:docs && git add schema.json docs",
|
||||||
|
"print-schema": "node scripts/print-schema.js",
|
||||||
|
"print-schema:json": "npm run print-schema -- --json",
|
||||||
|
"print-schema:md": "printf '```graphql\\n%s\\n```' \"$(npm run -s print-schema)\"",
|
||||||
|
"start": "node cli.js",
|
||||||
|
"start:dev": "nodemon cli.js",
|
||||||
|
"test": "npm run lint && npm run test:coverage",
|
||||||
|
"test:coverage": "c8 --all npm run test:only",
|
||||||
|
"test:only": "cross-env NOCK_MODE=play ava",
|
||||||
|
"test:record": "cross-env NOCK_MODE=record ava --concurrency=1 --timeout=1m",
|
||||||
|
"test:record-new": "cross-env NOCK_MODE=cache ava --concurrency=1 --timeout=1m",
|
||||||
|
"test:watch": "npm run test:only -- --watch",
|
||||||
|
"update-schema": "npm run -s print-schema:json > schema.json"
|
||||||
},
|
},
|
||||||
"ava": {
|
"ava": {
|
||||||
"require": [
|
"require": [
|
||||||
"dotenv/config",
|
"dotenv/config"
|
||||||
"babel-register"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nyc": {
|
"ava-nock": {
|
||||||
"include": [
|
"fixtureDir": "fixtures",
|
||||||
"src/**"
|
"pathFilter": [
|
||||||
],
|
"(([?&]api_key=)(\\w+))|((/json/)(\\w+)(/[\\w-]+-mb\\.php))",
|
||||||
"reporter": [
|
"$2$5*$7"
|
||||||
"lcov",
|
]
|
||||||
"text"
|
},
|
||||||
],
|
"dependencies": {
|
||||||
"all": true,
|
"@graphql-tools/schema": "^7.1.3",
|
||||||
"cache": true,
|
"compression": "^1.7.3",
|
||||||
"sourceMap": false,
|
"cors": "^2.8.4",
|
||||||
"instrument": false
|
"dashify": "^2.0.0",
|
||||||
|
"dataloader": "^2.0.0",
|
||||||
|
"debug": "^4.3.1",
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
|
"es6-error": "^4.1.1",
|
||||||
|
"express": "^4.16.3",
|
||||||
|
"express-graphql": "^0.12.0",
|
||||||
|
"got": "^11.8.2",
|
||||||
|
"graphql": "^15.5.0",
|
||||||
|
"graphql-relay": "^0.6.0",
|
||||||
|
"lru-cache": "^6.0.0",
|
||||||
|
"pascalcase": "^1.0.0",
|
||||||
|
"read-pkg-up": "^8.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"ava": "^3.15.0",
|
||||||
|
"ava-nock": "^2.1.0",
|
||||||
|
"c8": "^7.7.1",
|
||||||
|
"coveralls": "^3.0.2",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"doctoc": "^2.0.0",
|
||||||
|
"eslint": "^7.24.0",
|
||||||
|
"eslint-config-prettier": "^8.1.0",
|
||||||
|
"eslint-plugin-import": "^2.13.0",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-prettier": "^3.3.1",
|
||||||
|
"eslint-plugin-promise": "^5.1.0",
|
||||||
|
"graphql-markdown": "^6.0.0",
|
||||||
|
"nodemon": "^2.0.7",
|
||||||
|
"prettier": "^2.2.1",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"sinon": "^10.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
631
schema.json
631
schema.json
|
|
@ -476,16 +476,6 @@
|
||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
"possibleTypes": null
|
"possibleTypes": null
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"kind": "SCALAR",
|
|
||||||
"name": "MBID",
|
|
||||||
"description": "The MBID scalar represents MusicBrainz identifiers, which are\n36-character UUIDs.",
|
|
||||||
"fields": null,
|
|
||||||
"inputFields": null,
|
|
||||||
"interfaces": null,
|
|
||||||
"enumValues": null,
|
|
||||||
"possibleTypes": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"kind": "OBJECT",
|
"kind": "OBJECT",
|
||||||
"name": "Area",
|
"name": "Area",
|
||||||
|
|
@ -937,7 +927,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"inputFields": null,
|
"inputFields": null,
|
||||||
"interfaces": null,
|
"interfaces": [],
|
||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
"possibleTypes": [
|
"possibleTypes": [
|
||||||
{
|
{
|
||||||
|
|
@ -1045,7 +1035,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"inputFields": null,
|
"inputFields": null,
|
||||||
"interfaces": null,
|
"interfaces": [],
|
||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
"possibleTypes": [
|
"possibleTypes": [
|
||||||
{
|
{
|
||||||
|
|
@ -1068,6 +1058,11 @@
|
||||||
"name": "Release",
|
"name": "Release",
|
||||||
"ofType": null
|
"ofType": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "OBJECT",
|
||||||
|
"name": "Track",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "OBJECT",
|
"kind": "OBJECT",
|
||||||
"name": "Label",
|
"name": "Label",
|
||||||
|
|
@ -1115,6 +1110,16 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "MBID",
|
||||||
|
"description": "The MBID scalar represents MusicBrainz identifiers, which are\n36-character UUIDs.",
|
||||||
|
"fields": null,
|
||||||
|
"inputFields": null,
|
||||||
|
"interfaces": null,
|
||||||
|
"enumValues": null,
|
||||||
|
"possibleTypes": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "SCALAR",
|
"kind": "SCALAR",
|
||||||
"name": "String",
|
"name": "String",
|
||||||
|
|
@ -1228,16 +1233,6 @@
|
||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
"possibleTypes": null
|
"possibleTypes": null
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"kind": "SCALAR",
|
|
||||||
"name": "Int",
|
|
||||||
"description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ",
|
|
||||||
"fields": null,
|
|
||||||
"inputFields": null,
|
|
||||||
"interfaces": null,
|
|
||||||
"enumValues": null,
|
|
||||||
"possibleTypes": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"kind": "OBJECT",
|
"kind": "OBJECT",
|
||||||
"name": "ArtistConnection",
|
"name": "ArtistConnection",
|
||||||
|
|
@ -2554,145 +2549,13 @@
|
||||||
"possibleTypes": null
|
"possibleTypes": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ENUM",
|
"kind": "SCALAR",
|
||||||
"name": "ReleaseGroupType",
|
"name": "Int",
|
||||||
"description": "A type used to describe release groups, e.g. album, single, EP,\netc.",
|
"description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.",
|
||||||
"fields": null,
|
"fields": null,
|
||||||
"inputFields": null,
|
"inputFields": null,
|
||||||
"interfaces": null,
|
"interfaces": null,
|
||||||
"enumValues": [
|
"enumValues": null,
|
||||||
{
|
|
||||||
"name": "ALBUM",
|
|
||||||
"description": "An album, perhaps better defined as a “Long Play” (LP)\nrelease, generally consists of previously unreleased material (unless this type\nis combined with secondary types which change that, such as “Compilation”). This\nincludes album re-issues, with or without bonus tracks.",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "SINGLE",
|
|
||||||
"description": "A single typically has one main song and possibly a handful\nof additional tracks or remixes of the main track. A single is usually named\nafter its main song.",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "EP",
|
|
||||||
"description": "An EP is a so-called “Extended Play” release and often\ncontains the letters EP in the title. Generally an EP will be shorter than a\nfull length release (an LP or “Long Play”) and the tracks are usually exclusive\nto the EP, in other words the tracks don’t come from a previously issued\nrelease. EP is fairly difficult to define; usually it should only be assumed\nthat a release is an EP if the artist defines it as such.",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "OTHER",
|
|
||||||
"description": "Any release that does not fit any of the other categories.",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "BROADCAST",
|
|
||||||
"description": "An episodic release that was originally broadcast via radio,\ntelevision, or the Internet, including podcasts.",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "COMPILATION",
|
|
||||||
"description": "A compilation is a collection of previously released tracks\nby one or more artists.",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "SOUNDTRACK",
|
|
||||||
"description": "A soundtrack is the musical score to a movie, TV series,\nstage show, computer game, etc.",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "SPOKENWORD",
|
|
||||||
"description": "A non-music spoken word release.",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "INTERVIEW",
|
|
||||||
"description": "An interview release contains an interview, generally with\nan artist.",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "AUDIOBOOK",
|
|
||||||
"description": "An audiobook is a book read by a narrator without music.",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "LIVE",
|
|
||||||
"description": "A release that was recorded live.",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "REMIX",
|
|
||||||
"description": "A release that was (re)mixed from previously released\nmaterial.",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "DJMIX",
|
|
||||||
"description": "A DJ-mix is a sequence of several recordings played one\nafter the other, each one modified so that they blend together into a continuous\nflow of music. A DJ mix release requires that the recordings be modified in some\nmanner, and the DJ who does this modification is usually (although not always)\ncredited in a fairly prominent way.",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "MIXTAPE",
|
|
||||||
"description": "Promotional in nature (but not necessarily free), mixtapes\nand street albums are often released by artists to promote new artists, or\nupcoming studio albums by prominent artists. They are also sometimes used to\nkeep fans’ attention between studio releases and are most common in rap & hip\nhop genres. They are often not sanctioned by the artist’s label, may lack proper\nsample or song clearances and vary widely in production and recording quality.\nWhile mixtapes are generally DJ-mixed, they are distinct from commercial DJ\nmixes (which are usually deemed compilations) and are defined by having a\nsignificant proportion of new material, including original production or\noriginal vocals over top of other artists’ instrumentals. They are distinct from\ndemos in that they are designed for release directly to the public and fans, not\nto labels.",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "DEMO",
|
|
||||||
"description": "A release that was recorded for limited circulation or\nreference use rather than for general public release.",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "NAT",
|
|
||||||
"description": "A non-album track (special case).",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"possibleTypes": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "ENUM",
|
|
||||||
"name": "ReleaseStatus",
|
|
||||||
"description": "A type used to describe the status of releases, e.g. official,\nbootleg, etc.",
|
|
||||||
"fields": null,
|
|
||||||
"inputFields": null,
|
|
||||||
"interfaces": null,
|
|
||||||
"enumValues": [
|
|
||||||
{
|
|
||||||
"name": "OFFICIAL",
|
|
||||||
"description": "Any release officially sanctioned by the artist and/or their\nrecord company. (Most releases will fit into this category.)",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "PROMOTION",
|
|
||||||
"description": "A giveaway release or a release intended to promote an\nupcoming official release, e.g. prerelease albums or releases included with a\nmagazine.",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "BOOTLEG",
|
|
||||||
"description": "An unofficial/underground release that was not sanctioned by\nthe artist and/or the record company.",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "PSEUDORELEASE",
|
|
||||||
"description": "A pseudo-release is a duplicate release for\ntranslation/transliteration purposes.",
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"possibleTypes": null
|
"possibleTypes": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -3352,6 +3215,41 @@
|
||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
"possibleTypes": null
|
"possibleTypes": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "ENUM",
|
||||||
|
"name": "ReleaseStatus",
|
||||||
|
"description": "A type used to describe the status of releases, e.g. official,\nbootleg, etc.",
|
||||||
|
"fields": null,
|
||||||
|
"inputFields": null,
|
||||||
|
"interfaces": null,
|
||||||
|
"enumValues": [
|
||||||
|
{
|
||||||
|
"name": "OFFICIAL",
|
||||||
|
"description": "Any release officially sanctioned by the artist and/or their\nrecord company. (Most releases will fit into this category.)",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PROMOTION",
|
||||||
|
"description": "A giveaway release or a release intended to promote an\nupcoming official release, e.g. prerelease albums or releases included with a\nmagazine.",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BOOTLEG",
|
||||||
|
"description": "An unofficial/underground release that was not sanctioned by\nthe artist and/or the record company.",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PSEUDORELEASE",
|
||||||
|
"description": "A pseudo-release is a duplicate release for\ntranslation/transliteration purposes.",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"possibleTypes": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "OBJECT",
|
"kind": "OBJECT",
|
||||||
"name": "Medium",
|
"name": "Medium",
|
||||||
|
|
@ -3432,6 +3330,22 @@
|
||||||
},
|
},
|
||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
"deprecationReason": null
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tracks",
|
||||||
|
"description": "The list of tracks on the given media.",
|
||||||
|
"args": [],
|
||||||
|
"type": {
|
||||||
|
"kind": "LIST",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "OBJECT",
|
||||||
|
"name": "Track",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"inputFields": null,
|
"inputFields": null,
|
||||||
|
|
@ -3579,6 +3493,99 @@
|
||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
"possibleTypes": null
|
"possibleTypes": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "OBJECT",
|
||||||
|
"name": "Track",
|
||||||
|
"description": "A track is the way a recording is represented on a particular\n release (or, more exactly, on a particular medium). Every track has a title\n (see the guidelines for titles) and is credited to one or more artists.",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "mbid",
|
||||||
|
"description": "The MBID of the entity.",
|
||||||
|
"args": [],
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "MBID",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "title",
|
||||||
|
"description": "The official title of the entity.",
|
||||||
|
"args": [],
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "position",
|
||||||
|
"description": "The track’s position on the overall release (including all\ntracks from all discs).",
|
||||||
|
"args": [],
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "Int",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "number",
|
||||||
|
"description": "The track number, which may include information about the\ndisc or side it appears on, e.g. “A1” or “B3”.",
|
||||||
|
"args": [],
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "length",
|
||||||
|
"description": "The length of the track.",
|
||||||
|
"args": [],
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "Duration",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "recording",
|
||||||
|
"description": "The recording that appears on the track.",
|
||||||
|
"args": [],
|
||||||
|
"type": {
|
||||||
|
"kind": "OBJECT",
|
||||||
|
"name": "Recording",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inputFields": null,
|
||||||
|
"interfaces": [
|
||||||
|
{
|
||||||
|
"kind": "INTERFACE",
|
||||||
|
"name": "Entity",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enumValues": null,
|
||||||
|
"possibleTypes": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "OBJECT",
|
"kind": "OBJECT",
|
||||||
"name": "LabelConnection",
|
"name": "LabelConnection",
|
||||||
|
|
@ -4046,6 +4053,113 @@
|
||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
"possibleTypes": null
|
"possibleTypes": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "ENUM",
|
||||||
|
"name": "ReleaseGroupType",
|
||||||
|
"description": "A type used to describe release groups, e.g. album, single, EP,\netc.",
|
||||||
|
"fields": null,
|
||||||
|
"inputFields": null,
|
||||||
|
"interfaces": null,
|
||||||
|
"enumValues": [
|
||||||
|
{
|
||||||
|
"name": "ALBUM",
|
||||||
|
"description": "An album, perhaps better defined as a “Long Play” (LP)\nrelease, generally consists of previously unreleased material (unless this type\nis combined with secondary types which change that, such as “Compilation”). This\nincludes album re-issues, with or without bonus tracks.",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SINGLE",
|
||||||
|
"description": "A single typically has one main song and possibly a handful\nof additional tracks or remixes of the main track. A single is usually named\nafter its main song.",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "EP",
|
||||||
|
"description": "An EP is a so-called “Extended Play” release and often\ncontains the letters EP in the title. Generally an EP will be shorter than a\nfull length release (an LP or “Long Play”) and the tracks are usually exclusive\nto the EP, in other words the tracks don’t come from a previously issued\nrelease. EP is fairly difficult to define; usually it should only be assumed\nthat a release is an EP if the artist defines it as such.",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "OTHER",
|
||||||
|
"description": "Any release that does not fit any of the other categories.",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BROADCAST",
|
||||||
|
"description": "An episodic release that was originally broadcast via radio,\ntelevision, or the Internet, including podcasts.",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "COMPILATION",
|
||||||
|
"description": "A compilation is a collection of previously released tracks\nby one or more artists.",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SOUNDTRACK",
|
||||||
|
"description": "A soundtrack is the musical score to a movie, TV series,\nstage show, computer game, etc.",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SPOKENWORD",
|
||||||
|
"description": "A non-music spoken word release.",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "INTERVIEW",
|
||||||
|
"description": "An interview release contains an interview, generally with\nan artist.",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AUDIOBOOK",
|
||||||
|
"description": "An audiobook is a book read by a narrator without music.",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LIVE",
|
||||||
|
"description": "A release that was recorded live.",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "REMIX",
|
||||||
|
"description": "A release that was (re)mixed from previously released\nmaterial.",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DJMIX",
|
||||||
|
"description": "A DJ-mix is a sequence of several recordings played one\nafter the other, each one modified so that they blend together into a continuous\nflow of music. A DJ mix release requires that the recordings be modified in some\nmanner, and the DJ who does this modification is usually (although not always)\ncredited in a fairly prominent way.",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MIXTAPE",
|
||||||
|
"description": "Promotional in nature (but not necessarily free), mixtapes\nand street albums are often released by artists to promote new artists, or\nupcoming studio albums by prominent artists. They are also sometimes used to\nkeep fans’ attention between studio releases and are most common in rap & hip\nhop genres. They are often not sanctioned by the artist’s label, may lack proper\nsample or song clearances and vary widely in production and recording quality.\nWhile mixtapes are generally DJ-mixed, they are distinct from commercial DJ\nmixes (which are usually deemed compilations) and are defined by having a\nsignificant proportion of new material, including original production or\noriginal vocals over top of other artists’ instrumentals. They are distinct from\ndemos in that they are designed for release directly to the public and fans, not\nto labels.",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DEMO",
|
||||||
|
"description": "A release that was recorded for limited circulation or\nreference use rather than for general public release.",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NAT",
|
||||||
|
"description": "A non-album track (special case).",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"possibleTypes": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "OBJECT",
|
"kind": "OBJECT",
|
||||||
"name": "Relationships",
|
"name": "Relationships",
|
||||||
|
|
@ -6535,7 +6649,7 @@
|
||||||
{
|
{
|
||||||
"kind": "SCALAR",
|
"kind": "SCALAR",
|
||||||
"name": "Float",
|
"name": "Float",
|
||||||
"description": "The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). ",
|
"description": "The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).",
|
||||||
"fields": null,
|
"fields": null,
|
||||||
"inputFields": null,
|
"inputFields": null,
|
||||||
"interfaces": null,
|
"interfaces": null,
|
||||||
|
|
@ -8670,16 +8784,6 @@
|
||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
"possibleTypes": null
|
"possibleTypes": null
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"kind": "SCALAR",
|
|
||||||
"name": "URLString",
|
|
||||||
"description": "A web address.",
|
|
||||||
"fields": null,
|
|
||||||
"inputFields": null,
|
|
||||||
"interfaces": null,
|
|
||||||
"enumValues": null,
|
|
||||||
"possibleTypes": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"kind": "OBJECT",
|
"kind": "OBJECT",
|
||||||
"name": "URL",
|
"name": "URL",
|
||||||
|
|
@ -8762,6 +8866,16 @@
|
||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
"possibleTypes": null
|
"possibleTypes": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "URLString",
|
||||||
|
"description": "A web address.",
|
||||||
|
"fields": null,
|
||||||
|
"inputFields": null,
|
||||||
|
"interfaces": null,
|
||||||
|
"enumValues": null,
|
||||||
|
"possibleTypes": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "OBJECT",
|
"kind": "OBJECT",
|
||||||
"name": "BrowseQuery",
|
"name": "BrowseQuery",
|
||||||
|
|
@ -10138,6 +10252,18 @@
|
||||||
"name": "__Schema",
|
"name": "__Schema",
|
||||||
"description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.",
|
"description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.",
|
||||||
"fields": [
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "description",
|
||||||
|
"description": null,
|
||||||
|
"args": [],
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "types",
|
"name": "types",
|
||||||
"description": "A list of all types supported by this server.",
|
"description": "A list of all types supported by this server.",
|
||||||
|
|
@ -10235,7 +10361,7 @@
|
||||||
{
|
{
|
||||||
"kind": "OBJECT",
|
"kind": "OBJECT",
|
||||||
"name": "__Type",
|
"name": "__Type",
|
||||||
"description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.",
|
"description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByUrl`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"name": "kind",
|
"name": "kind",
|
||||||
|
|
@ -10277,6 +10403,18 @@
|
||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
"deprecationReason": null
|
"deprecationReason": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "specifiedByUrl",
|
||||||
|
"description": null,
|
||||||
|
"args": [],
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "fields",
|
"name": "fields",
|
||||||
"description": null,
|
"description": null,
|
||||||
|
|
@ -10382,7 +10520,18 @@
|
||||||
{
|
{
|
||||||
"name": "inputFields",
|
"name": "inputFields",
|
||||||
"description": null,
|
"description": null,
|
||||||
"args": [],
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "includeDeprecated",
|
||||||
|
"description": null,
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "Boolean",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"defaultValue": "false"
|
||||||
|
}
|
||||||
|
],
|
||||||
"type": {
|
"type": {
|
||||||
"kind": "LIST",
|
"kind": "LIST",
|
||||||
"name": null,
|
"name": null,
|
||||||
|
|
@ -10439,7 +10588,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "INTERFACE",
|
"name": "INTERFACE",
|
||||||
"description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.",
|
"description": "Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.",
|
||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
"deprecationReason": null
|
"deprecationReason": null
|
||||||
},
|
},
|
||||||
|
|
@ -10512,7 +10661,18 @@
|
||||||
{
|
{
|
||||||
"name": "args",
|
"name": "args",
|
||||||
"description": null,
|
"description": null,
|
||||||
"args": [],
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "includeDeprecated",
|
||||||
|
"description": null,
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "Boolean",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"defaultValue": "false"
|
||||||
|
}
|
||||||
|
],
|
||||||
"type": {
|
"type": {
|
||||||
"kind": "NON_NULL",
|
"kind": "NON_NULL",
|
||||||
"name": null,
|
"name": null,
|
||||||
|
|
@ -10643,6 +10803,34 @@
|
||||||
},
|
},
|
||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
"deprecationReason": null
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isDeprecated",
|
||||||
|
"description": null,
|
||||||
|
"args": [],
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "Boolean",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deprecationReason",
|
||||||
|
"description": null,
|
||||||
|
"args": [],
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"inputFields": null,
|
"inputFields": null,
|
||||||
|
|
@ -10750,6 +10938,22 @@
|
||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
"deprecationReason": null
|
"deprecationReason": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "isRepeatable",
|
||||||
|
"description": null,
|
||||||
|
"args": [],
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "Boolean",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "locations",
|
"name": "locations",
|
||||||
"description": null,
|
"description": null,
|
||||||
|
|
@ -10797,54 +11001,6 @@
|
||||||
},
|
},
|
||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
"deprecationReason": null
|
"deprecationReason": null
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "onOperation",
|
|
||||||
"description": null,
|
|
||||||
"args": [],
|
|
||||||
"type": {
|
|
||||||
"kind": "NON_NULL",
|
|
||||||
"name": null,
|
|
||||||
"ofType": {
|
|
||||||
"kind": "SCALAR",
|
|
||||||
"name": "Boolean",
|
|
||||||
"ofType": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"isDeprecated": true,
|
|
||||||
"deprecationReason": "Use `locations`."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "onFragment",
|
|
||||||
"description": null,
|
|
||||||
"args": [],
|
|
||||||
"type": {
|
|
||||||
"kind": "NON_NULL",
|
|
||||||
"name": null,
|
|
||||||
"ofType": {
|
|
||||||
"kind": "SCALAR",
|
|
||||||
"name": "Boolean",
|
|
||||||
"ofType": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"isDeprecated": true,
|
|
||||||
"deprecationReason": "Use `locations`."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "onField",
|
|
||||||
"description": null,
|
|
||||||
"args": [],
|
|
||||||
"type": {
|
|
||||||
"kind": "NON_NULL",
|
|
||||||
"name": null,
|
|
||||||
"ofType": {
|
|
||||||
"kind": "SCALAR",
|
|
||||||
"name": "Boolean",
|
|
||||||
"ofType": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"isDeprecated": true,
|
|
||||||
"deprecationReason": "Use `locations`."
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"inputFields": null,
|
"inputFields": null,
|
||||||
|
|
@ -10902,6 +11058,12 @@
|
||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
"deprecationReason": null
|
"deprecationReason": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "VARIABLE_DEFINITION",
|
||||||
|
"description": "Location adjacent to a variable definition.",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "SCHEMA",
|
"name": "SCHEMA",
|
||||||
"description": "Location adjacent to a schema definition.",
|
"description": "Location adjacent to a schema definition.",
|
||||||
|
|
@ -11028,12 +11190,14 @@
|
||||||
"description": "Marks an element of a GraphQL schema as no longer supported.",
|
"description": "Marks an element of a GraphQL schema as no longer supported.",
|
||||||
"locations": [
|
"locations": [
|
||||||
"FIELD_DEFINITION",
|
"FIELD_DEFINITION",
|
||||||
|
"ARGUMENT_DEFINITION",
|
||||||
|
"INPUT_FIELD_DEFINITION",
|
||||||
"ENUM_VALUE"
|
"ENUM_VALUE"
|
||||||
],
|
],
|
||||||
"args": [
|
"args": [
|
||||||
{
|
{
|
||||||
"name": "reason",
|
"name": "reason",
|
||||||
"description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/).",
|
"description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).",
|
||||||
"type": {
|
"type": {
|
||||||
"kind": "SCALAR",
|
"kind": "SCALAR",
|
||||||
"name": "String",
|
"name": "String",
|
||||||
|
|
@ -11042,6 +11206,29 @@
|
||||||
"defaultValue": "\"No longer supported\""
|
"defaultValue": "\"No longer supported\""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "specifiedBy",
|
||||||
|
"description": "Exposes a URL that specifies the behaviour of this scalar.",
|
||||||
|
"locations": [
|
||||||
|
"SCALAR"
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "url",
|
||||||
|
"description": "The URL that specifies the behaviour of this scalar.",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultValue": null
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,62 @@
|
||||||
import path from 'path'
|
import path from 'path';
|
||||||
import { graphql, introspectionQuery } from 'graphql'
|
import { fileURLToPath } from 'url';
|
||||||
import { updateSchema, diffSchema } from 'graphql-markdown'
|
import GraphQL from 'graphql';
|
||||||
import baseSchema, { createSchema } from '../src/schema'
|
import GraphQLMarkdown from 'graphql-markdown';
|
||||||
|
import { baseSchema, createSchema } from '../src/schema.js';
|
||||||
|
|
||||||
const extensionModules = [
|
const { graphql, getIntrospectionQuery } = GraphQL;
|
||||||
'cover-art-archive',
|
const { updateSchema, diffSchema } = GraphQLMarkdown;
|
||||||
'fanart-tv',
|
|
||||||
'mediawiki',
|
|
||||||
'the-audio-db'
|
|
||||||
]
|
|
||||||
|
|
||||||
function getSchemaJSON(schema) {
|
async function getSchemaJSON(schema) {
|
||||||
return graphql(schema, introspectionQuery).then(result => result.data)
|
const result = await graphql(schema, getIntrospectionQuery());
|
||||||
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.all(
|
async function buildExtensionDocs(extensionModules) {
|
||||||
extensionModules.map(extensionModule => {
|
return Promise.all(
|
||||||
const extension = require(`../src/extensions/${extensionModule}`).default
|
extensionModules.map(async (extensionName) => {
|
||||||
console.log(`Generating docs for “${extension.name}” extension...`)
|
const extensionModule = await import(
|
||||||
const schema = createSchema(baseSchema, { extensions: [extension] })
|
`../src/extensions/${extensionName}/index.js`
|
||||||
return Promise.all([getSchemaJSON(baseSchema), getSchemaJSON(schema)]).then(
|
);
|
||||||
([baseSchemaJSON, schemaJSON]) => {
|
const extension = extensionModule.default;
|
||||||
|
console.log(`Generating docs for “${extension.name}” extension...`);
|
||||||
|
const schema = createSchema(baseSchema, { extensions: [extension] });
|
||||||
|
const [baseSchemaJSON, schemaJSON] = await Promise.all([
|
||||||
|
getSchemaJSON(baseSchema),
|
||||||
|
getSchemaJSON(schema),
|
||||||
|
]);
|
||||||
const outputSchema = diffSchema(baseSchemaJSON, schemaJSON, {
|
const outputSchema = diffSchema(baseSchemaJSON, schemaJSON, {
|
||||||
processTypeDiff(type) {
|
processTypeDiff(type) {
|
||||||
if (type.description === undefined) {
|
if (type.description === undefined) {
|
||||||
type.description =
|
type.description =
|
||||||
':small_blue_diamond: *This type has been extended. See the ' +
|
':small_blue_diamond: *This type has been extended. See the ' +
|
||||||
'[base schema](../types.md)\nfor a description and additional ' +
|
'[base schema](../types.md)\nfor a description and additional ' +
|
||||||
'fields.*'
|
'fields.*';
|
||||||
}
|
}
|
||||||
return type
|
return type;
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
const outputPath = path.resolve(
|
const outputPath = path.resolve(
|
||||||
__dirname,
|
path.dirname(fileURLToPath(import.meta.url)),
|
||||||
`../docs/extensions/${extensionModule}.md`
|
`../docs/extensions/${extensionName}.md`
|
||||||
)
|
);
|
||||||
return updateSchema(outputPath, outputSchema, {
|
return updateSchema(outputPath, outputSchema, {
|
||||||
unknownTypeURL: '../types.md',
|
unknownTypeURL: '../types.md',
|
||||||
headingLevel: 2
|
headingLevel: 2,
|
||||||
|
});
|
||||||
})
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
)
|
|
||||||
})
|
buildExtensionDocs([
|
||||||
)
|
'cover-art-archive',
|
||||||
.then(extensions => {
|
'fanart-tv',
|
||||||
console.log(`Built docs for ${extensions.length} extension(s).`)
|
'mediawiki',
|
||||||
})
|
'the-audio-db',
|
||||||
.catch(err => {
|
])
|
||||||
console.log('Error:', err)
|
.then((extensions) => {
|
||||||
|
console.log(`Built docs for ${extensions.length} extension(s).`);
|
||||||
})
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('Error:', err);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import { graphql, introspectionQuery, printSchema } from 'graphql'
|
import GraphQL from 'graphql';
|
||||||
import schema from '../src/schema'
|
import { baseSchema as schema } from '../src/schema.js';
|
||||||
|
|
||||||
|
const { graphql, getIntrospectionQuery, printSchema } = GraphQL;
|
||||||
|
|
||||||
if (process.argv[2] === '--json') {
|
if (process.argv[2] === '--json') {
|
||||||
graphql(schema, introspectionQuery)
|
graphql(schema, getIntrospectionQuery())
|
||||||
.then(result => {
|
.then((result) => {
|
||||||
console.log(JSON.stringify(result, null, 2))
|
console.log(JSON.stringify(result, null, 2));
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error(err)
|
|
||||||
})
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log(printSchema(schema))
|
console.log(printSchema(schema));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,15 @@
|
||||||
import request from 'request'
|
import { fileURLToPath } from 'url';
|
||||||
import retry from 'retry'
|
import createDebug from 'debug';
|
||||||
import ExtendableError from 'es6-error'
|
import got from 'got';
|
||||||
import RateLimit from '../rate-limit'
|
import { readPackageUpSync } from 'read-pkg-up';
|
||||||
import pkg from '../../package.json'
|
import RateLimit from '../rate-limit.js';
|
||||||
|
import { filterObjectValues, getTypeName } from '../util.js';
|
||||||
|
|
||||||
const debug = require('debug')('graphbrainz:api/client')
|
const debug = createDebug('graphbrainz:api/client');
|
||||||
|
|
||||||
// If the `request` callback returns an error, it indicates a failure at a lower
|
const { packageJson: pkg } = readPackageUpSync({
|
||||||
// level than the HTTP response itself. If it's any of the following error
|
cwd: fileURLToPath(import.meta.url),
|
||||||
// codes, we should retry.
|
});
|
||||||
const RETRY_CODES = {
|
|
||||||
ECONNRESET: true,
|
|
||||||
ENOTFOUND: true,
|
|
||||||
ESOCKETTIMEDOUT: true,
|
|
||||||
ETIMEDOUT: true,
|
|
||||||
ECONNREFUSED: true,
|
|
||||||
EHOSTUNREACH: true,
|
|
||||||
EPIPE: true,
|
|
||||||
EAI_AGAIN: true
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ClientError extends ExtendableError {
|
|
||||||
constructor(message, statusCode) {
|
|
||||||
super(message)
|
|
||||||
this.statusCode = statusCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Client {
|
export default class Client {
|
||||||
constructor({
|
constructor({
|
||||||
|
|
@ -33,112 +17,72 @@ export default class Client {
|
||||||
userAgent = `${pkg.name}/${pkg.version} ` +
|
userAgent = `${pkg.name}/${pkg.version} ` +
|
||||||
`( ${pkg.homepage || pkg.author.url || pkg.author.email} )`,
|
`( ${pkg.homepage || pkg.author.url || pkg.author.email} )`,
|
||||||
extraHeaders = {},
|
extraHeaders = {},
|
||||||
errorClass = ClientError,
|
|
||||||
timeout = 60000,
|
timeout = 60000,
|
||||||
limit = 1,
|
limit = 1,
|
||||||
period = 1000,
|
period = 1000,
|
||||||
concurrency = 10,
|
concurrency = 10,
|
||||||
retries = 10,
|
retry,
|
||||||
// It's OK for `retryDelayMin` to be less than one second, even 0, because
|
|
||||||
// `RateLimit` will already make sure we don't exceed the API rate limit.
|
|
||||||
// We're not doing exponential backoff because it will help with being
|
|
||||||
// rate limited, but rather to be chill in case MusicBrainz is returning
|
|
||||||
// some other error or our network is failing.
|
|
||||||
retryDelayMin = 100,
|
|
||||||
retryDelayMax = 60000,
|
|
||||||
randomizeRetry = true
|
|
||||||
} = {}) {
|
} = {}) {
|
||||||
this.baseURL = baseURL
|
this.baseURL = baseURL;
|
||||||
this.userAgent = userAgent
|
this.userAgent = userAgent;
|
||||||
this.extraHeaders = extraHeaders
|
this.extraHeaders = extraHeaders;
|
||||||
this.errorClass = errorClass
|
this.timeout = timeout;
|
||||||
this.timeout = timeout
|
this.limiter = new RateLimit({ limit, period, concurrency });
|
||||||
this.limiter = new RateLimit({ limit, period, concurrency })
|
this.retryOptions = retry;
|
||||||
this.retryOptions = {
|
|
||||||
retries,
|
|
||||||
minTimeout: retryDelayMin,
|
|
||||||
maxTimeout: retryDelayMax,
|
|
||||||
randomize: randomizeRetry
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parseErrorMessage(err) {
|
||||||
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if we should retry the request based on the given error.
|
* Send a request without any rate limiting.
|
||||||
* Retry any 5XX response from MusicBrainz, as well as any error in
|
|
||||||
* `RETRY_CODES`.
|
|
||||||
*/
|
|
||||||
shouldRetry(err) {
|
|
||||||
if (err instanceof this.errorClass) {
|
|
||||||
return err.statusCode >= 500 && err.statusCode < 600
|
|
||||||
}
|
|
||||||
return RETRY_CODES[err.code] || false
|
|
||||||
}
|
|
||||||
|
|
||||||
parseErrorMessage(response, body) {
|
|
||||||
return typeof body === 'string' && body ? body : `${response.statusCode}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a request without any retrying or rate limiting.
|
|
||||||
* Use `get` instead.
|
* Use `get` instead.
|
||||||
*/
|
*/
|
||||||
_get(path, options = {}, info = {}) {
|
async _get(path, { searchParams, ...options } = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
const url = new URL(path, this.baseURL);
|
||||||
|
if (searchParams) {
|
||||||
|
if (getTypeName(searchParams) === 'Object') {
|
||||||
|
searchParams = filterObjectValues(
|
||||||
|
searchParams,
|
||||||
|
(value) => value != null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const moreSearchParams = new URLSearchParams(searchParams);
|
||||||
|
moreSearchParams.forEach((value, key) => {
|
||||||
|
url.searchParams.set(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
options = {
|
options = {
|
||||||
baseUrl: this.baseURL,
|
responseType: 'json',
|
||||||
url: path,
|
|
||||||
gzip: true,
|
|
||||||
timeout: this.timeout,
|
timeout: this.timeout,
|
||||||
|
retry: this.retryOptions,
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': this.userAgent,
|
'User-Agent': this.userAgent,
|
||||||
...this.extraHeaders,
|
...this.extraHeaders,
|
||||||
...options.headers
|
...options.headers,
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
const url = `${options.baseUrl}${options.url}`
|
let response;
|
||||||
|
try {
|
||||||
debug(`Sending request. url=${url} attempt=${info.currentAttempt}`)
|
debug(`Sending request. url=%s`, url);
|
||||||
|
response = await got(url.toString(), options);
|
||||||
request(options, (err, response, body) => {
|
debug(`Success: %s url=%s`, response.statusCode, url);
|
||||||
if (err) {
|
return response;
|
||||||
debug(`Error: “${err}” url=${url}`)
|
} catch (err) {
|
||||||
reject(err)
|
const parsedError = this.parseErrorMessage(err) || err;
|
||||||
} else if (response.statusCode >= 400) {
|
debug(`Error: “%s” url=%s`, parsedError, url);
|
||||||
const message = this.parseErrorMessage(response, body)
|
throw parsedError;
|
||||||
debug(`Error: “${message}” url=${url}`)
|
|
||||||
const ClientError = this.errorClass
|
|
||||||
reject(new ClientError(message, response.statusCode))
|
|
||||||
} else if (options.method === 'HEAD') {
|
|
||||||
resolve(response.headers)
|
|
||||||
} else {
|
|
||||||
resolve(body)
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a request with retrying and rate limiting.
|
* Send a request with rate limiting.
|
||||||
*/
|
*/
|
||||||
get(path, options = {}) {
|
get(path, options = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
const fn = this._get.bind(this);
|
||||||
const fn = this._get.bind(this)
|
return this.limiter.enqueue(fn, [path, options]);
|
||||||
const operation = retry.operation(this.retryOptions)
|
|
||||||
operation.attempt(currentAttempt => {
|
|
||||||
// This will increase the priority in our `RateLimit` queue for each
|
|
||||||
// retry, so that newer requests don't delay this one further.
|
|
||||||
const priority = currentAttempt
|
|
||||||
this.limiter
|
|
||||||
.enqueue(fn, [path, options, { currentAttempt }], priority)
|
|
||||||
.then(resolve)
|
|
||||||
.catch(err => {
|
|
||||||
if (!this.shouldRetry(err) || !operation.retry(err)) {
|
|
||||||
reject(operation.mainError() || err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
import MusicBrainz, { MusicBrainzError } from './musicbrainz'
|
import MusicBrainz from './musicbrainz.js';
|
||||||
|
|
||||||
export { MusicBrainz as default, MusicBrainz, MusicBrainzError }
|
export { MusicBrainz as default, MusicBrainz };
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,18 @@
|
||||||
import qs from 'qs'
|
import ExtendableError from 'es6-error';
|
||||||
import Client, { ClientError } from './client'
|
import Client from './client.js';
|
||||||
|
import { filterObjectValues } from '../util.js';
|
||||||
|
|
||||||
export class MusicBrainzError extends ClientError {}
|
export class MusicBrainzError extends ExtendableError {
|
||||||
|
constructor(message, response) {
|
||||||
|
super(message);
|
||||||
|
this.response = response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default class MusicBrainz extends Client {
|
export default class MusicBrainz extends Client {
|
||||||
constructor({
|
constructor({
|
||||||
baseURL = process.env.MUSICBRAINZ_BASE_URL ||
|
baseURL = process.env.MUSICBRAINZ_BASE_URL ||
|
||||||
'http://musicbrainz.org/ws/2/',
|
'http://musicbrainz.org/ws/2/',
|
||||||
errorClass = MusicBrainzError,
|
|
||||||
// MusicBrainz API requests are limited to an *average* of 1 req/sec.
|
// MusicBrainz API requests are limited to an *average* of 1 req/sec.
|
||||||
// That means if, for example, we only need to make a few API requests to
|
// That means if, for example, we only need to make a few API requests to
|
||||||
// fulfill a query, we might as well make them all at once - as long as
|
// fulfill a query, we might as well make them all at once - as long as
|
||||||
|
|
@ -18,73 +23,87 @@ export default class MusicBrainz extends Client {
|
||||||
period = 5500,
|
period = 5500,
|
||||||
...options
|
...options
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super({ baseURL, errorClass, limit, period, ...options })
|
super({ baseURL, limit, period, ...options });
|
||||||
}
|
}
|
||||||
|
|
||||||
parseErrorMessage(response, body) {
|
parseErrorMessage(err) {
|
||||||
|
if (err.name === 'HTTPError') {
|
||||||
|
const { body } = err.response;
|
||||||
if (body && body.error) {
|
if (body && body.error) {
|
||||||
return body.error
|
return new MusicBrainzError(`${body.error}`, err.response);
|
||||||
}
|
}
|
||||||
return super.parseErrorMessage(response, body)
|
}
|
||||||
|
return super.parseErrorMessage(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(url, options = {}) {
|
||||||
|
options = {
|
||||||
|
resolveBodyOnly: true,
|
||||||
|
...options,
|
||||||
|
searchParams: {
|
||||||
|
fmt: 'json',
|
||||||
|
...options.searchParams,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return super.get(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
stringifyParams(params) {
|
stringifyParams(params) {
|
||||||
if (Array.isArray(params.inc)) {
|
if (Array.isArray(params.inc)) {
|
||||||
params = {
|
params = {
|
||||||
...params,
|
...params,
|
||||||
inc: params.inc.join('+')
|
inc: params.inc.join('+'),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
if (Array.isArray(params.type)) {
|
if (Array.isArray(params.type)) {
|
||||||
params = {
|
params = {
|
||||||
...params,
|
...params,
|
||||||
type: params.type.join('|')
|
type: params.type.join('|'),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
if (Array.isArray(params.status)) {
|
if (Array.isArray(params.status)) {
|
||||||
params = {
|
params = {
|
||||||
...params,
|
...params,
|
||||||
status: params.status.join('|')
|
status: params.status.join('|'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
return new URLSearchParams(
|
||||||
return qs.stringify(params, {
|
filterObjectValues(params, (value) => value != null && value !== '')
|
||||||
skipNulls: true,
|
).toString();
|
||||||
filter: (key, value) => (value === '' ? undefined : value)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getURL(path, params) {
|
getURL(path, params) {
|
||||||
const query = params ? this.stringifyParams(params) : ''
|
const query = params ? this.stringifyParams(params) : '';
|
||||||
return query ? `${path}?${query}` : path
|
return query ? `${path}?${query}` : path;
|
||||||
}
|
}
|
||||||
|
|
||||||
getLookupURL(entity, id, params) {
|
getLookupURL(entity, id, params) {
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
return this.getBrowseURL(entity, params)
|
return this.getBrowseURL(entity, params);
|
||||||
}
|
}
|
||||||
return this.getURL(`${entity}/${id}`, params)
|
return this.getURL(`${entity}/${id}`, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
lookup(entity, id, params = {}) {
|
lookup(entity, id, params = {}) {
|
||||||
const url = this.getLookupURL(entity, id, params)
|
const url = this.getLookupURL(entity, id, params);
|
||||||
return this.get(url, { json: true, qs: { fmt: 'json' } })
|
return this.get(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBrowseURL(entity, params) {
|
getBrowseURL(entity, params) {
|
||||||
return this.getURL(entity, params)
|
return this.getURL(entity, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
browse(entity, params = {}) {
|
browse(entity, params = {}) {
|
||||||
const url = this.getBrowseURL(entity, params)
|
const url = this.getBrowseURL(entity, params);
|
||||||
return this.get(url, { json: true, qs: { fmt: 'json' } })
|
return this.get(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSearchURL(entity, query, params) {
|
getSearchURL(entity, query, params) {
|
||||||
return this.getURL(entity, { ...params, query })
|
return this.getURL(entity, { ...params, query });
|
||||||
}
|
}
|
||||||
|
|
||||||
search(entity, query, params = {}) {
|
search(entity, query, params = {}) {
|
||||||
const url = this.getSearchURL(entity, query, params)
|
const url = this.getSearchURL(entity, query, params);
|
||||||
return this.get(url, { json: true, qs: { fmt: 'json' } })
|
return this.get(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,30 @@
|
||||||
import createLoaders from './loaders'
|
import createLoaders from './loaders.js';
|
||||||
import { loadExtension } from './extensions'
|
import createDebug from 'debug';
|
||||||
|
|
||||||
const debug = require('debug')('graphbrainz:context')
|
const debug = createDebug('graphbrainz:context');
|
||||||
|
|
||||||
export function extendContext(extension, context, options) {
|
export function extendContext(extension, context, options) {
|
||||||
if (extension.extendContext) {
|
if (extension.extendContext) {
|
||||||
if (typeof extension.extendContext === 'function') {
|
if (typeof extension.extendContext === 'function') {
|
||||||
debug(
|
debug(
|
||||||
`Extending context via a function from the “${
|
`Extending context via a function from the “${extension.name}” extension.`
|
||||||
extension.name
|
);
|
||||||
}” extension.`
|
context = extension.extendContext(context, options);
|
||||||
)
|
|
||||||
context = extension.extendContext(context, options)
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Extension “${extension.name}” contains an invalid \`extendContext\` ` +
|
`Extension “${extension.name}” contains an invalid \`extendContext\` ` +
|
||||||
`value: ${extension.extendContext}`
|
`value: ${extension.extendContext}`
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return context
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createContext(options = {}) {
|
export function createContext(options = {}) {
|
||||||
const { client } = options
|
const { client, extensions = [] } = options;
|
||||||
const loaders = createLoaders(client)
|
const loaders = createLoaders(client);
|
||||||
const context = { client, loaders }
|
let context = { client, loaders };
|
||||||
const { extensions = [] } = options
|
|
||||||
return extensions.reduce((context, extension) => {
|
return extensions.reduce((context, extension) => {
|
||||||
return extendContext(loadExtension(extension), context, options)
|
return extendContext(extension, context, options);
|
||||||
}, context)
|
}, context);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,12 @@
|
||||||
import Client from '../../api/client'
|
import ExtendableError from 'es6-error';
|
||||||
|
import Client from '../../api/client.js';
|
||||||
|
|
||||||
|
export class CoverArtArchiveError extends ExtendableError {
|
||||||
|
constructor(message, response) {
|
||||||
|
super(message);
|
||||||
|
this.response = response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default class CoverArtArchiveClient extends Client {
|
export default class CoverArtArchiveClient extends Client {
|
||||||
constructor({
|
constructor({
|
||||||
|
|
@ -8,32 +16,40 @@ export default class CoverArtArchiveClient extends Client {
|
||||||
period = 1000,
|
period = 1000,
|
||||||
...options
|
...options
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super({ baseURL, limit, period, ...options })
|
super({ baseURL, limit, period, ...options });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sinfully attempt to parse HTML responses for the error message.
|
* Sinfully attempt to parse HTML responses for the error message.
|
||||||
*/
|
*/
|
||||||
parseErrorMessage(response, body) {
|
parseErrorMessage(err) {
|
||||||
|
if (err.name === 'HTTPError') {
|
||||||
|
const { body } = err.response;
|
||||||
if (typeof body === 'string' && body.startsWith('<!')) {
|
if (typeof body === 'string' && body.startsWith('<!')) {
|
||||||
const heading = /<h1>([^<]+)<\/h1>/i.exec(body)
|
const heading = /<h1>([^<]+)<\/h1>/i.exec(body);
|
||||||
const message = /<p>([^<]+)<\/p>/i.exec(body)
|
const message = /<p>([^<]+)<\/p>/i.exec(body);
|
||||||
return `${heading ? heading[1] + ': ' : ''}${message ? message[1] : ''}`
|
return new CoverArtArchiveError(
|
||||||
|
`${heading ? heading[1] + ': ' : ''}${message ? message[1] : ''}`,
|
||||||
|
err.response
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return super.parseErrorMessage(response, body)
|
}
|
||||||
|
return super.parseErrorMessage(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
images(entityType, mbid) {
|
images(entityType, mbid) {
|
||||||
return this.get(`${entityType}/${mbid}`, { json: true })
|
return this.get(`${entityType}/${mbid}`, { resolveBodyOnly: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
imageURL(entityType, mbid, typeOrID = 'front', size) {
|
async imageURL(entityType, mbid, typeOrID = 'front', size) {
|
||||||
let url = `${entityType}/${mbid}/${typeOrID}`
|
let url = `${entityType}/${mbid}/${typeOrID}`;
|
||||||
if (size != null) {
|
if (size != null) {
|
||||||
url += `-${size}`
|
url += `-${size}`;
|
||||||
}
|
}
|
||||||
return this.get(url, { method: 'HEAD', followRedirect: false }).then(
|
const response = await this.get(url, {
|
||||||
headers => headers.location
|
method: 'HEAD',
|
||||||
)
|
followRedirect: false,
|
||||||
|
});
|
||||||
|
return response.headers.location;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,27 @@
|
||||||
import schema from './schema'
|
import schema from './schema.js';
|
||||||
import resolvers from './resolvers'
|
import resolvers from './resolvers.js';
|
||||||
import createLoaders from './loaders'
|
import createLoaders from './loaders.js';
|
||||||
import CoverArtArchiveClient from './client'
|
import CoverArtArchiveClient from './client.js';
|
||||||
import { ONE_DAY } from '../../util'
|
import { ONE_DAY } from '../../util.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Cover Art Archive',
|
name: 'Cover Art Archive',
|
||||||
description: `Retrieve cover art images for releases from the [Cover Art
|
description: `Retrieve cover art images for releases from the [Cover Art
|
||||||
Archive](https://coverartarchive.org/).`,
|
Archive](https://coverartarchive.org/).`,
|
||||||
extendContext(context, { coverArtClient, coverArtArchive = {} } = {}) {
|
extendContext(context, { coverArtClient, coverArtArchive = {} } = {}) {
|
||||||
const client = coverArtClient || new CoverArtArchiveClient(coverArtArchive)
|
const client = coverArtClient || new CoverArtArchiveClient(coverArtArchive);
|
||||||
const cacheSize = parseInt(
|
const cacheSize = parseInt(
|
||||||
process.env.COVER_ART_ARCHIVE_CACHE_SIZE ||
|
process.env.COVER_ART_ARCHIVE_CACHE_SIZE ||
|
||||||
process.env.GRAPHBRAINZ_CACHE_SIZE ||
|
process.env.GRAPHBRAINZ_CACHE_SIZE ||
|
||||||
8192,
|
8192,
|
||||||
10
|
10
|
||||||
)
|
);
|
||||||
const cacheTTL = parseInt(
|
const cacheTTL = parseInt(
|
||||||
process.env.COVER_ART_ARCHIVE_CACHE_TTL ||
|
process.env.COVER_ART_ARCHIVE_CACHE_TTL ||
|
||||||
process.env.GRAPHBRAINZ_CACHE_TTL ||
|
process.env.GRAPHBRAINZ_CACHE_TTL ||
|
||||||
ONE_DAY,
|
ONE_DAY,
|
||||||
10
|
10
|
||||||
)
|
);
|
||||||
return {
|
return {
|
||||||
...context,
|
...context,
|
||||||
// Add the client instance directly onto `context` for backwards
|
// Add the client instance directly onto `context` for backwards
|
||||||
|
|
@ -29,12 +29,12 @@ Archive](https://coverartarchive.org/).`,
|
||||||
coverArtClient: client,
|
coverArtClient: client,
|
||||||
loaders: {
|
loaders: {
|
||||||
...context.loaders,
|
...context.loaders,
|
||||||
...createLoaders({ client, cacheSize, cacheTTL })
|
...createLoaders({ client, cacheSize, cacheTTL }),
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
extendSchema: {
|
extendSchema: {
|
||||||
schemas: [schema],
|
schemas: [schema],
|
||||||
resolvers
|
resolvers,
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,67 +1,68 @@
|
||||||
import DataLoader from 'dataloader'
|
import createDebug from 'debug';
|
||||||
import LRUCache from 'lru-cache'
|
import DataLoader from 'dataloader';
|
||||||
|
import LRUCache from 'lru-cache';
|
||||||
|
|
||||||
const debug = require('debug')('graphbrainz:extensions/cover-art-archive')
|
const debug = createDebug('graphbrainz:extensions/cover-art-archive');
|
||||||
|
|
||||||
export default function createLoaders(options) {
|
export default function createLoaders(options) {
|
||||||
const { client } = options
|
const { client } = options;
|
||||||
const cache = LRUCache({
|
const cache = new LRUCache({
|
||||||
max: options.cacheSize,
|
max: options.cacheSize,
|
||||||
maxAge: options.cacheTTL,
|
maxAge: options.cacheTTL,
|
||||||
dispose(key) {
|
dispose(key) {
|
||||||
debug(`Removed from cache. key=${key}`)
|
debug(`Removed from cache. key=${key}`);
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
// Make the cache Map-like.
|
// Make the cache Map-like.
|
||||||
cache.delete = cache.del
|
cache.delete = cache.del;
|
||||||
cache.clear = cache.reset
|
cache.clear = cache.reset;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
coverArtArchive: new DataLoader(
|
coverArtArchive: new DataLoader(
|
||||||
keys => {
|
(keys) => {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
keys.map(key => {
|
keys.map((key) => {
|
||||||
const [entityType, id] = key
|
const [entityType, id] = key;
|
||||||
return client
|
return client
|
||||||
.images(entityType, id)
|
.images(entityType, id)
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
if (err.statusCode === 404) {
|
if (err.response.statusCode === 404) {
|
||||||
return { images: [] }
|
return { images: [] };
|
||||||
}
|
}
|
||||||
throw err
|
throw err;
|
||||||
})
|
})
|
||||||
.then(coverArt => ({
|
.then((coverArt) => ({
|
||||||
...coverArt,
|
...coverArt,
|
||||||
_entityType: entityType,
|
_entityType: entityType,
|
||||||
_id: id,
|
_id: id,
|
||||||
_releaseID:
|
_releaseID:
|
||||||
coverArt.release && coverArt.release.split('/').pop()
|
coverArt.release && coverArt.release.split('/').pop(),
|
||||||
}))
|
}));
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cacheKeyFn: ([entityType, id]) => `${entityType}/${id}`,
|
cacheKeyFn: ([entityType, id]) => `${entityType}/${id}`,
|
||||||
cacheMap: cache
|
cacheMap: cache,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
coverArtArchiveURL: new DataLoader(
|
coverArtArchiveURL: new DataLoader(
|
||||||
keys => {
|
(keys) => {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
keys.map(key => {
|
keys.map((key) => {
|
||||||
const [entityType, id, type, size] = key
|
const [entityType, id, type, size] = key;
|
||||||
return client.imageURL(entityType, id, type, size)
|
return client.imageURL(entityType, id, type, size);
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
batch: false,
|
batch: false,
|
||||||
cacheKeyFn: ([entityType, id, type, size]) => {
|
cacheKeyFn: ([entityType, id, type, size]) => {
|
||||||
const key = `${entityType}/${id}/${type}`
|
const key = `${entityType}/${id}/${type}`;
|
||||||
return size ? `${key}-${size}` : key
|
return size ? `${key}-${size}` : key;
|
||||||
},
|
},
|
||||||
cacheMap: cache
|
cacheMap: cache,
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { resolveLookup } from '../../resolvers'
|
import { resolveLookup } from '../../resolvers.js';
|
||||||
|
|
||||||
const SIZES = new Map([
|
const SIZES = new Map([
|
||||||
[null, null],
|
[null, null],
|
||||||
|
|
@ -6,70 +6,70 @@ const SIZES = new Map([
|
||||||
[500, 500],
|
[500, 500],
|
||||||
['FULL', null],
|
['FULL', null],
|
||||||
['SMALL', 250],
|
['SMALL', 250],
|
||||||
['LARGE', 500]
|
['LARGE', 500],
|
||||||
])
|
]);
|
||||||
|
|
||||||
function resolveImage(coverArt, args, { loaders }, info) {
|
function resolveImage(coverArt, args, { loaders }, info) {
|
||||||
// Since migrating the schema to an extension, we lost custom enum values
|
// Since migrating the schema to an extension, we lost custom enum values
|
||||||
// for the time being. Translate any incoming `size` arg to the old enum
|
// for the time being. Translate any incoming `size` arg to the old enum
|
||||||
// values.
|
// values.
|
||||||
const size = SIZES.get(args.size)
|
const size = SIZES.get(args.size);
|
||||||
// Field should be `front` or `back`.
|
// Field should be `front` or `back`.
|
||||||
const field = info.fieldName
|
const field = info.fieldName;
|
||||||
if (coverArt.images) {
|
if (coverArt.images) {
|
||||||
const matches = coverArt.images.filter(image => image[field])
|
const matches = coverArt.images.filter((image) => image[field]);
|
||||||
if (!matches.length) {
|
if (!matches.length) {
|
||||||
return null
|
return null;
|
||||||
} else if (matches.length === 1) {
|
} else if (matches.length === 1) {
|
||||||
const match = matches[0]
|
const match = matches[0];
|
||||||
if (size === 250) {
|
if (size === 250) {
|
||||||
return match.thumbnails.small
|
return match.thumbnails.small;
|
||||||
} else if (size === 500) {
|
} else if (size === 500) {
|
||||||
return match.thumbnails.large
|
return match.thumbnails.large;
|
||||||
} else {
|
} else {
|
||||||
return match.image
|
return match.image;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const entityType = coverArt._entityType
|
const entityType = coverArt._entityType;
|
||||||
const id = coverArt._id
|
const id = coverArt._id;
|
||||||
const releaseID = coverArt._releaseID
|
const releaseID = coverArt._releaseID;
|
||||||
if (entityType === 'release-group' && field === 'front') {
|
if (entityType === 'release-group' && field === 'front') {
|
||||||
// Release groups only have an endpoint to retrieve the front image.
|
// Release groups only have an endpoint to retrieve the front image.
|
||||||
// If someone requests the back of a release group, return the back of the
|
// If someone requests the back of a release group, return the back of the
|
||||||
// release that the release group's cover art response points to.
|
// release that the release group's cover art response points to.
|
||||||
return loaders.coverArtArchiveURL.load(['release-group', id, field, size])
|
return loaders.coverArtArchiveURL.load(['release-group', id, field, size]);
|
||||||
} else {
|
} else {
|
||||||
return loaders.coverArtArchiveURL.load(['release', releaseID, field, size])
|
return loaders.coverArtArchiveURL.load(['release', releaseID, field, size]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
CoverArtArchiveImage: {
|
CoverArtArchiveImage: {
|
||||||
fileID: image => image.id
|
fileID: (image) => image.id,
|
||||||
},
|
},
|
||||||
CoverArtArchiveRelease: {
|
CoverArtArchiveRelease: {
|
||||||
front: resolveImage,
|
front: resolveImage,
|
||||||
back: resolveImage,
|
back: resolveImage,
|
||||||
images: coverArt => coverArt.images,
|
images: (coverArt) => coverArt.images,
|
||||||
artwork: coverArt => coverArt.images.length > 0,
|
artwork: (coverArt) => coverArt.images.length > 0,
|
||||||
count: coverArt => coverArt.images.length,
|
count: (coverArt) => coverArt.images.length,
|
||||||
release: (coverArt, args, context, info) => {
|
release: (coverArt, args, context, info) => {
|
||||||
const mbid = coverArt._releaseID
|
const mbid = coverArt._releaseID;
|
||||||
if (mbid) {
|
if (mbid) {
|
||||||
return resolveLookup(coverArt, { mbid }, context, info)
|
return resolveLookup(coverArt, { mbid }, context, info);
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Release: {
|
Release: {
|
||||||
coverArtArchive: (release, args, { loaders }) => {
|
coverArtArchive: (release, args, { loaders }) => {
|
||||||
return loaders.coverArtArchive.load(['release', release.id])
|
return loaders.coverArtArchive.load(['release', release.id]);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
ReleaseGroup: {
|
ReleaseGroup: {
|
||||||
coverArtArchive: (releaseGroup, args, { loaders }) => {
|
coverArtArchive: (releaseGroup, args, { loaders }) => {
|
||||||
return loaders.coverArtArchive.load(['release-group', releaseGroup.id])
|
return loaders.coverArtArchive.load(['release-group', releaseGroup.id]);
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import gql from '../../tag'
|
import gql from '../../tag.js';
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
"""
|
"""
|
||||||
|
|
@ -173,4 +173,4 @@ export default gql`
|
||||||
"""
|
"""
|
||||||
coverArtArchive: CoverArtArchiveRelease
|
coverArtArchive: CoverArtArchiveRelease
|
||||||
}
|
}
|
||||||
`
|
`;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
import Client from '../../api/client'
|
import ExtendableError from 'es6-error';
|
||||||
|
import Client from '../../api/client.js';
|
||||||
|
|
||||||
|
export class FanArtError extends ExtendableError {}
|
||||||
|
|
||||||
export default class FanArtClient extends Client {
|
export default class FanArtClient extends Client {
|
||||||
constructor({
|
constructor({
|
||||||
|
|
@ -8,53 +11,51 @@ export default class FanArtClient extends Client {
|
||||||
period = 1000,
|
period = 1000,
|
||||||
...options
|
...options
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super({ baseURL, limit, period, ...options })
|
super({ baseURL, limit, period, ...options });
|
||||||
this.apiKey = apiKey
|
this.apiKey = apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
get(path, options = {}) {
|
get(path, options = {}) {
|
||||||
const ClientError = this.errorClass
|
|
||||||
if (!this.apiKey) {
|
if (!this.apiKey) {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new ClientError('No API key was configured for the fanart.tv client.')
|
new FanArtError('No API key was configured for the fanart.tv client.')
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
options = {
|
options = {
|
||||||
json: true,
|
resolveBodyOnly: true,
|
||||||
...options,
|
...options,
|
||||||
qs: {
|
searchParams: {
|
||||||
...options.qs,
|
...options.searchParams,
|
||||||
api_key: this.apiKey
|
api_key: this.apiKey,
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
return super.get(path, options)
|
return super.get(path, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
musicEntity(entityType, mbid) {
|
musicEntity(entityType, mbid) {
|
||||||
const ClientError = this.errorClass
|
|
||||||
switch (entityType) {
|
switch (entityType) {
|
||||||
case 'artist':
|
case 'artist':
|
||||||
return this.musicArtist(mbid)
|
return this.musicArtist(mbid);
|
||||||
case 'label':
|
case 'label':
|
||||||
return this.musicLabel(mbid)
|
return this.musicLabel(mbid);
|
||||||
case 'release-group':
|
case 'release-group':
|
||||||
return this.musicAlbum(mbid)
|
return this.musicAlbum(mbid);
|
||||||
default:
|
default:
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new ClientError(`Entity type unsupported: ${entityType}`)
|
new FanArtError(`Entity type unsupported: ${entityType}`)
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
musicArtist(mbid) {
|
musicArtist(mbid) {
|
||||||
return this.get(`music/${mbid}`)
|
return this.get(`music/${mbid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
musicAlbum(mbid) {
|
musicAlbum(mbid) {
|
||||||
return this.get(`music/albums/${mbid}`)
|
return this.get(`music/albums/${mbid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
musicLabel(mbid) {
|
musicLabel(mbid) {
|
||||||
return this.get(`music/${mbid}`)
|
return this.get(`music/${mbid}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,37 @@
|
||||||
import schema from './schema'
|
import schema from './schema.js';
|
||||||
import resolvers from './resolvers'
|
import resolvers from './resolvers.js';
|
||||||
import createLoader from './loader'
|
import createLoader from './loader.js';
|
||||||
import FanArtClient from './client'
|
import FanArtClient from './client.js';
|
||||||
import { ONE_DAY } from '../../util'
|
import { ONE_DAY } from '../../util.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'fanart.tv',
|
name: 'fanart.tv',
|
||||||
description: `Retrieve high quality artwork for artists, releases, and labels
|
description: `Retrieve high quality artwork for artists, releases, and labels
|
||||||
from [fanart.tv](https://fanart.tv/).`,
|
from [fanart.tv](https://fanart.tv/).`,
|
||||||
extendContext(context, { fanArt = {} } = {}) {
|
extendContext(context, { fanArt = {} } = {}) {
|
||||||
const client = new FanArtClient(fanArt)
|
const client = new FanArtClient(fanArt);
|
||||||
const cacheSize = parseInt(
|
const cacheSize = parseInt(
|
||||||
process.env.FANART_CACHE_SIZE ||
|
process.env.FANART_CACHE_SIZE ||
|
||||||
process.env.GRAPHBRAINZ_CACHE_SIZE ||
|
process.env.GRAPHBRAINZ_CACHE_SIZE ||
|
||||||
8192,
|
8192,
|
||||||
10
|
10
|
||||||
)
|
);
|
||||||
const cacheTTL = parseInt(
|
const cacheTTL = parseInt(
|
||||||
process.env.FANART_CACHE_TTL ||
|
process.env.FANART_CACHE_TTL ||
|
||||||
process.env.GRAPHBRAINZ_CACHE_TTL ||
|
process.env.GRAPHBRAINZ_CACHE_TTL ||
|
||||||
ONE_DAY,
|
ONE_DAY,
|
||||||
10
|
10
|
||||||
)
|
);
|
||||||
return {
|
return {
|
||||||
...context,
|
...context,
|
||||||
loaders: {
|
loaders: {
|
||||||
...context.loaders,
|
...context.loaders,
|
||||||
fanArt: createLoader({ client, cacheSize, cacheTTL })
|
fanArt: createLoader({ client, cacheSize, cacheTTL }),
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
extendSchema: {
|
extendSchema: {
|
||||||
schemas: [schema],
|
schemas: [schema],
|
||||||
resolvers
|
resolvers,
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,30 @@
|
||||||
import DataLoader from 'dataloader'
|
import createDebug from 'debug';
|
||||||
import LRUCache from 'lru-cache'
|
import DataLoader from 'dataloader';
|
||||||
|
import LRUCache from 'lru-cache';
|
||||||
|
|
||||||
const debug = require('debug')('graphbrainz:extensions/fanart-tv')
|
const debug = createDebug('graphbrainz:extensions/fanart-tv');
|
||||||
|
|
||||||
export default function createLoader(options) {
|
export default function createLoader(options) {
|
||||||
const { client } = options
|
const { client } = options;
|
||||||
const cache = LRUCache({
|
const cache = new LRUCache({
|
||||||
max: options.cacheSize,
|
max: options.cacheSize,
|
||||||
maxAge: options.cacheTTL,
|
maxAge: options.cacheTTL,
|
||||||
dispose(key) {
|
dispose(key) {
|
||||||
debug(`Removed from cache. key=${key}`)
|
debug(`Removed from cache. key=${key}`);
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
// Make the cache Map-like.
|
// Make the cache Map-like.
|
||||||
cache.delete = cache.del
|
cache.delete = cache.del;
|
||||||
cache.clear = cache.reset
|
cache.clear = cache.reset;
|
||||||
|
|
||||||
const loader = new DataLoader(
|
const loader = new DataLoader(
|
||||||
keys => {
|
(keys) => {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
keys.map(key => {
|
keys.map((key) => {
|
||||||
const [entityType, id] = key
|
const [entityType, id] = key;
|
||||||
return client
|
return client
|
||||||
.musicEntity(entityType, id)
|
.musicEntity(entityType, id)
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
if (err.statusCode === 404) {
|
if (err.statusCode === 404) {
|
||||||
// 404s are OK, just return empty data.
|
// 404s are OK, just return empty data.
|
||||||
return {
|
return {
|
||||||
|
|
@ -33,32 +34,32 @@ export default function createLoader(options) {
|
||||||
hdmusiclogo: [],
|
hdmusiclogo: [],
|
||||||
musicbanner: [],
|
musicbanner: [],
|
||||||
musiclabel: [],
|
musiclabel: [],
|
||||||
albums: {}
|
albums: {},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
throw err;
|
||||||
throw err
|
|
||||||
})
|
})
|
||||||
.then(body => {
|
.then((body) => {
|
||||||
if (entityType === 'artist') {
|
if (entityType === 'artist') {
|
||||||
const releaseGroupIDs = Object.keys(body.albums || {})
|
const releaseGroupIDs = Object.keys(body.albums || {});
|
||||||
debug(
|
debug(
|
||||||
`Priming album cache with ${releaseGroupIDs.length} album(s).`
|
`Priming album cache with ${releaseGroupIDs.length} album(s).`
|
||||||
)
|
);
|
||||||
releaseGroupIDs.forEach(key =>
|
releaseGroupIDs.forEach((key) =>
|
||||||
loader.prime(['release-group', key], body)
|
loader.prime(['release-group', key], body)
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
return body
|
return body;
|
||||||
|
});
|
||||||
})
|
})
|
||||||
})
|
);
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
batch: false,
|
batch: false,
|
||||||
cacheKeyFn: ([entityType, id]) => `${entityType}/${id}`,
|
cacheKeyFn: ([entityType, id]) => `${entityType}/${id}`,
|
||||||
cacheMap: cache
|
cacheMap: cache,
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
return loader
|
return loader;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,64 +1,64 @@
|
||||||
const imageResolvers = {
|
const imageResolvers = {
|
||||||
imageID: image => image.id,
|
imageID: (image) => image.id,
|
||||||
url: (image, args) => {
|
url: (image, args) => {
|
||||||
return args.size === 'PREVIEW'
|
return args.size === 'PREVIEW'
|
||||||
? image.url.replace('/fanart/', '/preview/')
|
? image.url.replace('/fanart/', '/preview/')
|
||||||
: image.url
|
: image.url;
|
||||||
},
|
},
|
||||||
likeCount: image => image.likes
|
likeCount: (image) => image.likes,
|
||||||
}
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
FanArtImage: {
|
FanArtImage: {
|
||||||
...imageResolvers
|
...imageResolvers,
|
||||||
},
|
},
|
||||||
FanArtDiscImage: {
|
FanArtDiscImage: {
|
||||||
...imageResolvers,
|
...imageResolvers,
|
||||||
discNumber: image => image.disc
|
discNumber: (image) => image.disc,
|
||||||
},
|
},
|
||||||
FanArtLabelImage: {
|
FanArtLabelImage: {
|
||||||
...imageResolvers,
|
...imageResolvers,
|
||||||
color: image => image.colour
|
color: (image) => image.colour,
|
||||||
},
|
},
|
||||||
FanArtArtist: {
|
FanArtArtist: {
|
||||||
backgrounds: artist => {
|
backgrounds: (artist) => {
|
||||||
return artist.artistbackground || []
|
return artist.artistbackground || [];
|
||||||
},
|
},
|
||||||
thumbnails: artist => {
|
thumbnails: (artist) => {
|
||||||
return artist.artistthumb || []
|
return artist.artistthumb || [];
|
||||||
},
|
},
|
||||||
logos: artist => {
|
logos: (artist) => {
|
||||||
return artist.musiclogo || []
|
return artist.musiclogo || [];
|
||||||
},
|
},
|
||||||
logosHD: artist => {
|
logosHD: (artist) => {
|
||||||
return artist.hdmusiclogo || []
|
return artist.hdmusiclogo || [];
|
||||||
|
},
|
||||||
|
banners: (artist) => {
|
||||||
|
return artist.musicbanner || [];
|
||||||
},
|
},
|
||||||
banners: artist => {
|
|
||||||
return artist.musicbanner || []
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
FanArtLabel: {
|
FanArtLabel: {
|
||||||
logos: label => label.musiclabel || []
|
logos: (label) => label.musiclabel || [],
|
||||||
},
|
},
|
||||||
FanArtAlbum: {
|
FanArtAlbum: {
|
||||||
albumCovers: album => album.albumcover || [],
|
albumCovers: (album) => album.albumcover || [],
|
||||||
discImages: album => album.cdart || []
|
discImages: (album) => album.cdart || [],
|
||||||
},
|
},
|
||||||
Artist: {
|
Artist: {
|
||||||
fanArt: (artist, args, context) => {
|
fanArt: (artist, args, context) => {
|
||||||
return context.loaders.fanArt.load(['artist', artist.id])
|
return context.loaders.fanArt.load(['artist', artist.id]);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
Label: {
|
Label: {
|
||||||
fanArt: (label, args, context) => {
|
fanArt: (label, args, context) => {
|
||||||
return context.loaders.fanArt.load(['label', label.id])
|
return context.loaders.fanArt.load(['label', label.id]);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
ReleaseGroup: {
|
ReleaseGroup: {
|
||||||
fanArt: (releaseGroup, args, context) => {
|
fanArt: (releaseGroup, args, context) => {
|
||||||
return context.loaders.fanArt
|
return context.loaders.fanArt
|
||||||
.load(['release-group', releaseGroup.id])
|
.load(['release-group', releaseGroup.id])
|
||||||
.then(artist => artist.albums[releaseGroup.id])
|
.then((artist) => artist.albums[releaseGroup.id]);
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import gql from '../../tag'
|
import gql from '../../tag.js';
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
"""
|
"""
|
||||||
|
|
@ -194,4 +194,4 @@ export default gql`
|
||||||
"""
|
"""
|
||||||
fanArt: FanArtAlbum
|
fanArt: FanArtAlbum
|
||||||
}
|
}
|
||||||
`
|
`;
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
export function loadExtension(extensionModule) {
|
export async function loadExtension(extensionModule) {
|
||||||
let extension
|
let extension;
|
||||||
if (typeof extensionModule === 'string') {
|
if (typeof extensionModule === 'string') {
|
||||||
extension = require(extensionModule)
|
extension = await import(extensionModule);
|
||||||
} else {
|
} else {
|
||||||
extension = extensionModule
|
extension = extensionModule;
|
||||||
}
|
}
|
||||||
if (extension == null || typeof extension !== 'object') {
|
if (extension == null || typeof extension !== 'object') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Expected ${extensionModule} to export an extension but instead ` +
|
`Expected ${extensionModule} to export an extension but instead ` +
|
||||||
`got: ${extension}`
|
`got: ${extension}`
|
||||||
)
|
);
|
||||||
} else if (extension.default) {
|
} else if (extension.default) {
|
||||||
// ECMAScript module interop.
|
// ECMAScript module interop.
|
||||||
extension = extension.default
|
extension = extension.default;
|
||||||
}
|
}
|
||||||
return extension
|
return extension;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,54 @@
|
||||||
import URL from 'url'
|
import ExtendableError from 'es6-error';
|
||||||
import Client from '../../api/client'
|
import Client from '../../api/client.js';
|
||||||
|
|
||||||
|
export class MediaWikiError extends ExtendableError {}
|
||||||
|
|
||||||
export default class MediaWikiClient extends Client {
|
export default class MediaWikiClient extends Client {
|
||||||
constructor({ limit = 10, period = 1000, ...options } = {}) {
|
constructor({ limit = 10, period = 1000, ...options } = {}) {
|
||||||
super({ limit, period, ...options })
|
super({ limit, period, ...options });
|
||||||
}
|
}
|
||||||
|
|
||||||
imageInfo(page) {
|
imageInfo(page) {
|
||||||
const pageURL = URL.parse(page, true)
|
const pageURL = new URL(page);
|
||||||
const ClientError = this.errorClass
|
|
||||||
|
|
||||||
if (!pageURL.pathname.startsWith('/wiki/')) {
|
if (!pageURL.pathname.startsWith('/wiki/')) {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new ClientError(
|
new MediaWikiError(
|
||||||
`MediaWiki page URL does not have the expected /wiki/ prefix: ${page}`
|
`MediaWiki page URL does not have the expected /wiki/ prefix: ${page}`
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiURL = URL.format({
|
const apiURL = new URL('/w/api.php', pageURL);
|
||||||
protocol: pageURL.protocol,
|
apiURL.search = new URLSearchParams({
|
||||||
auth: pageURL.auth,
|
|
||||||
host: pageURL.host,
|
|
||||||
pathname: '/w/api.php',
|
|
||||||
query: {
|
|
||||||
action: 'query',
|
action: 'query',
|
||||||
titles: decodeURI(pageURL.pathname.slice(6)),
|
titles: decodeURI(pageURL.pathname.slice(6)),
|
||||||
prop: 'imageinfo',
|
prop: 'imageinfo',
|
||||||
iiprop: 'url|size|canonicaltitle|user|extmetadata',
|
iiprop: 'url|size|canonicaltitle|user|extmetadata',
|
||||||
format: 'json'
|
format: 'json',
|
||||||
}
|
}).toString();
|
||||||
})
|
|
||||||
|
|
||||||
return this.get(apiURL, { json: true }).then(body => {
|
return this.get(apiURL.toString(), { resolveBodyOnly: true }).then(
|
||||||
const pageIDs = Object.keys(body.query.pages)
|
(body) => {
|
||||||
|
const pageIDs = Object.keys(body.query.pages);
|
||||||
if (pageIDs.length !== 1) {
|
if (pageIDs.length !== 1) {
|
||||||
throw new ClientError(
|
throw new MediaWikiError(
|
||||||
`Query returned multiple pages: [${pageIDs.join(', ')}]`
|
`Query returned multiple pages: [${pageIDs.join(', ')}]`
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
if (pageIDs[0] === '-1') {
|
if (pageIDs[0] === '-1') {
|
||||||
throw new ClientError(
|
throw new MediaWikiError(
|
||||||
body.query.pages['-1'].invalidreason || 'Unknown error'
|
body.query.pages['-1'].invalidreason || 'Unknown error'
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
const imageInfo = body.query.pages[pageIDs[0]].imageinfo
|
const imageInfo = body.query.pages[pageIDs[0]].imageinfo;
|
||||||
if (imageInfo.length !== 1) {
|
if (imageInfo.length !== 1) {
|
||||||
throw new ClientError(
|
throw new MediaWikiError(
|
||||||
`Query returned info for ${imageInfo.length} images, expected 1.`
|
`Query returned info for ${imageInfo.length} images, expected 1.`
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
return imageInfo[0]
|
return imageInfo[0];
|
||||||
})
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,37 @@
|
||||||
import schema from './schema'
|
import schema from './schema.js';
|
||||||
import resolvers from './resolvers'
|
import resolvers from './resolvers.js';
|
||||||
import createLoader from './loader'
|
import createLoader from './loader.js';
|
||||||
import MediaWikiClient from './client'
|
import MediaWikiClient from './client.js';
|
||||||
import { ONE_DAY } from '../../util'
|
import { ONE_DAY } from '../../util.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MediaWiki',
|
name: 'MediaWiki',
|
||||||
description: `Retrieve information from MediaWiki image pages, like the actual
|
description: `Retrieve information from MediaWiki image pages, like the actual
|
||||||
image file URL and EXIF metadata.`,
|
image file URL and EXIF metadata.`,
|
||||||
extendContext(context, { mediaWiki = {} } = {}) {
|
extendContext(context, { mediaWiki = {} } = {}) {
|
||||||
const client = new MediaWikiClient(mediaWiki)
|
const client = new MediaWikiClient(mediaWiki);
|
||||||
const cacheSize = parseInt(
|
const cacheSize = parseInt(
|
||||||
process.env.MEDIAWIKI_CACHE_SIZE ||
|
process.env.MEDIAWIKI_CACHE_SIZE ||
|
||||||
process.env.GRAPHBRAINZ_CACHE_SIZE ||
|
process.env.GRAPHBRAINZ_CACHE_SIZE ||
|
||||||
8192,
|
8192,
|
||||||
10
|
10
|
||||||
)
|
);
|
||||||
const cacheTTL = parseInt(
|
const cacheTTL = parseInt(
|
||||||
process.env.MEDIAWIKI_CACHE_TTL ||
|
process.env.MEDIAWIKI_CACHE_TTL ||
|
||||||
process.env.GRAPHBRAINZ_CACHE_TTL ||
|
process.env.GRAPHBRAINZ_CACHE_TTL ||
|
||||||
ONE_DAY,
|
ONE_DAY,
|
||||||
10
|
10
|
||||||
)
|
);
|
||||||
return {
|
return {
|
||||||
...context,
|
...context,
|
||||||
loaders: {
|
loaders: {
|
||||||
...context.loaders,
|
...context.loaders,
|
||||||
mediaWiki: createLoader({ client, cacheSize, cacheTTL })
|
mediaWiki: createLoader({ client, cacheSize, cacheTTL }),
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
extendSchema: {
|
extendSchema: {
|
||||||
schemas: [schema],
|
schemas: [schema],
|
||||||
resolvers
|
resolvers,
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,30 @@
|
||||||
import DataLoader from 'dataloader'
|
import createDebug from 'debug';
|
||||||
import LRUCache from 'lru-cache'
|
import DataLoader from 'dataloader';
|
||||||
|
import LRUCache from 'lru-cache';
|
||||||
|
|
||||||
const debug = require('debug')('graphbrainz:extensions/mediawiki')
|
const debug = createDebug('graphbrainz:extensions/mediawiki');
|
||||||
|
|
||||||
export default function createLoader(options) {
|
export default function createLoader(options) {
|
||||||
const { client } = options
|
const { client } = options;
|
||||||
const cache = LRUCache({
|
const cache = new LRUCache({
|
||||||
max: options.cacheSize,
|
max: options.cacheSize,
|
||||||
maxAge: options.cacheTTL,
|
maxAge: options.cacheTTL,
|
||||||
dispose(key) {
|
dispose(key) {
|
||||||
debug(`Removed from cache. key=${key}`)
|
debug(`Removed from cache. key=${key}`);
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
// Make the cache Map-like.
|
// Make the cache Map-like.
|
||||||
cache.delete = cache.del
|
cache.delete = cache.del;
|
||||||
cache.clear = cache.reset
|
cache.clear = cache.reset;
|
||||||
|
|
||||||
return new DataLoader(
|
return new DataLoader(
|
||||||
keys => {
|
(keys) => {
|
||||||
return Promise.all(keys.map(key => client.imageInfo(key)))
|
return Promise.allSettled(
|
||||||
|
keys.map((key) => client.imageInfo(key))
|
||||||
|
).then((results) =>
|
||||||
|
results.map((result) => result.reason || result.value)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
{ batch: false, cacheMap: cache }
|
{ batch: false, cacheMap: cache }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,84 +1,80 @@
|
||||||
import URL from 'url'
|
async function resolveMediaWikiImages(source, args, { loaders }) {
|
||||||
|
const isURL = (relation) => relation['target-type'] === 'url';
|
||||||
function resolveMediaWikiImages(source, args, { loaders }) {
|
let rels = source.relations ? source.relations.filter(isURL) : [];
|
||||||
const isURL = relation => relation['target-type'] === 'url'
|
|
||||||
let rels = source.relations ? source.relations.filter(isURL) : []
|
|
||||||
if (!rels.length) {
|
if (!rels.length) {
|
||||||
rels = loaders.lookup
|
rels = await loaders.lookup
|
||||||
.load([source._type, source.id, { inc: 'url-rels' }])
|
.load([source._type, source.id, { inc: 'url-rels' }])
|
||||||
.then(source => source.relations.filter(isURL))
|
.then((source) => source.relations.filter(isURL));
|
||||||
}
|
}
|
||||||
return Promise.resolve(rels).then(rels => {
|
|
||||||
const pages = rels
|
const pages = rels
|
||||||
.filter(rel => {
|
.filter((rel) => {
|
||||||
if (rel.type === args.type) {
|
if (rel.type === args.type) {
|
||||||
const url = URL.parse(rel.url.resource)
|
const url = new URL(rel.url.resource);
|
||||||
if (url.pathname.match(/^\/wiki\/(File|Image):/)) {
|
if (url.pathname.match(/^\/wiki\/(File|Image):/)) {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false;
|
||||||
})
|
|
||||||
.map(rel => rel.url.resource)
|
|
||||||
return loaders.mediaWiki.loadMany(pages)
|
|
||||||
})
|
})
|
||||||
|
.map((rel) => rel.url.resource);
|
||||||
|
return Promise.all(pages.map((page) => loaders.mediaWiki.load(page)));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
MediaWikiImage: {
|
MediaWikiImage: {
|
||||||
descriptionURL: imageInfo => imageInfo.descriptionurl,
|
descriptionURL: (imageInfo) => imageInfo.descriptionurl,
|
||||||
canonicalTitle: imageInfo => imageInfo.canonicaltitle,
|
canonicalTitle: (imageInfo) => imageInfo.canonicaltitle,
|
||||||
objectName: imageInfo => {
|
objectName: (imageInfo) => {
|
||||||
const data = imageInfo.extmetadata.ObjectName
|
const data = imageInfo.extmetadata.ObjectName;
|
||||||
return data ? data.value : null
|
return data ? data.value : null;
|
||||||
},
|
},
|
||||||
descriptionHTML: imageInfo => {
|
descriptionHTML: (imageInfo) => {
|
||||||
const data = imageInfo.extmetadata.ImageDescription
|
const data = imageInfo.extmetadata.ImageDescription;
|
||||||
return data ? data.value : null
|
return data ? data.value : null;
|
||||||
},
|
},
|
||||||
originalDateTimeHTML: imageInfo => {
|
originalDateTimeHTML: (imageInfo) => {
|
||||||
const data = imageInfo.extmetadata.DateTimeOriginal
|
const data = imageInfo.extmetadata.DateTimeOriginal;
|
||||||
return data ? data.value : null
|
return data ? data.value : null;
|
||||||
},
|
},
|
||||||
categories: imageInfo => {
|
categories: (imageInfo) => {
|
||||||
const data = imageInfo.extmetadata.Categories
|
const data = imageInfo.extmetadata.Categories;
|
||||||
return data ? data.value.split('|') : []
|
return data ? data.value.split('|') : [];
|
||||||
},
|
},
|
||||||
artistHTML: imageInfo => {
|
artistHTML: (imageInfo) => {
|
||||||
const data = imageInfo.extmetadata.Artist
|
const data = imageInfo.extmetadata.Artist;
|
||||||
return data ? data.value : null
|
return data ? data.value : null;
|
||||||
},
|
},
|
||||||
creditHTML: imageInfo => {
|
creditHTML: (imageInfo) => {
|
||||||
const data = imageInfo.extmetadata.Credit
|
const data = imageInfo.extmetadata.Credit;
|
||||||
return data ? data.value : null
|
return data ? data.value : null;
|
||||||
},
|
},
|
||||||
licenseShortName: imageInfo => {
|
licenseShortName: (imageInfo) => {
|
||||||
const data = imageInfo.extmetadata.LicenseShortName
|
const data = imageInfo.extmetadata.LicenseShortName;
|
||||||
return data ? data.value : null
|
return data ? data.value : null;
|
||||||
},
|
},
|
||||||
licenseURL: imageInfo => {
|
licenseURL: (imageInfo) => {
|
||||||
const data = imageInfo.extmetadata.LicenseUrl
|
const data = imageInfo.extmetadata.LicenseUrl;
|
||||||
return data ? data.value : null
|
return data ? data.value : null;
|
||||||
},
|
},
|
||||||
metadata: imageInfo =>
|
metadata: (imageInfo) =>
|
||||||
Object.keys(imageInfo.extmetadata).map(key => {
|
Object.keys(imageInfo.extmetadata).map((key) => {
|
||||||
const data = imageInfo.extmetadata[key]
|
const data = imageInfo.extmetadata[key];
|
||||||
return { ...data, name: key }
|
return { ...data, name: key };
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
MediaWikiImageMetadata: {
|
MediaWikiImageMetadata: {
|
||||||
value: obj => (obj.value == null ? obj.value : `${obj.value}`)
|
value: (obj) => (obj.value == null ? obj.value : `${obj.value}`),
|
||||||
},
|
},
|
||||||
Artist: {
|
Artist: {
|
||||||
mediaWikiImages: resolveMediaWikiImages
|
mediaWikiImages: resolveMediaWikiImages,
|
||||||
},
|
},
|
||||||
Instrument: {
|
Instrument: {
|
||||||
mediaWikiImages: resolveMediaWikiImages
|
mediaWikiImages: resolveMediaWikiImages,
|
||||||
},
|
},
|
||||||
Label: {
|
Label: {
|
||||||
mediaWikiImages: resolveMediaWikiImages
|
mediaWikiImages: resolveMediaWikiImages,
|
||||||
},
|
},
|
||||||
Place: {
|
Place: {
|
||||||
mediaWikiImages: resolveMediaWikiImages
|
mediaWikiImages: resolveMediaWikiImages,
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import gql from '../../tag'
|
import gql from '../../tag.js';
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
"""
|
"""
|
||||||
|
|
@ -165,4 +165,4 @@ export default gql`
|
||||||
type: String = "image"
|
type: String = "image"
|
||||||
): [MediaWikiImage]!
|
): [MediaWikiImage]!
|
||||||
}
|
}
|
||||||
`
|
`;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
import Client from '../../api/client'
|
import ExtendableError from 'es6-error';
|
||||||
|
import Client from '../../api/client.js';
|
||||||
|
|
||||||
|
export class TheAudioDBError extends ExtendableError {}
|
||||||
|
|
||||||
export default class TheAudioDBClient extends Client {
|
export default class TheAudioDBClient extends Client {
|
||||||
constructor({
|
constructor({
|
||||||
|
|
@ -9,66 +12,67 @@ export default class TheAudioDBClient extends Client {
|
||||||
period = 1000,
|
period = 1000,
|
||||||
...options
|
...options
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super({ baseURL, limit, period, ...options })
|
super({ baseURL, limit, period, ...options });
|
||||||
this.apiKey = apiKey
|
this.apiKey = apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
get(path, options = {}) {
|
get(path, options = {}) {
|
||||||
const ClientError = this.errorClass
|
|
||||||
if (!this.apiKey) {
|
if (!this.apiKey) {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new ClientError('No API key was configured for TheAudioDB client.')
|
new TheAudioDBError('No API key was configured for TheAudioDB client.')
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
return super.get(`${this.apiKey}/${path}`, {
|
return super.get(`${this.apiKey}/${path}`, {
|
||||||
json: true,
|
resolveBodyOnly: true,
|
||||||
// FIXME: TheAudioDB's SSL terminator seems to be broken and only works
|
...options,
|
||||||
// by forcing TLS 1.0.
|
});
|
||||||
agentOptions: { secureProtocol: 'TLSv1_method' },
|
|
||||||
...options
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entity(entityType, mbid) {
|
entity(entityType, mbid) {
|
||||||
const ClientError = this.errorClass
|
|
||||||
switch (entityType) {
|
switch (entityType) {
|
||||||
case 'artist':
|
case 'artist':
|
||||||
return this.artist(mbid)
|
return this.artist(mbid);
|
||||||
case 'release-group':
|
case 'release-group':
|
||||||
return this.album(mbid)
|
return this.album(mbid);
|
||||||
case 'recording':
|
case 'recording':
|
||||||
return this.track(mbid)
|
return this.track(mbid);
|
||||||
default:
|
default:
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new ClientError(`Entity type unsupported: ${entityType}`)
|
new TheAudioDBError(`Entity type unsupported: ${entityType}`)
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
artist(mbid) {
|
artist(mbid) {
|
||||||
return this.get('artist-mb.php', { qs: { i: mbid } }).then(body => {
|
return this.get('artist-mb.php', { searchParams: { i: mbid } }).then(
|
||||||
|
(body) => {
|
||||||
if (body.artists && body.artists.length === 1) {
|
if (body.artists && body.artists.length === 1) {
|
||||||
return body.artists[0]
|
return body.artists[0];
|
||||||
}
|
}
|
||||||
return null
|
return null;
|
||||||
})
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
album(mbid) {
|
album(mbid) {
|
||||||
return this.get('album-mb.php', { qs: { i: mbid } }).then(body => {
|
return this.get('album-mb.php', { searchParams: { i: mbid } }).then(
|
||||||
|
(body) => {
|
||||||
if (body.album && body.album.length === 1) {
|
if (body.album && body.album.length === 1) {
|
||||||
return body.album[0]
|
return body.album[0];
|
||||||
}
|
}
|
||||||
return null
|
return null;
|
||||||
})
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
track(mbid) {
|
track(mbid) {
|
||||||
return this.get('track-mb.php', { qs: { i: mbid } }).then(body => {
|
return this.get('track-mb.php', { searchParams: { i: mbid } }).then(
|
||||||
|
(body) => {
|
||||||
if (body.track && body.track.length === 1) {
|
if (body.track && body.track.length === 1) {
|
||||||
return body.track[0]
|
return body.track[0];
|
||||||
}
|
}
|
||||||
return null
|
return null;
|
||||||
})
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,37 @@
|
||||||
import schema from './schema'
|
import schema from './schema.js';
|
||||||
import resolvers from './resolvers'
|
import resolvers from './resolvers.js';
|
||||||
import createLoader from './loader'
|
import createLoader from './loader.js';
|
||||||
import TheAudioDBClient from './client'
|
import TheAudioDBClient from './client.js';
|
||||||
import { ONE_DAY } from '../../util'
|
import { ONE_DAY } from '../../util.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TheAudioDB',
|
name: 'TheAudioDB',
|
||||||
description: `Retrieve images and information about artists, releases, and
|
description: `Retrieve images and information about artists, releases, and
|
||||||
recordings from [TheAudioDB.com](http://www.theaudiodb.com/).`,
|
recordings from [TheAudioDB.com](http://www.theaudiodb.com/).`,
|
||||||
extendContext(context, { theAudioDB = {} } = {}) {
|
extendContext(context, { theAudioDB = {} } = {}) {
|
||||||
const client = new TheAudioDBClient(theAudioDB)
|
const client = new TheAudioDBClient(theAudioDB);
|
||||||
const cacheSize = parseInt(
|
const cacheSize = parseInt(
|
||||||
process.env.THEAUDIODB_CACHE_SIZE ||
|
process.env.THEAUDIODB_CACHE_SIZE ||
|
||||||
process.env.GRAPHBRAINZ_CACHE_SIZE ||
|
process.env.GRAPHBRAINZ_CACHE_SIZE ||
|
||||||
8192,
|
8192,
|
||||||
10
|
10
|
||||||
)
|
);
|
||||||
const cacheTTL = parseInt(
|
const cacheTTL = parseInt(
|
||||||
process.env.THEAUDIODB_CACHE_TTL ||
|
process.env.THEAUDIODB_CACHE_TTL ||
|
||||||
process.env.GRAPHBRAINZ_CACHE_TTL ||
|
process.env.GRAPHBRAINZ_CACHE_TTL ||
|
||||||
ONE_DAY,
|
ONE_DAY,
|
||||||
10
|
10
|
||||||
)
|
);
|
||||||
return {
|
return {
|
||||||
...context,
|
...context,
|
||||||
loaders: {
|
loaders: {
|
||||||
...context.loaders,
|
...context.loaders,
|
||||||
theAudioDB: createLoader({ client, cacheSize, cacheTTL })
|
theAudioDB: createLoader({ client, cacheSize, cacheTTL }),
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
extendSchema: {
|
extendSchema: {
|
||||||
schemas: [schema],
|
schemas: [schema],
|
||||||
resolvers
|
resolvers,
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,35 @@
|
||||||
import DataLoader from 'dataloader'
|
import createDebug from 'debug';
|
||||||
import LRUCache from 'lru-cache'
|
import DataLoader from 'dataloader';
|
||||||
|
import LRUCache from 'lru-cache';
|
||||||
|
|
||||||
const debug = require('debug')('graphbrainz:extensions/the-audio-db')
|
const debug = createDebug('graphbrainz:extensions/the-audio-db');
|
||||||
|
|
||||||
export default function createLoader(options) {
|
export default function createLoader(options) {
|
||||||
const { client } = options
|
const { client } = options;
|
||||||
const cache = LRUCache({
|
const cache = new LRUCache({
|
||||||
max: options.cacheSize,
|
max: options.cacheSize,
|
||||||
maxAge: options.cacheTTL,
|
maxAge: options.cacheTTL,
|
||||||
dispose(key) {
|
dispose(key) {
|
||||||
debug(`Removed from cache. key=${key}`)
|
debug(`Removed from cache. key=${key}`);
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
// Make the cache Map-like.
|
// Make the cache Map-like.
|
||||||
cache.delete = cache.del
|
cache.delete = cache.del;
|
||||||
cache.clear = cache.reset
|
cache.clear = cache.reset;
|
||||||
|
|
||||||
return new DataLoader(
|
return new DataLoader(
|
||||||
keys => {
|
(keys) => {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
keys.map(key => {
|
keys.map((key) => {
|
||||||
const [entityType, id] = key
|
const [entityType, id] = key;
|
||||||
return client.entity(entityType, id)
|
return client.entity(entityType, id);
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
batch: false,
|
batch: false,
|
||||||
cacheKeyFn: ([entityType, id]) => `${entityType}/${id}`,
|
cacheKeyFn: ([entityType, id]) => `${entityType}/${id}`,
|
||||||
cacheMap: cache
|
cacheMap: cache,
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,115 +1,118 @@
|
||||||
function handleImageSize(resolver) {
|
function handleImageSize(resolver) {
|
||||||
return (source, args, context, info) => {
|
return (source, args, context, info) => {
|
||||||
const getURL = url => (args.size === 'PREVIEW' ? `${url}/preview` : url)
|
const getURL = (url) => (args.size === 'PREVIEW' ? `${url}/preview` : url);
|
||||||
const url = resolver(source, args, context, info)
|
const url = resolver(source, args, context, info);
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return null
|
return null;
|
||||||
} else if (Array.isArray(url)) {
|
} else if (Array.isArray(url)) {
|
||||||
return url.map(getURL)
|
return url.map(getURL);
|
||||||
} else {
|
} else {
|
||||||
return getURL(url)
|
return getURL(url);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
TheAudioDBArtist: {
|
TheAudioDBArtist: {
|
||||||
artistID: artist => artist.idArtist,
|
artistID: (artist) => artist.idArtist,
|
||||||
biography: (artist, args) => {
|
biography: (artist, args) => {
|
||||||
const lang = args.lang.toUpperCase()
|
const lang = args.lang.toUpperCase();
|
||||||
return artist[`strBiography${lang}`] || null
|
return artist[`strBiography${lang}`] || null;
|
||||||
},
|
},
|
||||||
memberCount: artist => artist.intMembers,
|
memberCount: (artist) => artist.intMembers,
|
||||||
banner: handleImageSize(artist => artist.strArtistBanner),
|
banner: handleImageSize((artist) => artist.strArtistBanner),
|
||||||
fanArt: handleImageSize(artist => {
|
fanArt: handleImageSize((artist) => {
|
||||||
return [
|
return [
|
||||||
artist.strArtistFanart,
|
artist.strArtistFanart,
|
||||||
artist.strArtistFanart2,
|
artist.strArtistFanart2,
|
||||||
artist.strArtistFanart3
|
artist.strArtistFanart3,
|
||||||
].filter(Boolean)
|
].filter(Boolean);
|
||||||
}),
|
}),
|
||||||
logo: handleImageSize(artist => artist.strArtistLogo),
|
logo: handleImageSize((artist) => artist.strArtistLogo),
|
||||||
thumbnail: handleImageSize(artist => artist.strArtistThumb),
|
thumbnail: handleImageSize((artist) => artist.strArtistThumb),
|
||||||
genre: artist => artist.strGenre || null,
|
genre: (artist) => artist.strGenre || null,
|
||||||
mood: artist => artist.strMood || null,
|
mood: (artist) => artist.strMood || null,
|
||||||
style: artist => artist.strStyle || null
|
style: (artist) => artist.strStyle || null,
|
||||||
},
|
},
|
||||||
TheAudioDBAlbum: {
|
TheAudioDBAlbum: {
|
||||||
albumID: album => album.idAlbum,
|
albumID: (album) => album.idAlbum,
|
||||||
artistID: album => album.idArtist,
|
artistID: (album) => album.idArtist,
|
||||||
description: (album, args) => {
|
description: (album, args) => {
|
||||||
const lang = args.lang.toUpperCase()
|
const lang = args.lang.toUpperCase();
|
||||||
return album[`strDescription${lang}`] || null
|
return album[`strDescription${lang}`] || null;
|
||||||
},
|
},
|
||||||
salesCount: album => album.intSales,
|
salesCount: (album) => album.intSales,
|
||||||
score: album => album.intScore,
|
score: (album) => album.intScore,
|
||||||
scoreVotes: album => album.intScoreVotes,
|
scoreVotes: (album) => album.intScoreVotes,
|
||||||
discImage: handleImageSize(album => album.strAlbumCDart),
|
discImage: handleImageSize((album) => album.strAlbumCDart),
|
||||||
spineImage: handleImageSize(album => album.strAlbumSpine),
|
spineImage: handleImageSize((album) => album.strAlbumSpine),
|
||||||
frontImage: handleImageSize(album => album.strAlbumThumb),
|
frontImage: handleImageSize((album) => album.strAlbumThumb),
|
||||||
backImage: handleImageSize(album => album.strAlbumThumbBack),
|
backImage: handleImageSize((album) => album.strAlbumThumbBack),
|
||||||
review: album => album.strReview || null,
|
review: (album) => album.strReview || null,
|
||||||
genre: album => album.strGenre || null,
|
genre: (album) => album.strGenre || null,
|
||||||
mood: album => album.strMood || null,
|
mood: (album) => album.strMood || null,
|
||||||
style: album => album.strStyle || null,
|
style: (album) => album.strStyle || null,
|
||||||
speed: album => album.strSpeed || null,
|
speed: (album) => album.strSpeed || null,
|
||||||
theme: album => album.strTheme || null
|
theme: (album) => album.strTheme || null,
|
||||||
},
|
},
|
||||||
TheAudioDBTrack: {
|
TheAudioDBTrack: {
|
||||||
trackID: track => track.idTrack,
|
trackID: (track) => track.idTrack,
|
||||||
albumID: track => track.idAlbum,
|
albumID: (track) => track.idAlbum,
|
||||||
artistID: track => track.idArtist,
|
artistID: (track) => track.idArtist,
|
||||||
description: (track, args) => {
|
description: (track, args) => {
|
||||||
const lang = args.lang.toUpperCase()
|
const lang = args.lang.toUpperCase();
|
||||||
return track[`strDescription${lang}`] || null
|
return track[`strDescription${lang}`] || null;
|
||||||
},
|
},
|
||||||
thumbnail: handleImageSize(track => track.strTrackThumb),
|
thumbnail: handleImageSize((track) => track.strTrackThumb),
|
||||||
score: track => track.intScore,
|
score: (track) => track.intScore,
|
||||||
scoreVotes: track => track.intScoreVotes,
|
scoreVotes: (track) => track.intScoreVotes,
|
||||||
trackNumber: track => track.intTrackNumber,
|
trackNumber: (track) => track.intTrackNumber,
|
||||||
musicVideo: track => track,
|
musicVideo: (track) => track,
|
||||||
genre: track => track.strGenre || null,
|
genre: (track) => track.strGenre || null,
|
||||||
mood: track => track.strMood || null,
|
mood: (track) => track.strMood || null,
|
||||||
style: track => track.strStyle || null,
|
style: (track) => track.strStyle || null,
|
||||||
theme: track => track.strTheme || null
|
theme: (track) => track.strTheme || null,
|
||||||
},
|
},
|
||||||
TheAudioDBMusicVideo: {
|
TheAudioDBMusicVideo: {
|
||||||
url: track => {
|
url: (track) => {
|
||||||
let url = track.strMusicVid || null
|
let url = track.strMusicVid || null;
|
||||||
// Many of these are missing the protocol and start with www, so add it
|
// Many of these are missing the protocol and start with www, so add it
|
||||||
// in that case.
|
// in that case.
|
||||||
if (url && url.startsWith('www.')) {
|
if (url && url.startsWith('www.')) {
|
||||||
url = `https://${url}`
|
url = `https://${url}`;
|
||||||
}
|
}
|
||||||
return url
|
return url;
|
||||||
},
|
},
|
||||||
companyName: track => track.strMusicVidCompany || null,
|
companyName: (track) => track.strMusicVidCompany || null,
|
||||||
directorName: track => track.strMusicVidDirector || null,
|
directorName: (track) => track.strMusicVidDirector || null,
|
||||||
screenshots: handleImageSize(track => {
|
screenshots: handleImageSize((track) => {
|
||||||
return [
|
return [
|
||||||
track.strMusicVidScreen1,
|
track.strMusicVidScreen1,
|
||||||
track.strMusicVidScreen2,
|
track.strMusicVidScreen2,
|
||||||
track.strMusicVidScreen3
|
track.strMusicVidScreen3,
|
||||||
].filter(Boolean)
|
].filter(Boolean);
|
||||||
}),
|
}),
|
||||||
viewCount: track => track.intMusicVidViews,
|
viewCount: (track) => track.intMusicVidViews,
|
||||||
likeCount: track => track.intMusicVidLikes,
|
likeCount: (track) => track.intMusicVidLikes,
|
||||||
dislikeCount: track => track.intMusicVidDislikes,
|
dislikeCount: (track) => track.intMusicVidDislikes,
|
||||||
commentCount: track => track.intMusicVidComments
|
commentCount: (track) => track.intMusicVidComments,
|
||||||
},
|
},
|
||||||
Artist: {
|
Artist: {
|
||||||
theAudioDB: (artist, args, context) => {
|
theAudioDB: (artist, args, context) => {
|
||||||
return context.loaders.theAudioDB.load(['artist', artist.id])
|
return context.loaders.theAudioDB.load(['artist', artist.id]);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
Recording: {
|
Recording: {
|
||||||
theAudioDB: (recording, args, context) => {
|
theAudioDB: (recording, args, context) => {
|
||||||
return context.loaders.theAudioDB.load(['recording', recording.id])
|
return context.loaders.theAudioDB.load(['recording', recording.id]);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
ReleaseGroup: {
|
ReleaseGroup: {
|
||||||
theAudioDB: (releaseGroup, args, context) => {
|
theAudioDB: (releaseGroup, args, context) => {
|
||||||
return context.loaders.theAudioDB.load(['release-group', releaseGroup.id])
|
return context.loaders.theAudioDB.load([
|
||||||
}
|
'release-group',
|
||||||
}
|
releaseGroup.id,
|
||||||
}
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import gql from '../../tag'
|
import gql from '../../tag.js';
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
"""
|
"""
|
||||||
|
|
@ -369,4 +369,4 @@ export default gql`
|
||||||
"""
|
"""
|
||||||
theAudioDB: TheAudioDBAlbum
|
theAudioDB: TheAudioDBAlbum
|
||||||
}
|
}
|
||||||
`
|
`;
|
||||||
|
|
|
||||||
127
src/index.js
127
src/index.js
|
|
@ -1,71 +1,104 @@
|
||||||
import express from 'express'
|
import express from 'express';
|
||||||
import graphqlHTTP from 'express-graphql'
|
import ExpressGraphQL from 'express-graphql';
|
||||||
import compression from 'compression'
|
import compression from 'compression';
|
||||||
import cors from 'cors'
|
import cors from 'cors';
|
||||||
import MusicBrainz from './api'
|
import MusicBrainz from './api/index.js';
|
||||||
import schema, { createSchema } from './schema'
|
import Client from './api/client.js';
|
||||||
import { createContext } from './context'
|
import { baseSchema, createSchema } from './schema.js';
|
||||||
|
import { createContext } from './context.js';
|
||||||
|
import { loadExtension } from './extensions/index.js';
|
||||||
|
import gql from './tag.js';
|
||||||
|
|
||||||
const formatError = err => ({
|
const { graphqlHTTP } = ExpressGraphQL;
|
||||||
|
|
||||||
|
const formatError = (err) => ({
|
||||||
message: err.message,
|
message: err.message,
|
||||||
locations: err.locations,
|
locations: err.locations,
|
||||||
stack: err.stack
|
stack: err.stack,
|
||||||
})
|
});
|
||||||
|
|
||||||
export const defaultExtensions = [
|
const defaultExtensions = [
|
||||||
require.resolve('./extensions/cover-art-archive'),
|
'graphbrainz/extensions/cover-art-archive',
|
||||||
require.resolve('./extensions/fanart-tv'),
|
'graphbrainz/extensions/fanart-tv',
|
||||||
require.resolve('./extensions/mediawiki'),
|
'graphbrainz/extensions/mediawiki',
|
||||||
require.resolve('./extensions/the-audio-db')
|
'graphbrainz/extensions/the-audio-db',
|
||||||
]
|
];
|
||||||
|
|
||||||
const middleware = ({
|
function middleware({
|
||||||
client = new MusicBrainz(),
|
client = new MusicBrainz(),
|
||||||
extensions = process.env.GRAPHBRAINZ_EXTENSIONS
|
extensions = process.env.GRAPHBRAINZ_EXTENSIONS
|
||||||
? JSON.parse(process.env.GRAPHBRAINZ_EXTENSIONS)
|
? JSON.parse(process.env.GRAPHBRAINZ_EXTENSIONS)
|
||||||
: defaultExtensions,
|
: defaultExtensions,
|
||||||
...middlewareOptions
|
...middlewareOptions
|
||||||
} = {}) => {
|
} = {}) {
|
||||||
const options = { client, extensions, ...middlewareOptions }
|
const DEV = process.env.NODE_ENV !== 'production';
|
||||||
const DEV = process.env.NODE_ENV !== 'production'
|
const graphiql = DEV || process.env.GRAPHBRAINZ_GRAPHIQL === 'true';
|
||||||
const graphiql = DEV || process.env.GRAPHBRAINZ_GRAPHIQL === 'true'
|
const getAsyncMiddleware = async () => {
|
||||||
|
const loadedExtensions = await Promise.all(
|
||||||
|
extensions.map((extensionSpecifier) => loadExtension(extensionSpecifier))
|
||||||
|
);
|
||||||
|
const options = {
|
||||||
|
client,
|
||||||
|
extensions: loadedExtensions,
|
||||||
|
...middlewareOptions,
|
||||||
|
};
|
||||||
|
const schema = createSchema(baseSchema, options);
|
||||||
|
const context = createContext(options);
|
||||||
return graphqlHTTP({
|
return graphqlHTTP({
|
||||||
schema: createSchema(schema, options),
|
schema,
|
||||||
context: createContext(options),
|
context,
|
||||||
pretty: DEV,
|
pretty: DEV,
|
||||||
graphiql,
|
graphiql,
|
||||||
formatError: DEV ? formatError : undefined,
|
customFormatErrorFn: DEV ? formatError : undefined,
|
||||||
...middlewareOptions
|
...middlewareOptions,
|
||||||
})
|
});
|
||||||
|
};
|
||||||
|
const asyncMiddleware = getAsyncMiddleware();
|
||||||
|
return async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const middleware = await asyncMiddleware;
|
||||||
|
middleware(req, res, next);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default middleware
|
async function start(options) {
|
||||||
|
const dotenv = await import('dotenv');
|
||||||
export function start(options) {
|
dotenv.config({ silent: true });
|
||||||
require('dotenv').config({ silent: true })
|
const app = express();
|
||||||
const app = express()
|
const port = process.env.PORT || 3000;
|
||||||
const port = process.env.PORT || 3000
|
const route = process.env.GRAPHBRAINZ_PATH || '/';
|
||||||
const route = process.env.GRAPHBRAINZ_PATH || '/'
|
|
||||||
const corsOptions = {
|
const corsOptions = {
|
||||||
origin: process.env.GRAPHBRAINZ_CORS_ORIGIN || false,
|
origin: process.env.GRAPHBRAINZ_CORS_ORIGIN || false,
|
||||||
methods: 'HEAD,GET,POST'
|
methods: 'HEAD,GET,POST',
|
||||||
}
|
};
|
||||||
switch (corsOptions.origin) {
|
switch (corsOptions.origin) {
|
||||||
case 'true':
|
case 'true':
|
||||||
corsOptions.origin = true
|
corsOptions.origin = true;
|
||||||
break
|
break;
|
||||||
case 'false':
|
case 'false':
|
||||||
corsOptions.origin = false
|
corsOptions.origin = false;
|
||||||
break
|
break;
|
||||||
default:
|
default:
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
app.use(compression())
|
app.use(compression());
|
||||||
app.use(route, cors(corsOptions), middleware(options))
|
app.use(route, cors(corsOptions), middleware(options));
|
||||||
app.listen(port)
|
app.listen(port);
|
||||||
console.log(`Listening on port ${port}.`)
|
console.log(`Listening on port ${port}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) {
|
export {
|
||||||
start()
|
Client,
|
||||||
}
|
MusicBrainz,
|
||||||
|
gql,
|
||||||
|
baseSchema,
|
||||||
|
createContext,
|
||||||
|
createSchema,
|
||||||
|
defaultExtensions,
|
||||||
|
loadExtension,
|
||||||
|
middleware,
|
||||||
|
start,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,92 +1,92 @@
|
||||||
import DataLoader from 'dataloader'
|
import createDebug from 'debug';
|
||||||
import LRUCache from 'lru-cache'
|
import DataLoader from 'dataloader';
|
||||||
import { toPlural } from './types/helpers'
|
import LRUCache from 'lru-cache';
|
||||||
import { ONE_DAY } from './util'
|
import { ONE_DAY, toPlural } from './util.js';
|
||||||
|
|
||||||
const debug = require('debug')('graphbrainz:loaders')
|
const debug = createDebug('graphbrainz:loaders');
|
||||||
|
|
||||||
export default function createLoaders(client) {
|
export default function createLoaders(client) {
|
||||||
// All loaders share a single LRU cache that will remember 8192 responses,
|
// All loaders share a single LRU cache that will remember 8192 responses,
|
||||||
// each cached for 1 day.
|
// each cached for 1 day.
|
||||||
const cache = LRUCache({
|
const cache = new LRUCache({
|
||||||
max: parseInt(process.env.GRAPHBRAINZ_CACHE_SIZE || 8192, 10),
|
max: parseInt(process.env.GRAPHBRAINZ_CACHE_SIZE || 8192, 10),
|
||||||
maxAge: parseInt(process.env.GRAPHBRAINZ_CACHE_TTL || ONE_DAY, 10),
|
maxAge: parseInt(process.env.GRAPHBRAINZ_CACHE_TTL || ONE_DAY, 10),
|
||||||
dispose(key) {
|
dispose(key) {
|
||||||
debug(`Removed from cache. key=${key}`)
|
debug(`Removed from cache. key=${key}`);
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
// Make the cache Map-like.
|
// Make the cache Map-like.
|
||||||
cache.delete = cache.del
|
cache.delete = cache.del;
|
||||||
cache.clear = cache.reset
|
cache.clear = cache.reset;
|
||||||
|
|
||||||
const lookup = new DataLoader(
|
const lookup = new DataLoader(
|
||||||
keys => {
|
(keys) => {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
keys.map(key => {
|
keys.map((key) => {
|
||||||
const [entityType, id, params = {}] = key
|
const [entityType, id, params = {}] = key;
|
||||||
return client.lookup(entityType, id, params).then(entity => {
|
return client.lookup(entityType, id, params).then((entity) => {
|
||||||
if (entity) {
|
if (entity) {
|
||||||
// Store the entity type so we can determine what type of object this
|
// Store the entity type so we can determine what type of object this
|
||||||
// is elsewhere in the code.
|
// is elsewhere in the code.
|
||||||
entity._type = entityType
|
entity._type = entityType;
|
||||||
}
|
}
|
||||||
return entity
|
return entity;
|
||||||
|
});
|
||||||
})
|
})
|
||||||
})
|
);
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
batch: false,
|
batch: false,
|
||||||
cacheKeyFn: key => client.getLookupURL(...key),
|
cacheKeyFn: (key) => client.getLookupURL(...key),
|
||||||
cacheMap: cache
|
cacheMap: cache,
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
const browse = new DataLoader(
|
const browse = new DataLoader(
|
||||||
keys => {
|
(keys) => {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
keys.map(key => {
|
keys.map((key) => {
|
||||||
const [entityType, params = {}] = key
|
const [entityType, params = {}] = key;
|
||||||
return client.browse(entityType, params).then(list => {
|
return client.browse(entityType, params).then((list) => {
|
||||||
list[toPlural(entityType)].forEach(entity => {
|
list[toPlural(entityType)].forEach((entity) => {
|
||||||
// Store the entity type so we can determine what type of object this
|
// Store the entity type so we can determine what type of object this
|
||||||
// is elsewhere in the code.
|
// is elsewhere in the code.
|
||||||
entity._type = entityType
|
entity._type = entityType;
|
||||||
|
});
|
||||||
|
return list;
|
||||||
|
});
|
||||||
})
|
})
|
||||||
return list
|
);
|
||||||
})
|
|
||||||
})
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
batch: false,
|
batch: false,
|
||||||
cacheKeyFn: key => client.getBrowseURL(...key),
|
cacheKeyFn: (key) => client.getBrowseURL(...key),
|
||||||
cacheMap: cache
|
cacheMap: cache,
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
const search = new DataLoader(
|
const search = new DataLoader(
|
||||||
keys => {
|
(keys) => {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
keys.map(key => {
|
keys.map((key) => {
|
||||||
const [entityType, query, params = {}] = key
|
const [entityType, query, params = {}] = key;
|
||||||
return client.search(entityType, query, params).then(list => {
|
return client.search(entityType, query, params).then((list) => {
|
||||||
list[toPlural(entityType)].forEach(entity => {
|
list[toPlural(entityType)].forEach((entity) => {
|
||||||
// Store the entity type so we can determine what type of object this
|
// Store the entity type so we can determine what type of object this
|
||||||
// is elsewhere in the code.
|
// is elsewhere in the code.
|
||||||
entity._type = entityType
|
entity._type = entityType;
|
||||||
|
});
|
||||||
|
return list;
|
||||||
|
});
|
||||||
})
|
})
|
||||||
return list
|
);
|
||||||
})
|
|
||||||
})
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
batch: false,
|
batch: false,
|
||||||
cacheKeyFn: key => client.getSearchURL(...key),
|
cacheKeyFn: (key) => client.getSearchURL(...key),
|
||||||
cacheMap: cache
|
cacheMap: cache,
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
return { lookup, browse, search }
|
return { lookup, browse, search };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { GraphQLObjectType, GraphQLString } from 'graphql'
|
import GraphQL from 'graphql';
|
||||||
import { forwardConnectionArgs } from 'graphql-relay'
|
import GraphQLRelay from 'graphql-relay';
|
||||||
import { resolveBrowse } from '../resolvers'
|
import { resolveBrowse } from '../resolvers.js';
|
||||||
import {
|
import {
|
||||||
MBID,
|
MBID,
|
||||||
AreaConnection,
|
AreaConnection,
|
||||||
|
|
@ -15,62 +15,66 @@ import {
|
||||||
RecordingConnection,
|
RecordingConnection,
|
||||||
ReleaseConnection,
|
ReleaseConnection,
|
||||||
ReleaseGroupConnection,
|
ReleaseGroupConnection,
|
||||||
WorkConnection
|
WorkConnection,
|
||||||
} from '../types'
|
} from '../types/index.js';
|
||||||
import { toWords, releaseGroupType, releaseStatus } from '../types/helpers'
|
import { releaseGroupType, releaseStatus } from '../types/helpers.js';
|
||||||
|
import { toWords } from '../util.js';
|
||||||
|
|
||||||
|
const { GraphQLObjectType, GraphQLString } = GraphQL;
|
||||||
|
const { forwardConnectionArgs } = GraphQLRelay;
|
||||||
|
|
||||||
const area = {
|
const area = {
|
||||||
type: MBID,
|
type: MBID,
|
||||||
description: 'The MBID of an area to which the entity is linked.'
|
description: 'The MBID of an area to which the entity is linked.',
|
||||||
}
|
};
|
||||||
const artist = {
|
const artist = {
|
||||||
type: MBID,
|
type: MBID,
|
||||||
description: 'The MBID of an artist to which the entity is linked.'
|
description: 'The MBID of an artist to which the entity is linked.',
|
||||||
}
|
};
|
||||||
const collection = {
|
const collection = {
|
||||||
type: MBID,
|
type: MBID,
|
||||||
description: 'The MBID of a collection in which the entity is found.'
|
description: 'The MBID of a collection in which the entity is found.',
|
||||||
}
|
};
|
||||||
const event = {
|
const event = {
|
||||||
type: MBID,
|
type: MBID,
|
||||||
description: 'The MBID of an event to which the entity is linked.'
|
description: 'The MBID of an event to which the entity is linked.',
|
||||||
}
|
};
|
||||||
const label = {
|
const label = {
|
||||||
type: MBID,
|
type: MBID,
|
||||||
description: 'The MBID of a label to which the entity is linked.'
|
description: 'The MBID of a label to which the entity is linked.',
|
||||||
}
|
};
|
||||||
const place = {
|
const place = {
|
||||||
type: MBID,
|
type: MBID,
|
||||||
description: 'The MBID of a place to which the entity is linked.'
|
description: 'The MBID of a place to which the entity is linked.',
|
||||||
}
|
};
|
||||||
const recording = {
|
const recording = {
|
||||||
type: MBID,
|
type: MBID,
|
||||||
description: 'The MBID of a recording to which the entity is linked.'
|
description: 'The MBID of a recording to which the entity is linked.',
|
||||||
}
|
};
|
||||||
const release = {
|
const release = {
|
||||||
type: MBID,
|
type: MBID,
|
||||||
description: 'The MBID of a release to which the entity is linked.'
|
description: 'The MBID of a release to which the entity is linked.',
|
||||||
}
|
};
|
||||||
const releaseGroup = {
|
const releaseGroup = {
|
||||||
type: MBID,
|
type: MBID,
|
||||||
description: 'The MBID of a release group to which the entity is linked.'
|
description: 'The MBID of a release group to which the entity is linked.',
|
||||||
}
|
};
|
||||||
const work = {
|
const work = {
|
||||||
type: MBID,
|
type: MBID,
|
||||||
description: 'The MBID of a work to which the entity is linked.'
|
description: 'The MBID of a work to which the entity is linked.',
|
||||||
}
|
};
|
||||||
|
|
||||||
function createBrowseField(connectionType, args) {
|
function createBrowseField(connectionType, args) {
|
||||||
const typeName = toWords(connectionType.name.slice(0, -10))
|
const typeName = toWords(connectionType.name.slice(0, -10));
|
||||||
return {
|
return {
|
||||||
type: connectionType,
|
type: connectionType,
|
||||||
description: `Browse ${typeName} entities linked to the given arguments.`,
|
description: `Browse ${typeName} entities linked to the given arguments.`,
|
||||||
args: {
|
args: {
|
||||||
...args,
|
...args,
|
||||||
...forwardConnectionArgs
|
...forwardConnectionArgs,
|
||||||
},
|
},
|
||||||
resolve: resolveBrowse
|
resolve: resolveBrowse,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BrowseQuery = new GraphQLObjectType({
|
export const BrowseQuery = new GraphQLObjectType({
|
||||||
|
|
@ -79,7 +83,7 @@ export const BrowseQuery = new GraphQLObjectType({
|
||||||
entity.`,
|
entity.`,
|
||||||
fields: {
|
fields: {
|
||||||
areas: createBrowseField(AreaConnection, {
|
areas: createBrowseField(AreaConnection, {
|
||||||
collection
|
collection,
|
||||||
}),
|
}),
|
||||||
artists: createBrowseField(ArtistConnection, {
|
artists: createBrowseField(ArtistConnection, {
|
||||||
area,
|
area,
|
||||||
|
|
@ -87,14 +91,14 @@ entity.`,
|
||||||
recording,
|
recording,
|
||||||
release,
|
release,
|
||||||
releaseGroup,
|
releaseGroup,
|
||||||
work
|
work,
|
||||||
}),
|
}),
|
||||||
collections: createBrowseField(CollectionConnection, {
|
collections: createBrowseField(CollectionConnection, {
|
||||||
area,
|
area,
|
||||||
artist,
|
artist,
|
||||||
editor: {
|
editor: {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: 'The username of the editor who created the collection.'
|
description: 'The username of the editor who created the collection.',
|
||||||
},
|
},
|
||||||
event,
|
event,
|
||||||
label,
|
label,
|
||||||
|
|
@ -102,22 +106,22 @@ entity.`,
|
||||||
recording,
|
recording,
|
||||||
release,
|
release,
|
||||||
releaseGroup,
|
releaseGroup,
|
||||||
work
|
work,
|
||||||
}),
|
}),
|
||||||
events: createBrowseField(EventConnection, {
|
events: createBrowseField(EventConnection, {
|
||||||
area,
|
area,
|
||||||
artist,
|
artist,
|
||||||
collection,
|
collection,
|
||||||
place
|
place,
|
||||||
}),
|
}),
|
||||||
labels: createBrowseField(LabelConnection, {
|
labels: createBrowseField(LabelConnection, {
|
||||||
area,
|
area,
|
||||||
collection,
|
collection,
|
||||||
release
|
release,
|
||||||
}),
|
}),
|
||||||
places: createBrowseField(PlaceConnection, {
|
places: createBrowseField(PlaceConnection, {
|
||||||
area,
|
area,
|
||||||
collection
|
collection,
|
||||||
}),
|
}),
|
||||||
recordings: createBrowseField(RecordingConnection, {
|
recordings: createBrowseField(RecordingConnection, {
|
||||||
artist,
|
artist,
|
||||||
|
|
@ -125,9 +129,9 @@ entity.`,
|
||||||
isrc: {
|
isrc: {
|
||||||
type: ISRC,
|
type: ISRC,
|
||||||
description: `The [International Standard Recording Code](https://musicbrainz.org/doc/ISRC)
|
description: `The [International Standard Recording Code](https://musicbrainz.org/doc/ISRC)
|
||||||
(ISRC) of the recording.`
|
(ISRC) of the recording.`,
|
||||||
},
|
},
|
||||||
release
|
release,
|
||||||
}),
|
}),
|
||||||
releases: createBrowseField(ReleaseConnection, {
|
releases: createBrowseField(ReleaseConnection, {
|
||||||
area,
|
area,
|
||||||
|
|
@ -136,28 +140,28 @@ entity.`,
|
||||||
discID: {
|
discID: {
|
||||||
type: DiscID,
|
type: DiscID,
|
||||||
description: `A [disc ID](https://musicbrainz.org/doc/Disc_ID)
|
description: `A [disc ID](https://musicbrainz.org/doc/Disc_ID)
|
||||||
associated with the release.`
|
associated with the release.`,
|
||||||
},
|
},
|
||||||
label,
|
label,
|
||||||
recording,
|
recording,
|
||||||
releaseGroup,
|
releaseGroup,
|
||||||
track: {
|
track: {
|
||||||
type: MBID,
|
type: MBID,
|
||||||
description: 'The MBID of a track that is included in the release.'
|
description: 'The MBID of a track that is included in the release.',
|
||||||
},
|
},
|
||||||
trackArtist: {
|
trackArtist: {
|
||||||
type: MBID,
|
type: MBID,
|
||||||
description: `The MBID of an artist that appears on a track in the
|
description: `The MBID of an artist that appears on a track in the
|
||||||
release, but is not included in the credits for the release itself.`
|
release, but is not included in the credits for the release itself.`,
|
||||||
},
|
},
|
||||||
type: releaseGroupType,
|
type: releaseGroupType,
|
||||||
status: releaseStatus
|
status: releaseStatus,
|
||||||
}),
|
}),
|
||||||
releaseGroups: createBrowseField(ReleaseGroupConnection, {
|
releaseGroups: createBrowseField(ReleaseGroupConnection, {
|
||||||
artist,
|
artist,
|
||||||
collection,
|
collection,
|
||||||
release,
|
release,
|
||||||
type: releaseGroupType
|
type: releaseGroupType,
|
||||||
}),
|
}),
|
||||||
works: createBrowseField(WorkConnection, {
|
works: createBrowseField(WorkConnection, {
|
||||||
artist,
|
artist,
|
||||||
|
|
@ -165,11 +169,11 @@ release, but is not included in the credits for the release itself.`
|
||||||
iswc: {
|
iswc: {
|
||||||
type: ISWC,
|
type: ISWC,
|
||||||
description: `The [International Standard Musical Work Code](https://musicbrainz.org/doc/ISWC)
|
description: `The [International Standard Musical Work Code](https://musicbrainz.org/doc/ISWC)
|
||||||
(ISWC) of the work.`
|
(ISWC) of the work.`,
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
export const browse = {
|
export const browse = {
|
||||||
type: BrowseQuery,
|
type: BrowseQuery,
|
||||||
|
|
@ -177,7 +181,7 @@ export const browse = {
|
||||||
'Browse all MusicBrainz entities directly linked to another entity.',
|
'Browse all MusicBrainz entities directly linked to another entity.',
|
||||||
// We only have work to do once we know what entity types are being requested,
|
// We only have work to do once we know what entity types are being requested,
|
||||||
// so this can just resolve to an empty object.
|
// so this can just resolve to an empty object.
|
||||||
resolve: () => ({})
|
resolve: () => ({}),
|
||||||
}
|
};
|
||||||
|
|
||||||
export default BrowseQuery
|
export default BrowseQuery;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
export { LookupQuery, lookup } from './lookup'
|
export { LookupQuery, lookup } from './lookup.js';
|
||||||
export { BrowseQuery, browse } from './browse'
|
export { BrowseQuery, browse } from './browse.js';
|
||||||
export { SearchQuery, search } from './search'
|
export { SearchQuery, search } from './search.js';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { GraphQLObjectType, GraphQLNonNull } from 'graphql'
|
import GraphQL from 'graphql';
|
||||||
import { resolveLookup } from '../resolvers'
|
import { resolveLookup } from '../resolvers.js';
|
||||||
import { mbid, toWords } from '../types/helpers'
|
import { mbid } from '../types/helpers.js';
|
||||||
import {
|
import {
|
||||||
Area,
|
Area,
|
||||||
Artist,
|
Artist,
|
||||||
|
|
@ -18,17 +18,20 @@ import {
|
||||||
Series,
|
Series,
|
||||||
URL,
|
URL,
|
||||||
URLString,
|
URLString,
|
||||||
Work
|
Work,
|
||||||
} from '../types'
|
} from '../types/index.js';
|
||||||
|
import { toWords } from '../util.js';
|
||||||
|
|
||||||
|
const { GraphQLObjectType, GraphQLNonNull } = GraphQL;
|
||||||
|
|
||||||
function createLookupField(entity, args) {
|
function createLookupField(entity, args) {
|
||||||
const typeName = toWords(entity.name)
|
const typeName = toWords(entity.name);
|
||||||
return {
|
return {
|
||||||
type: entity,
|
type: entity,
|
||||||
description: `Look up a specific ${typeName} by its MBID.`,
|
description: `Look up a specific ${typeName} by its MBID.`,
|
||||||
args: { mbid, ...args },
|
args: { mbid, ...args },
|
||||||
resolve: resolveLookup
|
resolve: resolveLookup,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LookupQuery = new GraphQLObjectType({
|
export const LookupQuery = new GraphQLObjectType({
|
||||||
|
|
@ -45,12 +48,12 @@ export const LookupQuery = new GraphQLObjectType({
|
||||||
discID: {
|
discID: {
|
||||||
type: new GraphQLNonNull(DiscID),
|
type: new GraphQLNonNull(DiscID),
|
||||||
description: `The [disc ID](https://musicbrainz.org/doc/Disc_ID)
|
description: `The [disc ID](https://musicbrainz.org/doc/Disc_ID)
|
||||||
of the disc.`
|
of the disc.`,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
resolve: (root, { discID }, { loaders }, info) => {
|
resolve: (root, { discID }, { loaders }, info) => {
|
||||||
return loaders.lookup.load(['discid', discID])
|
return loaders.lookup.load(['discid', discID]);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
event: createLookupField(Event),
|
event: createLookupField(Event),
|
||||||
instrument: createLookupField(Instrument),
|
instrument: createLookupField(Instrument),
|
||||||
|
|
@ -65,23 +68,23 @@ of the disc.`
|
||||||
...mbid,
|
...mbid,
|
||||||
// Remove the non-null requirement that is usually on the `mbid` field
|
// Remove the non-null requirement that is usually on the `mbid` field
|
||||||
// so that URLs can be looked up by `resource`.
|
// so that URLs can be looked up by `resource`.
|
||||||
type: MBID
|
type: MBID,
|
||||||
},
|
},
|
||||||
resource: {
|
resource: {
|
||||||
type: URLString,
|
type: URLString,
|
||||||
description: 'The web address of the URL entity to look up.'
|
description: 'The web address of the URL entity to look up.',
|
||||||
}
|
},
|
||||||
}),
|
}),
|
||||||
work: createLookupField(Work)
|
work: createLookupField(Work),
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
export const lookup = {
|
export const lookup = {
|
||||||
type: LookupQuery,
|
type: LookupQuery,
|
||||||
description: 'Perform a lookup of a MusicBrainz entity by its MBID.',
|
description: 'Perform a lookup of a MusicBrainz entity by its MBID.',
|
||||||
// We only have work to do once we know what entity types are being requested,
|
// We only have work to do once we know what entity types are being requested,
|
||||||
// so this can just resolve to an empty object.
|
// so this can just resolve to an empty object.
|
||||||
resolve: () => ({})
|
resolve: () => ({}),
|
||||||
}
|
};
|
||||||
|
|
||||||
export default LookupQuery
|
export default LookupQuery;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { GraphQLObjectType, GraphQLNonNull, GraphQLString } from 'graphql'
|
import GraphQL from 'graphql';
|
||||||
import { forwardConnectionArgs } from 'graphql-relay'
|
import GraphQLRelay from 'graphql-relay';
|
||||||
import { resolveSearch } from '../resolvers'
|
import { resolveSearch } from '../resolvers.js';
|
||||||
import {
|
import {
|
||||||
AreaConnection,
|
AreaConnection,
|
||||||
ArtistConnection,
|
ArtistConnection,
|
||||||
|
|
@ -12,12 +12,15 @@ import {
|
||||||
ReleaseConnection,
|
ReleaseConnection,
|
||||||
ReleaseGroupConnection,
|
ReleaseGroupConnection,
|
||||||
SeriesConnection,
|
SeriesConnection,
|
||||||
WorkConnection
|
WorkConnection,
|
||||||
} from '../types'
|
} from '../types/index.js';
|
||||||
import { toWords } from '../types/helpers'
|
import { toWords } from '../util.js';
|
||||||
|
|
||||||
|
const { GraphQLObjectType, GraphQLNonNull, GraphQLString } = GraphQL;
|
||||||
|
const { forwardConnectionArgs } = GraphQLRelay;
|
||||||
|
|
||||||
function createSearchField(connectionType) {
|
function createSearchField(connectionType) {
|
||||||
const typeName = toWords(connectionType.name.slice(0, -10))
|
const typeName = toWords(connectionType.name.slice(0, -10));
|
||||||
return {
|
return {
|
||||||
type: connectionType,
|
type: connectionType,
|
||||||
description: `Search for ${typeName} entities matching the given query.`,
|
description: `Search for ${typeName} entities matching the given query.`,
|
||||||
|
|
@ -25,12 +28,12 @@ function createSearchField(connectionType) {
|
||||||
query: {
|
query: {
|
||||||
type: new GraphQLNonNull(GraphQLString),
|
type: new GraphQLNonNull(GraphQLString),
|
||||||
description: `The query terms, in Lucene search syntax. See [examples
|
description: `The query terms, in Lucene search syntax. See [examples
|
||||||
and search fields](https://musicbrainz.org/doc/Development/XML_Web_Service/Version_2/Search).`
|
and search fields](https://musicbrainz.org/doc/Development/XML_Web_Service/Version_2/Search).`,
|
||||||
},
|
},
|
||||||
...forwardConnectionArgs
|
...forwardConnectionArgs,
|
||||||
},
|
},
|
||||||
resolve: resolveSearch
|
resolve: resolveSearch,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SearchQuery = new GraphQLObjectType({
|
export const SearchQuery = new GraphQLObjectType({
|
||||||
|
|
@ -47,16 +50,16 @@ export const SearchQuery = new GraphQLObjectType({
|
||||||
releases: createSearchField(ReleaseConnection),
|
releases: createSearchField(ReleaseConnection),
|
||||||
releaseGroups: createSearchField(ReleaseGroupConnection),
|
releaseGroups: createSearchField(ReleaseGroupConnection),
|
||||||
series: createSearchField(SeriesConnection),
|
series: createSearchField(SeriesConnection),
|
||||||
works: createSearchField(WorkConnection)
|
works: createSearchField(WorkConnection),
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
export const search = {
|
export const search = {
|
||||||
type: SearchQuery,
|
type: SearchQuery,
|
||||||
description: 'Search for MusicBrainz entities using Lucene query syntax.',
|
description: 'Search for MusicBrainz entities using Lucene query syntax.',
|
||||||
// We only have work to do once we know what entity types are being requested,
|
// We only have work to do once we know what entity types are being requested,
|
||||||
// so this can just resolve to an empty object.
|
// so this can just resolve to an empty object.
|
||||||
resolve: () => ({})
|
resolve: () => ({}),
|
||||||
}
|
};
|
||||||
|
|
||||||
export default SearchQuery
|
export default SearchQuery;
|
||||||
|
|
|
||||||
|
|
@ -1,101 +1,103 @@
|
||||||
const debug = require('debug')('graphbrainz:rate-limit')
|
import createDebug from 'debug';
|
||||||
|
|
||||||
|
const debug = createDebug('graphbrainz:rate-limit');
|
||||||
|
|
||||||
export default class RateLimit {
|
export default class RateLimit {
|
||||||
constructor({
|
constructor({
|
||||||
limit = 1,
|
limit = 1,
|
||||||
period = 1000,
|
period = 1000,
|
||||||
concurrency = limit || 1,
|
concurrency = limit || 1,
|
||||||
defaultPriority = 1
|
defaultPriority = 1,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
this.limit = limit
|
this.limit = limit;
|
||||||
this.period = period
|
this.period = period;
|
||||||
this.defaultPriority = defaultPriority
|
this.defaultPriority = defaultPriority;
|
||||||
this.concurrency = concurrency
|
this.concurrency = concurrency;
|
||||||
this.queues = []
|
this.queues = [];
|
||||||
this.numPending = 0
|
this.numPending = 0;
|
||||||
this.periodStart = null
|
this.periodStart = null;
|
||||||
this.periodCapacity = this.limit
|
this.periodCapacity = this.limit;
|
||||||
this.timer = null
|
this.timer = null;
|
||||||
this.pendingFlush = false
|
this.pendingFlush = false;
|
||||||
this.prevTaskID = null
|
this.prevTaskID = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTaskID(prevTaskID = this.prevTaskID) {
|
nextTaskID(prevTaskID = this.prevTaskID) {
|
||||||
const id = (prevTaskID || 0) + 1
|
const id = (prevTaskID || 0) + 1;
|
||||||
this.prevTaskID = id
|
this.prevTaskID = id;
|
||||||
return id
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
enqueue(fn, args, priority = this.defaultPriority) {
|
enqueue(fn, args, priority = this.defaultPriority) {
|
||||||
priority = Math.max(0, priority)
|
priority = Math.max(0, priority);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const queue = (this.queues[priority] = this.queues[priority] || [])
|
const queue = (this.queues[priority] = this.queues[priority] || []);
|
||||||
const id = this.nextTaskID()
|
const id = this.nextTaskID();
|
||||||
debug(`Enqueuing task. id=${id} priority=${priority}`)
|
debug(`Enqueuing task. id=${id} priority=${priority}`);
|
||||||
queue.push({ fn, args, resolve, reject, id })
|
queue.push({ fn, args, resolve, reject, id });
|
||||||
if (!this.pendingFlush) {
|
if (!this.pendingFlush) {
|
||||||
this.pendingFlush = true
|
this.pendingFlush = true;
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
this.pendingFlush = false
|
this.pendingFlush = false;
|
||||||
this.flush()
|
this.flush();
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dequeue() {
|
dequeue() {
|
||||||
let task
|
let task;
|
||||||
for (let i = this.queues.length - 1; i >= 0; i--) {
|
for (let i = this.queues.length - 1; i >= 0; i--) {
|
||||||
const queue = this.queues[i]
|
const queue = this.queues[i];
|
||||||
if (queue && queue.length) {
|
if (queue && queue.length) {
|
||||||
task = queue.shift()
|
task = queue.shift();
|
||||||
}
|
}
|
||||||
if (!queue || !queue.length) {
|
if (!queue || !queue.length) {
|
||||||
this.queues.length = i
|
this.queues.length = i;
|
||||||
}
|
}
|
||||||
if (task) {
|
if (task) {
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return task
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
flush() {
|
flush() {
|
||||||
if (this.numPending < this.concurrency && this.periodCapacity > 0) {
|
if (this.numPending < this.concurrency && this.periodCapacity > 0) {
|
||||||
const task = this.dequeue()
|
const task = this.dequeue();
|
||||||
if (task) {
|
if (task) {
|
||||||
const { resolve, reject, fn, args, id } = task
|
const { resolve, reject, fn, args, id } = task;
|
||||||
if (this.timer == null) {
|
if (this.timer == null) {
|
||||||
const now = Date.now()
|
const now = Date.now();
|
||||||
let timeout = this.period
|
let timeout = this.period;
|
||||||
if (this.periodStart != null) {
|
if (this.periodStart != null) {
|
||||||
const delay = now - (this.periodStart + timeout)
|
const delay = now - (this.periodStart + timeout);
|
||||||
if (delay > 0 && delay <= timeout) {
|
if (delay > 0 && delay <= timeout) {
|
||||||
timeout -= delay
|
timeout -= delay;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.periodStart = now
|
this.periodStart = now;
|
||||||
this.timer = setTimeout(() => {
|
this.timer = setTimeout(() => {
|
||||||
this.timer = null
|
this.timer = null;
|
||||||
this.periodCapacity = this.limit
|
this.periodCapacity = this.limit;
|
||||||
this.flush()
|
this.flush();
|
||||||
}, timeout)
|
}, timeout);
|
||||||
}
|
}
|
||||||
this.numPending += 1
|
this.numPending += 1;
|
||||||
this.periodCapacity -= 1
|
this.periodCapacity -= 1;
|
||||||
const onResolve = value => {
|
const onResolve = (value) => {
|
||||||
this.numPending -= 1
|
this.numPending -= 1;
|
||||||
resolve(value)
|
resolve(value);
|
||||||
this.flush()
|
this.flush();
|
||||||
}
|
};
|
||||||
const onReject = err => {
|
const onReject = (err) => {
|
||||||
this.numPending -= 1
|
this.numPending -= 1;
|
||||||
reject(err)
|
reject(err);
|
||||||
this.flush()
|
this.flush();
|
||||||
}
|
};
|
||||||
debug(`Running task. id=${id}`)
|
debug(`Running task. id=${id}`);
|
||||||
fn(...args).then(onResolve, onReject)
|
fn(...args).then(onResolve, onReject);
|
||||||
this.flush()
|
this.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
228
src/resolvers.js
228
src/resolvers.js
|
|
@ -1,39 +1,40 @@
|
||||||
import { toDashed, toSingular } from './types/helpers'
|
import GraphQLRelay from 'graphql-relay';
|
||||||
import {
|
import { getFields, extendIncludes, toDashed, toSingular } from './util.js';
|
||||||
|
|
||||||
|
const {
|
||||||
getOffsetWithDefault,
|
getOffsetWithDefault,
|
||||||
connectionFromArray,
|
connectionFromArray,
|
||||||
connectionFromArraySlice
|
connectionFromArraySlice,
|
||||||
} from 'graphql-relay'
|
} = GraphQLRelay;
|
||||||
import { getFields, extendIncludes } from './util'
|
|
||||||
|
|
||||||
export function includeRelationships(params, info, fragments = info.fragments) {
|
export function includeRelationships(params, info, fragments = info.fragments) {
|
||||||
let fields = getFields(info, fragments)
|
let fields = getFields(info, fragments);
|
||||||
if (info.fieldName !== 'relationships') {
|
if (info.fieldName !== 'relationships') {
|
||||||
if (fields.relationships) {
|
if (fields.relationships) {
|
||||||
fields = getFields(fields.relationships, fragments)
|
fields = getFields(fields.relationships, fragments);
|
||||||
} else {
|
} else {
|
||||||
if (fields.edges) {
|
if (fields.edges) {
|
||||||
fields = getFields(fields.edges, fragments)
|
fields = getFields(fields.edges, fragments);
|
||||||
if (fields.node) {
|
if (fields.node) {
|
||||||
return includeRelationships(params, fields.node, fragments)
|
return includeRelationships(params, fields.node, fragments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return params
|
return params;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (fields) {
|
if (fields) {
|
||||||
const relationships = Object.keys(fields)
|
const relationships = Object.keys(fields);
|
||||||
const includeRels = relationships.map(field => {
|
const includeRels = relationships.map((field) => {
|
||||||
return `${toDashed(toSingular(field))}-rels`
|
return `${toDashed(toSingular(field))}-rels`;
|
||||||
})
|
});
|
||||||
if (includeRels.length) {
|
if (includeRels.length) {
|
||||||
params = {
|
params = {
|
||||||
...params,
|
...params,
|
||||||
inc: extendIncludes(params.inc, includeRels)
|
inc: extendIncludes(params.inc, includeRels),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return params;
|
||||||
return params
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function includeSubqueries(params, info, fragments = info.fragments) {
|
export function includeSubqueries(params, info, fragments = info.fragments) {
|
||||||
|
|
@ -42,39 +43,41 @@ export function includeSubqueries(params, info, fragments = info.fragments) {
|
||||||
artistCredit: ['artist-credits'],
|
artistCredit: ['artist-credits'],
|
||||||
artistCredits: ['artist-credits'],
|
artistCredits: ['artist-credits'],
|
||||||
isrcs: ['isrcs'],
|
isrcs: ['isrcs'],
|
||||||
media: ['media', 'discids'],
|
media: ['media'],
|
||||||
|
'media.discs': ['discids'],
|
||||||
|
'media.tracks': ['recordings'],
|
||||||
rating: ['ratings'],
|
rating: ['ratings'],
|
||||||
tags: ['tags']
|
tags: ['tags'],
|
||||||
}
|
};
|
||||||
let fields = getFields(info, fragments)
|
let fields = getFields(info, fragments, 1);
|
||||||
const include = []
|
const include = [];
|
||||||
for (const key in subqueryIncludes) {
|
for (const key in subqueryIncludes) {
|
||||||
if (fields[key]) {
|
const field = fields[key];
|
||||||
const value = subqueryIncludes[key]
|
if (field) {
|
||||||
include.push(...value)
|
const value = subqueryIncludes[key];
|
||||||
|
include.push(...value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
params = {
|
params = {
|
||||||
...params,
|
...params,
|
||||||
inc: extendIncludes(params.inc, include)
|
inc: extendIncludes(params.inc, include),
|
||||||
|
};
|
||||||
|
if (fields['edges.node']) {
|
||||||
|
params = includeSubqueries(params, fields['edges.node'], fragments);
|
||||||
}
|
}
|
||||||
if (fields.edges) {
|
return params;
|
||||||
fields = getFields(fields.edges, fragments)
|
|
||||||
if (fields.node) {
|
|
||||||
params = includeSubqueries(params, fields.node, fragments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return params
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveLookup(root, { mbid, ...params }, { loaders }, info) {
|
export function resolveLookup(root, { mbid, ...params }, { loaders }, info) {
|
||||||
if (!mbid && !params.resource) {
|
if (!mbid && !params.resource) {
|
||||||
throw new Error('Lookups by a field other than MBID must provide: resource')
|
throw new Error(
|
||||||
|
'Lookups by a field other than MBID must provide: resource'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const entityType = toDashed(info.fieldName)
|
const entityType = toDashed(info.fieldName);
|
||||||
params = includeSubqueries(params, info)
|
params = includeSubqueries(params, info);
|
||||||
params = includeRelationships(params, info)
|
params = includeRelationships(params, info);
|
||||||
return loaders.lookup.load([entityType, mbid, params])
|
return loaders.lookup.load([entityType, mbid, params]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveBrowse(
|
export function resolveBrowse(
|
||||||
|
|
@ -83,57 +86,62 @@ export function resolveBrowse(
|
||||||
{ loaders },
|
{ loaders },
|
||||||
info
|
info
|
||||||
) {
|
) {
|
||||||
const pluralName = toDashed(info.fieldName)
|
const pluralName = toDashed(info.fieldName);
|
||||||
const singularName = toSingular(pluralName)
|
const singularName = toSingular(pluralName);
|
||||||
let params = {
|
let params = {
|
||||||
...args,
|
...args,
|
||||||
type,
|
type,
|
||||||
status,
|
status,
|
||||||
limit: first,
|
limit: first,
|
||||||
offset: getOffsetWithDefault(after, -1) + 1 || undefined
|
offset: getOffsetWithDefault(after, -1) + 1 || undefined,
|
||||||
}
|
};
|
||||||
params = includeSubqueries(params, info)
|
params = includeSubqueries(params, info);
|
||||||
params = includeRelationships(params, info, info.fragments)
|
params = includeRelationships(params, info, info.fragments);
|
||||||
const formatParam = value => value.toLowerCase().replace(/ /g, '')
|
const formatParam = (value) => value.toLowerCase().replace(/ /g, '');
|
||||||
params.type = params.type.map(formatParam)
|
params.type = params.type.map(formatParam);
|
||||||
params.status = params.status.map(formatParam)
|
params.status = params.status.map(formatParam);
|
||||||
let request
|
let request;
|
||||||
if (discID) {
|
if (discID) {
|
||||||
request = loaders.lookup.load(['discid', discID, params])
|
request = loaders.lookup.load(['discid', discID, params]);
|
||||||
// If fetching releases by disc ID, they will already include the `media`
|
// If fetching releases by disc ID, they will already include the `media`
|
||||||
// and `discids` subqueries, and it is invalid to specify them.
|
// and `discids` subqueries, and it is invalid to specify them.
|
||||||
if (params.inc) {
|
if (params.inc) {
|
||||||
params.inc = params.inc.filter(value => {
|
params.inc = params.inc.filter((value) => {
|
||||||
return value !== 'media' && value !== 'discids'
|
return value !== 'media' && value !== 'discids';
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
} else if (isrc) {
|
} else if (isrc) {
|
||||||
request = loaders.lookup.load(['isrc', isrc, params])
|
request = loaders.lookup.load(['isrc', isrc, params]).then((result) => {
|
||||||
|
result[pluralName].forEach((entity) => {
|
||||||
|
entity._type = singularName;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
});
|
||||||
} else if (iswc) {
|
} else if (iswc) {
|
||||||
request = loaders.lookup.load(['iswc', iswc, params])
|
request = loaders.lookup.load(['iswc', iswc, params]);
|
||||||
} else {
|
} else {
|
||||||
request = loaders.browse.load([singularName, params])
|
request = loaders.browse.load([singularName, params]);
|
||||||
}
|
}
|
||||||
return request.then(list => {
|
return request.then((list) => {
|
||||||
// Grab the list, offet, and count from the response and use them to build
|
// Grab the list, offet, and count from the response and use them to build
|
||||||
// a Relay connection object.
|
// a Relay connection object.
|
||||||
const {
|
const {
|
||||||
[pluralName]: arraySlice,
|
[pluralName]: arraySlice,
|
||||||
[`${singularName}-offset`]: sliceStart = 0,
|
[`${singularName}-offset`]: sliceStart = 0,
|
||||||
[`${singularName}-count`]: arrayLength = arraySlice.length
|
[`${singularName}-count`]: arrayLength = arraySlice.length,
|
||||||
} = list
|
} = list;
|
||||||
const meta = { sliceStart, arrayLength }
|
const meta = { sliceStart, arrayLength };
|
||||||
const connection = connectionFromArraySlice(
|
const connection = connectionFromArraySlice(
|
||||||
arraySlice,
|
arraySlice,
|
||||||
{ first, after },
|
{ first, after },
|
||||||
meta
|
meta
|
||||||
)
|
);
|
||||||
return {
|
return {
|
||||||
nodes: connection.edges.map(edge => edge.node),
|
nodes: connection.edges.map((edge) => edge.node),
|
||||||
totalCount: arrayLength,
|
totalCount: arrayLength,
|
||||||
...connection
|
...connection,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveSearch(
|
export function resolveSearch(
|
||||||
|
|
@ -142,67 +150,67 @@ export function resolveSearch(
|
||||||
{ loaders },
|
{ loaders },
|
||||||
info
|
info
|
||||||
) {
|
) {
|
||||||
const pluralName = toDashed(info.fieldName)
|
const pluralName = toDashed(info.fieldName);
|
||||||
const singularName = toSingular(pluralName)
|
const singularName = toSingular(pluralName);
|
||||||
let params = {
|
let params = {
|
||||||
...args,
|
...args,
|
||||||
limit: first,
|
limit: first,
|
||||||
offset: getOffsetWithDefault(after, -1) + 1 || undefined
|
offset: getOffsetWithDefault(after, -1) + 1 || undefined,
|
||||||
}
|
};
|
||||||
params = includeSubqueries(params, info)
|
params = includeSubqueries(params, info);
|
||||||
return loaders.search.load([singularName, query, params]).then(list => {
|
return loaders.search.load([singularName, query, params]).then((list) => {
|
||||||
const {
|
const {
|
||||||
[pluralName]: arraySlice,
|
[pluralName]: arraySlice,
|
||||||
offset: sliceStart,
|
offset: sliceStart,
|
||||||
count: arrayLength
|
count: arrayLength,
|
||||||
} = list
|
} = list;
|
||||||
const meta = { sliceStart, arrayLength }
|
const meta = { sliceStart, arrayLength };
|
||||||
const connection = connectionFromArraySlice(
|
const connection = connectionFromArraySlice(
|
||||||
arraySlice,
|
arraySlice,
|
||||||
{ first, after },
|
{ first, after },
|
||||||
meta
|
meta
|
||||||
)
|
);
|
||||||
// Move the `score` field up to the edge object and make sure it's a
|
// Move the `score` field up to the edge object and make sure it's a
|
||||||
// number (MusicBrainz returns a string).
|
// number (MusicBrainz returns a string).
|
||||||
const edges = connection.edges.map(edge => ({
|
const edges = connection.edges.map((edge) => ({
|
||||||
...edge,
|
...edge,
|
||||||
score: +edge.node.score
|
score: +edge.node.score,
|
||||||
}))
|
}));
|
||||||
const connectionWithExtras = {
|
const connectionWithExtras = {
|
||||||
nodes: edges.map(edge => edge.node),
|
nodes: edges.map((edge) => edge.node),
|
||||||
totalCount: arrayLength,
|
totalCount: arrayLength,
|
||||||
...connection,
|
...connection,
|
||||||
edges
|
edges,
|
||||||
}
|
};
|
||||||
return connectionWithExtras
|
return connectionWithExtras;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveRelationship(rels, args, context, info) {
|
export function resolveRelationship(rels, args, context, info) {
|
||||||
const targetType = toDashed(toSingular(info.fieldName)).replace('-', '_')
|
const targetType = toDashed(toSingular(info.fieldName)).replace('-', '_');
|
||||||
let matches = rels.filter(rel => rel['target-type'] === targetType)
|
let matches = rels.filter((rel) => rel['target-type'] === targetType);
|
||||||
// There's no way to filter these at the API level, so do it here.
|
// There's no way to filter these at the API level, so do it here.
|
||||||
if (args.direction != null) {
|
if (args.direction != null) {
|
||||||
matches = matches.filter(rel => rel.direction === args.direction)
|
matches = matches.filter((rel) => rel.direction === args.direction);
|
||||||
}
|
}
|
||||||
if (args.type != null) {
|
if (args.type != null) {
|
||||||
matches = matches.filter(rel => rel.type === args.type)
|
matches = matches.filter((rel) => rel.type === args.type);
|
||||||
}
|
}
|
||||||
if (args.typeID != null) {
|
if (args.typeID != null) {
|
||||||
matches = matches.filter(rel => rel['type-id'] === args.typeID)
|
matches = matches.filter((rel) => rel['type-id'] === args.typeID);
|
||||||
}
|
}
|
||||||
const connection = connectionFromArray(matches, args)
|
const connection = connectionFromArray(matches, args);
|
||||||
return {
|
return {
|
||||||
nodes: connection.edges.map(edge => edge.node),
|
nodes: connection.edges.map((edge) => edge.node),
|
||||||
totalCount: matches.length,
|
totalCount: matches.length,
|
||||||
...connection
|
...connection,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveLinked(entity, args, context, info) {
|
export function resolveLinked(entity, args, context, info) {
|
||||||
const parentEntity = toDashed(info.parentType.name)
|
const parentEntity = toDashed(info.parentType.name);
|
||||||
args = { ...args, [parentEntity]: entity.id }
|
args = { ...args, [parentEntity]: entity.id };
|
||||||
return resolveBrowse(entity, args, context, info)
|
return resolveBrowse(entity, args, context, info);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -212,32 +220,32 @@ export function resolveLinked(entity, args, context, info) {
|
||||||
*/
|
*/
|
||||||
export function createSubqueryResolver(
|
export function createSubqueryResolver(
|
||||||
{ inc, key } = {},
|
{ inc, key } = {},
|
||||||
handler = value => value
|
handler = (value) => value
|
||||||
) {
|
) {
|
||||||
return (entity, args, { loaders }, info) => {
|
return (entity, args, { loaders }, info) => {
|
||||||
key = key || toDashed(info.fieldName)
|
key = key || toDashed(info.fieldName);
|
||||||
let promise
|
let promise;
|
||||||
if (key in entity) {
|
if (key in entity) {
|
||||||
promise = Promise.resolve(entity)
|
promise = Promise.resolve(entity);
|
||||||
} else {
|
} else {
|
||||||
const { _type: entityType, id } = entity
|
const { _type: entityType, id } = entity;
|
||||||
const params = { inc: [inc || key] }
|
const params = { inc: [inc || key] };
|
||||||
promise = loaders.lookup.load([entityType, id, params])
|
promise = loaders.lookup.load([entityType, id, params]);
|
||||||
}
|
|
||||||
return promise.then(entity => handler(entity[key], args))
|
|
||||||
}
|
}
|
||||||
|
return promise.then((entity) => handler(entity[key], args));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveDiscReleases(disc, args, context, info) {
|
export function resolveDiscReleases(disc, args, context, info) {
|
||||||
const { releases } = disc
|
const { releases } = disc;
|
||||||
if (releases != null) {
|
if (releases != null) {
|
||||||
const connection = connectionFromArray(releases, args)
|
const connection = connectionFromArray(releases, args);
|
||||||
return {
|
return {
|
||||||
nodes: connection.edges.map(edge => edge.node),
|
nodes: connection.edges.map((edge) => edge.node),
|
||||||
totalCount: releases.length,
|
totalCount: releases.length,
|
||||||
...connection
|
...connection,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
args = { ...args, discID: disc.id };
|
||||||
args = { ...args, discID: disc.id }
|
return resolveBrowse(disc, args, context, info);
|
||||||
return resolveBrowse(disc, args, context, info)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,62 @@
|
||||||
import { GraphQLSchema, GraphQLObjectType, extendSchema, parse } from 'graphql'
|
import createDebug from 'debug';
|
||||||
import { addResolveFunctionsToSchema } from 'graphql-tools'
|
import GraphQL from 'graphql';
|
||||||
import { lookup, browse, search } from './queries'
|
import GraphQLToolsSchema from '@graphql-tools/schema';
|
||||||
import { nodeField } from './types/node'
|
import { lookup, browse, search } from './queries/index.js';
|
||||||
import { loadExtension } from './extensions'
|
import { nodeField } from './types/node.js';
|
||||||
|
|
||||||
const debug = require('debug')('graphbrainz:schema')
|
const { GraphQLSchema, GraphQLObjectType, extendSchema, parse } = GraphQL;
|
||||||
|
const { addResolversToSchema } = GraphQLToolsSchema;
|
||||||
|
|
||||||
|
const debug = createDebug('graphbrainz:schema');
|
||||||
|
|
||||||
export function applyExtension(extension, schema, options = {}) {
|
export function applyExtension(extension, schema, options = {}) {
|
||||||
let outputSchema = schema
|
let outputSchema = schema;
|
||||||
if (extension.extendSchema) {
|
if (extension.extendSchema) {
|
||||||
if (typeof extension.extendSchema === 'object') {
|
if (typeof extension.extendSchema === 'object') {
|
||||||
debug(
|
debug(
|
||||||
`Extending schema via an object from the “${extension.name}” extension.`
|
`Extending schema via an object from the “${extension.name}” extension.`
|
||||||
)
|
);
|
||||||
const { schemas = [], resolvers } = extension.extendSchema
|
const { schemas = [], resolvers } = extension.extendSchema;
|
||||||
outputSchema = schemas.reduce((updatedSchema, extensionSchema) => {
|
outputSchema = schemas.reduce((updatedSchema, extensionSchema) => {
|
||||||
if (typeof extensionSchema === 'string') {
|
if (typeof extensionSchema === 'string') {
|
||||||
extensionSchema = parse(extensionSchema)
|
extensionSchema = parse(extensionSchema);
|
||||||
}
|
}
|
||||||
return extendSchema(updatedSchema, extensionSchema)
|
return extendSchema(updatedSchema, extensionSchema);
|
||||||
}, outputSchema)
|
}, outputSchema);
|
||||||
if (resolvers) {
|
if (resolvers) {
|
||||||
addResolveFunctionsToSchema({ schema: outputSchema, resolvers })
|
outputSchema = addResolversToSchema({
|
||||||
|
schema: outputSchema,
|
||||||
|
resolvers,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (typeof extension.extendSchema === 'function') {
|
} else if (typeof extension.extendSchema === 'function') {
|
||||||
debug(
|
debug(
|
||||||
`Extending schema via a function from the “${
|
`Extending schema via a function from the “${extension.name}” extension.`
|
||||||
extension.name
|
);
|
||||||
}” extension.`
|
outputSchema = extension.extendSchema(schema, options);
|
||||||
)
|
|
||||||
outputSchema = extension.extendSchema(schema, options)
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The “${extension.name}” extension contains an invalid ` +
|
`The “${extension.name}” extension contains an invalid ` +
|
||||||
`\`extendSchema\` value: ${extension.extendSchema}`
|
`\`extendSchema\` value: ${extension.extendSchema}`
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix for `graphql-tools` creating a new Query type with no description.
|
// Fix for `graphql-tools` creating a new Query type with no description.
|
||||||
if (outputSchema._queryType.description === undefined) {
|
if (outputSchema._queryType.description === undefined) {
|
||||||
outputSchema._queryType.description = schema._queryType.description
|
outputSchema._queryType.description = schema._queryType.description;
|
||||||
}
|
}
|
||||||
return outputSchema
|
return outputSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSchema(schema, options = {}) {
|
export function createSchema(schema, options = {}) {
|
||||||
const { extensions = [] } = options
|
const { extensions = [] } = options;
|
||||||
return extensions.reduce((updatedSchema, extension) => {
|
return extensions.reduce((updatedSchema, extension) => {
|
||||||
return applyExtension(loadExtension(extension), updatedSchema, options)
|
return applyExtension(extension, updatedSchema, options);
|
||||||
}, schema)
|
}, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new GraphQLSchema({
|
export const baseSchema = new GraphQLSchema({
|
||||||
query: new GraphQLObjectType({
|
query: new GraphQLObjectType({
|
||||||
name: 'Query',
|
name: 'Query',
|
||||||
description: `The query root, from which multiple types of MusicBrainz
|
description: `The query root, from which multiple types of MusicBrainz
|
||||||
|
|
@ -61,7 +65,7 @@ requests can be made.`,
|
||||||
lookup,
|
lookup,
|
||||||
browse,
|
browse,
|
||||||
search,
|
search,
|
||||||
node: nodeField
|
node: nodeField,
|
||||||
})
|
}),
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,13 @@
|
||||||
* whatsoever, but will parse the GraphQL document, allow syntax highlighting,
|
* whatsoever, but will parse the GraphQL document, allow syntax highlighting,
|
||||||
* and enable Prettier formatting.
|
* and enable Prettier formatting.
|
||||||
*/
|
*/
|
||||||
import { parse } from 'graphql'
|
import GraphQL from 'graphql';
|
||||||
|
|
||||||
|
const { parse } = GraphQL;
|
||||||
|
|
||||||
export default function gql(literals, ...interpolations) {
|
export default function gql(literals, ...interpolations) {
|
||||||
if (literals.length !== 1 || interpolations.length) {
|
if (literals.length !== 1 || interpolations.length) {
|
||||||
throw new Error('The gql template tag does not support interpolation.')
|
throw new Error('The gql template tag does not support interpolation.');
|
||||||
}
|
}
|
||||||
return parse(literals[0])
|
return parse(literals[0]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
import { GraphQLObjectType, GraphQLBoolean } from 'graphql/type'
|
import GraphQL from 'graphql';
|
||||||
import { Locale } from './scalars'
|
import { Locale } from './scalars.js';
|
||||||
import { name, sortName, fieldWithID } from './helpers'
|
import { name, sortName, fieldWithID } from './helpers.js';
|
||||||
|
import { createSubqueryResolver } from '../resolvers.js';
|
||||||
|
|
||||||
export default new GraphQLObjectType({
|
const { GraphQLObjectType, GraphQLBoolean, GraphQLList } = GraphQL;
|
||||||
|
|
||||||
|
export const Alias = new GraphQLObjectType({
|
||||||
name: 'Alias',
|
name: 'Alias',
|
||||||
description: `[Aliases](https://musicbrainz.org/doc/Aliases) are variant names
|
description: `[Aliases](https://musicbrainz.org/doc/Aliases) are variant names
|
||||||
that are mostly used as search help: if a search matches an entity’s alias, the
|
that are mostly used as search help: if a search matches an entity’s alias, the
|
||||||
|
|
@ -10,22 +13,29 @@ entity will be given as a result – even if the actual name wouldn’t be.`,
|
||||||
fields: () => ({
|
fields: () => ({
|
||||||
name: {
|
name: {
|
||||||
...name,
|
...name,
|
||||||
description: 'The aliased name of the entity.'
|
description: 'The aliased name of the entity.',
|
||||||
},
|
},
|
||||||
sortName,
|
sortName,
|
||||||
locale: {
|
locale: {
|
||||||
type: Locale,
|
type: Locale,
|
||||||
description: `The locale (language and/or country) in which the alias is
|
description: `The locale (language and/or country) in which the alias is
|
||||||
used.`
|
used.`,
|
||||||
},
|
},
|
||||||
primary: {
|
primary: {
|
||||||
type: GraphQLBoolean,
|
type: GraphQLBoolean,
|
||||||
description: `Whether this is the main alias for the entity in the
|
description: `Whether this is the main alias for the entity in the
|
||||||
specified locale (this could mean the most recent or the most common).`
|
specified locale (this could mean the most recent or the most common).`,
|
||||||
},
|
},
|
||||||
...fieldWithID('type', {
|
...fieldWithID('type', {
|
||||||
description: `The type or purpose of the alias – whether it is a variant,
|
description: `The type or purpose of the alias – whether it is a variant,
|
||||||
search hint, etc.`
|
search hint, etc.`,
|
||||||
})
|
}),
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
|
export const aliases = {
|
||||||
|
type: new GraphQLList(Alias),
|
||||||
|
description: `[Aliases](https://musicbrainz.org/doc/Aliases) are used to store
|
||||||
|
alternate names or misspellings.`,
|
||||||
|
resolve: createSubqueryResolver(),
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,29 @@
|
||||||
import { GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql'
|
import GraphQL from 'graphql';
|
||||||
import Node from './node'
|
import { Node } from './node.js';
|
||||||
import Entity from './entity'
|
import { Entity } from './entity.js';
|
||||||
import {
|
import {
|
||||||
id,
|
id,
|
||||||
mbid,
|
mbid,
|
||||||
name,
|
name,
|
||||||
sortName,
|
sortName,
|
||||||
disambiguation,
|
disambiguation,
|
||||||
aliases,
|
|
||||||
artists,
|
|
||||||
events,
|
|
||||||
labels,
|
|
||||||
places,
|
|
||||||
releases,
|
|
||||||
relationships,
|
|
||||||
collections,
|
|
||||||
tags,
|
|
||||||
fieldWithID,
|
fieldWithID,
|
||||||
connectionWithExtras
|
connectionWithExtras,
|
||||||
} from './helpers'
|
linkedQuery,
|
||||||
|
} from './helpers.js';
|
||||||
|
import { events } from './event.js';
|
||||||
|
import { aliases } from './alias.js';
|
||||||
|
import { artists } from './artist.js';
|
||||||
|
import { labels } from './label.js';
|
||||||
|
import { places } from './place.js';
|
||||||
|
import { releases } from './release.js';
|
||||||
|
import { relationships } from './relationship.js';
|
||||||
|
import { collections } from './collection.js';
|
||||||
|
import { tags } from './tag.js';
|
||||||
|
|
||||||
const Area = new GraphQLObjectType({
|
const { GraphQLObjectType, GraphQLString, GraphQLList } = GraphQL;
|
||||||
|
|
||||||
|
export const Area = new GraphQLObjectType({
|
||||||
name: 'Area',
|
name: 'Area',
|
||||||
description: `[Areas](https://musicbrainz.org/doc/Area) are geographic regions
|
description: `[Areas](https://musicbrainz.org/doc/Area) are geographic regions
|
||||||
or settlements (countries, cities, or the like).`,
|
or settlements (countries, cities, or the like).`,
|
||||||
|
|
@ -41,17 +44,17 @@ the codes assigned by ISO to countries and subdivisions.`,
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: `Specify the particular ISO standard codes to retrieve.
|
description: `Specify the particular ISO standard codes to retrieve.
|
||||||
Available ISO standards are 3166-1, 3166-2, and 3166-3.`,
|
Available ISO standards are 3166-1, 3166-2, and 3166-3.`,
|
||||||
defaultValue: '3166-1'
|
defaultValue: '3166-1',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
resolve: (data, args) => {
|
resolve: (data, args) => {
|
||||||
const { standard = '3166-1' } = args
|
const { standard = '3166-1' } = args;
|
||||||
return data[`iso-${standard}-codes`]
|
return data[`iso-${standard}-codes`];
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
...fieldWithID('type', {
|
...fieldWithID('type', {
|
||||||
description: `The type of area (country, city, etc. – see the [possible
|
description: `The type of area (country, city, etc. – see the [possible
|
||||||
values](https://musicbrainz.org/doc/Area)).`
|
values](https://musicbrainz.org/doc/Area)).`,
|
||||||
}),
|
}),
|
||||||
artists,
|
artists,
|
||||||
events,
|
events,
|
||||||
|
|
@ -60,9 +63,10 @@ values](https://musicbrainz.org/doc/Area)).`
|
||||||
releases,
|
releases,
|
||||||
relationships,
|
relationships,
|
||||||
collections,
|
collections,
|
||||||
tags
|
tags,
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const AreaConnection = connectionWithExtras(Area)
|
export const AreaConnection = connectionWithExtras(Area);
|
||||||
export default Area
|
|
||||||
|
export const areas = linkedQuery(AreaConnection);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import { GraphQLObjectType, GraphQLString } from 'graphql/type'
|
import GraphQL from 'graphql';
|
||||||
import Artist from './artist'
|
import { Artist } from './artist.js';
|
||||||
|
import { createSubqueryResolver } from '../resolvers.js';
|
||||||
|
|
||||||
export default new GraphQLObjectType({
|
const { GraphQLObjectType, GraphQLString, GraphQLList } = GraphQL;
|
||||||
|
|
||||||
|
export const ArtistCredit = new GraphQLObjectType({
|
||||||
name: 'ArtistCredit',
|
name: 'ArtistCredit',
|
||||||
description: `[Artist credits](https://musicbrainz.org/doc/Artist_Credits)
|
description: `[Artist credits](https://musicbrainz.org/doc/Artist_Credits)
|
||||||
indicate who is the main credited artist (or artists) for releases, release
|
indicate who is the main credited artist (or artists) for releases, release
|
||||||
|
|
@ -13,24 +16,42 @@ track, etc., and join phrases between them.`,
|
||||||
type: Artist,
|
type: Artist,
|
||||||
description: `The entity representing the artist referenced in the
|
description: `The entity representing the artist referenced in the
|
||||||
credits.`,
|
credits.`,
|
||||||
resolve: source => {
|
resolve: (source) => {
|
||||||
const { artist } = source
|
const { artist } = source;
|
||||||
if (artist) {
|
if (artist) {
|
||||||
artist._type = 'artist'
|
artist._type = 'artist';
|
||||||
}
|
|
||||||
return artist
|
|
||||||
}
|
}
|
||||||
|
return artist;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: `The name of the artist as credited in the specific release,
|
description: `The name of the artist as credited in the specific release,
|
||||||
track, etc.`
|
track, etc.`,
|
||||||
},
|
},
|
||||||
joinPhrase: {
|
joinPhrase: {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: `Join phrases might include words and/or punctuation to
|
description: `Join phrases might include words and/or punctuation to
|
||||||
separate artist names as they appear on the release, track, etc.`,
|
separate artist names as they appear on the release, track, etc.`,
|
||||||
resolve: data => data.joinphrase
|
resolve: (data) => data.joinphrase,
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
|
export const artistCredits = {
|
||||||
|
type: new GraphQLList(ArtistCredit),
|
||||||
|
description: 'The main credited artist(s).',
|
||||||
|
resolve: createSubqueryResolver({
|
||||||
|
inc: 'artist-credits',
|
||||||
|
key: 'artist-credit',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const artistCredit = {
|
||||||
|
...artistCredits,
|
||||||
|
deprecationReason: `The \`artistCredit\` field has been renamed to
|
||||||
|
\`artistCredits\`, since it is a list of credits and is referred to in the
|
||||||
|
plural form throughout the MusicBrainz documentation. This field is deprecated
|
||||||
|
and will be removed in a major release in the future. Use the equivalent
|
||||||
|
\`artistCredits\` field.`,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,18 @@
|
||||||
import { GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql/type'
|
import GraphQL from 'graphql';
|
||||||
import Node from './node'
|
import { Node } from './node.js';
|
||||||
import Entity from './entity'
|
import { Entity } from './entity.js';
|
||||||
import Area from './area'
|
import { Area } from './area.js';
|
||||||
import { IPI, ISNI } from './scalars'
|
import { aliases } from './alias.js';
|
||||||
|
import { collections } from './collection.js';
|
||||||
|
import { lifeSpan } from './life-span.js';
|
||||||
|
import { recordings } from './recording.js';
|
||||||
|
import { releases } from './release.js';
|
||||||
|
import { releaseGroups } from './release-group.js';
|
||||||
|
import { works } from './work.js';
|
||||||
|
import { relationships } from './relationship.js';
|
||||||
|
import { rating } from './rating.js';
|
||||||
|
import { tags } from './tag.js';
|
||||||
|
import { IPI, ISNI } from './scalars.js';
|
||||||
import {
|
import {
|
||||||
resolveWithFallback,
|
resolveWithFallback,
|
||||||
fieldWithID,
|
fieldWithID,
|
||||||
|
|
@ -11,20 +21,13 @@ import {
|
||||||
name,
|
name,
|
||||||
sortName,
|
sortName,
|
||||||
disambiguation,
|
disambiguation,
|
||||||
aliases,
|
connectionWithExtras,
|
||||||
lifeSpan,
|
linkedQuery,
|
||||||
recordings,
|
} from './helpers.js';
|
||||||
releases,
|
|
||||||
releaseGroups,
|
|
||||||
works,
|
|
||||||
relationships,
|
|
||||||
collections,
|
|
||||||
rating,
|
|
||||||
tags,
|
|
||||||
connectionWithExtras
|
|
||||||
} from './helpers'
|
|
||||||
|
|
||||||
const Artist = new GraphQLObjectType({
|
const { GraphQLObjectType, GraphQLString, GraphQLList } = GraphQL;
|
||||||
|
|
||||||
|
export const Artist = new GraphQLObjectType({
|
||||||
name: 'Artist',
|
name: 'Artist',
|
||||||
description: `An [artist](https://musicbrainz.org/doc/Artist) is generally a
|
description: `An [artist](https://musicbrainz.org/doc/Artist) is generally a
|
||||||
musician, group of musicians, or other music professional (like a producer or
|
musician, group of musicians, or other music professional (like a producer or
|
||||||
|
|
@ -42,42 +45,42 @@ even a fictional character.`,
|
||||||
country: {
|
country: {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: `The country with which an artist is primarily identified. It
|
description: `The country with which an artist is primarily identified. It
|
||||||
is often, but not always, its birth/formation country.`
|
is often, but not always, its birth/formation country.`,
|
||||||
},
|
},
|
||||||
area: {
|
area: {
|
||||||
type: Area,
|
type: Area,
|
||||||
description: `The area with which an artist is primarily identified. It
|
description: `The area with which an artist is primarily identified. It
|
||||||
is often, but not always, its birth/formation country.`
|
is often, but not always, its birth/formation country.`,
|
||||||
},
|
},
|
||||||
beginArea: {
|
beginArea: {
|
||||||
type: Area,
|
type: Area,
|
||||||
description: `The area in which an artist began their career (or where
|
description: `The area in which an artist began their career (or where
|
||||||
they were born, if the artist is a person).`,
|
they were born, if the artist is a person).`,
|
||||||
resolve: resolveWithFallback(['begin-area', 'begin_area'])
|
resolve: resolveWithFallback(['begin-area', 'begin_area']),
|
||||||
},
|
},
|
||||||
endArea: {
|
endArea: {
|
||||||
type: Area,
|
type: Area,
|
||||||
description: `The area in which an artist ended their career (or where
|
description: `The area in which an artist ended their career (or where
|
||||||
they died, if the artist is a person).`,
|
they died, if the artist is a person).`,
|
||||||
resolve: resolveWithFallback(['end-area', 'end_area'])
|
resolve: resolveWithFallback(['end-area', 'end_area']),
|
||||||
},
|
},
|
||||||
lifeSpan,
|
lifeSpan,
|
||||||
...fieldWithID('gender', {
|
...fieldWithID('gender', {
|
||||||
description: `Whether a person or character identifies as male, female, or
|
description: `Whether a person or character identifies as male, female, or
|
||||||
neither. Groups do not have genders.`
|
neither. Groups do not have genders.`,
|
||||||
}),
|
}),
|
||||||
...fieldWithID('type', {
|
...fieldWithID('type', {
|
||||||
description: 'Whether an artist is a person, a group, or something else.'
|
description: 'Whether an artist is a person, a group, or something else.',
|
||||||
}),
|
}),
|
||||||
ipis: {
|
ipis: {
|
||||||
type: new GraphQLList(IPI),
|
type: new GraphQLList(IPI),
|
||||||
description: `List of [Interested Parties Information](https://musicbrainz.org/doc/IPI)
|
description: `List of [Interested Parties Information](https://musicbrainz.org/doc/IPI)
|
||||||
(IPI) codes for the artist.`
|
(IPI) codes for the artist.`,
|
||||||
},
|
},
|
||||||
isnis: {
|
isnis: {
|
||||||
type: new GraphQLList(ISNI),
|
type: new GraphQLList(ISNI),
|
||||||
description: `List of [International Standard Name Identifier](https://musicbrainz.org/doc/ISNI)
|
description: `List of [International Standard Name Identifier](https://musicbrainz.org/doc/ISNI)
|
||||||
(ISNI) codes for the artist.`
|
(ISNI) codes for the artist.`,
|
||||||
},
|
},
|
||||||
recordings,
|
recordings,
|
||||||
releases,
|
releases,
|
||||||
|
|
@ -86,9 +89,10 @@ neither. Groups do not have genders.`
|
||||||
relationships,
|
relationships,
|
||||||
collections,
|
collections,
|
||||||
rating,
|
rating,
|
||||||
tags
|
tags,
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const ArtistConnection = connectionWithExtras(Artist)
|
export const ArtistConnection = connectionWithExtras(Artist);
|
||||||
export default Artist
|
|
||||||
|
export const artists = linkedQuery(ArtistConnection);
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,31 @@
|
||||||
import { GraphQLObjectType, GraphQLNonNull, GraphQLString } from 'graphql/type'
|
import GraphQL from 'graphql';
|
||||||
import Node from './node'
|
import { Node } from './node.js';
|
||||||
import Entity from './entity'
|
import { Entity } from './entity.js';
|
||||||
import {
|
import {
|
||||||
id,
|
id,
|
||||||
mbid,
|
mbid,
|
||||||
name,
|
name,
|
||||||
areas,
|
|
||||||
artists,
|
|
||||||
events,
|
|
||||||
instruments,
|
|
||||||
labels,
|
|
||||||
places,
|
|
||||||
recordings,
|
|
||||||
releases,
|
|
||||||
releaseGroups,
|
|
||||||
series,
|
|
||||||
works,
|
|
||||||
fieldWithID,
|
fieldWithID,
|
||||||
resolveHyphenated,
|
resolveHyphenated,
|
||||||
createCollectionField,
|
createCollectionField,
|
||||||
connectionWithExtras
|
connectionWithExtras,
|
||||||
} from './helpers'
|
linkedQuery,
|
||||||
|
} from './helpers.js';
|
||||||
|
import { areas } from './area.js';
|
||||||
|
import { artists } from './artist.js';
|
||||||
|
import { events } from './event.js';
|
||||||
|
import { instruments } from './instrument.js';
|
||||||
|
import { labels } from './label.js';
|
||||||
|
import { places } from './place.js';
|
||||||
|
import { recordings } from './recording.js';
|
||||||
|
import { releases } from './release.js';
|
||||||
|
import { releaseGroups } from './release-group.js';
|
||||||
|
import { series } from './series.js';
|
||||||
|
import { works } from './work.js';
|
||||||
|
|
||||||
const Collection = new GraphQLObjectType({
|
const { GraphQLObjectType, GraphQLNonNull, GraphQLString } = GraphQL;
|
||||||
|
|
||||||
|
export const Collection = new GraphQLObjectType({
|
||||||
name: 'Collection',
|
name: 'Collection',
|
||||||
description: `[Collections](https://musicbrainz.org/doc/Collections) are
|
description: `[Collections](https://musicbrainz.org/doc/Collections) are
|
||||||
lists of entities that users can create.`,
|
lists of entities that users can create.`,
|
||||||
|
|
@ -33,15 +36,15 @@ lists of entities that users can create.`,
|
||||||
name,
|
name,
|
||||||
editor: {
|
editor: {
|
||||||
type: new GraphQLNonNull(GraphQLString),
|
type: new GraphQLNonNull(GraphQLString),
|
||||||
description: 'The username of the editor who created the collection.'
|
description: 'The username of the editor who created the collection.',
|
||||||
},
|
},
|
||||||
entityType: {
|
entityType: {
|
||||||
type: new GraphQLNonNull(GraphQLString),
|
type: new GraphQLNonNull(GraphQLString),
|
||||||
description: 'The type of entity listed in the collection.',
|
description: 'The type of entity listed in the collection.',
|
||||||
resolve: resolveHyphenated
|
resolve: resolveHyphenated,
|
||||||
},
|
},
|
||||||
...fieldWithID('type', {
|
...fieldWithID('type', {
|
||||||
description: 'The type of collection.'
|
description: 'The type of collection.',
|
||||||
}),
|
}),
|
||||||
areas: createCollectionField(areas),
|
areas: createCollectionField(areas),
|
||||||
artists: createCollectionField(artists),
|
artists: createCollectionField(artists),
|
||||||
|
|
@ -53,9 +56,12 @@ lists of entities that users can create.`,
|
||||||
releases: createCollectionField(releases),
|
releases: createCollectionField(releases),
|
||||||
releaseGroups: createCollectionField(releaseGroups),
|
releaseGroups: createCollectionField(releaseGroups),
|
||||||
series: createCollectionField(series),
|
series: createCollectionField(series),
|
||||||
works: createCollectionField(works)
|
works: createCollectionField(works),
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const CollectionConnection = connectionWithExtras(Collection)
|
export const CollectionConnection = connectionWithExtras(Collection);
|
||||||
export default Collection
|
|
||||||
|
export const collections = linkedQuery(CollectionConnection, {
|
||||||
|
description: 'A list of collections containing this entity.',
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,15 @@
|
||||||
import {
|
import GraphQL from 'graphql';
|
||||||
GraphQLObjectType,
|
import GraphQLRelay from 'graphql-relay';
|
||||||
GraphQLNonNull,
|
import { Node } from './node.js';
|
||||||
GraphQLList,
|
import { DiscID } from './scalars.js';
|
||||||
GraphQLInt
|
import { ReleaseConnection } from './release.js';
|
||||||
} from 'graphql/type'
|
import { resolveDiscReleases } from '../resolvers.js';
|
||||||
import { forwardConnectionArgs } from 'graphql-relay'
|
import { id, resolveHyphenated } from './helpers.js';
|
||||||
import Node from './node'
|
|
||||||
import { DiscID } from './scalars'
|
|
||||||
import { ReleaseConnection } from './release'
|
|
||||||
import { resolveDiscReleases } from '../resolvers'
|
|
||||||
import { id, resolveHyphenated } from './helpers'
|
|
||||||
|
|
||||||
export default new GraphQLObjectType({
|
const { GraphQLObjectType, GraphQLNonNull, GraphQLList, GraphQLInt } = GraphQL;
|
||||||
|
const { forwardConnectionArgs } = GraphQLRelay;
|
||||||
|
|
||||||
|
export const Disc = new GraphQLObjectType({
|
||||||
name: 'Disc',
|
name: 'Disc',
|
||||||
description: `Information about the physical CD and releases associated with a
|
description: `Information about the physical CD and releases associated with a
|
||||||
particular [disc ID](https://musicbrainz.org/doc/Disc_ID).`,
|
particular [disc ID](https://musicbrainz.org/doc/Disc_ID).`,
|
||||||
|
|
@ -21,26 +19,26 @@ particular [disc ID](https://musicbrainz.org/doc/Disc_ID).`,
|
||||||
discID: {
|
discID: {
|
||||||
type: new GraphQLNonNull(DiscID),
|
type: new GraphQLNonNull(DiscID),
|
||||||
description: `The [disc ID](https://musicbrainz.org/doc/Disc_ID) of this disc.`,
|
description: `The [disc ID](https://musicbrainz.org/doc/Disc_ID) of this disc.`,
|
||||||
resolve: disc => disc.id
|
resolve: (disc) => disc.id,
|
||||||
},
|
},
|
||||||
offsetCount: {
|
offsetCount: {
|
||||||
type: new GraphQLNonNull(GraphQLInt),
|
type: new GraphQLNonNull(GraphQLInt),
|
||||||
description: 'The number of offsets (tracks) on the disc.',
|
description: 'The number of offsets (tracks) on the disc.',
|
||||||
resolve: resolveHyphenated
|
resolve: resolveHyphenated,
|
||||||
},
|
},
|
||||||
offsets: {
|
offsets: {
|
||||||
type: new GraphQLList(GraphQLInt),
|
type: new GraphQLList(GraphQLInt),
|
||||||
description: 'The sector offset of each track on the disc.'
|
description: 'The sector offset of each track on the disc.',
|
||||||
},
|
},
|
||||||
sectors: {
|
sectors: {
|
||||||
type: new GraphQLNonNull(GraphQLInt),
|
type: new GraphQLNonNull(GraphQLInt),
|
||||||
description: 'The sector offset of the lead-out (the end of the disc).'
|
description: 'The sector offset of the lead-out (the end of the disc).',
|
||||||
},
|
},
|
||||||
releases: {
|
releases: {
|
||||||
type: ReleaseConnection,
|
type: ReleaseConnection,
|
||||||
description: 'The list of releases linked to this disc ID.',
|
description: 'The list of releases linked to this disc ID.',
|
||||||
args: forwardConnectionArgs,
|
args: forwardConnectionArgs,
|
||||||
resolve: resolveDiscReleases
|
resolve: resolveDiscReleases,
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,13 @@
|
||||||
import { GraphQLInterfaceType } from 'graphql'
|
import GraphQL from 'graphql';
|
||||||
import { mbid, connectionWithExtras } from './helpers'
|
import { mbid, connectionWithExtras, resolveType } from './helpers.js';
|
||||||
|
|
||||||
const debug = require('debug')('graphbrainz:types/entity')
|
const { GraphQLInterfaceType } = GraphQL;
|
||||||
|
|
||||||
const Entity = new GraphQLInterfaceType({
|
export const Entity = new GraphQLInterfaceType({
|
||||||
name: 'Entity',
|
name: 'Entity',
|
||||||
description: 'An entity in the MusicBrainz schema.',
|
description: 'An entity in the MusicBrainz schema.',
|
||||||
resolveType(value, context, info) {
|
resolveType,
|
||||||
if (value._type) {
|
fields: () => ({ mbid }),
|
||||||
let originalType
|
});
|
||||||
try {
|
|
||||||
originalType = require(`./${value._type}`).default
|
|
||||||
} catch (err) {
|
|
||||||
debug(`Failed to load type: ${value._type}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Don't use `originalType`! The schema may have been extended in which
|
|
||||||
// case the types have all been replaced. Instead, find the current type
|
|
||||||
// of the same name.
|
|
||||||
const typeMap = info.schema.getTypeMap()
|
|
||||||
return typeMap[originalType.name]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fields: () => ({ mbid })
|
|
||||||
})
|
|
||||||
|
|
||||||
export const EntityConnection = connectionWithExtras(Entity)
|
export const EntityConnection = connectionWithExtras(Entity);
|
||||||
export default Entity
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { GraphQLEnumType } from 'graphql/type'
|
import GraphQL from 'graphql';
|
||||||
|
|
||||||
|
const { GraphQLEnumType } = GraphQL;
|
||||||
|
|
||||||
export const ArtistType = new GraphQLEnumType({
|
export const ArtistType = new GraphQLEnumType({
|
||||||
name: 'ArtistType',
|
name: 'ArtistType',
|
||||||
|
|
@ -8,37 +10,37 @@ etc.`,
|
||||||
PERSON: {
|
PERSON: {
|
||||||
name: 'Person',
|
name: 'Person',
|
||||||
description: 'This indicates an individual person.',
|
description: 'This indicates an individual person.',
|
||||||
value: 'Person'
|
value: 'Person',
|
||||||
},
|
},
|
||||||
GROUP: {
|
GROUP: {
|
||||||
name: 'Group',
|
name: 'Group',
|
||||||
description: `This indicates a group of people that may or may not have a
|
description: `This indicates a group of people that may or may not have a
|
||||||
distinctive name.`,
|
distinctive name.`,
|
||||||
value: 'Group'
|
value: 'Group',
|
||||||
},
|
},
|
||||||
ORCHESTRA: {
|
ORCHESTRA: {
|
||||||
name: 'Orchestra',
|
name: 'Orchestra',
|
||||||
description:
|
description:
|
||||||
'This indicates an orchestra (a large instrumental ensemble).',
|
'This indicates an orchestra (a large instrumental ensemble).',
|
||||||
value: 'Orchestra'
|
value: 'Orchestra',
|
||||||
},
|
},
|
||||||
CHOIR: {
|
CHOIR: {
|
||||||
name: 'Choir',
|
name: 'Choir',
|
||||||
description: 'This indicates a choir/chorus (a large vocal ensemble).',
|
description: 'This indicates a choir/chorus (a large vocal ensemble).',
|
||||||
value: 'Choir'
|
value: 'Choir',
|
||||||
},
|
},
|
||||||
CHARACTER: {
|
CHARACTER: {
|
||||||
name: 'Character',
|
name: 'Character',
|
||||||
description: 'This indicates an individual fictional character.',
|
description: 'This indicates an individual fictional character.',
|
||||||
value: 'Character'
|
value: 'Character',
|
||||||
},
|
},
|
||||||
OTHER: {
|
OTHER: {
|
||||||
name: 'Other',
|
name: 'Other',
|
||||||
description: 'An artist which does not fit into the other categories.',
|
description: 'An artist which does not fit into the other categories.',
|
||||||
value: 'Other'
|
value: 'Other',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
export const CoverArtImageSize = new GraphQLEnumType({
|
export const CoverArtImageSize = new GraphQLEnumType({
|
||||||
name: 'CoverArtImageSize',
|
name: 'CoverArtImageSize',
|
||||||
|
|
@ -48,20 +50,20 @@ Archive](https://musicbrainz.org/doc/Cover_Art_Archive).`,
|
||||||
SMALL: {
|
SMALL: {
|
||||||
name: 'Small',
|
name: 'Small',
|
||||||
description: 'A maximum dimension of 250px.',
|
description: 'A maximum dimension of 250px.',
|
||||||
value: 250
|
value: 250,
|
||||||
},
|
},
|
||||||
LARGE: {
|
LARGE: {
|
||||||
name: 'Large',
|
name: 'Large',
|
||||||
description: 'A maximum dimension of 500px.',
|
description: 'A maximum dimension of 500px.',
|
||||||
value: 500
|
value: 500,
|
||||||
},
|
},
|
||||||
FULL: {
|
FULL: {
|
||||||
name: 'Full',
|
name: 'Full',
|
||||||
description: 'The image’s original dimensions, with no maximum.',
|
description: 'The image’s original dimensions, with no maximum.',
|
||||||
value: null
|
value: null,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
export const ReleaseStatus = new GraphQLEnumType({
|
export const ReleaseStatus = new GraphQLEnumType({
|
||||||
name: 'ReleaseStatus',
|
name: 'ReleaseStatus',
|
||||||
|
|
@ -72,29 +74,29 @@ bootleg, etc.`,
|
||||||
name: 'Official',
|
name: 'Official',
|
||||||
description: `Any release officially sanctioned by the artist and/or their
|
description: `Any release officially sanctioned by the artist and/or their
|
||||||
record company. (Most releases will fit into this category.)`,
|
record company. (Most releases will fit into this category.)`,
|
||||||
value: 'Official'
|
value: 'Official',
|
||||||
},
|
},
|
||||||
PROMOTION: {
|
PROMOTION: {
|
||||||
name: 'Promotion',
|
name: 'Promotion',
|
||||||
description: `A giveaway release or a release intended to promote an
|
description: `A giveaway release or a release intended to promote an
|
||||||
upcoming official release, e.g. prerelease albums or releases included with a
|
upcoming official release, e.g. prerelease albums or releases included with a
|
||||||
magazine.`,
|
magazine.`,
|
||||||
value: 'Promotion'
|
value: 'Promotion',
|
||||||
},
|
},
|
||||||
BOOTLEG: {
|
BOOTLEG: {
|
||||||
name: 'Bootleg',
|
name: 'Bootleg',
|
||||||
description: `An unofficial/underground release that was not sanctioned by
|
description: `An unofficial/underground release that was not sanctioned by
|
||||||
the artist and/or the record company.`,
|
the artist and/or the record company.`,
|
||||||
value: 'Bootleg'
|
value: 'Bootleg',
|
||||||
},
|
},
|
||||||
PSEUDORELEASE: {
|
PSEUDORELEASE: {
|
||||||
name: 'Pseudo-Release',
|
name: 'Pseudo-Release',
|
||||||
description: `A pseudo-release is a duplicate release for
|
description: `A pseudo-release is a duplicate release for
|
||||||
translation/transliteration purposes.`,
|
translation/transliteration purposes.`,
|
||||||
value: 'Pseudo-Release'
|
value: 'Pseudo-Release',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
export const ReleaseGroupType = new GraphQLEnumType({
|
export const ReleaseGroupType = new GraphQLEnumType({
|
||||||
name: 'ReleaseGroupType',
|
name: 'ReleaseGroupType',
|
||||||
|
|
@ -107,14 +109,14 @@ etc.`,
|
||||||
release, generally consists of previously unreleased material (unless this type
|
release, generally consists of previously unreleased material (unless this type
|
||||||
is combined with secondary types which change that, such as “Compilation”). This
|
is combined with secondary types which change that, such as “Compilation”). This
|
||||||
includes album re-issues, with or without bonus tracks.`,
|
includes album re-issues, with or without bonus tracks.`,
|
||||||
value: 'Album'
|
value: 'Album',
|
||||||
},
|
},
|
||||||
SINGLE: {
|
SINGLE: {
|
||||||
name: 'Single',
|
name: 'Single',
|
||||||
description: `A single typically has one main song and possibly a handful
|
description: `A single typically has one main song and possibly a handful
|
||||||
of additional tracks or remixes of the main track. A single is usually named
|
of additional tracks or remixes of the main track. A single is usually named
|
||||||
after its main song.`,
|
after its main song.`,
|
||||||
value: 'Single'
|
value: 'Single',
|
||||||
},
|
},
|
||||||
EP: {
|
EP: {
|
||||||
name: 'EP',
|
name: 'EP',
|
||||||
|
|
@ -124,57 +126,57 @@ full length release (an LP or “Long Play”) and the tracks are usually exclus
|
||||||
to the EP, in other words the tracks don’t come from a previously issued
|
to the EP, in other words the tracks don’t come from a previously issued
|
||||||
release. EP is fairly difficult to define; usually it should only be assumed
|
release. EP is fairly difficult to define; usually it should only be assumed
|
||||||
that a release is an EP if the artist defines it as such.`,
|
that a release is an EP if the artist defines it as such.`,
|
||||||
value: 'EP'
|
value: 'EP',
|
||||||
},
|
},
|
||||||
OTHER: {
|
OTHER: {
|
||||||
name: 'Other',
|
name: 'Other',
|
||||||
description: 'Any release that does not fit any of the other categories.',
|
description: 'Any release that does not fit any of the other categories.',
|
||||||
value: 'Other'
|
value: 'Other',
|
||||||
},
|
},
|
||||||
BROADCAST: {
|
BROADCAST: {
|
||||||
name: 'Broadcast',
|
name: 'Broadcast',
|
||||||
description: `An episodic release that was originally broadcast via radio,
|
description: `An episodic release that was originally broadcast via radio,
|
||||||
television, or the Internet, including podcasts.`,
|
television, or the Internet, including podcasts.`,
|
||||||
value: 'Broadcast'
|
value: 'Broadcast',
|
||||||
},
|
},
|
||||||
COMPILATION: {
|
COMPILATION: {
|
||||||
name: 'Compilation',
|
name: 'Compilation',
|
||||||
description: `A compilation is a collection of previously released tracks
|
description: `A compilation is a collection of previously released tracks
|
||||||
by one or more artists.`,
|
by one or more artists.`,
|
||||||
value: 'Compilation'
|
value: 'Compilation',
|
||||||
},
|
},
|
||||||
SOUNDTRACK: {
|
SOUNDTRACK: {
|
||||||
name: 'Soundtrack',
|
name: 'Soundtrack',
|
||||||
description: `A soundtrack is the musical score to a movie, TV series,
|
description: `A soundtrack is the musical score to a movie, TV series,
|
||||||
stage show, computer game, etc.`,
|
stage show, computer game, etc.`,
|
||||||
value: 'Soundtrack'
|
value: 'Soundtrack',
|
||||||
},
|
},
|
||||||
SPOKENWORD: {
|
SPOKENWORD: {
|
||||||
name: 'Spoken Word',
|
name: 'Spoken Word',
|
||||||
description: 'A non-music spoken word release.',
|
description: 'A non-music spoken word release.',
|
||||||
value: 'Spoken Word'
|
value: 'Spoken Word',
|
||||||
},
|
},
|
||||||
INTERVIEW: {
|
INTERVIEW: {
|
||||||
name: 'Interview',
|
name: 'Interview',
|
||||||
description: `An interview release contains an interview, generally with
|
description: `An interview release contains an interview, generally with
|
||||||
an artist.`,
|
an artist.`,
|
||||||
value: 'Interview'
|
value: 'Interview',
|
||||||
},
|
},
|
||||||
AUDIOBOOK: {
|
AUDIOBOOK: {
|
||||||
name: 'Audiobook',
|
name: 'Audiobook',
|
||||||
description: 'An audiobook is a book read by a narrator without music.',
|
description: 'An audiobook is a book read by a narrator without music.',
|
||||||
value: 'Audiobook'
|
value: 'Audiobook',
|
||||||
},
|
},
|
||||||
LIVE: {
|
LIVE: {
|
||||||
name: 'Live',
|
name: 'Live',
|
||||||
description: 'A release that was recorded live.',
|
description: 'A release that was recorded live.',
|
||||||
value: 'Live'
|
value: 'Live',
|
||||||
},
|
},
|
||||||
REMIX: {
|
REMIX: {
|
||||||
name: 'Remix',
|
name: 'Remix',
|
||||||
description: `A release that was (re)mixed from previously released
|
description: `A release that was (re)mixed from previously released
|
||||||
material.`,
|
material.`,
|
||||||
value: 'Remix'
|
value: 'Remix',
|
||||||
},
|
},
|
||||||
DJMIX: {
|
DJMIX: {
|
||||||
name: 'DJ-mix',
|
name: 'DJ-mix',
|
||||||
|
|
@ -183,7 +185,7 @@ after the other, each one modified so that they blend together into a continuous
|
||||||
flow of music. A DJ mix release requires that the recordings be modified in some
|
flow of music. A DJ mix release requires that the recordings be modified in some
|
||||||
manner, and the DJ who does this modification is usually (although not always)
|
manner, and the DJ who does this modification is usually (although not always)
|
||||||
credited in a fairly prominent way.`,
|
credited in a fairly prominent way.`,
|
||||||
value: 'DJ-mix'
|
value: 'DJ-mix',
|
||||||
},
|
},
|
||||||
MIXTAPE: {
|
MIXTAPE: {
|
||||||
name: 'Mixtape/Street',
|
name: 'Mixtape/Street',
|
||||||
|
|
@ -199,18 +201,18 @@ significant proportion of new material, including original production or
|
||||||
original vocals over top of other artists’ instrumentals. They are distinct from
|
original vocals over top of other artists’ instrumentals. They are distinct from
|
||||||
demos in that they are designed for release directly to the public and fans, not
|
demos in that they are designed for release directly to the public and fans, not
|
||||||
to labels.`,
|
to labels.`,
|
||||||
value: 'Mixtape/Street'
|
value: 'Mixtape/Street',
|
||||||
},
|
},
|
||||||
DEMO: {
|
DEMO: {
|
||||||
name: 'Demo',
|
name: 'Demo',
|
||||||
description: `A release that was recorded for limited circulation or
|
description: `A release that was recorded for limited circulation or
|
||||||
reference use rather than for general public release.`,
|
reference use rather than for general public release.`,
|
||||||
value: 'Demo'
|
value: 'Demo',
|
||||||
},
|
},
|
||||||
NAT: {
|
NAT: {
|
||||||
name: 'Non-Album Track',
|
name: 'Non-Album Track',
|
||||||
description: 'A non-album track (special case).',
|
description: 'A non-album track (special case).',
|
||||||
value: 'NAT'
|
value: 'NAT',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,26 @@
|
||||||
import { GraphQLObjectType, GraphQLString, GraphQLBoolean } from 'graphql/type'
|
import GraphQL from 'graphql';
|
||||||
import Node from './node'
|
import { Node } from './node.js';
|
||||||
import Entity from './entity'
|
import { Entity } from './entity.js';
|
||||||
import { Time } from './scalars'
|
import { Time } from './scalars.js';
|
||||||
import {
|
import {
|
||||||
fieldWithID,
|
fieldWithID,
|
||||||
id,
|
id,
|
||||||
mbid,
|
mbid,
|
||||||
name,
|
name,
|
||||||
disambiguation,
|
disambiguation,
|
||||||
aliases,
|
connectionWithExtras,
|
||||||
lifeSpan,
|
linkedQuery,
|
||||||
relationships,
|
} from './helpers.js';
|
||||||
collections,
|
import { aliases } from './alias.js';
|
||||||
rating,
|
import { collections } from './collection.js';
|
||||||
tags,
|
import { lifeSpan } from './life-span.js';
|
||||||
connectionWithExtras
|
import { rating } from './rating.js';
|
||||||
} from './helpers'
|
import { relationships } from './relationship.js';
|
||||||
|
import { tags } from './tag.js';
|
||||||
|
|
||||||
const Event = new GraphQLObjectType({
|
const { GraphQLObjectType, GraphQLString, GraphQLBoolean } = GraphQL;
|
||||||
|
|
||||||
|
export const Event = new GraphQLObjectType({
|
||||||
name: 'Event',
|
name: 'Event',
|
||||||
description: `An [event](https://musicbrainz.org/doc/Event) refers to an
|
description: `An [event](https://musicbrainz.org/doc/Event) refers to an
|
||||||
organised event which people can attend, and is relevant to MusicBrainz.
|
organised event which people can attend, and is relevant to MusicBrainz.
|
||||||
|
|
@ -32,28 +35,29 @@ Generally this means live performances, like concerts and festivals.`,
|
||||||
lifeSpan,
|
lifeSpan,
|
||||||
time: {
|
time: {
|
||||||
type: Time,
|
type: Time,
|
||||||
description: 'The start time of the event.'
|
description: 'The start time of the event.',
|
||||||
},
|
},
|
||||||
cancelled: {
|
cancelled: {
|
||||||
type: GraphQLBoolean,
|
type: GraphQLBoolean,
|
||||||
description: 'Whether or not the event took place.'
|
description: 'Whether or not the event took place.',
|
||||||
},
|
},
|
||||||
setlist: {
|
setlist: {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: `A list of songs performed, optionally including links to
|
description: `A list of songs performed, optionally including links to
|
||||||
artists and works. See the [setlist documentation](https://musicbrainz.org/doc/Event/Setlist)
|
artists and works. See the [setlist documentation](https://musicbrainz.org/doc/Event/Setlist)
|
||||||
for syntax and examples.`
|
for syntax and examples.`,
|
||||||
},
|
},
|
||||||
...fieldWithID('type', {
|
...fieldWithID('type', {
|
||||||
description:
|
description:
|
||||||
'What kind of event the event is, e.g. concert, festival, etc.'
|
'What kind of event the event is, e.g. concert, festival, etc.',
|
||||||
}),
|
}),
|
||||||
relationships,
|
relationships,
|
||||||
collections,
|
collections,
|
||||||
rating,
|
rating,
|
||||||
tags
|
tags,
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const EventConnection = connectionWithExtras(Event)
|
export const EventConnection = connectionWithExtras(Event);
|
||||||
export default Event
|
|
||||||
|
export const events = linkedQuery(EventConnection);
|
||||||
|
|
|
||||||
|
|
@ -1,298 +1,134 @@
|
||||||
import dashify from 'dashify'
|
import GraphQL from 'graphql';
|
||||||
import pascalCase from 'pascalcase'
|
import GraphQLRelay from 'graphql-relay';
|
||||||
import {
|
import { MBID } from './scalars.js';
|
||||||
GraphQLObjectType,
|
import { ReleaseGroupType, ReleaseStatus } from './enums.js';
|
||||||
GraphQLString,
|
import { resolveLinked } from '../resolvers.js';
|
||||||
GraphQLInt,
|
import { toDashed, toPascal, toSingular, toPlural, toWords } from '../util.js';
|
||||||
GraphQLList,
|
|
||||||
GraphQLNonNull
|
const { GraphQLString, GraphQLInt, GraphQLList, GraphQLNonNull } = GraphQL;
|
||||||
} from 'graphql'
|
const {
|
||||||
import {
|
|
||||||
globalIdField,
|
globalIdField,
|
||||||
connectionArgs,
|
|
||||||
connectionDefinitions,
|
connectionDefinitions,
|
||||||
connectionFromArray,
|
forwardConnectionArgs,
|
||||||
forwardConnectionArgs
|
} = GraphQLRelay;
|
||||||
} from 'graphql-relay'
|
|
||||||
import { MBID } from './scalars'
|
|
||||||
import { ReleaseGroupType, ReleaseStatus } from './enums'
|
|
||||||
import Alias from './alias'
|
|
||||||
import ArtistCredit from './artist-credit'
|
|
||||||
import { AreaConnection } from './area'
|
|
||||||
import { ArtistConnection } from './artist'
|
|
||||||
import { CollectionConnection } from './collection'
|
|
||||||
import { EventConnection } from './event'
|
|
||||||
import { InstrumentConnection } from './instrument'
|
|
||||||
import { LabelConnection } from './label'
|
|
||||||
import LifeSpan from './life-span'
|
|
||||||
import { PlaceConnection } from './place'
|
|
||||||
import Rating from './rating'
|
|
||||||
import { RecordingConnection } from './recording'
|
|
||||||
import { RelationshipConnection } from './relationship'
|
|
||||||
import { ReleaseConnection } from './release'
|
|
||||||
import { ReleaseGroupConnection } from './release-group'
|
|
||||||
import { SeriesConnection } from './series'
|
|
||||||
import { TagConnection } from './tag'
|
|
||||||
import { WorkConnection } from './work'
|
|
||||||
import {
|
|
||||||
resolveLinked,
|
|
||||||
resolveRelationship,
|
|
||||||
createSubqueryResolver,
|
|
||||||
includeRelationships
|
|
||||||
} from '../resolvers'
|
|
||||||
|
|
||||||
export const toPascal = pascalCase
|
const TYPE_NAMES = {
|
||||||
export const toDashed = dashify
|
discid: 'Disc',
|
||||||
|
url: 'URL',
|
||||||
|
};
|
||||||
|
|
||||||
export function toPlural(name) {
|
export function resolveType(value, context, info) {
|
||||||
return name.endsWith('s') ? name : name + 's'
|
const typeName = TYPE_NAMES[value._type] || toPascal(value._type);
|
||||||
}
|
const typeMap = info.schema.getTypeMap();
|
||||||
|
return typeMap[typeName];
|
||||||
export function toSingular(name) {
|
|
||||||
return name.endsWith('s') && !/series/i.test(name) ? name.slice(0, -1) : name
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toWords(name) {
|
|
||||||
return toPascal(name).replace(/([^A-Z])?([A-Z]+)/g, (match, tail, head) => {
|
|
||||||
tail = tail ? tail + ' ' : ''
|
|
||||||
head = head.length > 1 ? head : head.toLowerCase()
|
|
||||||
return `${tail}${head}`
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveHyphenated(obj, args, context, info) {
|
export function resolveHyphenated(obj, args, context, info) {
|
||||||
const name = toDashed(info.fieldName)
|
const name = toDashed(info.fieldName);
|
||||||
return obj[name]
|
return obj[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveWithFallback(keys) {
|
export function resolveWithFallback(keys) {
|
||||||
return obj => {
|
return (obj) => {
|
||||||
for (let i = 0; i < keys.length; i++) {
|
for (let i = 0; i < keys.length; i++) {
|
||||||
const key = keys[i]
|
const key = keys[i];
|
||||||
if (key in obj) {
|
if (key in obj) {
|
||||||
return obj[key]
|
return obj[key];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fieldWithID(name, config = {}) {
|
export function fieldWithID(name, config = {}) {
|
||||||
config = {
|
config = {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
resolve: resolveHyphenated,
|
resolve: resolveHyphenated,
|
||||||
...config
|
...config,
|
||||||
}
|
};
|
||||||
const isPlural = config.type instanceof GraphQLList
|
const isPlural = config.type instanceof GraphQLList;
|
||||||
const singularName = isPlural ? toSingular(name) : name
|
const singularName = isPlural ? toSingular(name) : name;
|
||||||
const idName = isPlural ? `${singularName}IDs` : `${name}ID`
|
const idName = isPlural ? `${singularName}IDs` : `${name}ID`;
|
||||||
const s = isPlural ? 's' : ''
|
const s = isPlural ? 's' : '';
|
||||||
const idConfig = {
|
const idConfig = {
|
||||||
type: isPlural ? new GraphQLList(MBID) : MBID,
|
type: isPlural ? new GraphQLList(MBID) : MBID,
|
||||||
description: `The MBID${s} associated with the value${s} of the \`${name}\`
|
description: `The MBID${s} associated with the value${s} of the \`${name}\`
|
||||||
field.`,
|
field.`,
|
||||||
resolve: (entity, args, { loaders }) => {
|
resolve: (entity, args, { loaders }) => {
|
||||||
const fieldName = toDashed(idName)
|
const fieldName = toDashed(idName);
|
||||||
if (fieldName in entity) {
|
if (fieldName in entity) {
|
||||||
return entity[fieldName]
|
return entity[fieldName];
|
||||||
}
|
}
|
||||||
return loaders.lookup
|
return loaders.lookup
|
||||||
.load([entity._type, entity.id])
|
.load([entity._type, entity.id])
|
||||||
.then(data => data[fieldName])
|
.then((data) => data[fieldName]);
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
return {
|
return {
|
||||||
[name]: config,
|
[name]: config,
|
||||||
[idName]: idConfig
|
[idName]: idConfig,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCollectionField(config) {
|
export function createCollectionField(config) {
|
||||||
const typeName = toPlural(toWords(config.type.name.slice(0, -10)))
|
const typeName = toPlural(toWords(config.type.name.slice(0, -10)));
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
description: `The list of ${typeName} found in this collection.`
|
description: `The list of ${typeName} found in this collection.`,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const id = globalIdField()
|
export const id = globalIdField();
|
||||||
export const mbid = {
|
export const mbid = {
|
||||||
type: new GraphQLNonNull(MBID),
|
type: new GraphQLNonNull(MBID),
|
||||||
description: 'The MBID of the entity.',
|
description: 'The MBID of the entity.',
|
||||||
resolve: entity => entity.id
|
resolve: (entity) => entity.id,
|
||||||
}
|
};
|
||||||
export const name = {
|
export const name = {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: 'The official name of the entity.'
|
description: 'The official name of the entity.',
|
||||||
}
|
};
|
||||||
export const sortName = {
|
export const sortName = {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: `The string to use for the purpose of ordering by name (for
|
description: `The string to use for the purpose of ordering by name (for
|
||||||
example, by moving articles like ‘the’ to the end or a person’s last name to
|
example, by moving articles like ‘the’ to the end or a person’s last name to
|
||||||
the front).`,
|
the front).`,
|
||||||
resolve: resolveHyphenated
|
resolve: resolveHyphenated,
|
||||||
}
|
};
|
||||||
export const title = {
|
export const title = {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: 'The official title of the entity.'
|
description: 'The official title of the entity.',
|
||||||
}
|
};
|
||||||
export const disambiguation = {
|
export const disambiguation = {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: 'A comment used to help distinguish identically named entitites.'
|
description:
|
||||||
}
|
'A comment used to help distinguish identically named entitites.',
|
||||||
export const lifeSpan = {
|
};
|
||||||
type: LifeSpan,
|
|
||||||
description: `The begin and end dates of the entity’s existence. Its exact
|
|
||||||
meaning depends on the type of entity.`,
|
|
||||||
resolve: resolveHyphenated
|
|
||||||
}
|
|
||||||
|
|
||||||
function linkedQuery(connectionType, { args, ...config } = {}) {
|
export function linkedQuery(connectionType, { args, ...config } = {}) {
|
||||||
const typeName = toPlural(toWords(connectionType.name.slice(0, -10)))
|
const typeName = toPlural(toWords(connectionType.name.slice(0, -10)));
|
||||||
return {
|
return {
|
||||||
type: connectionType,
|
type: connectionType,
|
||||||
description: `A list of ${typeName} linked to this entity.`,
|
description: `A list of ${typeName} linked to this entity.`,
|
||||||
args: {
|
args: {
|
||||||
...args,
|
...args,
|
||||||
...forwardConnectionArgs
|
...forwardConnectionArgs,
|
||||||
},
|
},
|
||||||
resolve: resolveLinked,
|
resolve: resolveLinked,
|
||||||
...config
|
...config,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export const relationship = {
|
|
||||||
type: RelationshipConnection,
|
|
||||||
description: 'A list of relationships between these two entity types.',
|
|
||||||
args: {
|
|
||||||
direction: {
|
|
||||||
type: GraphQLString,
|
|
||||||
description: 'Filter by the relationship direction.'
|
|
||||||
},
|
|
||||||
...fieldWithID('type', {
|
|
||||||
description: 'Filter by the relationship type.'
|
|
||||||
}),
|
|
||||||
...connectionArgs
|
|
||||||
},
|
|
||||||
resolve: resolveRelationship
|
|
||||||
}
|
|
||||||
|
|
||||||
export const relationships = {
|
|
||||||
type: new GraphQLObjectType({
|
|
||||||
name: 'Relationships',
|
|
||||||
description: 'Lists of entity relationships for each entity type.',
|
|
||||||
fields: () => ({
|
|
||||||
areas: relationship,
|
|
||||||
artists: relationship,
|
|
||||||
events: relationship,
|
|
||||||
instruments: relationship,
|
|
||||||
labels: relationship,
|
|
||||||
places: relationship,
|
|
||||||
recordings: relationship,
|
|
||||||
releases: relationship,
|
|
||||||
releaseGroups: relationship,
|
|
||||||
series: relationship,
|
|
||||||
urls: relationship,
|
|
||||||
works: relationship
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
description: 'Relationships between this entity and other entitites.',
|
|
||||||
resolve: (entity, args, { loaders }, info) => {
|
|
||||||
let promise
|
|
||||||
if (entity.relations != null) {
|
|
||||||
promise = Promise.resolve(entity)
|
|
||||||
} else {
|
|
||||||
const entityType = toDashed(info.parentType.name)
|
|
||||||
const id = entity.id
|
|
||||||
const params = includeRelationships({}, info)
|
|
||||||
promise = loaders.lookup.load([entityType, id, params])
|
|
||||||
}
|
|
||||||
return promise.then(entity => entity.relations)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const aliases = {
|
|
||||||
type: new GraphQLList(Alias),
|
|
||||||
description: `[Aliases](https://musicbrainz.org/doc/Aliases) are used to store
|
|
||||||
alternate names or misspellings.`,
|
|
||||||
resolve: createSubqueryResolver()
|
|
||||||
}
|
|
||||||
|
|
||||||
export const artistCredits = {
|
|
||||||
type: new GraphQLList(ArtistCredit),
|
|
||||||
description: 'The main credited artist(s).',
|
|
||||||
resolve: createSubqueryResolver({ key: 'artist-credit' })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const artistCredit = {
|
|
||||||
...artistCredits,
|
|
||||||
deprecationReason: `The \`artistCredit\` field has been renamed to
|
|
||||||
\`artistCredits\`, since it is a list of credits and is referred to in the
|
|
||||||
plural form throughout the MusicBrainz documentation. This field is deprecated
|
|
||||||
and will be removed in a major release in the future. Use the equivalent
|
|
||||||
\`artistCredits\` field.`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const rating = {
|
|
||||||
type: Rating,
|
|
||||||
description: 'The rating users have given to this entity.',
|
|
||||||
resolve: createSubqueryResolver({ inc: 'ratings' })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const releaseGroupType = {
|
|
||||||
type: new GraphQLList(ReleaseGroupType),
|
|
||||||
description: 'Filter by one or more release group types.'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const releaseStatus = {
|
|
||||||
type: new GraphQLList(ReleaseStatus),
|
|
||||||
description: 'Filter by one or more release statuses.'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const areas = linkedQuery(AreaConnection)
|
|
||||||
export const artists = linkedQuery(ArtistConnection)
|
|
||||||
export const collections = linkedQuery(CollectionConnection, {
|
|
||||||
description: 'A list of collections containing this entity.'
|
|
||||||
})
|
|
||||||
export const events = linkedQuery(EventConnection)
|
|
||||||
export const instruments = linkedQuery(InstrumentConnection)
|
|
||||||
export const labels = linkedQuery(LabelConnection)
|
|
||||||
export const places = linkedQuery(PlaceConnection)
|
|
||||||
export const recordings = linkedQuery(RecordingConnection)
|
|
||||||
export const releases = linkedQuery(ReleaseConnection, {
|
|
||||||
args: {
|
|
||||||
type: releaseGroupType,
|
|
||||||
status: releaseStatus
|
|
||||||
}
|
|
||||||
})
|
|
||||||
export const releaseGroups = linkedQuery(ReleaseGroupConnection, {
|
|
||||||
args: {
|
|
||||||
type: releaseGroupType
|
|
||||||
}
|
|
||||||
})
|
|
||||||
export const series = linkedQuery(SeriesConnection)
|
|
||||||
export const tags = linkedQuery(TagConnection, {
|
|
||||||
resolve: createSubqueryResolver({}, (value = [], args) => {
|
|
||||||
const connection = connectionFromArray(value, args)
|
|
||||||
return {
|
|
||||||
nodes: connection.edges.map(edge => edge.node),
|
|
||||||
totalCount: value.length,
|
|
||||||
...connection
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
export const works = linkedQuery(WorkConnection)
|
|
||||||
|
|
||||||
export const totalCount = {
|
export const totalCount = {
|
||||||
type: GraphQLInt,
|
type: GraphQLInt,
|
||||||
description: `A count of the total number of items in this connection,
|
description: `A count of the total number of items in this connection,
|
||||||
ignoring pagination.`
|
ignoring pagination.`,
|
||||||
}
|
};
|
||||||
|
|
||||||
export const score = {
|
export const score = {
|
||||||
type: GraphQLInt,
|
type: GraphQLInt,
|
||||||
description: `The relevancy score (0–100) assigned by the search engine, if
|
description: `The relevancy score (0–100) assigned by the search engine, if
|
||||||
these results were found through a search.`
|
these results were found through a search.`,
|
||||||
}
|
};
|
||||||
|
|
||||||
export function connectionWithExtras(nodeType) {
|
export function connectionWithExtras(nodeType) {
|
||||||
return connectionDefinitions({
|
return connectionDefinitions({
|
||||||
|
|
@ -301,10 +137,20 @@ export function connectionWithExtras(nodeType) {
|
||||||
nodes: {
|
nodes: {
|
||||||
type: new GraphQLList(nodeType),
|
type: new GraphQLList(nodeType),
|
||||||
description: `A list of nodes in the connection (without going through the
|
description: `A list of nodes in the connection (without going through the
|
||||||
\`edges\` field).`
|
\`edges\` field).`,
|
||||||
},
|
},
|
||||||
totalCount
|
totalCount,
|
||||||
}),
|
}),
|
||||||
edgeFields: () => ({ score })
|
edgeFields: () => ({ score }),
|
||||||
}).connectionType
|
}).connectionType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const releaseGroupType = {
|
||||||
|
type: new GraphQLList(ReleaseGroupType),
|
||||||
|
description: 'Filter by one or more release group types.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const releaseStatus = {
|
||||||
|
type: new GraphQLList(ReleaseStatus),
|
||||||
|
description: 'Filter by one or more release statuses.',
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,27 @@
|
||||||
export { DateType, DiscID, IPI, ISRC, ISWC, MBID, URLString } from './scalars'
|
|
||||||
export { ReleaseGroupType, ReleaseStatus } from './enums'
|
|
||||||
export { default as Node } from './node'
|
|
||||||
export { default as Entity, EntityConnection } from './entity'
|
|
||||||
export { default as Area, AreaConnection } from './area'
|
|
||||||
export { default as Artist, ArtistConnection } from './artist'
|
|
||||||
export { default as Collection, CollectionConnection } from './collection'
|
|
||||||
export { default as Disc } from './disc'
|
|
||||||
export { default as Event, EventConnection } from './event'
|
|
||||||
export { default as Instrument, InstrumentConnection } from './instrument'
|
|
||||||
export { default as Label, LabelConnection } from './label'
|
|
||||||
export { default as Place, PlaceConnection } from './place'
|
|
||||||
export { default as Recording, RecordingConnection } from './recording'
|
|
||||||
export { default as Release, ReleaseConnection } from './release'
|
|
||||||
export {
|
export {
|
||||||
default as ReleaseGroup,
|
DateType,
|
||||||
ReleaseGroupConnection
|
DiscID,
|
||||||
} from './release-group'
|
IPI,
|
||||||
export { default as Series, SeriesConnection } from './series'
|
ISRC,
|
||||||
export { default as Tag, TagConnection } from './tag'
|
ISWC,
|
||||||
export { default as URL, URLConnection } from './url'
|
MBID,
|
||||||
export { default as Work, WorkConnection } from './work'
|
URLString,
|
||||||
|
} from './scalars.js';
|
||||||
|
export { ReleaseGroupType, ReleaseStatus } from './enums.js';
|
||||||
|
export { Node } from './node.js';
|
||||||
|
export { Entity, EntityConnection } from './entity.js';
|
||||||
|
export { Area, AreaConnection } from './area.js';
|
||||||
|
export { Artist, ArtistConnection } from './artist.js';
|
||||||
|
export { Collection, CollectionConnection } from './collection.js';
|
||||||
|
export { Disc } from './disc.js';
|
||||||
|
export { Event, EventConnection } from './event.js';
|
||||||
|
export { Instrument, InstrumentConnection } from './instrument.js';
|
||||||
|
export { Label, LabelConnection } from './label.js';
|
||||||
|
export { Place, PlaceConnection } from './place.js';
|
||||||
|
export { Recording, RecordingConnection } from './recording.js';
|
||||||
|
export { Release, ReleaseConnection } from './release.js';
|
||||||
|
export { ReleaseGroup, ReleaseGroupConnection } from './release-group.js';
|
||||||
|
export { Series, SeriesConnection } from './series.js';
|
||||||
|
export { Tag, TagConnection } from './tag.js';
|
||||||
|
export { URL, URLConnection } from './url.js';
|
||||||
|
export { Work, WorkConnection } from './work.js';
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,23 @@
|
||||||
import { GraphQLObjectType, GraphQLString } from 'graphql/type'
|
import GraphQL from 'graphql';
|
||||||
import Node from './node'
|
import { Node } from './node.js';
|
||||||
import Entity from './entity'
|
import { Entity } from './entity.js';
|
||||||
import {
|
import {
|
||||||
fieldWithID,
|
fieldWithID,
|
||||||
id,
|
id,
|
||||||
mbid,
|
mbid,
|
||||||
name,
|
name,
|
||||||
disambiguation,
|
disambiguation,
|
||||||
aliases,
|
connectionWithExtras,
|
||||||
relationships,
|
linkedQuery,
|
||||||
collections,
|
} from './helpers.js';
|
||||||
tags,
|
import { aliases } from './alias.js';
|
||||||
connectionWithExtras
|
import { collections } from './collection.js';
|
||||||
} from './helpers'
|
import { relationships } from './relationship.js';
|
||||||
|
import { tags } from './tag.js';
|
||||||
|
|
||||||
const Instrument = new GraphQLObjectType({
|
const { GraphQLObjectType, GraphQLString } = GraphQL;
|
||||||
|
|
||||||
|
export const Instrument = new GraphQLObjectType({
|
||||||
name: 'Instrument',
|
name: 'Instrument',
|
||||||
description: `[Instruments](https://musicbrainz.org/doc/Instrument) are
|
description: `[Instruments](https://musicbrainz.org/doc/Instrument) are
|
||||||
devices created or adapted to make musical sounds. Instruments are primarily
|
devices created or adapted to make musical sounds. Instruments are primarily
|
||||||
|
|
@ -29,18 +32,19 @@ used in relationships between two other entities.`,
|
||||||
description: {
|
description: {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: `A brief description of the main characteristics of the
|
description: `A brief description of the main characteristics of the
|
||||||
instrument.`
|
instrument.`,
|
||||||
},
|
},
|
||||||
...fieldWithID('type', {
|
...fieldWithID('type', {
|
||||||
description: `The type categorises the instrument by the way the sound is
|
description: `The type categorises the instrument by the way the sound is
|
||||||
created, similar to the [Hornbostel-Sachs](https://en.wikipedia.org/wiki/Hornbostel%E2%80%93Sachs)
|
created, similar to the [Hornbostel-Sachs](https://en.wikipedia.org/wiki/Hornbostel%E2%80%93Sachs)
|
||||||
classification.`
|
classification.`,
|
||||||
}),
|
}),
|
||||||
relationships,
|
relationships,
|
||||||
collections,
|
collections,
|
||||||
tags
|
tags,
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const InstrumentConnection = connectionWithExtras(Instrument)
|
export const InstrumentConnection = connectionWithExtras(Instrument);
|
||||||
export default Instrument
|
|
||||||
|
export const instruments = linkedQuery(InstrumentConnection);
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,29 @@
|
||||||
import {
|
import GraphQL from 'graphql';
|
||||||
GraphQLObjectType,
|
import { Node } from './node.js';
|
||||||
GraphQLList,
|
import { Entity } from './entity.js';
|
||||||
GraphQLString,
|
import { IPI } from './scalars.js';
|
||||||
GraphQLInt
|
import { Area } from './area.js';
|
||||||
} from 'graphql/type'
|
|
||||||
import Node from './node'
|
|
||||||
import Entity from './entity'
|
|
||||||
import { IPI } from './scalars'
|
|
||||||
import Area from './area'
|
|
||||||
import {
|
import {
|
||||||
id,
|
id,
|
||||||
mbid,
|
mbid,
|
||||||
name,
|
name,
|
||||||
sortName,
|
sortName,
|
||||||
disambiguation,
|
disambiguation,
|
||||||
aliases,
|
|
||||||
lifeSpan,
|
|
||||||
releases,
|
|
||||||
relationships,
|
|
||||||
collections,
|
|
||||||
tags,
|
|
||||||
rating,
|
|
||||||
fieldWithID,
|
fieldWithID,
|
||||||
connectionWithExtras
|
connectionWithExtras,
|
||||||
} from './helpers'
|
linkedQuery,
|
||||||
|
} from './helpers.js';
|
||||||
|
import { aliases } from './alias.js';
|
||||||
|
import { collections } from './collection.js';
|
||||||
|
import { lifeSpan } from './life-span.js';
|
||||||
|
import { tags } from './tag.js';
|
||||||
|
import { rating } from './rating.js';
|
||||||
|
import { relationships } from './relationship.js';
|
||||||
|
import { releases } from './release.js';
|
||||||
|
|
||||||
const Label = new GraphQLObjectType({
|
const { GraphQLObjectType, GraphQLList, GraphQLString, GraphQLInt } = GraphQL;
|
||||||
|
|
||||||
|
export const Label = new GraphQLObjectType({
|
||||||
name: 'Label',
|
name: 'Label',
|
||||||
description: `[Labels](https://musicbrainz.org/doc/Label) represent mostly
|
description: `[Labels](https://musicbrainz.org/doc/Label) represent mostly
|
||||||
(but not only) imprints. To a lesser extent, a label entity may be created to
|
(but not only) imprints. To a lesser extent, a label entity may be created to
|
||||||
|
|
@ -40,34 +38,35 @@ represent a record company.`,
|
||||||
aliases,
|
aliases,
|
||||||
country: {
|
country: {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: 'The country of origin for the label.'
|
description: 'The country of origin for the label.',
|
||||||
},
|
},
|
||||||
area: {
|
area: {
|
||||||
type: Area,
|
type: Area,
|
||||||
description: 'The area in which the label is based.'
|
description: 'The area in which the label is based.',
|
||||||
},
|
},
|
||||||
lifeSpan,
|
lifeSpan,
|
||||||
labelCode: {
|
labelCode: {
|
||||||
type: GraphQLInt,
|
type: GraphQLInt,
|
||||||
description: `The [“LC” code](https://musicbrainz.org/doc/Label/Label_Code)
|
description: `The [“LC” code](https://musicbrainz.org/doc/Label/Label_Code)
|
||||||
of the label.`
|
of the label.`,
|
||||||
},
|
},
|
||||||
ipis: {
|
ipis: {
|
||||||
type: new GraphQLList(IPI),
|
type: new GraphQLList(IPI),
|
||||||
description: `List of [Interested Parties Information](https://musicbrainz.org/doc/IPI)
|
description: `List of [Interested Parties Information](https://musicbrainz.org/doc/IPI)
|
||||||
codes for the label.`
|
codes for the label.`,
|
||||||
},
|
},
|
||||||
...fieldWithID('type', {
|
...fieldWithID('type', {
|
||||||
description: `A type describing the main activity of the label, e.g.
|
description: `A type describing the main activity of the label, e.g.
|
||||||
imprint, production, distributor, rights society, etc.`
|
imprint, production, distributor, rights society, etc.`,
|
||||||
}),
|
}),
|
||||||
releases,
|
releases,
|
||||||
relationships,
|
relationships,
|
||||||
collections,
|
collections,
|
||||||
rating,
|
rating,
|
||||||
tags
|
tags,
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const LabelConnection = connectionWithExtras(Label)
|
export const LabelConnection = connectionWithExtras(Label);
|
||||||
export default Label
|
|
||||||
|
export const labels = linkedQuery(LabelConnection);
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,32 @@
|
||||||
import { GraphQLObjectType, GraphQLBoolean } from 'graphql/type'
|
import GraphQL from 'graphql';
|
||||||
import { DateType } from './scalars'
|
import { DateType } from './scalars.js';
|
||||||
|
import { resolveHyphenated } from './helpers.js';
|
||||||
|
|
||||||
export default new GraphQLObjectType({
|
const { GraphQLObjectType, GraphQLBoolean } = GraphQL;
|
||||||
|
|
||||||
|
export const LifeSpan = new GraphQLObjectType({
|
||||||
name: 'LifeSpan',
|
name: 'LifeSpan',
|
||||||
description: `Fields indicating the begin and end date of an entity’s
|
description: `Fields indicating the begin and end date of an entity’s
|
||||||
lifetime, including whether it has ended (even if the date is unknown).`,
|
lifetime, including whether it has ended (even if the date is unknown).`,
|
||||||
fields: () => ({
|
fields: () => ({
|
||||||
begin: {
|
begin: {
|
||||||
type: DateType,
|
type: DateType,
|
||||||
description: 'The start date of the entity’s life span.'
|
description: 'The start date of the entity’s life span.',
|
||||||
},
|
},
|
||||||
end: {
|
end: {
|
||||||
type: DateType,
|
type: DateType,
|
||||||
description: 'The end date of the entity’s life span.'
|
description: 'The end date of the entity’s life span.',
|
||||||
},
|
},
|
||||||
ended: {
|
ended: {
|
||||||
type: GraphQLBoolean,
|
type: GraphQLBoolean,
|
||||||
description: 'Whether or not the entity’s life span has ended.'
|
description: 'Whether or not the entity’s life span has ended.',
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
|
export const lifeSpan = {
|
||||||
|
type: LifeSpan,
|
||||||
|
description: `The begin and end dates of the entity’s existence. Its exact
|
||||||
|
meaning depends on the type of entity.`,
|
||||||
|
resolve: resolveHyphenated,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import {
|
import GraphQL from 'graphql';
|
||||||
GraphQLObjectType,
|
import { Disc } from './disc.js';
|
||||||
GraphQLList,
|
import { Track } from './track.js';
|
||||||
GraphQLString,
|
import { resolveHyphenated, fieldWithID } from './helpers.js';
|
||||||
GraphQLInt
|
import { createSubqueryResolver } from '../resolvers.js';
|
||||||
} from 'graphql/type'
|
|
||||||
import Disc from './disc'
|
|
||||||
import { resolveHyphenated, fieldWithID } from './helpers'
|
|
||||||
|
|
||||||
export default new GraphQLObjectType({
|
const { GraphQLObjectType, GraphQLList, GraphQLString, GraphQLInt } = GraphQL;
|
||||||
|
|
||||||
|
export const Media = new GraphQLObjectType({
|
||||||
name: 'Medium',
|
name: 'Medium',
|
||||||
description: `A medium is the actual physical medium the audio content is
|
description: `A medium is the actual physical medium the audio content is
|
||||||
stored upon. This means that each CD in a multi-disc release will be entered as
|
stored upon. This means that each CD in a multi-disc release will be entered as
|
||||||
|
|
@ -17,26 +16,34 @@ cassette) and can optionally also have a title.`,
|
||||||
fields: () => ({
|
fields: () => ({
|
||||||
title: {
|
title: {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: 'The title of this particular medium.'
|
description: 'The title of this particular medium.',
|
||||||
},
|
},
|
||||||
...fieldWithID('format', {
|
...fieldWithID('format', {
|
||||||
description: `The [format](https://musicbrainz.org/doc/Release/Format) of
|
description: `The [format](https://musicbrainz.org/doc/Release/Format) of
|
||||||
the medium (e.g. CD, DVD, vinyl, cassette).`
|
the medium (e.g. CD, DVD, vinyl, cassette).`,
|
||||||
}),
|
}),
|
||||||
position: {
|
position: {
|
||||||
type: GraphQLInt,
|
type: GraphQLInt,
|
||||||
description: `The order of this medium in the release (for example, in a
|
description: `The order of this medium in the release (for example, in a
|
||||||
multi-disc release).`
|
multi-disc release).`,
|
||||||
},
|
},
|
||||||
trackCount: {
|
trackCount: {
|
||||||
type: GraphQLInt,
|
type: GraphQLInt,
|
||||||
description: 'The number of audio tracks on this medium.',
|
description: 'The number of audio tracks on this medium.',
|
||||||
resolve: resolveHyphenated
|
resolve: resolveHyphenated,
|
||||||
},
|
},
|
||||||
discs: {
|
discs: {
|
||||||
type: new GraphQLList(Disc),
|
type: new GraphQLList(Disc),
|
||||||
description:
|
description:
|
||||||
'A list of physical discs and their disc IDs for this medium.'
|
'A list of physical discs and their disc IDs for this medium.',
|
||||||
}
|
},
|
||||||
})
|
tracks: {
|
||||||
})
|
type: new GraphQLList(Track),
|
||||||
|
description: 'The list of tracks on the given media.',
|
||||||
|
resolve: createSubqueryResolver({
|
||||||
|
inc: 'recordings',
|
||||||
|
key: 'tracks',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,17 @@
|
||||||
import { nodeDefinitions, fromGlobalId } from 'graphql-relay'
|
import GraphQLRelay from 'graphql-relay';
|
||||||
import { toDashed } from './helpers'
|
import { toDashed } from '../util.js';
|
||||||
|
import { resolveType } from './helpers.js';
|
||||||
|
|
||||||
const debug = require('debug')('graphbrainz:types/node')
|
const { nodeDefinitions, fromGlobalId } = GraphQLRelay;
|
||||||
|
|
||||||
const TYPE_MODULES = {
|
|
||||||
discid: 'disc'
|
|
||||||
}
|
|
||||||
|
|
||||||
const { nodeInterface, nodeField } = nodeDefinitions(
|
const { nodeInterface, nodeField } = nodeDefinitions(
|
||||||
(globalID, { loaders }) => {
|
(globalID, { loaders }) => {
|
||||||
const { type, id } = fromGlobalId(globalID)
|
const { type, id } = fromGlobalId(globalID);
|
||||||
const entityType = toDashed(type)
|
const entityType = toDashed(type);
|
||||||
return loaders.lookup.load([entityType, id])
|
return loaders.lookup.load([entityType, id]);
|
||||||
},
|
},
|
||||||
(obj, context, info) => {
|
resolveType
|
||||||
const type = TYPE_MODULES[obj._type] || obj._type
|
);
|
||||||
if (type) {
|
|
||||||
let originalType
|
|
||||||
try {
|
|
||||||
originalType = require(`./${type}`).default
|
|
||||||
} catch (err) {
|
|
||||||
debug(`Failed to load type: ${type}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Don't use `originalType`! The schema may have been extended in which
|
|
||||||
// case the types have all been replaced. Instead, find the current type
|
|
||||||
// of the same name.
|
|
||||||
const typeMap = info.schema.getTypeMap()
|
|
||||||
return typeMap[originalType.name]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export default nodeInterface
|
export const Node = nodeInterface;
|
||||||
export { nodeInterface, nodeField }
|
export { nodeInterface, nodeField };
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,25 @@
|
||||||
import { GraphQLObjectType, GraphQLString } from 'graphql/type'
|
import GraphQL from 'graphql';
|
||||||
import Node from './node'
|
import { Node } from './node.js';
|
||||||
import Entity from './entity'
|
import { Entity } from './entity.js';
|
||||||
import { Degrees } from './scalars'
|
import { Degrees } from './scalars.js';
|
||||||
import Area from './area'
|
import { Area } from './area.js';
|
||||||
import {
|
import {
|
||||||
id,
|
id,
|
||||||
mbid,
|
mbid,
|
||||||
name,
|
name,
|
||||||
disambiguation,
|
disambiguation,
|
||||||
aliases,
|
|
||||||
lifeSpan,
|
|
||||||
events,
|
|
||||||
fieldWithID,
|
fieldWithID,
|
||||||
relationships,
|
connectionWithExtras,
|
||||||
collections,
|
linkedQuery,
|
||||||
tags,
|
} from './helpers.js';
|
||||||
connectionWithExtras
|
import { aliases } from './alias.js';
|
||||||
} from './helpers'
|
import { collections } from './collection.js';
|
||||||
|
import { events } from './event.js';
|
||||||
|
import { lifeSpan } from './life-span.js';
|
||||||
|
import { relationships } from './relationship.js';
|
||||||
|
import { tags } from './tag.js';
|
||||||
|
|
||||||
|
const { GraphQLObjectType, GraphQLString } = GraphQL;
|
||||||
|
|
||||||
export const Coordinates = new GraphQLObjectType({
|
export const Coordinates = new GraphQLObjectType({
|
||||||
name: 'Coordinates',
|
name: 'Coordinates',
|
||||||
|
|
@ -24,16 +27,17 @@ export const Coordinates = new GraphQLObjectType({
|
||||||
fields: () => ({
|
fields: () => ({
|
||||||
latitude: {
|
latitude: {
|
||||||
type: Degrees,
|
type: Degrees,
|
||||||
description: 'The north–south position of a point on the Earth’s surface.'
|
description:
|
||||||
|
'The north–south position of a point on the Earth’s surface.',
|
||||||
},
|
},
|
||||||
longitude: {
|
longitude: {
|
||||||
type: Degrees,
|
type: Degrees,
|
||||||
description: 'The east–west position of a point on the Earth’s surface.'
|
description: 'The east–west position of a point on the Earth’s surface.',
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
const Place = new GraphQLObjectType({
|
export const Place = new GraphQLObjectType({
|
||||||
name: 'Place',
|
name: 'Place',
|
||||||
description: `A [place](https://musicbrainz.org/doc/Place) is a venue, studio,
|
description: `A [place](https://musicbrainz.org/doc/Place) is a venue, studio,
|
||||||
or other place where music is performed, recorded, engineered, etc.`,
|
or other place where music is performed, recorded, engineered, etc.`,
|
||||||
|
|
@ -47,28 +51,29 @@ or other place where music is performed, recorded, engineered, etc.`,
|
||||||
address: {
|
address: {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: `The address describes the location of the place using the
|
description: `The address describes the location of the place using the
|
||||||
standard addressing format for the country it is located in.`
|
standard addressing format for the country it is located in.`,
|
||||||
},
|
},
|
||||||
area: {
|
area: {
|
||||||
type: Area,
|
type: Area,
|
||||||
description: `The area entity representing the area, such as the city, in
|
description: `The area entity representing the area, such as the city, in
|
||||||
which the place is located.`
|
which the place is located.`,
|
||||||
},
|
},
|
||||||
coordinates: {
|
coordinates: {
|
||||||
type: Coordinates,
|
type: Coordinates,
|
||||||
description: 'The geographic coordinates of the place.'
|
description: 'The geographic coordinates of the place.',
|
||||||
},
|
},
|
||||||
lifeSpan,
|
lifeSpan,
|
||||||
...fieldWithID('type', {
|
...fieldWithID('type', {
|
||||||
description: `The type categorises the place based on its primary
|
description: `The type categorises the place based on its primary
|
||||||
function.`
|
function.`,
|
||||||
}),
|
}),
|
||||||
events,
|
events,
|
||||||
relationships,
|
relationships,
|
||||||
collections,
|
collections,
|
||||||
tags
|
tags,
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const PlaceConnection = connectionWithExtras(Place)
|
export const PlaceConnection = connectionWithExtras(Place);
|
||||||
export default Place
|
|
||||||
|
export const places = linkedQuery(PlaceConnection);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import {
|
import GraphQL from 'graphql';
|
||||||
GraphQLObjectType,
|
import { createSubqueryResolver } from '../resolvers.js';
|
||||||
GraphQLNonNull,
|
|
||||||
GraphQLInt,
|
|
||||||
GraphQLFloat
|
|
||||||
} from 'graphql/type'
|
|
||||||
|
|
||||||
export default new GraphQLObjectType({
|
const { GraphQLObjectType, GraphQLNonNull, GraphQLInt, GraphQLFloat } = GraphQL;
|
||||||
|
|
||||||
|
export const Rating = new GraphQLObjectType({
|
||||||
name: 'Rating',
|
name: 'Rating',
|
||||||
description: `[Ratings](https://musicbrainz.org/doc/Rating_System) allow users
|
description: `[Ratings](https://musicbrainz.org/doc/Rating_System) allow users
|
||||||
to rate MusicBrainz entities. User may assign a value between 1 and 5; these
|
to rate MusicBrainz entities. User may assign a value between 1 and 5; these
|
||||||
|
|
@ -15,11 +13,17 @@ for the entity.`,
|
||||||
voteCount: {
|
voteCount: {
|
||||||
type: new GraphQLNonNull(GraphQLInt),
|
type: new GraphQLNonNull(GraphQLInt),
|
||||||
description: 'The number of votes that have contributed to the rating.',
|
description: 'The number of votes that have contributed to the rating.',
|
||||||
resolve: rating => rating['votes-count']
|
resolve: (rating) => rating['votes-count'],
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
type: GraphQLFloat,
|
type: GraphQLFloat,
|
||||||
description: 'The average rating value based on the aggregated votes.'
|
description: 'The average rating value based on the aggregated votes.',
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
|
export const rating = {
|
||||||
|
type: Rating,
|
||||||
|
description: 'The rating users have given to this entity.',
|
||||||
|
resolve: createSubqueryResolver({ inc: 'ratings' }),
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,27 @@
|
||||||
import { GraphQLObjectType, GraphQLList, GraphQLBoolean } from 'graphql/type'
|
import GraphQL from 'graphql';
|
||||||
import Node from './node'
|
import { Node } from './node.js';
|
||||||
import Entity from './entity'
|
import { Entity } from './entity.js';
|
||||||
import { Duration, ISRC } from './scalars'
|
import { Duration, ISRC } from './scalars.js';
|
||||||
import {
|
import {
|
||||||
id,
|
id,
|
||||||
mbid,
|
mbid,
|
||||||
title,
|
title,
|
||||||
disambiguation,
|
disambiguation,
|
||||||
aliases,
|
connectionWithExtras,
|
||||||
artistCredit,
|
linkedQuery,
|
||||||
artistCredits,
|
} from './helpers.js';
|
||||||
artists,
|
import { aliases } from './alias.js';
|
||||||
releases,
|
import { artists } from './artist.js';
|
||||||
relationships,
|
import { artistCredit, artistCredits } from './artist-credit.js';
|
||||||
collections,
|
import { collections } from './collection.js';
|
||||||
rating,
|
import { tags } from './tag.js';
|
||||||
tags,
|
import { rating } from './rating.js';
|
||||||
connectionWithExtras
|
import { relationships } from './relationship.js';
|
||||||
} from './helpers'
|
import { releases } from './release.js';
|
||||||
|
|
||||||
const Recording = new GraphQLObjectType({
|
const { GraphQLObjectType, GraphQLList, GraphQLBoolean } = GraphQL;
|
||||||
|
|
||||||
|
export const Recording = new GraphQLObjectType({
|
||||||
name: 'Recording',
|
name: 'Recording',
|
||||||
description: `A [recording](https://musicbrainz.org/doc/Recording) is an
|
description: `A [recording](https://musicbrainz.org/doc/Recording) is an
|
||||||
entity in MusicBrainz which can be linked to tracks on releases. Each track must
|
entity in MusicBrainz which can be linked to tracks on releases. Each track must
|
||||||
|
|
@ -48,33 +50,34 @@ or mixing.`,
|
||||||
(ISRCs) for this recording.`,
|
(ISRCs) for this recording.`,
|
||||||
resolve: (source, args, context) => {
|
resolve: (source, args, context) => {
|
||||||
if (source.isrcs) {
|
if (source.isrcs) {
|
||||||
return source.isrcs
|
return source.isrcs;
|
||||||
}
|
}
|
||||||
// TODO: Add support for parent entities knowing to include this `inc`
|
// TODO: Add support for parent entities knowing to include this `inc`
|
||||||
// parameter in their own calls by inspecting what fields are requested
|
// parameter in their own calls by inspecting what fields are requested
|
||||||
// or batching things at the loader level.
|
// or batching things at the loader level.
|
||||||
return context.loaders.lookup
|
return context.loaders.lookup
|
||||||
.load(['recording', source.id, { inc: 'isrcs' }])
|
.load(['recording', source.id, { inc: 'isrcs' }])
|
||||||
.then(recording => recording.isrcs)
|
.then((recording) => recording.isrcs);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
length: {
|
length: {
|
||||||
type: Duration,
|
type: Duration,
|
||||||
description: `An approximation to the length of the recording, calculated
|
description: `An approximation to the length of the recording, calculated
|
||||||
from the lengths of the tracks using it.`
|
from the lengths of the tracks using it.`,
|
||||||
},
|
},
|
||||||
video: {
|
video: {
|
||||||
type: GraphQLBoolean,
|
type: GraphQLBoolean,
|
||||||
description: 'Whether this is a video recording.'
|
description: 'Whether this is a video recording.',
|
||||||
},
|
},
|
||||||
artists,
|
artists,
|
||||||
releases,
|
releases,
|
||||||
relationships,
|
relationships,
|
||||||
collections,
|
collections,
|
||||||
rating,
|
rating,
|
||||||
tags
|
tags,
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const RecordingConnection = connectionWithExtras(Recording)
|
export const RecordingConnection = connectionWithExtras(Recording);
|
||||||
export default Recording
|
|
||||||
|
export const recordings = linkedQuery(RecordingConnection);
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,25 @@
|
||||||
|
import GraphQL from 'graphql';
|
||||||
|
import GraphQLRelay from 'graphql-relay';
|
||||||
|
import { DateType } from './scalars.js';
|
||||||
|
import { Entity } from './entity.js';
|
||||||
import {
|
import {
|
||||||
|
resolveHyphenated,
|
||||||
|
fieldWithID,
|
||||||
|
connectionWithExtras,
|
||||||
|
} from './helpers.js';
|
||||||
|
import { resolveRelationship, includeRelationships } from '../resolvers.js';
|
||||||
|
import { toDashed } from '../util.js';
|
||||||
|
|
||||||
|
const {
|
||||||
GraphQLObjectType,
|
GraphQLObjectType,
|
||||||
GraphQLNonNull,
|
GraphQLNonNull,
|
||||||
GraphQLString,
|
GraphQLString,
|
||||||
GraphQLList,
|
GraphQLList,
|
||||||
GraphQLBoolean
|
GraphQLBoolean,
|
||||||
} from 'graphql/type'
|
} = GraphQL;
|
||||||
import { DateType } from './scalars'
|
const { connectionArgs } = GraphQLRelay;
|
||||||
import Entity from './entity'
|
|
||||||
import { resolveHyphenated, fieldWithID, connectionWithExtras } from './helpers'
|
|
||||||
|
|
||||||
const Relationship = new GraphQLObjectType({
|
export const Relationship = new GraphQLObjectType({
|
||||||
name: 'Relationship',
|
name: 'Relationship',
|
||||||
description: `[Relationships](https://musicbrainz.org/doc/Relationships) are a
|
description: `[Relationships](https://musicbrainz.org/doc/Relationships) are a
|
||||||
way to represent all the different ways in which entities are connected to each
|
way to represent all the different ways in which entities are connected to each
|
||||||
|
|
@ -18,63 +28,112 @@ other and to URLs outside MusicBrainz.`,
|
||||||
target: {
|
target: {
|
||||||
type: new GraphQLNonNull(Entity),
|
type: new GraphQLNonNull(Entity),
|
||||||
description: 'The target entity.',
|
description: 'The target entity.',
|
||||||
resolve: source => {
|
resolve: (source) => {
|
||||||
const targetType = source['target-type']
|
const targetType = source['target-type'];
|
||||||
const target = source[targetType]
|
const target = source[targetType];
|
||||||
target._type = targetType.replace('_', '-')
|
target._type = targetType.replace('_', '-');
|
||||||
return target
|
return target;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
direction: {
|
direction: {
|
||||||
type: new GraphQLNonNull(GraphQLString),
|
type: new GraphQLNonNull(GraphQLString),
|
||||||
description: 'The direction of the relationship.'
|
description: 'The direction of the relationship.',
|
||||||
},
|
},
|
||||||
targetType: {
|
targetType: {
|
||||||
type: new GraphQLNonNull(GraphQLString),
|
type: new GraphQLNonNull(GraphQLString),
|
||||||
description:
|
description:
|
||||||
'The type of entity on the receiving end of the relationship.',
|
'The type of entity on the receiving end of the relationship.',
|
||||||
resolve: resolveHyphenated
|
resolve: resolveHyphenated,
|
||||||
},
|
},
|
||||||
sourceCredit: {
|
sourceCredit: {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: `How the source entity was actually credited, if different
|
description: `How the source entity was actually credited, if different
|
||||||
from its main (performance) name.`,
|
from its main (performance) name.`,
|
||||||
resolve: resolveHyphenated
|
resolve: resolveHyphenated,
|
||||||
},
|
},
|
||||||
targetCredit: {
|
targetCredit: {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: `How the target entity was actually credited, if different
|
description: `How the target entity was actually credited, if different
|
||||||
from its main (performance) name.`,
|
from its main (performance) name.`,
|
||||||
resolve: resolveHyphenated
|
resolve: resolveHyphenated,
|
||||||
},
|
},
|
||||||
begin: {
|
begin: {
|
||||||
type: DateType,
|
type: DateType,
|
||||||
description: 'The date on which the relationship became applicable.'
|
description: 'The date on which the relationship became applicable.',
|
||||||
},
|
},
|
||||||
end: {
|
end: {
|
||||||
type: DateType,
|
type: DateType,
|
||||||
description:
|
description:
|
||||||
'The date on which the relationship became no longer applicable.'
|
'The date on which the relationship became no longer applicable.',
|
||||||
},
|
},
|
||||||
ended: {
|
ended: {
|
||||||
type: GraphQLBoolean,
|
type: GraphQLBoolean,
|
||||||
description: 'Whether the relationship still applies.'
|
description: 'Whether the relationship still applies.',
|
||||||
},
|
},
|
||||||
attributes: {
|
attributes: {
|
||||||
type: new GraphQLList(GraphQLString),
|
type: new GraphQLList(GraphQLString),
|
||||||
description: `Attributes which modify the relationship. There is a [list
|
description: `Attributes which modify the relationship. There is a [list
|
||||||
of all attributes](https://musicbrainz.org/relationship-attributes), but the
|
of all attributes](https://musicbrainz.org/relationship-attributes), but the
|
||||||
attributes which are available, and how they should be used, depends on the
|
attributes which are available, and how they should be used, depends on the
|
||||||
relationship type.`
|
relationship type.`,
|
||||||
},
|
},
|
||||||
// There doesn't seem to be any documentation for the `attribute-values`
|
// There doesn't seem to be any documentation for the `attribute-values`
|
||||||
// field.
|
// field.
|
||||||
// attributeValues: {},
|
// attributeValues: {},
|
||||||
...fieldWithID('type', {
|
...fieldWithID('type', {
|
||||||
description: 'The type of relationship.'
|
description: 'The type of relationship.',
|
||||||
})
|
}),
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const RelationshipConnection = connectionWithExtras(Relationship)
|
export const RelationshipConnection = connectionWithExtras(Relationship);
|
||||||
export default Relationship
|
|
||||||
|
export const relationship = {
|
||||||
|
type: RelationshipConnection,
|
||||||
|
description: 'A list of relationships between these two entity types.',
|
||||||
|
args: {
|
||||||
|
direction: {
|
||||||
|
type: GraphQLString,
|
||||||
|
description: 'Filter by the relationship direction.',
|
||||||
|
},
|
||||||
|
...fieldWithID('type', {
|
||||||
|
description: 'Filter by the relationship type.',
|
||||||
|
}),
|
||||||
|
...connectionArgs,
|
||||||
|
},
|
||||||
|
resolve: resolveRelationship,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const relationships = {
|
||||||
|
type: new GraphQLObjectType({
|
||||||
|
name: 'Relationships',
|
||||||
|
description: 'Lists of entity relationships for each entity type.',
|
||||||
|
fields: () => ({
|
||||||
|
areas: relationship,
|
||||||
|
artists: relationship,
|
||||||
|
events: relationship,
|
||||||
|
instruments: relationship,
|
||||||
|
labels: relationship,
|
||||||
|
places: relationship,
|
||||||
|
recordings: relationship,
|
||||||
|
releases: relationship,
|
||||||
|
releaseGroups: relationship,
|
||||||
|
series: relationship,
|
||||||
|
urls: relationship,
|
||||||
|
works: relationship,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
description: 'Relationships between this entity and other entitites.',
|
||||||
|
resolve: (entity, args, { loaders }, info) => {
|
||||||
|
let promise;
|
||||||
|
if (entity.relations != null) {
|
||||||
|
promise = Promise.resolve(entity);
|
||||||
|
} else {
|
||||||
|
const entityType = toDashed(info.parentType.name);
|
||||||
|
const id = entity.id;
|
||||||
|
const params = includeRelationships({}, info);
|
||||||
|
promise = loaders.lookup.load([entityType, id, params]);
|
||||||
|
}
|
||||||
|
return promise.then((entity) => entity.relations);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import { GraphQLObjectType } from 'graphql/type'
|
import GraphQL from 'graphql';
|
||||||
import { DateType } from './scalars'
|
import { DateType } from './scalars.js';
|
||||||
import Area from './area'
|
import { Area } from './area.js';
|
||||||
|
|
||||||
export default new GraphQLObjectType({
|
const { GraphQLObjectType } = GraphQL;
|
||||||
|
|
||||||
|
export const ReleaseEvent = new GraphQLObjectType({
|
||||||
name: 'ReleaseEvent',
|
name: 'ReleaseEvent',
|
||||||
description: `The date on which a release was issued in a country/region with
|
description: `The date on which a release was issued in a country/region with
|
||||||
a particular label, catalog number, barcode, and format.`,
|
a particular label, catalog number, barcode, and format.`,
|
||||||
fields: () => ({
|
fields: () => ({
|
||||||
area: { type: Area },
|
area: { type: Area },
|
||||||
date: { type: DateType }
|
date: { type: DateType },
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,31 @@
|
||||||
import { GraphQLObjectType, GraphQLList } from 'graphql/type'
|
import GraphQL from 'graphql';
|
||||||
import Node from './node'
|
import { Node } from './node.js';
|
||||||
import Entity from './entity'
|
import { Entity } from './entity.js';
|
||||||
import { DateType } from './scalars'
|
import { DateType } from './scalars.js';
|
||||||
import { ReleaseGroupType } from './enums'
|
import { ReleaseGroupType } from './enums.js';
|
||||||
import {
|
import {
|
||||||
id,
|
id,
|
||||||
mbid,
|
mbid,
|
||||||
title,
|
title,
|
||||||
disambiguation,
|
disambiguation,
|
||||||
aliases,
|
|
||||||
artistCredit,
|
|
||||||
artistCredits,
|
|
||||||
artists,
|
|
||||||
releases,
|
|
||||||
relationships,
|
|
||||||
collections,
|
|
||||||
rating,
|
|
||||||
tags,
|
|
||||||
fieldWithID,
|
fieldWithID,
|
||||||
|
releaseGroupType,
|
||||||
resolveHyphenated,
|
resolveHyphenated,
|
||||||
connectionWithExtras
|
connectionWithExtras,
|
||||||
} from './helpers'
|
linkedQuery,
|
||||||
|
} from './helpers.js';
|
||||||
|
import { aliases } from './alias.js';
|
||||||
|
import { artistCredit, artistCredits } from './artist-credit.js';
|
||||||
|
import { artists } from './artist.js';
|
||||||
|
import { releases } from './release.js';
|
||||||
|
import { relationships } from './relationship.js';
|
||||||
|
import { collections } from './collection.js';
|
||||||
|
import { rating } from './rating.js';
|
||||||
|
import { tags } from './tag.js';
|
||||||
|
|
||||||
const ReleaseGroup = new GraphQLObjectType({
|
const { GraphQLObjectType, GraphQLList } = GraphQL;
|
||||||
|
|
||||||
|
export const ReleaseGroup = new GraphQLObjectType({
|
||||||
name: 'ReleaseGroup',
|
name: 'ReleaseGroup',
|
||||||
description: `A [release group](https://musicbrainz.org/doc/Release_Group) is
|
description: `A [release group](https://musicbrainz.org/doc/Release_Group) is
|
||||||
used to group several different releases into a single logical entity. Every
|
used to group several different releases into a single logical entity. Every
|
||||||
|
|
@ -44,28 +47,33 @@ album – it doesn’t matter how many CDs or editions/versions it had.`,
|
||||||
firstReleaseDate: {
|
firstReleaseDate: {
|
||||||
type: DateType,
|
type: DateType,
|
||||||
description: 'The date of the earliest release in the group.',
|
description: 'The date of the earliest release in the group.',
|
||||||
resolve: resolveHyphenated
|
resolve: resolveHyphenated,
|
||||||
},
|
},
|
||||||
...fieldWithID('primaryType', {
|
...fieldWithID('primaryType', {
|
||||||
type: ReleaseGroupType,
|
type: ReleaseGroupType,
|
||||||
description: `The [type](https://musicbrainz.org/doc/Release_Group/Type)
|
description: `The [type](https://musicbrainz.org/doc/Release_Group/Type)
|
||||||
of a release group describes what kind of releases the release group represents,
|
of a release group describes what kind of releases the release group represents,
|
||||||
e.g. album, single, soundtrack, compilation, etc. A release group can have a
|
e.g. album, single, soundtrack, compilation, etc. A release group can have a
|
||||||
“main” type and an unspecified number of additional types.`
|
“main” type and an unspecified number of additional types.`,
|
||||||
}),
|
}),
|
||||||
...fieldWithID('secondaryTypes', {
|
...fieldWithID('secondaryTypes', {
|
||||||
type: new GraphQLList(ReleaseGroupType),
|
type: new GraphQLList(ReleaseGroupType),
|
||||||
description: `Additional [types](https://musicbrainz.org/doc/Release_Group/Type)
|
description: `Additional [types](https://musicbrainz.org/doc/Release_Group/Type)
|
||||||
that apply to this release group.`
|
that apply to this release group.`,
|
||||||
}),
|
}),
|
||||||
artists,
|
artists,
|
||||||
releases,
|
releases,
|
||||||
relationships,
|
relationships,
|
||||||
collections,
|
collections,
|
||||||
rating,
|
rating,
|
||||||
tags
|
tags,
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const ReleaseGroupConnection = connectionWithExtras(ReleaseGroup)
|
export const ReleaseGroupConnection = connectionWithExtras(ReleaseGroup);
|
||||||
export default ReleaseGroup
|
|
||||||
|
export const releaseGroups = linkedQuery(ReleaseGroupConnection, {
|
||||||
|
args: {
|
||||||
|
type: releaseGroupType,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,35 @@
|
||||||
import { GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql/type'
|
import GraphQL from 'graphql';
|
||||||
import Node from './node'
|
import { Node } from './node.js';
|
||||||
import Entity from './entity'
|
import { Entity } from './entity.js';
|
||||||
import { ASIN, DateType } from './scalars'
|
import { ASIN, DateType } from './scalars.js';
|
||||||
import Media from './media'
|
import { Media } from './media.js';
|
||||||
import { ReleaseStatus } from './enums'
|
import { ReleaseStatus } from './enums.js';
|
||||||
import ReleaseEvent from './release-event'
|
import { ReleaseEvent } from './release-event.js';
|
||||||
import {
|
import {
|
||||||
id,
|
id,
|
||||||
mbid,
|
mbid,
|
||||||
title,
|
title,
|
||||||
disambiguation,
|
disambiguation,
|
||||||
aliases,
|
|
||||||
artistCredit,
|
|
||||||
artistCredits,
|
|
||||||
artists,
|
|
||||||
labels,
|
|
||||||
recordings,
|
|
||||||
releaseGroups,
|
|
||||||
relationships,
|
|
||||||
collections,
|
|
||||||
tags,
|
|
||||||
fieldWithID,
|
fieldWithID,
|
||||||
|
releaseGroupType,
|
||||||
|
releaseStatus,
|
||||||
resolveHyphenated,
|
resolveHyphenated,
|
||||||
connectionWithExtras
|
connectionWithExtras,
|
||||||
} from './helpers'
|
linkedQuery,
|
||||||
|
} from './helpers.js';
|
||||||
|
import { aliases } from './alias.js';
|
||||||
|
import { artistCredit, artistCredits } from './artist-credit.js';
|
||||||
|
import { artists } from './artist.js';
|
||||||
|
import { collections } from './collection.js';
|
||||||
|
import { labels } from './label.js';
|
||||||
|
import { recordings } from './recording.js';
|
||||||
|
import { relationships } from './relationship.js';
|
||||||
|
import { releaseGroups } from './release-group.js';
|
||||||
|
import { tags } from './tag.js';
|
||||||
|
|
||||||
const Release = new GraphQLObjectType({
|
const { GraphQLObjectType, GraphQLString, GraphQLList } = GraphQL;
|
||||||
|
|
||||||
|
export const Release = new GraphQLObjectType({
|
||||||
name: 'Release',
|
name: 'Release',
|
||||||
description: `A [release](https://musicbrainz.org/doc/Release) represents the
|
description: `A [release](https://musicbrainz.org/doc/Release) represents the
|
||||||
unique release (i.e. issuing) of a product on a specific date with specific
|
unique release (i.e. issuing) of a product on a specific date with specific
|
||||||
|
|
@ -44,48 +48,48 @@ MusicBrainz as one release.`,
|
||||||
releaseEvents: {
|
releaseEvents: {
|
||||||
type: new GraphQLList(ReleaseEvent),
|
type: new GraphQLList(ReleaseEvent),
|
||||||
description: 'The release events for this release.',
|
description: 'The release events for this release.',
|
||||||
resolve: resolveHyphenated
|
resolve: resolveHyphenated,
|
||||||
},
|
},
|
||||||
date: {
|
date: {
|
||||||
type: DateType,
|
type: DateType,
|
||||||
description: `The [release date](https://musicbrainz.org/doc/Release/Date)
|
description: `The [release date](https://musicbrainz.org/doc/Release/Date)
|
||||||
is the date in which a release was made available through some sort of
|
is the date in which a release was made available through some sort of
|
||||||
distribution mechanism.`
|
distribution mechanism.`,
|
||||||
},
|
},
|
||||||
country: {
|
country: {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: 'The country in which the release was issued.'
|
description: 'The country in which the release was issued.',
|
||||||
},
|
},
|
||||||
asin: {
|
asin: {
|
||||||
type: ASIN,
|
type: ASIN,
|
||||||
description: `The [Amazon Standard Identification Number](https://musicbrainz.org/doc/ASIN)
|
description: `The [Amazon Standard Identification Number](https://musicbrainz.org/doc/ASIN)
|
||||||
of the release.`
|
of the release.`,
|
||||||
},
|
},
|
||||||
barcode: {
|
barcode: {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: `The [barcode](https://en.wikipedia.org/wiki/Barcode), if the
|
description: `The [barcode](https://en.wikipedia.org/wiki/Barcode), if the
|
||||||
release has one. The most common types found on releases are 12-digit
|
release has one. The most common types found on releases are 12-digit
|
||||||
[UPCs](https://en.wikipedia.org/wiki/Universal_Product_Code) and 13-digit
|
[UPCs](https://en.wikipedia.org/wiki/Universal_Product_Code) and 13-digit
|
||||||
[EANs](https://en.wikipedia.org/wiki/International_Article_Number).`
|
[EANs](https://en.wikipedia.org/wiki/International_Article_Number).`,
|
||||||
},
|
},
|
||||||
...fieldWithID('status', {
|
...fieldWithID('status', {
|
||||||
type: ReleaseStatus,
|
type: ReleaseStatus,
|
||||||
description: 'The status describes how “official” a release is.'
|
description: 'The status describes how “official” a release is.',
|
||||||
}),
|
}),
|
||||||
...fieldWithID('packaging', {
|
...fieldWithID('packaging', {
|
||||||
description: `The physical packaging that accompanies the release. See
|
description: `The physical packaging that accompanies the release. See
|
||||||
the [list of packaging](https://musicbrainz.org/doc/Release/Packaging) for more
|
the [list of packaging](https://musicbrainz.org/doc/Release/Packaging) for more
|
||||||
information.`
|
information.`,
|
||||||
}),
|
}),
|
||||||
quality: {
|
quality: {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: `Data quality indicates how good the data for a release is.
|
description: `Data quality indicates how good the data for a release is.
|
||||||
It is not a mark of how good or bad the music itself is – for that, use
|
It is not a mark of how good or bad the music itself is – for that, use
|
||||||
[ratings](https://musicbrainz.org/doc/Rating_System).`
|
[ratings](https://musicbrainz.org/doc/Rating_System).`,
|
||||||
},
|
},
|
||||||
media: {
|
media: {
|
||||||
type: new GraphQLList(Media),
|
type: new GraphQLList(Media),
|
||||||
description: 'The media on which the release was distributed.'
|
description: 'The media on which the release was distributed.',
|
||||||
},
|
},
|
||||||
artists,
|
artists,
|
||||||
labels,
|
labels,
|
||||||
|
|
@ -93,9 +97,15 @@ It is not a mark of how good or bad the music itself is – for that, use
|
||||||
releaseGroups,
|
releaseGroups,
|
||||||
relationships,
|
relationships,
|
||||||
collections,
|
collections,
|
||||||
tags
|
tags,
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const ReleaseConnection = connectionWithExtras(Release)
|
export const ReleaseConnection = connectionWithExtras(Release);
|
||||||
export default Release
|
|
||||||
|
export const releases = linkedQuery(ReleaseConnection, {
|
||||||
|
args: {
|
||||||
|
type: releaseGroupType,
|
||||||
|
status: releaseStatus,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,71 @@
|
||||||
import { Kind } from 'graphql/language'
|
import GraphQL from 'graphql';
|
||||||
import { GraphQLScalarType } from 'graphql/type'
|
|
||||||
|
const { Kind, GraphQLScalarType } = GraphQL;
|
||||||
|
|
||||||
function createScalar(config) {
|
function createScalar(config) {
|
||||||
return new GraphQLScalarType({
|
return new GraphQLScalarType({
|
||||||
serialize: value => value,
|
serialize: (value) => value,
|
||||||
parseValue: value => value,
|
parseValue: (value) => value,
|
||||||
parseLiteral(ast) {
|
parseLiteral(ast) {
|
||||||
if (ast.kind === Kind.STRING) {
|
if (ast.kind === Kind.STRING) {
|
||||||
return ast.value
|
return ast.value;
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined;
|
||||||
},
|
},
|
||||||
...config
|
...config,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
||||||
const locale = /^([a-z]{2})(_[A-Z]{2})?(\.[a-zA-Z0-9-]+)?$/
|
const locale = /^([a-z]{2})(_[A-Z]{2})?(\.[a-zA-Z0-9-]+)?$/;
|
||||||
// Be extremely lenient; just prevent major input errors.
|
// Be extremely lenient; just prevent major input errors.
|
||||||
const url = /^\w+:\/\/[\w-]+\.\w+/
|
const url = /^\w+:\/\/[\w-]+\.\w+/;
|
||||||
|
|
||||||
function validateMBID(value) {
|
function validateMBID(value) {
|
||||||
if (typeof value === 'string' && uuid.test(value)) {
|
if (typeof value === 'string' && uuid.test(value)) {
|
||||||
return value
|
return value;
|
||||||
}
|
}
|
||||||
throw new TypeError(`Malformed MBID: ${value}`)
|
throw new TypeError(`Malformed MBID: ${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validatePositive(value) {
|
function validatePositive(value) {
|
||||||
if (value >= 0) {
|
if (value >= 0) {
|
||||||
return value
|
return value;
|
||||||
}
|
}
|
||||||
throw new TypeError(`Expected positive value: ${value}`)
|
throw new TypeError(`Expected positive value: ${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateLocale(value) {
|
function validateLocale(value) {
|
||||||
if (typeof value === 'string' && locale.test(value)) {
|
if (typeof value === 'string' && locale.test(value)) {
|
||||||
return value
|
return value;
|
||||||
}
|
}
|
||||||
throw new TypeError(`Malformed locale: ${value}`)
|
throw new TypeError(`Malformed locale: ${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateURL(value) {
|
function validateURL(value) {
|
||||||
if (typeof value === 'string' && url.test(value)) {
|
if (typeof value === 'string' && url.test(value)) {
|
||||||
return value
|
return value;
|
||||||
}
|
}
|
||||||
throw new TypeError(`Malformed URL: ${value}`)
|
throw new TypeError(`Malformed URL: ${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ASIN = createScalar({
|
export const ASIN = createScalar({
|
||||||
name: 'ASIN',
|
name: 'ASIN',
|
||||||
description: `An [Amazon Standard Identification Number](https://musicbrainz.org/doc/ASIN)
|
description: `An [Amazon Standard Identification Number](https://musicbrainz.org/doc/ASIN)
|
||||||
(ASIN) is a 10-character alphanumeric unique identifier assigned by Amazon.com
|
(ASIN) is a 10-character alphanumeric unique identifier assigned by Amazon.com
|
||||||
and its partners for product identification within the Amazon organization.`
|
and its partners for product identification within the Amazon organization.`,
|
||||||
})
|
});
|
||||||
|
|
||||||
export const DateType = createScalar({
|
export const DateType = createScalar({
|
||||||
name: 'Date',
|
name: 'Date',
|
||||||
description:
|
description:
|
||||||
'Year, month (optional), and day (optional) in YYYY-MM-DD format.'
|
'Year, month (optional), and day (optional) in YYYY-MM-DD format.',
|
||||||
})
|
});
|
||||||
|
|
||||||
export const Degrees = createScalar({
|
export const Degrees = createScalar({
|
||||||
name: 'Degrees',
|
name: 'Degrees',
|
||||||
description: 'Decimal degrees, used for latitude and longitude.'
|
description: 'Decimal degrees, used for latitude and longitude.',
|
||||||
})
|
});
|
||||||
|
|
||||||
export const DiscID = createScalar({
|
export const DiscID = createScalar({
|
||||||
name: 'DiscID',
|
name: 'DiscID',
|
||||||
|
|
@ -80,8 +81,8 @@ Different pressing of a CD often have slightly different frame offsets, and
|
||||||
hence different disc IDs.
|
hence different disc IDs.
|
||||||
|
|
||||||
Conversely, two different CDs may happen to have exactly the same set of frame
|
Conversely, two different CDs may happen to have exactly the same set of frame
|
||||||
offsets and hence the same disc ID.`
|
offsets and hence the same disc ID.`,
|
||||||
})
|
});
|
||||||
|
|
||||||
export const Duration = createScalar({
|
export const Duration = createScalar({
|
||||||
name: 'Duration',
|
name: 'Duration',
|
||||||
|
|
@ -90,25 +91,25 @@ export const Duration = createScalar({
|
||||||
parseValue: validatePositive,
|
parseValue: validatePositive,
|
||||||
parseLiteral(ast) {
|
parseLiteral(ast) {
|
||||||
if (ast.kind === Kind.INT) {
|
if (ast.kind === Kind.INT) {
|
||||||
return validatePositive(parseInt(ast.value, 10))
|
return validatePositive(parseInt(ast.value, 10));
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
export const IPI = createScalar({
|
export const IPI = createScalar({
|
||||||
name: 'IPI',
|
name: 'IPI',
|
||||||
description: `An [Interested Parties Information](https://musicbrainz.org/doc/IPI)
|
description: `An [Interested Parties Information](https://musicbrainz.org/doc/IPI)
|
||||||
(IPI) code is an identifying number assigned by the CISAC database for musical
|
(IPI) code is an identifying number assigned by the CISAC database for musical
|
||||||
rights management.`
|
rights management.`,
|
||||||
})
|
});
|
||||||
|
|
||||||
export const ISNI = createScalar({
|
export const ISNI = createScalar({
|
||||||
name: 'ISNI',
|
name: 'ISNI',
|
||||||
description: `The [International Standard Name Identifier](https://musicbrainz.org/doc/ISNI)
|
description: `The [International Standard Name Identifier](https://musicbrainz.org/doc/ISNI)
|
||||||
(ISNI) is an ISO standard for uniquely identifying the public identities of
|
(ISNI) is an ISO standard for uniquely identifying the public identities of
|
||||||
contributors to media content.`
|
contributors to media content.`,
|
||||||
})
|
});
|
||||||
|
|
||||||
export const ISRC = createScalar({
|
export const ISRC = createScalar({
|
||||||
name: 'ISRC',
|
name: 'ISRC',
|
||||||
|
|
@ -121,15 +122,15 @@ not the song itself. Therefore, different recordings, edits, remixes and
|
||||||
remasters of the same song will each be assigned their own ISRC. However, note
|
remasters of the same song will each be assigned their own ISRC. However, note
|
||||||
that same recording should carry the same ISRC in all countries/territories.
|
that same recording should carry the same ISRC in all countries/territories.
|
||||||
Songs are identified by analogous [International Standard Musical Work Codes](https://musicbrainz.org/doc/ISWC)
|
Songs are identified by analogous [International Standard Musical Work Codes](https://musicbrainz.org/doc/ISWC)
|
||||||
(ISWCs).`
|
(ISWCs).`,
|
||||||
})
|
});
|
||||||
|
|
||||||
export const ISWC = createScalar({
|
export const ISWC = createScalar({
|
||||||
name: 'ISWC',
|
name: 'ISWC',
|
||||||
description: `The [International Standard Musical Work Code](https://musicbrainz.org/doc/ISWC)
|
description: `The [International Standard Musical Work Code](https://musicbrainz.org/doc/ISWC)
|
||||||
(ISWC) is an ISO standard similar to ISBNs for identifying musical works /
|
(ISWC) is an ISO standard similar to ISBNs for identifying musical works /
|
||||||
compositions.`
|
compositions.`,
|
||||||
})
|
});
|
||||||
|
|
||||||
export const Locale = createScalar({
|
export const Locale = createScalar({
|
||||||
name: 'Locale',
|
name: 'Locale',
|
||||||
|
|
@ -138,11 +139,11 @@ export const Locale = createScalar({
|
||||||
parseValue: validateLocale,
|
parseValue: validateLocale,
|
||||||
parseLiteral(ast) {
|
parseLiteral(ast) {
|
||||||
if (ast.kind === Kind.STRING) {
|
if (ast.kind === Kind.STRING) {
|
||||||
return validateLocale(ast.value)
|
return validateLocale(ast.value);
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
export const MBID = createScalar({
|
export const MBID = createScalar({
|
||||||
name: 'MBID',
|
name: 'MBID',
|
||||||
|
|
@ -152,16 +153,16 @@ export const MBID = createScalar({
|
||||||
parseValue: validateMBID,
|
parseValue: validateMBID,
|
||||||
parseLiteral(ast) {
|
parseLiteral(ast) {
|
||||||
if (ast.kind === Kind.STRING) {
|
if (ast.kind === Kind.STRING) {
|
||||||
return validateMBID(ast.value)
|
return validateMBID(ast.value);
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
export const Time = createScalar({
|
export const Time = createScalar({
|
||||||
name: 'Time',
|
name: 'Time',
|
||||||
description: 'A time of day, in 24-hour hh:mm notation.'
|
description: 'A time of day, in 24-hour hh:mm notation.',
|
||||||
})
|
});
|
||||||
|
|
||||||
export const URLString = createScalar({
|
export const URLString = createScalar({
|
||||||
name: 'URLString',
|
name: 'URLString',
|
||||||
|
|
@ -170,8 +171,8 @@ export const URLString = createScalar({
|
||||||
parseValue: validateURL,
|
parseValue: validateURL,
|
||||||
parseLiteral(ast) {
|
parseLiteral(ast) {
|
||||||
if (ast.kind === Kind.STRING) {
|
if (ast.kind === Kind.STRING) {
|
||||||
return validateURL(ast.value)
|
return validateURL(ast.value);
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,22 @@
|
||||||
import { GraphQLObjectType } from 'graphql/type'
|
import GraphQL from 'graphql';
|
||||||
import Node from './node'
|
import { Node } from './node.js';
|
||||||
import Entity from './entity'
|
import { Entity } from './entity.js';
|
||||||
import {
|
import {
|
||||||
id,
|
id,
|
||||||
mbid,
|
mbid,
|
||||||
name,
|
name,
|
||||||
disambiguation,
|
disambiguation,
|
||||||
relationships,
|
|
||||||
collections,
|
|
||||||
tags,
|
|
||||||
fieldWithID,
|
fieldWithID,
|
||||||
connectionWithExtras
|
connectionWithExtras,
|
||||||
} from './helpers'
|
linkedQuery,
|
||||||
|
} from './helpers.js';
|
||||||
|
import { collections } from './collection.js';
|
||||||
|
import { relationships } from './relationship.js';
|
||||||
|
import { tags } from './tag.js';
|
||||||
|
|
||||||
const Series = new GraphQLObjectType({
|
const { GraphQLObjectType } = GraphQL;
|
||||||
|
|
||||||
|
export const Series = new GraphQLObjectType({
|
||||||
name: 'Series',
|
name: 'Series',
|
||||||
description: `A [series](https://musicbrainz.org/doc/Series) is a sequence of
|
description: `A [series](https://musicbrainz.org/doc/Series) is a sequence of
|
||||||
separate release groups, releases, recordings, works or events with a common
|
separate release groups, releases, recordings, works or events with a common
|
||||||
|
|
@ -26,13 +29,14 @@ theme.`,
|
||||||
disambiguation,
|
disambiguation,
|
||||||
...fieldWithID('type', {
|
...fieldWithID('type', {
|
||||||
description: `The type primarily describes what type of entity the series
|
description: `The type primarily describes what type of entity the series
|
||||||
contains.`
|
contains.`,
|
||||||
}),
|
}),
|
||||||
relationships,
|
relationships,
|
||||||
collections,
|
collections,
|
||||||
tags
|
tags,
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const SeriesConnection = connectionWithExtras(Series)
|
export const SeriesConnection = connectionWithExtras(Series);
|
||||||
export default Series
|
|
||||||
|
export const series = linkedQuery(SeriesConnection);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
import {
|
import GraphQL from 'graphql';
|
||||||
|
import GraphQLRelay from 'graphql-relay';
|
||||||
|
import { connectionWithExtras, linkedQuery } from './helpers.js';
|
||||||
|
import { createSubqueryResolver } from '../resolvers.js';
|
||||||
|
|
||||||
|
const {
|
||||||
GraphQLObjectType,
|
GraphQLObjectType,
|
||||||
GraphQLNonNull,
|
GraphQLNonNull,
|
||||||
GraphQLString,
|
GraphQLString,
|
||||||
GraphQLInt
|
GraphQLInt,
|
||||||
} from 'graphql/type'
|
} = GraphQL;
|
||||||
import { connectionWithExtras } from './helpers'
|
const { connectionFromArray } = GraphQLRelay;
|
||||||
|
|
||||||
const Tag = new GraphQLObjectType({
|
export const Tag = new GraphQLObjectType({
|
||||||
name: 'Tag',
|
name: 'Tag',
|
||||||
description: `[Tags](https://musicbrainz.org/tags) are a way to mark entities
|
description: `[Tags](https://musicbrainz.org/tags) are a way to mark entities
|
||||||
with extra information – for example, the genres that apply to an artist,
|
with extra information – for example, the genres that apply to an artist,
|
||||||
|
|
@ -14,14 +19,24 @@ release, or recording.`,
|
||||||
fields: () => ({
|
fields: () => ({
|
||||||
name: {
|
name: {
|
||||||
type: new GraphQLNonNull(GraphQLString),
|
type: new GraphQLNonNull(GraphQLString),
|
||||||
description: 'The tag label.'
|
description: 'The tag label.',
|
||||||
},
|
},
|
||||||
count: {
|
count: {
|
||||||
type: GraphQLInt,
|
type: GraphQLInt,
|
||||||
description: 'How many times this tag has been applied to the entity.'
|
description: 'How many times this tag has been applied to the entity.',
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const TagConnection = connectionWithExtras(Tag)
|
export const TagConnection = connectionWithExtras(Tag);
|
||||||
export default Tag
|
|
||||||
|
export const tags = linkedQuery(TagConnection, {
|
||||||
|
resolve: createSubqueryResolver({}, (value = [], args) => {
|
||||||
|
const connection = connectionFromArray(value, args);
|
||||||
|
return {
|
||||||
|
nodes: connection.edges.map((edge) => edge.node),
|
||||||
|
totalCount: value.length,
|
||||||
|
...connection,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
|
||||||
44
src/types/track.js
Normal file
44
src/types/track.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import GraphQL from 'graphql';
|
||||||
|
import { Entity } from './entity.js';
|
||||||
|
import { Duration } from './scalars.js';
|
||||||
|
import { Recording } from './recording.js';
|
||||||
|
import { mbid, title } from './helpers.js';
|
||||||
|
|
||||||
|
const { GraphQLObjectType, GraphQLInt, GraphQLString } = GraphQL;
|
||||||
|
|
||||||
|
export const Track = new GraphQLObjectType({
|
||||||
|
name: 'Track',
|
||||||
|
description: `A track is the way a recording is represented on a particular
|
||||||
|
release (or, more exactly, on a particular medium). Every track has a title
|
||||||
|
(see the guidelines for titles) and is credited to one or more artists.`,
|
||||||
|
interfaces: () => [Entity],
|
||||||
|
fields: () => ({
|
||||||
|
mbid,
|
||||||
|
title,
|
||||||
|
position: {
|
||||||
|
type: GraphQLInt,
|
||||||
|
description: `The track’s position on the overall release (including all
|
||||||
|
tracks from all discs).`,
|
||||||
|
},
|
||||||
|
number: {
|
||||||
|
type: GraphQLString,
|
||||||
|
description: `The track number, which may include information about the
|
||||||
|
disc or side it appears on, e.g. “A1” or “B3”.`,
|
||||||
|
},
|
||||||
|
length: {
|
||||||
|
type: Duration,
|
||||||
|
description: 'The length of the track.',
|
||||||
|
},
|
||||||
|
recording: {
|
||||||
|
type: Recording,
|
||||||
|
description: 'The recording that appears on the track.',
|
||||||
|
resolve: (source) => {
|
||||||
|
const { recording } = source;
|
||||||
|
if (recording) {
|
||||||
|
recording._type = 'recording';
|
||||||
|
}
|
||||||
|
return recording;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import { GraphQLObjectType, GraphQLNonNull } from 'graphql/type'
|
import GraphQL from 'graphql';
|
||||||
import Node from './node'
|
import { Node } from './node.js';
|
||||||
import Entity from './entity'
|
import { Entity } from './entity.js';
|
||||||
import { URLString } from './scalars'
|
import { URLString } from './scalars.js';
|
||||||
import { id, mbid, relationships, connectionWithExtras } from './helpers'
|
import { id, mbid, connectionWithExtras } from './helpers.js';
|
||||||
|
import { relationships } from './relationship.js';
|
||||||
|
|
||||||
const URL = new GraphQLObjectType({
|
const { GraphQLObjectType, GraphQLNonNull } = GraphQL;
|
||||||
|
|
||||||
|
export const URL = new GraphQLObjectType({
|
||||||
name: 'URL',
|
name: 'URL',
|
||||||
description: `A [URL](https://musicbrainz.org/doc/URL) pointing to a resource
|
description: `A [URL](https://musicbrainz.org/doc/URL) pointing to a resource
|
||||||
external to MusicBrainz, i.e. an official homepage, a site where music can be
|
external to MusicBrainz, i.e. an official homepage, a site where music can be
|
||||||
|
|
@ -15,11 +18,10 @@ acquired, an entry in another database, etc.`,
|
||||||
mbid,
|
mbid,
|
||||||
resource: {
|
resource: {
|
||||||
type: new GraphQLNonNull(URLString),
|
type: new GraphQLNonNull(URLString),
|
||||||
description: 'The actual URL string.'
|
description: 'The actual URL string.',
|
||||||
},
|
},
|
||||||
relationships
|
relationships,
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const URLConnection = connectionWithExtras(URL)
|
export const URLConnection = connectionWithExtras(URL);
|
||||||
export default URL
|
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,25 @@
|
||||||
import { GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql/type'
|
import GraphQL from 'graphql';
|
||||||
import Node from './node'
|
import { Node } from './node.js';
|
||||||
import Entity from './entity'
|
import { Entity } from './entity.js';
|
||||||
import {
|
import {
|
||||||
id,
|
id,
|
||||||
mbid,
|
mbid,
|
||||||
title,
|
title,
|
||||||
disambiguation,
|
disambiguation,
|
||||||
aliases,
|
|
||||||
artists,
|
|
||||||
relationships,
|
|
||||||
collections,
|
|
||||||
rating,
|
|
||||||
tags,
|
|
||||||
fieldWithID,
|
fieldWithID,
|
||||||
connectionWithExtras
|
connectionWithExtras,
|
||||||
} from './helpers'
|
linkedQuery,
|
||||||
|
} from './helpers.js';
|
||||||
|
import { aliases } from './alias.js';
|
||||||
|
import { artists } from './artist.js';
|
||||||
|
import { collections } from './collection.js';
|
||||||
|
import { rating } from './rating.js';
|
||||||
|
import { relationships } from './relationship.js';
|
||||||
|
import { tags } from './tag.js';
|
||||||
|
|
||||||
const Work = new GraphQLObjectType({
|
const { GraphQLObjectType, GraphQLString, GraphQLList } = GraphQL;
|
||||||
|
|
||||||
|
export const Work = new GraphQLObjectType({
|
||||||
name: 'Work',
|
name: 'Work',
|
||||||
description: `A [work](https://musicbrainz.org/doc/Work) is a distinct
|
description: `A [work](https://musicbrainz.org/doc/Work) is a distinct
|
||||||
intellectual or artistic creation, which can be expressed in the form of one or
|
intellectual or artistic creation, which can be expressed in the form of one or
|
||||||
|
|
@ -31,22 +34,23 @@ more audio recordings.`,
|
||||||
iswcs: {
|
iswcs: {
|
||||||
type: new GraphQLList(GraphQLString),
|
type: new GraphQLList(GraphQLString),
|
||||||
description: `A list of [ISWCs](https://musicbrainz.org/doc/ISWC) assigned
|
description: `A list of [ISWCs](https://musicbrainz.org/doc/ISWC) assigned
|
||||||
to the work by copyright collecting agencies.`
|
to the work by copyright collecting agencies.`,
|
||||||
},
|
},
|
||||||
language: {
|
language: {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
description: 'The language in which the work was originally written.'
|
description: 'The language in which the work was originally written.',
|
||||||
},
|
},
|
||||||
...fieldWithID('type', {
|
...fieldWithID('type', {
|
||||||
description: 'The type of work.'
|
description: 'The type of work.',
|
||||||
}),
|
}),
|
||||||
artists,
|
artists,
|
||||||
relationships,
|
relationships,
|
||||||
collections,
|
collections,
|
||||||
rating,
|
rating,
|
||||||
tags
|
tags,
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const WorkConnection = connectionWithExtras(Work)
|
export const WorkConnection = connectionWithExtras(Work);
|
||||||
export default Work
|
|
||||||
|
export const works = linkedQuery(WorkConnection);
|
||||||
|
|
|
||||||
95
src/util.js
95
src/util.js
|
|
@ -1,50 +1,99 @@
|
||||||
import util from 'util'
|
import util from 'util';
|
||||||
|
import dashify from 'dashify';
|
||||||
|
import pascalCase from 'pascalcase';
|
||||||
|
|
||||||
export const ONE_DAY = 24 * 60 * 60 * 1000
|
export const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
export function getFields(info, fragments = info.fragments) {
|
export function getFields(
|
||||||
|
info,
|
||||||
|
fragments = info.fragments,
|
||||||
|
depth = 0,
|
||||||
|
prefix = ''
|
||||||
|
) {
|
||||||
if (info.kind !== 'Field') {
|
if (info.kind !== 'Field') {
|
||||||
info = info.fieldNodes[0]
|
info = info.fieldNodes[0];
|
||||||
}
|
}
|
||||||
const selections = info.selectionSet.selections
|
const selections = info.selectionSet.selections;
|
||||||
const reducer = (fields, selection) => {
|
const reducer = (fields, selection) => {
|
||||||
if (selection.kind === 'FragmentSpread') {
|
if (selection.kind === 'FragmentSpread') {
|
||||||
const name = selection.name.value
|
const name = selection.name.value;
|
||||||
const fragment = fragments[name]
|
const fragment = fragments[name];
|
||||||
if (!fragment) {
|
if (!fragment) {
|
||||||
throw new Error(`Fragment '${name}' was not passed to getFields()`)
|
throw new Error(`Fragment '${name}' was not passed to getFields()`);
|
||||||
}
|
}
|
||||||
fragment.selectionSet.selections.reduce(reducer, fields)
|
fragment.selectionSet.selections.reduce(reducer, fields);
|
||||||
} else if (selection.kind === 'InlineFragment') {
|
} else if (selection.kind === 'InlineFragment') {
|
||||||
selection.selectionSet.selections.reduce(reducer, fields)
|
selection.selectionSet.selections.reduce(reducer, fields);
|
||||||
} else {
|
} else {
|
||||||
fields[selection.name.value] = selection
|
const prefixedName = prefix + selection.name.value;
|
||||||
|
fields[prefixedName] = selection;
|
||||||
|
if (depth > 0 && selection.selectionSet) {
|
||||||
|
const subFields = getFields(
|
||||||
|
selection,
|
||||||
|
fragments,
|
||||||
|
depth - 1,
|
||||||
|
`${prefixedName}.`
|
||||||
|
);
|
||||||
|
Object.assign(fields, subFields);
|
||||||
}
|
}
|
||||||
return fields
|
|
||||||
}
|
}
|
||||||
return selections.reduce(reducer, {})
|
return fields;
|
||||||
|
};
|
||||||
|
return selections.reduce(reducer, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prettyPrint(
|
export function prettyPrint(
|
||||||
obj,
|
obj,
|
||||||
{ depth = 5, colors = true, breakLength = 120 } = {}
|
{ depth = 5, colors = true, breakLength = 120 } = {}
|
||||||
) {
|
) {
|
||||||
console.log(util.inspect(obj, { depth, colors, breakLength }))
|
console.log(util.inspect(obj, { depth, colors, breakLength }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toFilteredArray(obj) {
|
export function toFilteredArray(obj) {
|
||||||
return (Array.isArray(obj) ? obj : [obj]).filter(x => x)
|
return (Array.isArray(obj) ? obj : [obj]).filter((x) => x);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extendIncludes(includes, moreIncludes) {
|
export function extendIncludes(includes, moreIncludes) {
|
||||||
includes = toFilteredArray(includes)
|
includes = toFilteredArray(includes);
|
||||||
moreIncludes = toFilteredArray(moreIncludes)
|
moreIncludes = toFilteredArray(moreIncludes);
|
||||||
const seen = {}
|
const seen = {};
|
||||||
return includes.concat(moreIncludes).filter(x => {
|
return includes.concat(moreIncludes).filter((x) => {
|
||||||
if (seen[x]) {
|
if (seen[x]) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
seen[x] = true
|
seen[x] = true;
|
||||||
return true
|
return true;
|
||||||
})
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toPascal = pascalCase;
|
||||||
|
export const toDashed = dashify;
|
||||||
|
|
||||||
|
export function toPlural(name) {
|
||||||
|
return name.endsWith('s') ? name : name + 's';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toSingular(name) {
|
||||||
|
return name.endsWith('s') && !/series/i.test(name) ? name.slice(0, -1) : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toWords(name) {
|
||||||
|
return toPascal(name).replace(/([^A-Z])?([A-Z]+)/g, (match, tail, head) => {
|
||||||
|
tail = tail ? tail + ' ' : '';
|
||||||
|
head = head.length > 1 ? head : head.toLowerCase();
|
||||||
|
return `${tail}${head}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterObjectValues(obj, filter) {
|
||||||
|
return Object.entries(obj).reduce((obj, [key, value]) => {
|
||||||
|
if (filter(value)) {
|
||||||
|
obj[key] = value;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTypeName(value) {
|
||||||
|
return Object.prototype.toString.call(value).slice(8, -1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
597
test/_schema.js
597
test/_schema.js
|
|
@ -1,27 +1,25 @@
|
||||||
import test from 'ava'
|
import test from 'ava';
|
||||||
import { graphql } from 'graphql'
|
import GraphQL from 'graphql';
|
||||||
import schemas from './helpers/schema'
|
import context from './helpers/context.js';
|
||||||
import context from './helpers/context'
|
|
||||||
|
|
||||||
const TEST_SCHEMA = process.env.TEST_SCHEMA || 'baseSchema'
|
const { graphql } = GraphQL;
|
||||||
const schema = schemas[TEST_SCHEMA]
|
|
||||||
|
|
||||||
function testData(t, query, handler) {
|
function testData(t, query, handler) {
|
||||||
return graphql(schema, query, null, context).then(result => {
|
return graphql(t.context.schema, query, null, context).then((result) => {
|
||||||
if (result.errors !== undefined) {
|
if (result.errors !== undefined) {
|
||||||
result.errors.forEach(error => t.log(error))
|
result.errors.forEach((error) => t.log(error));
|
||||||
}
|
}
|
||||||
t.is(result.errors, undefined)
|
t.is(result.errors, undefined);
|
||||||
return handler(t, result.data)
|
return handler(t, result.data);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function testError(t, query, handler) {
|
function testError(t, query, handler) {
|
||||||
return graphql(schema, query, null, context).then(result => {
|
return graphql(t.context.schema, query, null, context).then((result) => {
|
||||||
t.truthy(result.errors)
|
t.truthy(result.errors);
|
||||||
t.true(result.errors.length > 0)
|
t.true(result.errors.length > 0);
|
||||||
return handler(t, result.errors)
|
return handler(t, result.errors);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
test(
|
test(
|
||||||
|
|
@ -41,11 +39,11 @@ test(
|
||||||
t.deepEqual(data, {
|
t.deepEqual(data, {
|
||||||
node: {
|
node: {
|
||||||
__typename: 'ReleaseGroup',
|
__typename: 'ReleaseGroup',
|
||||||
mbid: 'e37d2740-4503-4e3f-ab6d-e622a25e964d'
|
mbid: 'e37d2740-4503-4e3f-ab6d-e622a25e964d',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
);
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'schema has a lookup query',
|
'schema has a lookup query',
|
||||||
|
|
@ -67,12 +65,12 @@ test(
|
||||||
artist: {
|
artist: {
|
||||||
mbid: 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8',
|
mbid: 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8',
|
||||||
name: 'Lures',
|
name: 'Lures',
|
||||||
type: 'Group'
|
type: 'Group',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'schema has a search query',
|
'schema has a search query',
|
||||||
|
|
@ -94,12 +92,12 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { recordings } = data.search
|
const { recordings } = data.search;
|
||||||
t.true(recordings.totalCount > 0)
|
t.true(recordings.totalCount > 0);
|
||||||
t.true(recordings.edges.length === 25)
|
t.true(recordings.edges.length === 25);
|
||||||
recordings.edges.forEach(edge => t.true(edge.score > 0))
|
recordings.edges.forEach((edge) => t.true(edge.score > 0));
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'schema has a browse query',
|
'schema has a browse query',
|
||||||
|
|
@ -127,12 +125,12 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { releaseGroups } = data.browse
|
const { releaseGroups } = data.browse;
|
||||||
t.true(releaseGroups.totalCount > 0)
|
t.true(releaseGroups.totalCount > 0);
|
||||||
t.true(releaseGroups.edges.length > 0)
|
t.true(releaseGroups.edges.length > 0);
|
||||||
releaseGroups.edges.forEach(edge => t.truthy(edge.node.title))
|
releaseGroups.edges.forEach((edge) => t.truthy(edge.node.title));
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'supports deeply nested queries',
|
'supports deeply nested queries',
|
||||||
|
|
@ -207,11 +205,11 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { labels } = data.search
|
const { labels } = data.search;
|
||||||
t.true(labels.edges.length > 0)
|
t.true(labels.edges.length > 0);
|
||||||
t.is(labels.edges[0].node.releases.edges.length, 1)
|
t.is(labels.edges[0].node.releases.edges.length, 1);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'connections have a nodes shortcut field',
|
'connections have a nodes shortcut field',
|
||||||
|
|
@ -276,11 +274,11 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { labels } = data.search
|
const { labels } = data.search;
|
||||||
t.true(labels.nodes.length > 0)
|
t.true(labels.nodes.length > 0);
|
||||||
t.is(labels.nodes[0].releases.nodes.length, 1)
|
t.is(labels.nodes[0].releases.nodes.length, 1);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'throws an error if given a malformed MBID',
|
'throws an error if given a malformed MBID',
|
||||||
|
|
@ -295,9 +293,9 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, errors) => {
|
(t, errors) => {
|
||||||
t.regex(errors[0].message, /Malformed MBID: ABC123/)
|
t.regex(errors[0].message, /Malformed MBID: ABC123/);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'artist areas access begin_area/end_area for lookup queries',
|
'artist areas access begin_area/end_area for lookup queries',
|
||||||
|
|
@ -317,11 +315,11 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { artist } = data.lookup
|
const { artist } = data.lookup;
|
||||||
t.is(artist.beginArea.name, 'Westmount')
|
t.is(artist.beginArea.name, 'Westmount');
|
||||||
t.is(artist.endArea.name, 'Los Angeles')
|
t.is(artist.endArea.name, 'Los Angeles');
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'artist areas access begin_area/end_area for browse queries',
|
'artist areas access begin_area/end_area for browse queries',
|
||||||
|
|
@ -345,12 +343,12 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const artists = data.browse.artists.edges.map(edge => edge.node)
|
const artists = data.browse.artists.edges.map((edge) => edge.node);
|
||||||
t.true(artists.length > 1)
|
t.true(artists.length > 1);
|
||||||
t.true(artists.some(artist => artist.beginArea))
|
t.true(artists.some((artist) => artist.beginArea));
|
||||||
t.true(artists.some(artist => artist.endArea))
|
t.true(artists.some((artist) => artist.endArea));
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'artist areas access begin-area/end-area for search queries',
|
'artist areas access begin-area/end-area for search queries',
|
||||||
|
|
@ -374,12 +372,12 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const artists = data.search.artists.edges.map(edge => edge.node)
|
const artists = data.search.artists.edges.map((edge) => edge.node);
|
||||||
t.true(artists.length === 1)
|
t.true(artists.length === 1);
|
||||||
t.is(artists[0].beginArea.name, 'Westmount')
|
t.is(artists[0].beginArea.name, 'Westmount');
|
||||||
t.is(artists[0].endArea.name, 'Los Angeles')
|
t.is(artists[0].endArea.name, 'Los Angeles');
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'relationships are grouped by target type',
|
'relationships are grouped by target type',
|
||||||
|
|
@ -425,24 +423,24 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { relationships } = data.lookup.artist
|
const { relationships } = data.lookup.artist;
|
||||||
t.is(relationships.artists.edges.length, 5)
|
t.is(relationships.artists.edges.length, 5);
|
||||||
relationships.artists.edges.forEach(edge => {
|
relationships.artists.edges.forEach((edge) => {
|
||||||
t.is(edge.node.targetType, 'artist')
|
t.is(edge.node.targetType, 'artist');
|
||||||
t.is(edge.node.target.__typename, 'Artist')
|
t.is(edge.node.target.__typename, 'Artist');
|
||||||
})
|
});
|
||||||
t.is(relationships.recordings.edges.length, 5)
|
t.is(relationships.recordings.edges.length, 5);
|
||||||
relationships.recordings.edges.forEach(edge => {
|
relationships.recordings.edges.forEach((edge) => {
|
||||||
t.is(edge.node.targetType, 'recording')
|
t.is(edge.node.targetType, 'recording');
|
||||||
t.is(edge.node.target.__typename, 'Recording')
|
t.is(edge.node.target.__typename, 'Recording');
|
||||||
})
|
});
|
||||||
t.is(relationships.releases.edges.length, 5)
|
t.is(relationships.releases.edges.length, 5);
|
||||||
relationships.releases.edges.forEach(edge => {
|
relationships.releases.edges.forEach((edge) => {
|
||||||
t.is(edge.node.targetType, 'release')
|
t.is(edge.node.targetType, 'release');
|
||||||
t.is(edge.node.target.__typename, 'Release')
|
t.is(edge.node.target.__typename, 'Release');
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'relationships can be filtered by type',
|
'relationships can be filtered by type',
|
||||||
|
|
@ -466,15 +464,15 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { artist } = data.lookup
|
const { artist } = data.lookup;
|
||||||
const rels = artist.relationships.artists.edges.map(edge => edge.node)
|
const rels = artist.relationships.artists.edges.map((edge) => edge.node);
|
||||||
t.is(rels.length, 2)
|
t.is(rels.length, 2);
|
||||||
rels.forEach(rel => {
|
rels.forEach((rel) => {
|
||||||
t.is(rel.targetType, 'artist')
|
t.is(rel.targetType, 'artist');
|
||||||
t.is(rel.type, 'parent')
|
t.is(rel.type, 'parent');
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'relationships can be filtered by type ID',
|
'relationships can be filtered by type ID',
|
||||||
|
|
@ -498,15 +496,15 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { artist } = data.lookup
|
const { artist } = data.lookup;
|
||||||
const rels = artist.relationships.artists.edges.map(edge => edge.node)
|
const rels = artist.relationships.artists.edges.map((edge) => edge.node);
|
||||||
t.is(rels.length, 1)
|
t.is(rels.length, 1);
|
||||||
rels.forEach(rel => {
|
rels.forEach((rel) => {
|
||||||
t.is(rel.targetType, 'artist')
|
t.is(rel.targetType, 'artist');
|
||||||
t.is(rel.type, 'involved with')
|
t.is(rel.type, 'involved with');
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'relationships can be filtered by direction',
|
'relationships can be filtered by direction',
|
||||||
|
|
@ -538,21 +536,21 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { area } = data.lookup
|
const { area } = data.lookup;
|
||||||
const isPartOf = area.relationships.isPartOf.edges.map(edge => edge.node)
|
const isPartOf = area.relationships.isPartOf.edges.map((edge) => edge.node);
|
||||||
const hasParts = area.relationships.hasParts.edges.map(edge => edge.node)
|
const hasParts = area.relationships.hasParts.edges.map((edge) => edge.node);
|
||||||
t.true(isPartOf.length > 0)
|
t.true(isPartOf.length > 0);
|
||||||
t.true(hasParts.length > 0)
|
t.true(hasParts.length > 0);
|
||||||
isPartOf.forEach(rel => {
|
isPartOf.forEach((rel) => {
|
||||||
t.is(rel.type, 'part of')
|
t.is(rel.type, 'part of');
|
||||||
t.is(rel.direction, 'backward')
|
t.is(rel.direction, 'backward');
|
||||||
})
|
});
|
||||||
hasParts.forEach(rel => {
|
hasParts.forEach((rel) => {
|
||||||
t.is(rel.type, 'part of')
|
t.is(rel.type, 'part of');
|
||||||
t.is(rel.direction, 'forward')
|
t.is(rel.direction, 'forward');
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'area maps iso-3166-1-codes to isoCodes',
|
'area maps iso-3166-1-codes to isoCodes',
|
||||||
|
|
@ -568,9 +566,9 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
t.deepEqual(data.lookup.area.isoCodes, ['US'])
|
t.deepEqual(data.lookup.area.isoCodes, ['US']);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'area isoCodes accepts an argument to retrieve 3166-1, 3166-2, or 3166-3 codes',
|
'area isoCodes accepts an argument to retrieve 3166-1, 3166-2, or 3166-3 codes',
|
||||||
|
|
@ -596,17 +594,17 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
t.deepEqual(data.lookup.eastGermany.isoDefault, ['XG'])
|
t.deepEqual(data.lookup.eastGermany.isoDefault, ['XG']);
|
||||||
t.deepEqual(data.lookup.eastGermany.iso3166_1, ['XG'])
|
t.deepEqual(data.lookup.eastGermany.iso3166_1, ['XG']);
|
||||||
t.is(data.lookup.eastGermany.iso3166_2, null)
|
t.is(data.lookup.eastGermany.iso3166_2, null);
|
||||||
t.deepEqual(data.lookup.eastGermany.iso3166_3, ['DDDE'])
|
t.deepEqual(data.lookup.eastGermany.iso3166_3, ['DDDE']);
|
||||||
|
|
||||||
t.is(data.lookup.newYork.isoDefault, null)
|
t.is(data.lookup.newYork.isoDefault, null);
|
||||||
t.is(data.lookup.newYork.iso3166_1, null)
|
t.is(data.lookup.newYork.iso3166_1, null);
|
||||||
t.deepEqual(data.lookup.newYork.iso3166_2, ['US-NY'])
|
t.deepEqual(data.lookup.newYork.iso3166_2, ['US-NY']);
|
||||||
t.is(data.lookup.newYork.iso3166_3, null)
|
t.is(data.lookup.newYork.iso3166_3, null);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'areas have a type and typeID',
|
'areas have a type and typeID',
|
||||||
|
|
@ -625,9 +623,9 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
t.snapshot(data)
|
t.snapshot(data);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'alias locales use the locale scalar',
|
'alias locales use the locale scalar',
|
||||||
|
|
@ -645,11 +643,15 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { aliases } = data.lookup.artist
|
const { aliases } = data.lookup.artist;
|
||||||
t.is(aliases.find(alias => alias.locale === 'en').name, 'PSY')
|
t.true(
|
||||||
t.is(aliases.find(alias => alias.locale === 'ko').name, '싸이')
|
aliases.some((alias) => alias.locale === 'en' && alias.name === 'PSY')
|
||||||
|
);
|
||||||
|
t.true(
|
||||||
|
aliases.some((alias) => alias.locale === 'ko' && alias.name === '싸이')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'work ISWCs use the ISWC scalar',
|
'work ISWCs use the ISWC scalar',
|
||||||
|
|
@ -665,11 +667,11 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { work } = data.lookup
|
const { work } = data.lookup;
|
||||||
t.is(work.title, 'The Partisan')
|
t.is(work.title, 'The Partisan');
|
||||||
t.deepEqual(work.iswcs, ['T-900.755.682-3'])
|
t.deepEqual(work.iswcs, ['T-900.755.682-3']);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'URLs may be looked up by resource',
|
'URLs may be looked up by resource',
|
||||||
|
|
@ -685,11 +687,11 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { url } = data.lookup
|
const { url } = data.lookup;
|
||||||
t.is(url.mbid, '4347ffe2-82ec-4059-9520-6a1a3f73a304')
|
t.is(url.mbid, '4347ffe2-82ec-4059-9520-6a1a3f73a304');
|
||||||
t.is(url.resource, 'http://www.nirvana.com/')
|
t.is(url.resource, 'http://www.nirvana.com/');
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'throws an error if given a malformed resource URL',
|
'throws an error if given a malformed resource URL',
|
||||||
|
|
@ -705,9 +707,9 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, errors) => {
|
(t, errors) => {
|
||||||
t.regex(errors[0].message, /Malformed URL: http:foo/)
|
t.regex(errors[0].message, /Malformed URL: http:foo/);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'release groups can be browsed by type',
|
'release groups can be browsed by type',
|
||||||
|
|
@ -726,11 +728,15 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const releaseGroups = data.browse.releaseGroups.edges.map(edge => edge.node)
|
const releaseGroups = data.browse.releaseGroups.edges.map(
|
||||||
t.is(releaseGroups.length, 8)
|
(edge) => edge.node
|
||||||
releaseGroups.forEach(releaseGroup => t.is(releaseGroup.primaryType, 'EP'))
|
);
|
||||||
|
t.is(releaseGroups.length, 8);
|
||||||
|
releaseGroups.forEach((releaseGroup) =>
|
||||||
|
t.is(releaseGroup.primaryType, 'EP')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'releases can be browsed by type and status',
|
'releases can be browsed by type and status',
|
||||||
|
|
@ -749,11 +755,11 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const releases = data.browse.releases.edges.map(edge => edge.node)
|
const releases = data.browse.releases.edges.map((edge) => edge.node);
|
||||||
t.is(releases.length, 6)
|
t.is(releases.length, 6);
|
||||||
releases.forEach(release => t.is(release.status, 'BOOTLEG'))
|
releases.forEach((release) => t.is(release.status, 'BOOTLEG'));
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'releases have an ASIN field',
|
'releases have an ASIN field',
|
||||||
|
|
@ -768,10 +774,10 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { release } = data.lookup
|
const { release } = data.lookup;
|
||||||
t.is(release.asin, 'B01KN6XDS6')
|
t.is(release.asin, 'B01KN6XDS6');
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'artists have a list of ISNIs and IPIs',
|
'artists have a list of ISNIs and IPIs',
|
||||||
|
|
@ -787,11 +793,11 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { artist } = data.lookup
|
const { artist } = data.lookup;
|
||||||
t.deepEqual(artist.ipis, ['00006457004'])
|
t.deepEqual(artist.ipis, ['00006457004']);
|
||||||
t.deepEqual(artist.isnis, ['0000000110273481'])
|
t.deepEqual(artist.isnis, ['0000000110273481']);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'artistCredits is an alias for artistCredit',
|
'artistCredits is an alias for artistCredit',
|
||||||
|
|
@ -833,24 +839,42 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { recording, release, releaseGroup } = data.lookup
|
const { recording, release, releaseGroup } = data.lookup;
|
||||||
t.deepEqual(recording.artistCredit, [
|
t.deepEqual(recording.artistCredit, [
|
||||||
{ name: 'Holly Golightly & The Brokeoffs', joinPhrase: '' }
|
{ name: 'Holly Golightly & The Brokeoffs', joinPhrase: '' },
|
||||||
])
|
]);
|
||||||
t.deepEqual(recording.artistCredits, recording.artistCredit)
|
t.deepEqual(recording.artistCredits, recording.artistCredit);
|
||||||
|
|
||||||
t.deepEqual(release.artistCredit, [
|
t.deepEqual(release.artistCredit, [
|
||||||
{ name: 'Leonard Cohen', joinPhrase: '' }
|
{ name: 'Leonard Cohen', joinPhrase: '' },
|
||||||
])
|
]);
|
||||||
t.deepEqual(release.artistCredits, release.artistCredit)
|
t.deepEqual(release.artistCredits, release.artistCredit);
|
||||||
|
|
||||||
t.deepEqual(releaseGroup.artistCredit, [
|
t.deepEqual(releaseGroup.artistCredit, [
|
||||||
{ name: 'DJ Muggs', joinPhrase: ' vs. ' },
|
{ name: 'DJ Muggs', joinPhrase: ' vs. ' },
|
||||||
{ name: 'Ill Bill', joinPhrase: '' }
|
{ name: 'Ill Bill', joinPhrase: '' },
|
||||||
])
|
]);
|
||||||
t.deepEqual(releaseGroup.artistCredits, releaseGroup.artistCredit)
|
t.deepEqual(releaseGroup.artistCredits, releaseGroup.artistCredit);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'recordings have a list of ISRCs',
|
||||||
|
testData,
|
||||||
|
`
|
||||||
|
{
|
||||||
|
lookup {
|
||||||
|
recording(mbid: "9f9cf187-d6f9-437f-9d98-d59cdbd52757") {
|
||||||
|
isrcs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
(t, data) => {
|
||||||
|
t.true(data.lookup.recording.isrcs.includes('GBAYE9701376'));
|
||||||
|
t.true(data.lookup.recording.isrcs.includes('GBBKS1700108'));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'recordings can be browsed by ISRC',
|
'recordings can be browsed by ISRC',
|
||||||
|
|
@ -871,13 +895,13 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const recordings = data.browse.recordings.edges.map(edge => edge.node)
|
const recordings = data.browse.recordings.edges.map((edge) => edge.node);
|
||||||
t.is(data.browse.recordings.totalCount, 1)
|
t.true(data.browse.recordings.totalCount >= 1);
|
||||||
t.deepEqual(recordings, [
|
t.true(
|
||||||
{ title: 'About a Girl', isrcs: ['USSUB0200002', 'USUG10200084'] }
|
recordings.every((recording) => recording.isrcs.includes('USSUB0200002'))
|
||||||
])
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'releases can be browsed by Disc ID',
|
'releases can be browsed by Disc ID',
|
||||||
|
|
@ -898,20 +922,20 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const releases = data.browse.releases.edges.map(edge => edge.node)
|
const releases = data.browse.releases.edges.map((edge) => edge.node);
|
||||||
t.true(data.browse.releases.totalCount >= 2)
|
t.true(data.browse.releases.totalCount >= 2);
|
||||||
t.true(
|
t.true(
|
||||||
releases.some(
|
releases.some(
|
||||||
release => release.mbid === '5a6e5ad7-c2bd-3484-a20e-121bf981c883'
|
(release) => release.mbid === '5a6e5ad7-c2bd-3484-a20e-121bf981c883'
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
);
|
||||||
t.true(
|
t.true(
|
||||||
releases.some(
|
releases.some(
|
||||||
release => release.mbid === '96f6f90e-d831-4f37-bf72-ce2982e459fb'
|
(release) => release.mbid === '96f6f90e-d831-4f37-bf72-ce2982e459fb'
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'works can be browsed by ISWC',
|
'works can be browsed by ISWC',
|
||||||
|
|
@ -932,11 +956,11 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const works = data.browse.works.edges.map(edge => edge.node)
|
const works = data.browse.works.edges.map((edge) => edge.node);
|
||||||
t.is(data.browse.works.totalCount, 1)
|
t.is(data.browse.works.totalCount, 1);
|
||||||
t.deepEqual(works, [{ title: 'The Partisan', iswcs: ['T-900.755.682-3'] }])
|
t.deepEqual(works, [{ title: 'The Partisan', iswcs: ['T-900.755.682-3'] }]);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'recordings have a length in milliseconds',
|
'recordings have a length in milliseconds',
|
||||||
|
|
@ -951,10 +975,10 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { recording } = data.lookup
|
const { recording } = data.lookup;
|
||||||
t.is(recording.length, 383493)
|
t.is(recording.length, 383813);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'collections can be browsed by the entities they contain',
|
'collections can be browsed by the entities they contain',
|
||||||
|
|
@ -986,20 +1010,20 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const collections = data.browse.collections.edges.map(edge => edge.node)
|
const collections = data.browse.collections.edges.map((edge) => edge.node);
|
||||||
t.true(collections.length >= 2)
|
t.true(collections.length >= 2);
|
||||||
t.true(collections.some(collection => collection.editor === 'arist.on'))
|
t.true(collections.some((collection) => collection.editor === 'arist.on'));
|
||||||
t.true(
|
t.true(
|
||||||
collections.some(collection => collection.editor === 'ListMyCDs.com')
|
collections.some((collection) => collection.editor === 'ListMyCDs.com')
|
||||||
)
|
);
|
||||||
collections.forEach(collection => {
|
collections.forEach((collection) => {
|
||||||
t.is(collection.entityType, 'artist')
|
t.is(collection.entityType, 'artist');
|
||||||
t.is(collection.type, 'Artist')
|
t.is(collection.type, 'Artist');
|
||||||
t.true(collection.artists.totalCount > 0)
|
t.true(collection.artists.totalCount > 0);
|
||||||
t.true(collection.artists.edges.length > 0)
|
t.true(collection.artists.edges.length > 0);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'collections can be looked up by MBID',
|
'collections can be looked up by MBID',
|
||||||
|
|
@ -1021,11 +1045,11 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { collection } = data.lookup
|
const { collection } = data.lookup;
|
||||||
t.is(collection.name, 'Beets Music Collection')
|
t.is(collection.name, 'Beets Music Collection');
|
||||||
t.is(collection.releases.edges.length, 25)
|
t.is(collection.releases.edges.length, 25);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'entities have a collections field',
|
'entities have a collections field',
|
||||||
|
|
@ -1059,12 +1083,12 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { release, artist } = data.lookup
|
const { release, artist } = data.lookup;
|
||||||
t.true(release.collections.totalCount > 0)
|
t.true(release.collections.totalCount > 0);
|
||||||
t.true(release.collections.edges.length > 0)
|
t.true(release.collections.edges.length > 0);
|
||||||
t.true(artist.collections.edges.length > 0)
|
t.true(artist.collections.edges.length > 0);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'releases support a list of media',
|
'releases support a list of media',
|
||||||
|
|
@ -1085,25 +1109,25 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { release } = data.lookup
|
const { release } = data.lookup;
|
||||||
t.deepEqual(release.media, [
|
t.deepEqual(release.media, [
|
||||||
{
|
{
|
||||||
title: 'Left',
|
title: 'Left',
|
||||||
format: 'CD',
|
format: 'CD',
|
||||||
formatID: '9712d52a-4509-3d4b-a1a2-67c88c643e31',
|
formatID: '9712d52a-4509-3d4b-a1a2-67c88c643e31',
|
||||||
position: 1,
|
position: 1,
|
||||||
trackCount: 12
|
trackCount: 12,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Right',
|
title: 'Right',
|
||||||
format: 'CD',
|
format: 'CD',
|
||||||
formatID: '9712d52a-4509-3d4b-a1a2-67c88c643e31',
|
formatID: '9712d52a-4509-3d4b-a1a2-67c88c643e31',
|
||||||
position: 2,
|
position: 2,
|
||||||
trackCount: 11
|
trackCount: 11,
|
||||||
|
},
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
])
|
);
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'throws an error if looking up a URL without an argument',
|
'throws an error if looking up a URL without an argument',
|
||||||
|
|
@ -1121,9 +1145,9 @@ test(
|
||||||
t.is(
|
t.is(
|
||||||
errors[0].message,
|
errors[0].message,
|
||||||
'Lookups by a field other than MBID must provide: resource'
|
'Lookups by a field other than MBID must provide: resource'
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'some entities support ratings',
|
'some entities support ratings',
|
||||||
|
|
@ -1191,27 +1215,29 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { event, work } = data.lookup
|
const { event, work } = data.lookup;
|
||||||
const artists = data.browse.artists.edges.map(edge => edge.node)
|
const artists = data.browse.artists.edges.map((edge) => edge.node);
|
||||||
const recordings = data.browse.recordings.edges.map(edge => edge.node)
|
const recordings = data.browse.recordings.edges.map((edge) => edge.node);
|
||||||
const labels = data.search.labels.edges.map(edge => edge.node)
|
const labels = data.search.labels.edges.map((edge) => edge.node);
|
||||||
const releaseGroups = data.search.releaseGroups.edges.map(edge => edge.node)
|
const releaseGroups = data.search.releaseGroups.edges.map(
|
||||||
t.is(event.rating.voteCount, 0)
|
(edge) => edge.node
|
||||||
t.is(event.rating.value, null)
|
);
|
||||||
t.true(work.rating.voteCount > 0)
|
t.is(event.rating.voteCount, 0);
|
||||||
t.true(work.rating.value >= 4)
|
t.is(event.rating.value, null);
|
||||||
t.true(artists.some(artist => artist.rating.voteCount > 0))
|
t.true(work.rating.voteCount > 0);
|
||||||
t.true(artists.some(artist => artist.rating.value > 3))
|
t.true(work.rating.value >= 4);
|
||||||
t.true(recordings.some(recording => recording.rating.voteCount > 0))
|
t.true(artists.some((artist) => artist.rating.voteCount > 0));
|
||||||
t.true(recordings.some(recording => recording.rating.value > 3))
|
t.true(artists.some((artist) => artist.rating.value > 3));
|
||||||
t.true(labels.some(label => label.rating.voteCount > 0))
|
t.true(recordings.some((recording) => recording.rating.voteCount > 0));
|
||||||
t.true(labels.some(label => label.rating.value > 3))
|
t.true(recordings.some((recording) => recording.rating.value > 3));
|
||||||
|
t.true(labels.some((label) => label.rating.voteCount > 0));
|
||||||
|
t.true(labels.some((label) => label.rating.value > 3));
|
||||||
t.true(
|
t.true(
|
||||||
releaseGroups.some(releaseGroup => releaseGroup.rating.voteCount > 0)
|
releaseGroups.some((releaseGroup) => releaseGroup.rating.voteCount > 0)
|
||||||
)
|
);
|
||||||
t.true(releaseGroups.some(releaseGroup => releaseGroup.rating.value > 3))
|
t.true(releaseGroups.some((releaseGroup) => releaseGroup.rating.value > 3));
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'discs can be looked up by disc ID',
|
'discs can be looked up by disc ID',
|
||||||
|
|
@ -1238,10 +1264,10 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { disc } = data.lookup
|
const { disc } = data.lookup;
|
||||||
t.is(disc.discID, 'TMXdzZkTcc9Jq24PD0w5J9_AXms-')
|
t.is(disc.discID, 'TMXdzZkTcc9Jq24PD0w5J9_AXms-');
|
||||||
t.is(disc.offsetCount, 9)
|
t.is(disc.offsetCount, 9);
|
||||||
t.is(disc.sectors, 193443)
|
t.is(disc.sectors, 193443);
|
||||||
t.deepEqual(disc.offsets, [
|
t.deepEqual(disc.offsets, [
|
||||||
150,
|
150,
|
||||||
18190,
|
18190,
|
||||||
|
|
@ -1251,16 +1277,16 @@ test(
|
||||||
116853,
|
116853,
|
||||||
151413,
|
151413,
|
||||||
166833,
|
166833,
|
||||||
184123
|
184123,
|
||||||
])
|
]);
|
||||||
t.is(disc.releases.totalCount, 1)
|
t.is(disc.releases.totalCount, 1);
|
||||||
t.is(disc.releases.edges.length, 1)
|
t.is(disc.releases.edges.length, 1);
|
||||||
t.is(
|
t.is(
|
||||||
disc.releases.edges[0].node.mbid,
|
disc.releases.edges[0].node.mbid,
|
||||||
'7f6d3088-837d-495e-905f-be5c70ac2d82'
|
'7f6d3088-837d-495e-905f-be5c70ac2d82'
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'release media has a list of discs',
|
'release media has a list of discs',
|
||||||
|
|
@ -1287,14 +1313,14 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { release } = data.lookup
|
const { release } = data.lookup;
|
||||||
t.is(release.media.length, 1)
|
t.is(release.media.length, 1);
|
||||||
t.is(release.media[0].discs.length, 2)
|
t.is(release.media[0].discs.length, 2);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
// FIXME: API seems to have changed, potentially a bug in MusicBrainz.
|
// FIXME: API seems to have changed, potentially a bug in MusicBrainz.
|
||||||
test.skip(
|
test(
|
||||||
'disc queries can be deeply nested',
|
'disc queries can be deeply nested',
|
||||||
testData,
|
testData,
|
||||||
`
|
`
|
||||||
|
|
@ -1344,25 +1370,25 @@ test.skip(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { disc } = data.lookup
|
const { disc } = data.lookup;
|
||||||
t.true(disc.releases.edges.length > 0)
|
t.true(disc.releases.edges.length > 0);
|
||||||
disc.releases.edges.forEach(release => {
|
disc.releases.edges.forEach((release) => {
|
||||||
t.true(release.node.media.length > 0)
|
t.true(release.node.media.length > 0);
|
||||||
release.node.media.forEach(medium => {
|
release.node.media.forEach((medium) => {
|
||||||
t.true(medium.discs.length > 0)
|
t.true(medium.discs.length > 0);
|
||||||
medium.discs.forEach(disc => {
|
medium.discs.forEach((disc) => {
|
||||||
t.true(disc.releases.edges.length > 0)
|
t.true(disc.releases.edges.length > 0);
|
||||||
disc.releases.edges.forEach(release => {
|
disc.releases.edges.forEach((release) => {
|
||||||
t.true(release.node.media.length > 0)
|
t.true(release.node.media.length > 0);
|
||||||
release.node.media.forEach(medium => {
|
release.node.media.forEach((medium) => {
|
||||||
t.true(medium.discs.length > 0)
|
t.true(medium.discs.length > 0);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'entities support tags',
|
'entities support tags',
|
||||||
|
|
@ -1400,11 +1426,50 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { label } = data.lookup
|
const { label } = data.lookup;
|
||||||
const artists = data.search.artists.edges.map(edge => edge.node)
|
const artists = data.search.artists.edges.map((edge) => edge.node);
|
||||||
t.true(label.tags.edges.some(edge => edge.node.name === 'indie folk'))
|
t.true(label.tags.edges.some((edge) => edge.node.name === 'indie folk'));
|
||||||
t.true(label.tags.edges.some(edge => edge.node.count > 0))
|
t.true(label.tags.edges.some((edge) => edge.node.count > 0));
|
||||||
t.true(artists[0].tags.edges.some(edge => edge.node.name === 'blues rock'))
|
t.true(
|
||||||
t.true(artists[0].tags.edges.some(edge => edge.node.count > 0))
|
artists[0].tags.edges.some((edge) => edge.node.name === 'blues rock')
|
||||||
|
);
|
||||||
|
t.true(artists[0].tags.edges.some((edge) => edge.node.count > 0));
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'releases can include tracks',
|
||||||
|
testData,
|
||||||
|
`
|
||||||
|
{
|
||||||
|
lookup {
|
||||||
|
release(mbid: "fba5f8fe-c6c8-4511-8562-c9febf482674") {
|
||||||
|
media {
|
||||||
|
trackCount
|
||||||
|
position
|
||||||
|
formatID
|
||||||
|
format
|
||||||
|
tracks {
|
||||||
|
mbid
|
||||||
|
title
|
||||||
|
position
|
||||||
|
number
|
||||||
|
length
|
||||||
|
recording {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
(t, data) => {
|
||||||
|
t.true(data.lookup.release.media.every((media) => media.tracks.length > 0));
|
||||||
|
t.true(
|
||||||
|
data.lookup.release.media.every((media) =>
|
||||||
|
media.tracks.every((track) => track.recording)
|
||||||
)
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import test from 'ava'
|
import test from 'ava';
|
||||||
import Client from '../../src/api/client'
|
import Client from '../../src/api/client.js';
|
||||||
|
|
||||||
test('parseErrorMessage() returns the body or status code', t => {
|
test('parseErrorMessage() returns the input error by default', (t) => {
|
||||||
const client = new Client()
|
const client = new Client();
|
||||||
t.is(
|
const error = {
|
||||||
client.parseErrorMessage({ statusCode: 500 }, 'something went wrong'),
|
name: 'HTTPError',
|
||||||
'something went wrong'
|
response: {
|
||||||
)
|
statusCode: 500,
|
||||||
t.is(client.parseErrorMessage({ statusCode: 500 }, ''), '500')
|
body: 'something went wrong',
|
||||||
t.is(client.parseErrorMessage({ statusCode: 404 }, {}), '404')
|
},
|
||||||
})
|
};
|
||||||
|
t.is(client.parseErrorMessage(error), error);
|
||||||
|
});
|
||||||
|
|
|
||||||
116
test/api/fixtures/musicbrainz.js.nock
Normal file
116
test/api/fixtures/musicbrainz.js.nock
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"getBrowseURL() generates a browse URL",
|
||||||
|
[
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"getLookupURL() generates a lookup URL",
|
||||||
|
[
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"getSearchURL() generates a search URL",
|
||||||
|
[
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"lookup() sends a lookup query",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"body": "",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/ws/2/artist/c8da2e40-bd28-4d4e-813a-bd2f51958ba8?fmt=json",
|
||||||
|
"rawHeaders": [
|
||||||
|
"Date",
|
||||||
|
"Thu, 15 Apr 2021 09:07:10 GMT",
|
||||||
|
"Content-Type",
|
||||||
|
"application/json; charset=utf-8",
|
||||||
|
"Transfer-Encoding",
|
||||||
|
"chunked",
|
||||||
|
"Connection",
|
||||||
|
"close",
|
||||||
|
"Vary",
|
||||||
|
"Accept-Encoding",
|
||||||
|
"X-RateLimit-Limit",
|
||||||
|
"1200",
|
||||||
|
"X-RateLimit-Remaining",
|
||||||
|
"783",
|
||||||
|
"X-RateLimit-Reset",
|
||||||
|
"1618477631",
|
||||||
|
"Server",
|
||||||
|
"Plack::Handler::Starlet",
|
||||||
|
"ETag",
|
||||||
|
"W/\"80f1f0e96231d62805a0ac7d54414c6f\"",
|
||||||
|
"Access-Control-Allow-Origin",
|
||||||
|
"*",
|
||||||
|
"X-Cache-Status",
|
||||||
|
"STALE"
|
||||||
|
],
|
||||||
|
"reqheaders": {
|
||||||
|
"accept": "application/json",
|
||||||
|
"accept-encoding": "gzip, deflate, br",
|
||||||
|
"host": "musicbrainz.org"
|
||||||
|
},
|
||||||
|
"response": "{\"life-span\":{\"end\":null,\"ended\":false,\"begin\":\"2013\"},\"id\":\"c8da2e40-bd28-4d4e-813a-bd2f51958ba8\",\"type-id\":\"e431f5f6-b5d2-343d-8b36-72607fffb74b\",\"begin_area\":{\"type-id\":null,\"id\":\"10adc6b5-63bf-4b4e-993e-ed83b05c22fc\",\"sort-name\":\"Seattle\",\"name\":\"Seattle\",\"type\":null,\"disambiguation\":\"\"},\"country\":null,\"gender-id\":null,\"disambiguation\":\"Seattle trio\",\"isnis\":[],\"gender\":null,\"sort-name\":\"Lures\",\"name\":\"Lures\",\"end-area\":null,\"begin-area\":{\"type-id\":null,\"id\":\"10adc6b5-63bf-4b4e-993e-ed83b05c22fc\",\"sort-name\":\"Seattle\",\"name\":\"Seattle\",\"type\":null,\"disambiguation\":\"\"},\"end_area\":null,\"ipis\":[],\"type\":\"Group\",\"area\":{\"disambiguation\":\"\",\"type\":null,\"type-id\":null,\"id\":\"10adc6b5-63bf-4b4e-993e-ed83b05c22fc\",\"name\":\"Seattle\",\"sort-name\":\"Seattle\"}}",
|
||||||
|
"responseIsBinary": false,
|
||||||
|
"scope": "http://musicbrainz.org:80",
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"rejects non-MusicBrainz errors",
|
||||||
|
[
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"rejects the promise when the API returns an error",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"body": "",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/ws/2/artist/5b11f4ce-a62d-471e-81fc-a69a8278c7da?inc=foobar&fmt=json",
|
||||||
|
"rawHeaders": [
|
||||||
|
"Date",
|
||||||
|
"Thu, 15 Apr 2021 09:07:10 GMT",
|
||||||
|
"Content-Type",
|
||||||
|
"application/json; charset=utf-8",
|
||||||
|
"Content-Length",
|
||||||
|
"144",
|
||||||
|
"Connection",
|
||||||
|
"close",
|
||||||
|
"X-RateLimit-Limit",
|
||||||
|
"1200",
|
||||||
|
"X-RateLimit-Remaining",
|
||||||
|
"664",
|
||||||
|
"X-RateLimit-Reset",
|
||||||
|
"1618477631",
|
||||||
|
"Server",
|
||||||
|
"Plack::Handler::Starlet",
|
||||||
|
"ETag",
|
||||||
|
"\"294308a5f1930ea2b39414c0b8ec853c\"",
|
||||||
|
"Access-Control-Allow-Origin",
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"reqheaders": {
|
||||||
|
"accept": "application/json",
|
||||||
|
"accept-encoding": "gzip, deflate, br",
|
||||||
|
"host": "musicbrainz.org"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"error": "foobar is not a valid inc parameter for the artist resource.",
|
||||||
|
"help": "For usage, please see: https://musicbrainz.org/development/mmd"
|
||||||
|
},
|
||||||
|
"responseIsBinary": false,
|
||||||
|
"scope": "http://musicbrainz.org:80",
|
||||||
|
"status": 400
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"uses the default error impementation if there is no JSON error",
|
||||||
|
[
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
@ -1,84 +1,87 @@
|
||||||
import test from 'ava'
|
import test from 'ava';
|
||||||
import MusicBrainz, { MusicBrainzError } from '../../src/api'
|
import MusicBrainz from '../../src/api/index.js';
|
||||||
import client from '../helpers/client/musicbrainz'
|
import client from '../helpers/client/musicbrainz.js';
|
||||||
|
|
||||||
test('getLookupURL() generates a lookup URL', t => {
|
test('getLookupURL() generates a lookup URL', (t) => {
|
||||||
t.is(
|
t.is(
|
||||||
client.getLookupURL('artist', 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8', {
|
client.getLookupURL('artist', 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8', {
|
||||||
inc: ['recordings', 'release-groups']
|
inc: ['recordings', 'release-groups'],
|
||||||
}),
|
}),
|
||||||
'artist/c8da2e40-bd28-4d4e-813a-bd2f51958ba8?inc=recordings%2Brelease-groups'
|
'artist/c8da2e40-bd28-4d4e-813a-bd2f51958ba8?inc=recordings%2Brelease-groups'
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
test('getBrowseURL() generates a browse URL', t => {
|
test('getBrowseURL() generates a browse URL', (t) => {
|
||||||
t.is(
|
t.is(
|
||||||
client.getBrowseURL('recording', {
|
client.getBrowseURL('recording', {
|
||||||
artist: 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8',
|
artist: 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8',
|
||||||
limit: null,
|
limit: null,
|
||||||
offset: 0
|
offset: 0,
|
||||||
}),
|
}),
|
||||||
'recording?artist=c8da2e40-bd28-4d4e-813a-bd2f51958ba8&offset=0'
|
'recording?artist=c8da2e40-bd28-4d4e-813a-bd2f51958ba8&offset=0'
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
test('getSearchURL() generates a search URL', t => {
|
test('getSearchURL() generates a search URL', (t) => {
|
||||||
t.is(
|
t.is(
|
||||||
client.getSearchURL('artist', 'Lures', { inc: null }),
|
client.getSearchURL('artist', 'Lures', { inc: null }),
|
||||||
'artist?query=Lures'
|
'artist?query=Lures'
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
test('lookup() sends a lookup query', t => {
|
test('lookup() sends a lookup query', async (t) => {
|
||||||
return client
|
const response = await client.lookup(
|
||||||
.lookup('artist', 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8')
|
'artist',
|
||||||
.then(response => {
|
'c8da2e40-bd28-4d4e-813a-bd2f51958ba8'
|
||||||
t.is(response.id, 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8')
|
);
|
||||||
t.is(response.type, 'Group')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('rejects the promise when the API returns an error', t => {
|
t.is(response.id, 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8');
|
||||||
|
t.is(response.type, 'Group');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects the promise when the API returns an error', (t) => {
|
||||||
const req = client.lookup('artist', '5b11f4ce-a62d-471e-81fc-a69a8278c7da', {
|
const req = client.lookup('artist', '5b11f4ce-a62d-471e-81fc-a69a8278c7da', {
|
||||||
inc: ['foobar']
|
inc: ['foobar'],
|
||||||
})
|
});
|
||||||
return t.throws(req, MusicBrainzError)
|
return t.throwsAsync(req, {
|
||||||
})
|
name: 'MusicBrainzError',
|
||||||
|
message: 'foobar is not a valid inc parameter for the artist resource.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('shouldRetry() retries only 5xx responses from MusicBrainz', t => {
|
test('rejects non-MusicBrainz errors', (t) => {
|
||||||
t.true(client.shouldRetry(new MusicBrainzError('error', 500)))
|
const client = new MusicBrainz({ baseURL: '$!@#$' });
|
||||||
t.true(client.shouldRetry(new MusicBrainzError('error', 501)))
|
return t.throwsAsync(
|
||||||
t.true(client.shouldRetry(new MusicBrainzError('error', 598)))
|
|
||||||
t.true(client.shouldRetry(new MusicBrainzError('error', 599)))
|
|
||||||
t.false(client.shouldRetry(new MusicBrainzError('error', 404)))
|
|
||||||
t.false(client.shouldRetry(new MusicBrainzError('error', 499)))
|
|
||||||
t.false(client.shouldRetry(new MusicBrainzError('error', 600)))
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shouldRetry() retries only transient local connection issues', t => {
|
|
||||||
t.true(client.shouldRetry({ code: 'ECONNRESET' }))
|
|
||||||
t.true(client.shouldRetry({ code: 'ENOTFOUND' }))
|
|
||||||
t.true(client.shouldRetry({ code: 'ESOCKETTIMEDOUT' }))
|
|
||||||
t.true(client.shouldRetry({ code: 'ETIMEDOUT' }))
|
|
||||||
t.true(client.shouldRetry({ code: 'ECONNREFUSED' }))
|
|
||||||
t.true(client.shouldRetry({ code: 'EHOSTUNREACH' }))
|
|
||||||
t.true(client.shouldRetry({ code: 'EPIPE' }))
|
|
||||||
t.true(client.shouldRetry({ code: 'EAI_AGAIN' }))
|
|
||||||
t.false(client.shouldRetry({ code: 'ENOENT' }))
|
|
||||||
t.false(client.shouldRetry({ code: 'EACCES' }))
|
|
||||||
t.false(client.shouldRetry({ code: 'EPERM' }))
|
|
||||||
})
|
|
||||||
|
|
||||||
test('rejects non-MusicBrainz errors', t => {
|
|
||||||
const client = new MusicBrainz({ baseURL: '$!@#$' })
|
|
||||||
return t.throws(
|
|
||||||
client.get('artist/5b11f4ce-a62d-471e-81fc-a69a8278c7da'),
|
client.get('artist/5b11f4ce-a62d-471e-81fc-a69a8278c7da'),
|
||||||
Error
|
{
|
||||||
)
|
name: 'TypeError',
|
||||||
})
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('uses the default error impementation if there is no JSON error', t => {
|
test('uses the default error impementation if there is no JSON error', (t) => {
|
||||||
t.is(client.parseErrorMessage({ statusCode: 501 }, 'yikes'), 'yikes')
|
let error = {
|
||||||
t.is(client.parseErrorMessage({ statusCode: 500 }, {}), '500')
|
name: 'HTTPError',
|
||||||
t.is(client.parseErrorMessage({ statusCode: 404 }, null), '404')
|
response: {
|
||||||
})
|
statusCode: 501,
|
||||||
|
body: 'yikes',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
t.is(client.parseErrorMessage(error), error);
|
||||||
|
error = {
|
||||||
|
name: 'HTTPError',
|
||||||
|
response: {
|
||||||
|
statusCode: 500,
|
||||||
|
body: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
t.is(client.parseErrorMessage(error), error);
|
||||||
|
error = {
|
||||||
|
name: 'HTTPError',
|
||||||
|
response: {
|
||||||
|
statusCode: 404,
|
||||||
|
body: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
t.is(client.parseErrorMessage(error), error);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,8 @@
|
||||||
process.env.TEST_SCHEMA = 'baseSchema'
|
import test from 'ava';
|
||||||
require('./_schema')
|
import { baseSchema } from '../src/schema.js';
|
||||||
|
|
||||||
|
test.before((t) => {
|
||||||
|
t.context.schema = baseSchema;
|
||||||
|
});
|
||||||
|
|
||||||
|
import './_schema.js';
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,13 @@
|
||||||
process.env.TEST_SCHEMA = 'extendedSchema'
|
import test from 'ava';
|
||||||
require('./_schema')
|
import { baseSchema, createSchema } from '../src/schema.js';
|
||||||
|
import { defaultExtensions, loadExtension } from '../src/index.js';
|
||||||
|
|
||||||
|
test.before(async (t) => {
|
||||||
|
const extensions = await Promise.all(
|
||||||
|
defaultExtensions.map((extension) => loadExtension(extension))
|
||||||
|
);
|
||||||
|
const schema = createSchema(baseSchema, { extensions });
|
||||||
|
t.context.schema = schema;
|
||||||
|
});
|
||||||
|
|
||||||
|
import './_schema.js';
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,87 @@
|
||||||
import test from 'ava'
|
import test from 'ava';
|
||||||
import client from '../../helpers/client/cover-art-archive'
|
import client from '../../helpers/client/cover-art-archive.js';
|
||||||
|
|
||||||
|
test('can retrieve a front image URL', async (t) => {
|
||||||
|
const url = await client.imageURL(
|
||||||
|
'release',
|
||||||
|
'76df3287-6cda-33eb-8e9a-044b5e15ffdd',
|
||||||
|
'front'
|
||||||
|
);
|
||||||
|
|
||||||
test('can retrieve a front image URL', t => {
|
|
||||||
return client
|
|
||||||
.imageURL('release', '76df3287-6cda-33eb-8e9a-044b5e15ffdd', 'front')
|
|
||||||
.then(url => {
|
|
||||||
t.is(
|
t.is(
|
||||||
url,
|
url,
|
||||||
'http://archive.org/download/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd-829521842.jpg'
|
'http://archive.org/download/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd-829521842.jpg'
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
})
|
|
||||||
|
test('can retrieve a back image URL', async (t) => {
|
||||||
|
const url = await client.imageURL(
|
||||||
|
'release',
|
||||||
|
'76df3287-6cda-33eb-8e9a-044b5e15ffdd',
|
||||||
|
'back'
|
||||||
|
);
|
||||||
|
|
||||||
test('can retrieve a back image URL', t => {
|
|
||||||
return client
|
|
||||||
.imageURL('release', '76df3287-6cda-33eb-8e9a-044b5e15ffdd', 'back')
|
|
||||||
.then(url => {
|
|
||||||
t.is(
|
t.is(
|
||||||
url,
|
url,
|
||||||
'http://archive.org/download/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd-5769317885.jpg'
|
'http://archive.org/download/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd-5769317885.jpg'
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
})
|
|
||||||
|
test('can retrieve a list of release images', async (t) => {
|
||||||
|
const data = await client.images(
|
||||||
|
'release',
|
||||||
|
'76df3287-6cda-33eb-8e9a-044b5e15ffdd'
|
||||||
|
);
|
||||||
|
|
||||||
test('can retrieve a list of release images', t => {
|
|
||||||
return client
|
|
||||||
.images('release', '76df3287-6cda-33eb-8e9a-044b5e15ffdd')
|
|
||||||
.then(data => {
|
|
||||||
t.is(
|
t.is(
|
||||||
data.release,
|
data.release,
|
||||||
'http://musicbrainz.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd'
|
'https://musicbrainz.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd'
|
||||||
)
|
);
|
||||||
t.true(data.images.length >= 3)
|
t.true(data.images.length >= 3);
|
||||||
data.images.forEach(image => {
|
data.images.forEach((image) => {
|
||||||
t.true(image.approved)
|
t.true(image.approved);
|
||||||
t.truthy(image.image)
|
t.truthy(image.image);
|
||||||
t.truthy(image.id)
|
t.truthy(image.id);
|
||||||
t.truthy(image.thumbnails.small)
|
t.truthy(image.thumbnails.small);
|
||||||
t.truthy(image.thumbnails.large)
|
t.truthy(image.thumbnails.large);
|
||||||
})
|
});
|
||||||
t.true(data.images.some(image => image.front))
|
t.true(data.images.some((image) => image.front));
|
||||||
t.true(data.images.some(image => image.back))
|
t.true(data.images.some((image) => image.back));
|
||||||
t.true(data.images.some(image => image.types.indexOf('Front') !== -1))
|
t.true(data.images.some((image) => image.types.indexOf('Front') !== -1));
|
||||||
t.true(data.images.some(image => image.types.indexOf('Back') !== -1))
|
t.true(data.images.some((image) => image.types.indexOf('Back') !== -1));
|
||||||
t.true(data.images.some(image => image.types.indexOf('Medium') !== -1))
|
t.true(data.images.some((image) => image.types.indexOf('Medium') !== -1));
|
||||||
})
|
});
|
||||||
})
|
|
||||||
|
|
||||||
test('throws an error if given an invalid MBID', t => {
|
test('throws an error if given an invalid MBID', (t) => {
|
||||||
return t.throws(client.images('release', 'xyz'), client.errorClass)
|
return t.throwsAsync(client.images('release', 'xyz'), {
|
||||||
})
|
name: 'CoverArtArchiveError',
|
||||||
|
message: 'Bad Request: invalid MBID specified',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('uses the default error impementation if there is no HTML error', t => {
|
test('uses the default error impementation if there is no HTML error', (t) => {
|
||||||
t.is(client.parseErrorMessage({ statusCode: 501 }, 'yikes'), 'yikes')
|
let error = {
|
||||||
t.is(client.parseErrorMessage({ statusCode: 500 }, ''), '500')
|
name: 'HTTPError',
|
||||||
t.is(client.parseErrorMessage({ statusCode: 404 }, null), '404')
|
response: {
|
||||||
})
|
statusCode: 501,
|
||||||
|
body: 'yikes',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
t.is(client.parseErrorMessage(error), error);
|
||||||
|
error = {
|
||||||
|
name: 'HTTPError',
|
||||||
|
response: {
|
||||||
|
statusCode: 500,
|
||||||
|
body: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
t.is(client.parseErrorMessage(error), error);
|
||||||
|
error = {
|
||||||
|
name: 'HTTPError',
|
||||||
|
response: {
|
||||||
|
statusCode: 500,
|
||||||
|
body: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
t.is(client.parseErrorMessage(error), error);
|
||||||
|
});
|
||||||
|
|
|
||||||
332
test/extensions/cover-art-archive/fixtures/client.js.nock
Normal file
332
test/extensions/cover-art-archive/fixtures/client.js.nock
Normal file
|
|
@ -0,0 +1,332 @@
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"can retrieve a back image URL",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"body": "",
|
||||||
|
"method": "HEAD",
|
||||||
|
"path": "/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/back",
|
||||||
|
"rawHeaders": [
|
||||||
|
"Date",
|
||||||
|
"Thu, 15 Apr 2021 09:07:12 GMT",
|
||||||
|
"Content-Type",
|
||||||
|
"text/plain; charset=utf-8",
|
||||||
|
"Content-Length",
|
||||||
|
"132",
|
||||||
|
"Connection",
|
||||||
|
"close",
|
||||||
|
"Location",
|
||||||
|
"http://archive.org/download/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd-5769317885.jpg",
|
||||||
|
"Access-Control-Allow-Origin",
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"reqheaders": {
|
||||||
|
"accept": "application/json",
|
||||||
|
"accept-encoding": "gzip, deflate, br",
|
||||||
|
"host": "coverartarchive.org"
|
||||||
|
},
|
||||||
|
"response": "",
|
||||||
|
"responseIsBinary": false,
|
||||||
|
"scope": "http://coverartarchive.org:80",
|
||||||
|
"status": 307
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"can retrieve a front image URL",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"body": "",
|
||||||
|
"method": "HEAD",
|
||||||
|
"path": "/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/front",
|
||||||
|
"rawHeaders": [
|
||||||
|
"Date",
|
||||||
|
"Thu, 15 Apr 2021 09:07:12 GMT",
|
||||||
|
"Content-Type",
|
||||||
|
"text/plain; charset=utf-8",
|
||||||
|
"Content-Length",
|
||||||
|
"131",
|
||||||
|
"Connection",
|
||||||
|
"close",
|
||||||
|
"Location",
|
||||||
|
"http://archive.org/download/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd-829521842.jpg",
|
||||||
|
"Access-Control-Allow-Origin",
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"reqheaders": {
|
||||||
|
"accept": "application/json",
|
||||||
|
"accept-encoding": "gzip, deflate, br",
|
||||||
|
"host": "coverartarchive.org"
|
||||||
|
},
|
||||||
|
"response": "",
|
||||||
|
"responseIsBinary": false,
|
||||||
|
"scope": "http://coverartarchive.org:80",
|
||||||
|
"status": 307
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"can retrieve a list of release images",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"body": "",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd",
|
||||||
|
"rawHeaders": [
|
||||||
|
"Date",
|
||||||
|
"Thu, 15 Apr 2021 09:07:12 GMT",
|
||||||
|
"Content-Type",
|
||||||
|
"text/plain; charset=utf-8",
|
||||||
|
"Content-Length",
|
||||||
|
"86",
|
||||||
|
"Connection",
|
||||||
|
"close",
|
||||||
|
"Location",
|
||||||
|
"http://archive.org/download/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd/index.json",
|
||||||
|
"Access-Control-Allow-Origin",
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"reqheaders": {
|
||||||
|
"accept": "application/json",
|
||||||
|
"accept-encoding": "gzip, deflate, br",
|
||||||
|
"host": "coverartarchive.org"
|
||||||
|
},
|
||||||
|
"response": "See: http://archive.org/download/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd/index.json\n",
|
||||||
|
"responseIsBinary": false,
|
||||||
|
"scope": "http://coverartarchive.org:80",
|
||||||
|
"status": 307
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"body": "",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/download/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd/index.json",
|
||||||
|
"rawHeaders": [
|
||||||
|
"Server",
|
||||||
|
"nginx/1.16.1 (Ubuntu)",
|
||||||
|
"Date",
|
||||||
|
"Thu, 15 Apr 2021 09:07:12 GMT",
|
||||||
|
"Content-Type",
|
||||||
|
"text/html; charset=UTF-8",
|
||||||
|
"Transfer-Encoding",
|
||||||
|
"chunked",
|
||||||
|
"Connection",
|
||||||
|
"close",
|
||||||
|
"Access-Control-Allow-Origin",
|
||||||
|
"*",
|
||||||
|
"Accept-Ranges",
|
||||||
|
"bytes",
|
||||||
|
"Location",
|
||||||
|
"http://ia802607.us.archive.org/32/items/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd/index.json",
|
||||||
|
"Strict-Transport-Security",
|
||||||
|
"max-age=15724800",
|
||||||
|
"Referrer-Policy",
|
||||||
|
"no-referrer-when-downgrade"
|
||||||
|
],
|
||||||
|
"reqheaders": {
|
||||||
|
"accept": "application/json",
|
||||||
|
"accept-encoding": "gzip, deflate, br",
|
||||||
|
"host": "archive.org"
|
||||||
|
},
|
||||||
|
"response": "",
|
||||||
|
"responseIsBinary": false,
|
||||||
|
"scope": "http://archive.org:80",
|
||||||
|
"status": 302
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"body": "",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/32/items/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd/index.json",
|
||||||
|
"rawHeaders": [
|
||||||
|
"Server",
|
||||||
|
"nginx/1.18.0 (Ubuntu)",
|
||||||
|
"Date",
|
||||||
|
"Thu, 15 Apr 2021 09:07:13 GMT",
|
||||||
|
"Content-Type",
|
||||||
|
"application/json",
|
||||||
|
"Content-Length",
|
||||||
|
"4445",
|
||||||
|
"Last-Modified",
|
||||||
|
"Tue, 12 Nov 2019 22:11:45 GMT",
|
||||||
|
"Connection",
|
||||||
|
"close",
|
||||||
|
"ETag",
|
||||||
|
"\"5dcb2e21-115d\"",
|
||||||
|
"Strict-Transport-Security",
|
||||||
|
"max-age=15724800",
|
||||||
|
"Expires",
|
||||||
|
"Thu, 15 Apr 2021 15:07:13 GMT",
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=21600",
|
||||||
|
"Access-Control-Allow-Origin",
|
||||||
|
"*",
|
||||||
|
"Accept-Ranges",
|
||||||
|
"bytes"
|
||||||
|
],
|
||||||
|
"reqheaders": {
|
||||||
|
"accept": "application/json",
|
||||||
|
"accept-encoding": "gzip, deflate, br",
|
||||||
|
"host": "ia802607.us.archive.org"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"approved": true,
|
||||||
|
"back": false,
|
||||||
|
"comment": "",
|
||||||
|
"edit": 17462565,
|
||||||
|
"front": true,
|
||||||
|
"id": 829521842,
|
||||||
|
"image": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/829521842.jpg",
|
||||||
|
"thumbnails": {
|
||||||
|
"1200": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/829521842-1200.jpg",
|
||||||
|
"250": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/829521842-250.jpg",
|
||||||
|
"500": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/829521842-500.jpg",
|
||||||
|
"large": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/829521842-500.jpg",
|
||||||
|
"small": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/829521842-250.jpg"
|
||||||
|
},
|
||||||
|
"types": [
|
||||||
|
"Front"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"approved": true,
|
||||||
|
"back": false,
|
||||||
|
"comment": "",
|
||||||
|
"edit": 65037291,
|
||||||
|
"front": false,
|
||||||
|
"id": 24546038908,
|
||||||
|
"image": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/24546038908.jpg",
|
||||||
|
"thumbnails": {
|
||||||
|
"1200": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/24546038908-1200.jpg",
|
||||||
|
"250": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/24546038908-250.jpg",
|
||||||
|
"500": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/24546038908-500.jpg",
|
||||||
|
"large": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/24546038908-500.jpg",
|
||||||
|
"small": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/24546038908-250.jpg"
|
||||||
|
},
|
||||||
|
"types": [
|
||||||
|
"Front",
|
||||||
|
"Booklet"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"approved": true,
|
||||||
|
"back": false,
|
||||||
|
"comment": "",
|
||||||
|
"edit": 65037298,
|
||||||
|
"front": false,
|
||||||
|
"id": 24546039829,
|
||||||
|
"image": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/24546039829.jpg",
|
||||||
|
"thumbnails": {
|
||||||
|
"1200": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/24546039829-1200.jpg",
|
||||||
|
"250": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/24546039829-250.jpg",
|
||||||
|
"500": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/24546039829-500.jpg",
|
||||||
|
"large": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/24546039829-500.jpg",
|
||||||
|
"small": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/24546039829-250.jpg"
|
||||||
|
},
|
||||||
|
"types": [
|
||||||
|
"Booklet"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"approved": true,
|
||||||
|
"back": false,
|
||||||
|
"comment": "",
|
||||||
|
"edit": 65037300,
|
||||||
|
"front": false,
|
||||||
|
"id": 24546040945,
|
||||||
|
"image": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/24546040945.jpg",
|
||||||
|
"thumbnails": {
|
||||||
|
"1200": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/24546040945-1200.jpg",
|
||||||
|
"250": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/24546040945-250.jpg",
|
||||||
|
"500": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/24546040945-500.jpg",
|
||||||
|
"large": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/24546040945-500.jpg",
|
||||||
|
"small": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/24546040945-250.jpg"
|
||||||
|
},
|
||||||
|
"types": [
|
||||||
|
"Booklet"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"approved": true,
|
||||||
|
"back": true,
|
||||||
|
"comment": "",
|
||||||
|
"edit": 24923554,
|
||||||
|
"front": false,
|
||||||
|
"id": 5769317885,
|
||||||
|
"image": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/5769317885.jpg",
|
||||||
|
"thumbnails": {
|
||||||
|
"1200": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/5769317885-1200.jpg",
|
||||||
|
"250": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/5769317885-250.jpg",
|
||||||
|
"500": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/5769317885-500.jpg",
|
||||||
|
"large": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/5769317885-500.jpg",
|
||||||
|
"small": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/5769317885-250.jpg"
|
||||||
|
},
|
||||||
|
"types": [
|
||||||
|
"Back"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"approved": true,
|
||||||
|
"back": false,
|
||||||
|
"comment": "",
|
||||||
|
"edit": 24923552,
|
||||||
|
"front": false,
|
||||||
|
"id": 5769316809,
|
||||||
|
"image": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/5769316809.jpg",
|
||||||
|
"thumbnails": {
|
||||||
|
"1200": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/5769316809-1200.jpg",
|
||||||
|
"250": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/5769316809-250.jpg",
|
||||||
|
"500": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/5769316809-500.jpg",
|
||||||
|
"large": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/5769316809-500.jpg",
|
||||||
|
"small": "http://coverartarchive.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd/5769316809-250.jpg"
|
||||||
|
},
|
||||||
|
"types": [
|
||||||
|
"Medium"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"release": "https://musicbrainz.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd"
|
||||||
|
},
|
||||||
|
"responseIsBinary": false,
|
||||||
|
"scope": "http://ia802607.us.archive.org:80",
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"throws an error if given an invalid MBID",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"body": "",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/release/xyz",
|
||||||
|
"rawHeaders": [
|
||||||
|
"Date",
|
||||||
|
"Thu, 15 Apr 2021 09:07:13 GMT",
|
||||||
|
"Content-Type",
|
||||||
|
"text/html",
|
||||||
|
"Content-Length",
|
||||||
|
"138",
|
||||||
|
"Connection",
|
||||||
|
"close",
|
||||||
|
"Access-Control-Allow-Origin",
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"reqheaders": {
|
||||||
|
"accept": "application/json",
|
||||||
|
"accept-encoding": "gzip, deflate, br",
|
||||||
|
"host": "coverartarchive.org"
|
||||||
|
},
|
||||||
|
"response": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<title>400 Bad Request</title>\n<h1>Bad Request</h1>\n<p>invalid MBID specified</p>\n",
|
||||||
|
"responseIsBinary": false,
|
||||||
|
"scope": "http://coverartarchive.org:80",
|
||||||
|
"status": 400
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"uses the default error impementation if there is no HTML error",
|
||||||
|
[
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
3756
test/extensions/cover-art-archive/fixtures/schema.js.nock
Normal file
3756
test/extensions/cover-art-archive/fixtures/schema.js.nock
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,20 +1,27 @@
|
||||||
import test from 'ava'
|
import test from 'ava';
|
||||||
import { graphql } from 'graphql'
|
import GraphQL from 'graphql';
|
||||||
import extension from '../../../src/extensions/cover-art-archive'
|
import extension from '../../../src/extensions/cover-art-archive/index.js';
|
||||||
import baseSchema, { applyExtension } from '../../../src/schema'
|
import { baseSchema, applyExtension } from '../../../src/schema.js';
|
||||||
import baseContext from '../../helpers/context'
|
import baseContext from '../../helpers/context.js';
|
||||||
|
|
||||||
const schema = applyExtension(extension, baseSchema)
|
const { graphql } = GraphQL;
|
||||||
const context = extension.extendContext(baseContext)
|
|
||||||
|
const schema = applyExtension(extension, baseSchema);
|
||||||
|
const context = extension.extendContext(baseContext, {
|
||||||
|
coverArtArchive: {
|
||||||
|
limit: Infinity,
|
||||||
|
period: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
function testData(t, query, handler) {
|
function testData(t, query, handler) {
|
||||||
return graphql(schema, query, null, context).then(result => {
|
return graphql(schema, query, null, context).then((result) => {
|
||||||
if (result.errors !== undefined) {
|
if (result.errors !== undefined) {
|
||||||
result.errors.forEach(error => t.log(error))
|
result.errors.forEach((error) => t.log(error));
|
||||||
}
|
}
|
||||||
t.is(result.errors, undefined)
|
t.is(result.errors, undefined);
|
||||||
return handler(t, result.data)
|
return handler(t, result.data);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
test(
|
test(
|
||||||
|
|
@ -33,11 +40,11 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { coverArtArchive } = data.lookup.release
|
const { coverArtArchive } = data.lookup.release;
|
||||||
t.true(coverArtArchive.artwork)
|
t.true(coverArtArchive.artwork);
|
||||||
t.true(coverArtArchive.count >= 10)
|
t.true(coverArtArchive.count >= 10);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'releases have a set of cover art images',
|
'releases have a set of cover art images',
|
||||||
|
|
@ -69,48 +76,50 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { coverArtArchive } = data.lookup.release
|
const { coverArtArchive } = data.lookup.release;
|
||||||
t.is(
|
t.is(
|
||||||
coverArtArchive.front,
|
coverArtArchive.front,
|
||||||
'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/1611507818.jpg'
|
'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/1611507818.jpg'
|
||||||
)
|
);
|
||||||
t.is(
|
t.is(
|
||||||
coverArtArchive.back,
|
coverArtArchive.back,
|
||||||
'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/13536418798.jpg'
|
'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/13536418798.jpg'
|
||||||
)
|
);
|
||||||
t.true(coverArtArchive.images.length >= 10)
|
t.true(coverArtArchive.images.length >= 10);
|
||||||
t.true(coverArtArchive.images.some(image => image.front === true))
|
t.true(coverArtArchive.images.some((image) => image.front === true));
|
||||||
t.true(coverArtArchive.images.some(image => image.back === true))
|
t.true(coverArtArchive.images.some((image) => image.back === true));
|
||||||
t.true(
|
t.true(
|
||||||
coverArtArchive.images.some(image => image.types.indexOf('Front') >= 0)
|
coverArtArchive.images.some((image) => image.types.indexOf('Front') >= 0)
|
||||||
)
|
);
|
||||||
t.true(
|
t.true(
|
||||||
coverArtArchive.images.some(image => image.types.indexOf('Back') >= 0)
|
coverArtArchive.images.some((image) => image.types.indexOf('Back') >= 0)
|
||||||
)
|
);
|
||||||
t.true(
|
t.true(
|
||||||
coverArtArchive.images.some(image => image.types.indexOf('Liner') >= 0)
|
coverArtArchive.images.some((image) => image.types.indexOf('Liner') >= 0)
|
||||||
)
|
);
|
||||||
t.true(
|
t.true(
|
||||||
coverArtArchive.images.some(image => image.types.indexOf('Poster') >= 0)
|
coverArtArchive.images.some((image) => image.types.indexOf('Poster') >= 0)
|
||||||
)
|
);
|
||||||
t.true(
|
t.true(
|
||||||
coverArtArchive.images.some(image => image.types.indexOf('Medium') >= 0)
|
coverArtArchive.images.some((image) => image.types.indexOf('Medium') >= 0)
|
||||||
)
|
);
|
||||||
t.true(coverArtArchive.images.some(image => image.edit === 18544122))
|
t.true(coverArtArchive.images.some((image) => image.edit === 18544122));
|
||||||
t.true(coverArtArchive.images.some(image => image.comment === ''))
|
t.true(coverArtArchive.images.some((image) => image.comment === ''));
|
||||||
t.true(coverArtArchive.images.some(image => image.fileID === '1611507818'))
|
t.true(
|
||||||
|
coverArtArchive.images.some((image) => image.fileID === '1611507818')
|
||||||
|
);
|
||||||
t.true(
|
t.true(
|
||||||
coverArtArchive.images.some(
|
coverArtArchive.images.some(
|
||||||
image =>
|
(image) =>
|
||||||
image.image ===
|
image.image ===
|
||||||
'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/13536422691.jpg'
|
'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/13536422691.jpg'
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
t.true(coverArtArchive.images.every(image => image.approved === true))
|
t.true(coverArtArchive.images.every((image) => image.approved === true));
|
||||||
t.true(coverArtArchive.images.every(image => image.thumbnails.small))
|
t.true(coverArtArchive.images.every((image) => image.thumbnails.small));
|
||||||
t.true(coverArtArchive.images.every(image => image.thumbnails.large))
|
t.true(coverArtArchive.images.every((image) => image.thumbnails.large));
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'can request a size for front and back cover art',
|
'can request a size for front and back cover art',
|
||||||
|
|
@ -129,21 +138,21 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { coverArtArchive } = data.lookup.release
|
const { coverArtArchive } = data.lookup.release;
|
||||||
t.is(
|
t.is(
|
||||||
coverArtArchive.front,
|
coverArtArchive.front,
|
||||||
'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/1611507818-500.jpg'
|
'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/1611507818-500.jpg'
|
||||||
)
|
);
|
||||||
t.is(
|
t.is(
|
||||||
coverArtArchive.back,
|
coverArtArchive.back,
|
||||||
'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/13536418798-250.jpg'
|
'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/13536418798-250.jpg'
|
||||||
)
|
);
|
||||||
t.is(
|
t.is(
|
||||||
coverArtArchive.fullFront,
|
coverArtArchive.fullFront,
|
||||||
'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/1611507818.jpg'
|
'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/1611507818.jpg'
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'release groups have a front cover art image',
|
'release groups have a front cover art image',
|
||||||
|
|
@ -169,16 +178,16 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { coverArtArchive } = data.lookup.releaseGroup
|
const { coverArtArchive } = data.lookup.releaseGroup;
|
||||||
const { front } = coverArtArchive
|
const { front } = coverArtArchive;
|
||||||
t.true(coverArtArchive.artwork)
|
t.true(coverArtArchive.artwork);
|
||||||
t.snapshot({ front })
|
t.snapshot({ front });
|
||||||
t.is(coverArtArchive.release.mbid, '25fbfbb4-b1ee-4448-aadf-ae3bc2e2dd27')
|
t.is(coverArtArchive.release.mbid, '6ef6e6cd-ad36-4c2f-816d-121bfb2f6774');
|
||||||
t.is(coverArtArchive.release.title, 'The Dark Side of the Moon')
|
t.is(coverArtArchive.release.title, 'The Dark Side of the Moon');
|
||||||
t.is(coverArtArchive.images.length, 1)
|
t.is(coverArtArchive.images.length, 1);
|
||||||
t.true(coverArtArchive.images[0].front)
|
t.true(coverArtArchive.images[0].front);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'release groups have different cover art sizes available',
|
'release groups have different cover art sizes available',
|
||||||
|
|
@ -196,11 +205,11 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const { coverArtArchive } = data.lookup.releaseGroup
|
const { coverArtArchive } = data.lookup.releaseGroup;
|
||||||
const { small, large } = coverArtArchive
|
const { small, large } = coverArtArchive;
|
||||||
t.snapshot({ small, large })
|
t.snapshot({ small, large });
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'can retrieve cover art in searches',
|
'can retrieve cover art in searches',
|
||||||
|
|
@ -226,11 +235,15 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
const releases = data.search.releases.edges.map(edge => edge.node)
|
const releases = data.search.releases.edges.map((edge) => edge.node);
|
||||||
t.is(releases.length, 25)
|
t.is(releases.length, 25);
|
||||||
t.true(releases.some(release => release.coverArtArchive.artwork === true))
|
t.true(
|
||||||
t.true(releases.some(release => release.coverArtArchive.images.length > 0))
|
releases.some((release) => release.coverArtArchive.artwork === true)
|
||||||
t.true(releases.some(release => release.coverArtArchive.front === null))
|
);
|
||||||
t.true(releases.some(release => release.coverArtArchive.back === null))
|
t.true(
|
||||||
|
releases.some((release) => release.coverArtArchive.images.length > 0)
|
||||||
|
);
|
||||||
|
t.true(releases.some((release) => release.coverArtArchive.front === null));
|
||||||
|
t.true(releases.some((release) => release.coverArtArchive.back === null));
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@
|
||||||
|
|
||||||
The actual snapshot is saved in `schema.js.snap`.
|
The actual snapshot is saved in `schema.js.snap`.
|
||||||
|
|
||||||
Generated by [AVA](https://ava.li).
|
Generated by [AVA](https://avajs.dev).
|
||||||
|
|
||||||
## release groups have a front cover art image
|
## release groups have a front cover art image
|
||||||
|
|
||||||
> Snapshot 1
|
> Snapshot 1
|
||||||
|
|
||||||
{
|
{
|
||||||
front: 'http://coverartarchive.org/release/25fbfbb4-b1ee-4448-aadf-ae3bc2e2dd27/18974521469.jpg',
|
front: 'http://coverartarchive.org/release/6ef6e6cd-ad36-4c2f-816d-121bfb2f6774/12699438648.jpg',
|
||||||
}
|
}
|
||||||
|
|
||||||
## release groups have different cover art sizes available
|
## release groups have different cover art sizes available
|
||||||
|
|
@ -17,6 +17,6 @@ Generated by [AVA](https://ava.li).
|
||||||
> Snapshot 1
|
> Snapshot 1
|
||||||
|
|
||||||
{
|
{
|
||||||
large: 'http://coverartarchive.org/release/25fbfbb4-b1ee-4448-aadf-ae3bc2e2dd27/18974521469-500.jpg',
|
large: 'http://coverartarchive.org/release/6ef6e6cd-ad36-4c2f-816d-121bfb2f6774/12699438648-500.jpg',
|
||||||
small: 'http://coverartarchive.org/release/25fbfbb4-b1ee-4448-aadf-ae3bc2e2dd27/18974521469-250.jpg',
|
small: 'http://coverartarchive.org/release/6ef6e6cd-ad36-4c2f-816d-121bfb2f6774/12699438648-250.jpg',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
2824
test/extensions/fanart-tv/fixtures/schema.js.nock
Normal file
2824
test/extensions/fanart-tv/fixtures/schema.js.nock
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,20 +1,27 @@
|
||||||
import test from 'ava'
|
import test from 'ava';
|
||||||
import { graphql } from 'graphql'
|
import GraphQL from 'graphql';
|
||||||
import extension from '../../../src/extensions/fanart-tv'
|
import extension from '../../../src/extensions/fanart-tv/index.js';
|
||||||
import baseSchema, { applyExtension } from '../../../src/schema'
|
import { baseSchema, applyExtension } from '../../../src/schema.js';
|
||||||
import baseContext from '../../helpers/context'
|
import baseContext from '../../helpers/context.js';
|
||||||
|
|
||||||
const schema = applyExtension(extension, baseSchema)
|
const { graphql } = GraphQL;
|
||||||
const context = extension.extendContext(baseContext)
|
|
||||||
|
const schema = applyExtension(extension, baseSchema);
|
||||||
|
const context = extension.extendContext(baseContext, {
|
||||||
|
fanArt: {
|
||||||
|
limit: Infinity,
|
||||||
|
period: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
function testData(t, query, handler) {
|
function testData(t, query, handler) {
|
||||||
return graphql(schema, query, null, context).then(result => {
|
return graphql(schema, query, null, context).then((result) => {
|
||||||
if (result.errors !== undefined) {
|
if (result.errors !== undefined) {
|
||||||
result.errors.forEach(error => t.log(error))
|
result.errors.forEach((error) => t.log(error));
|
||||||
}
|
}
|
||||||
t.is(result.errors, undefined)
|
t.is(result.errors, undefined);
|
||||||
return handler(t, result.data)
|
return handler(t, result.data);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
test(
|
test(
|
||||||
|
|
@ -61,19 +68,19 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
t.snapshot(data)
|
t.snapshot(data);
|
||||||
const { fanArt } = data.lookup.artist
|
const { fanArt } = data.lookup.artist;
|
||||||
const allImages = []
|
const allImages = []
|
||||||
.concat(fanArt.backgrounds)
|
.concat(fanArt.backgrounds)
|
||||||
.concat(fanArt.banners)
|
.concat(fanArt.banners)
|
||||||
.concat(fanArt.logos)
|
.concat(fanArt.logos)
|
||||||
.concat(fanArt.logosHD)
|
.concat(fanArt.logosHD)
|
||||||
.concat(fanArt.thumbnails)
|
.concat(fanArt.thumbnails);
|
||||||
allImages.forEach(image => {
|
allImages.forEach((image) => {
|
||||||
t.not(image.url, image.fullSizeURL)
|
t.not(image.url, image.fullSizeURL);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'release groups have a fanArt field and preview images',
|
'release groups have a fanArt field and preview images',
|
||||||
|
|
@ -102,14 +109,14 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
t.snapshot(data)
|
t.snapshot(data);
|
||||||
const { fanArt } = data.lookup.releaseGroup
|
const { fanArt } = data.lookup.releaseGroup;
|
||||||
const allImages = [].concat(fanArt.albumCovers).concat(fanArt.discImages)
|
const allImages = [].concat(fanArt.albumCovers).concat(fanArt.discImages);
|
||||||
allImages.forEach(image => {
|
allImages.forEach((image) => {
|
||||||
t.not(image.url, image.fullSizeURL)
|
t.not(image.url, image.fullSizeURL);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'labels have a fanArt field and preview images',
|
'labels have a fanArt field and preview images',
|
||||||
|
|
@ -132,10 +139,10 @@ test(
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
(t, data) => {
|
(t, data) => {
|
||||||
t.snapshot(data)
|
t.snapshot(data);
|
||||||
const { fanArt } = data.lookup.label
|
const { fanArt } = data.lookup.label;
|
||||||
fanArt.logos.forEach(image => {
|
fanArt.logos.forEach((image) => {
|
||||||
t.not(image.url, image.fullSizeURL)
|
t.not(image.url, image.fullSizeURL);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
The actual snapshot is saved in `schema.js.snap`.
|
The actual snapshot is saved in `schema.js.snap`.
|
||||||
|
|
||||||
Generated by [AVA](https://ava.li).
|
Generated by [AVA](https://avajs.dev).
|
||||||
|
|
||||||
## artists have a fanArt field and preview images
|
## artists have a fanArt field and preview images
|
||||||
|
|
||||||
|
|
@ -16,79 +16,79 @@ Generated by [AVA](https://ava.li).
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-4ddaf131354a8.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-4ddaf131354a8.jpg',
|
||||||
imageID: '2539',
|
imageID: '2539',
|
||||||
likeCount: 9,
|
likeCount: 11,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-4ddaf131354a8.jpg',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-4ddaf131354a8.jpg',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-524daf0607406.jpg',
|
|
||||||
imageID: '99990',
|
|
||||||
likeCount: 8,
|
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-524daf0607406.jpg',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-52c91734c39dd.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-52c91734c39dd.jpg',
|
||||||
imageID: '108996',
|
imageID: '108996',
|
||||||
likeCount: 8,
|
likeCount: 10,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-52c91734c39dd.jpg',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-52c91734c39dd.jpg',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-524daf0607406.jpg',
|
||||||
|
imageID: '99990',
|
||||||
|
likeCount: 9,
|
||||||
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-524daf0607406.jpg',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-500187e32be79.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-500187e32be79.jpg',
|
||||||
imageID: '42530',
|
imageID: '42530',
|
||||||
likeCount: 4,
|
likeCount: 5,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-500187e32be79.jpg',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-500187e32be79.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-4de90b913b2a1.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-4de90b913b2a1.jpg',
|
||||||
imageID: '4153',
|
imageID: '4153',
|
||||||
likeCount: 4,
|
likeCount: 5,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-4de90b913b2a1.jpg',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-4de90b913b2a1.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-56d0287053099.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-56d0287053099.jpg',
|
||||||
imageID: '190330',
|
imageID: '190330',
|
||||||
likeCount: 4,
|
likeCount: 5,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-56d0287053099.jpg',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-56d0287053099.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-56d02870536ef.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-56d02870536ef.jpg',
|
||||||
imageID: '190331',
|
imageID: '190331',
|
||||||
likeCount: 4,
|
likeCount: 5,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-56d02870536ef.jpg',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-56d02870536ef.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-54dc843050470.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-54dc843050470.jpg',
|
||||||
imageID: '151698',
|
imageID: '151698',
|
||||||
likeCount: 4,
|
likeCount: 5,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-54dc843050470.jpg',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-54dc843050470.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-524daf0607e44.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-524daf0607e44.jpg',
|
||||||
imageID: '99991',
|
imageID: '99991',
|
||||||
likeCount: 4,
|
likeCount: 5,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-524daf0607e44.jpg',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-524daf0607e44.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-52c917ac71459.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-52c917ac71459.jpg',
|
||||||
imageID: '108997',
|
imageID: '108997',
|
||||||
likeCount: 4,
|
likeCount: 5,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-52c917ac71459.jpg',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-52c917ac71459.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-55baa96c8c47f.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-55baa96c8c47f.jpg',
|
||||||
imageID: '172578',
|
imageID: '172578',
|
||||||
likeCount: 3,
|
likeCount: 4,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-55baa96c8c47f.jpg',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-55baa96c8c47f.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-54dc845f81d1b.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-54dc845f81d1b.jpg',
|
||||||
imageID: '151699',
|
imageID: '151699',
|
||||||
likeCount: 3,
|
likeCount: 4,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-54dc845f81d1b.jpg',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-54dc845f81d1b.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-54ac79e578054.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-54ac79e578054.jpg',
|
||||||
imageID: '146472',
|
imageID: '146472',
|
||||||
likeCount: 1,
|
likeCount: 2,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-54ac79e578054.jpg',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistbackground/nirvana-54ac79e578054.jpg',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -96,13 +96,13 @@ Generated by [AVA](https://ava.li).
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/musicbanner/nirvana-591789a12da78.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/musicbanner/nirvana-591789a12da78.jpg',
|
||||||
imageID: '218845',
|
imageID: '218845',
|
||||||
likeCount: 4,
|
likeCount: 5,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/musicbanner/nirvana-591789a12da78.jpg',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/musicbanner/nirvana-591789a12da78.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/musicbanner/nirvana-515f7e1a6f50b.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/musicbanner/nirvana-515f7e1a6f50b.jpg',
|
||||||
imageID: '78008',
|
imageID: '78008',
|
||||||
likeCount: 3,
|
likeCount: 4,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/musicbanner/nirvana-515f7e1a6f50b.jpg',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/musicbanner/nirvana-515f7e1a6f50b.jpg',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -110,7 +110,7 @@ Generated by [AVA](https://ava.li).
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/musiclogo/nirvana-4e4b9bc06dcc4.png',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/musiclogo/nirvana-4e4b9bc06dcc4.png',
|
||||||
imageID: '8957',
|
imageID: '8957',
|
||||||
likeCount: 5,
|
likeCount: 6,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/musiclogo/nirvana-4e4b9bc06dcc4.png',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/musiclogo/nirvana-4e4b9bc06dcc4.png',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -118,49 +118,73 @@ Generated by [AVA](https://ava.li).
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-5261733fe6c60.png',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-5261733fe6c60.png',
|
||||||
imageID: '101686',
|
imageID: '101686',
|
||||||
likeCount: 6,
|
likeCount: 7,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-5261733fe6c60.png',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-5261733fe6c60.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-518a696cda12f.png',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-518a696cda12f.png',
|
||||||
imageID: '81480',
|
imageID: '81480',
|
||||||
likeCount: 5,
|
likeCount: 7,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-518a696cda12f.png',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-518a696cda12f.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-518ada7d98805.png',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-518ada7d98805.png',
|
||||||
imageID: '81521',
|
imageID: '81521',
|
||||||
likeCount: 5,
|
likeCount: 7,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-518ada7d98805.png',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-518ada7d98805.png',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-518a696c1ab0b.png',
|
|
||||||
imageID: '81479',
|
|
||||||
likeCount: 3,
|
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-518a696c1ab0b.png',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-561900ee87f11.png',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-561900ee87f11.png',
|
||||||
imageID: '181150',
|
imageID: '181150',
|
||||||
likeCount: 3,
|
likeCount: 5,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-561900ee87f11.png',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-561900ee87f11.png',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-5c7c5d116b427.png',
|
||||||
|
imageID: '260035',
|
||||||
|
likeCount: 5,
|
||||||
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-5c7c5d116b427.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-5c0d8d0dbb1cd.png',
|
||||||
|
imageID: '252735',
|
||||||
|
likeCount: 4,
|
||||||
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-5c0d8d0dbb1cd.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-5c0d8d16333f9.png',
|
||||||
|
imageID: '252736',
|
||||||
|
likeCount: 4,
|
||||||
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-5c0d8d16333f9.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-5c0d8d1ddf738.png',
|
||||||
|
imageID: '252737',
|
||||||
|
likeCount: 4,
|
||||||
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-5c0d8d1ddf738.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-518a696c1ab0b.png',
|
||||||
|
imageID: '81479',
|
||||||
|
likeCount: 4,
|
||||||
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-518a696c1ab0b.png',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-561900db6b999.png',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-561900db6b999.png',
|
||||||
imageID: '181149',
|
imageID: '181149',
|
||||||
likeCount: 2,
|
likeCount: 3,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-561900db6b999.png',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-561900db6b999.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-5619010106ffb.png',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-5619010106ffb.png',
|
||||||
imageID: '181151',
|
imageID: '181151',
|
||||||
likeCount: 2,
|
likeCount: 3,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-5619010106ffb.png',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-5619010106ffb.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-59039625adb45.png',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-59039625adb45.png',
|
||||||
imageID: '217621',
|
imageID: '217621',
|
||||||
likeCount: 1,
|
likeCount: 2,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-59039625adb45.png',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/hdmusiclogo/nirvana-59039625adb45.png',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -168,25 +192,25 @@ Generated by [AVA](https://ava.li).
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistthumb/nirvana-4fb61ff40a15a.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistthumb/nirvana-4fb61ff40a15a.jpg',
|
||||||
imageID: '31455',
|
imageID: '31455',
|
||||||
likeCount: 5,
|
likeCount: 6,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistthumb/nirvana-4fb61ff40a15a.jpg',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistthumb/nirvana-4fb61ff40a15a.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistthumb/nirvana-4fb6205204d6e.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistthumb/nirvana-4fb6205204d6e.jpg',
|
||||||
imageID: '31456',
|
imageID: '31456',
|
||||||
likeCount: 4,
|
likeCount: 5,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistthumb/nirvana-4fb6205204d6e.jpg',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistthumb/nirvana-4fb6205204d6e.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistthumb/nirvana-515ddb61b444b.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistthumb/nirvana-515ddb61b444b.jpg',
|
||||||
imageID: '77828',
|
imageID: '77828',
|
||||||
likeCount: 3,
|
likeCount: 4,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistthumb/nirvana-515ddb61b444b.jpg',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistthumb/nirvana-515ddb61b444b.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistthumb/nirvana-4fb61fd2f3204.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistthumb/nirvana-4fb61fd2f3204.jpg',
|
||||||
imageID: '31454',
|
imageID: '31454',
|
||||||
likeCount: 3,
|
likeCount: 4,
|
||||||
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistthumb/nirvana-4fb61fd2f3204.jpg',
|
url: 'https://assets.fanart.tv/preview/music/5b11f4ce-a62d-471e-81fc-a69a8278c7da/artistthumb/nirvana-4fb61fd2f3204.jpg',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -195,20 +219,6 @@ Generated by [AVA](https://ava.li).
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
## labels have a fanArt field and preview images
|
|
||||||
|
|
||||||
> Snapshot 1
|
|
||||||
|
|
||||||
{
|
|
||||||
lookup: {
|
|
||||||
label: {
|
|
||||||
fanArt: {
|
|
||||||
logos: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
## release groups have a fanArt field and preview images
|
## release groups have a fanArt field and preview images
|
||||||
|
|
||||||
> Snapshot 1
|
> Snapshot 1
|
||||||
|
|
@ -218,82 +228,94 @@ Generated by [AVA](https://ava.li).
|
||||||
releaseGroup: {
|
releaseGroup: {
|
||||||
fanArt: {
|
fanArt: {
|
||||||
albumCovers: [
|
albumCovers: [
|
||||||
{
|
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-4decb408b6604.jpg',
|
|
||||||
imageID: '4417',
|
|
||||||
likeCount: 12,
|
|
||||||
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-4decb408b6604.jpg',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-53afcfaa65a86.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-53afcfaa65a86.jpg',
|
||||||
imageID: '128223',
|
imageID: '128223',
|
||||||
likeCount: 6,
|
likeCount: 13,
|
||||||
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-53afcfaa65a86.jpg',
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-53afcfaa65a86.jpg',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-535054d4e2e32.jpg',
|
||||||
|
imageID: '118729',
|
||||||
|
likeCount: 7,
|
||||||
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-535054d4e2e32.jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-5e1bd6d08e565.jpg',
|
||||||
|
imageID: '283340',
|
||||||
|
likeCount: 7,
|
||||||
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-5e1bd6d08e565.jpg',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-5647389f76c9a.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-5647389f76c9a.jpg',
|
||||||
imageID: '183166',
|
imageID: '183166',
|
||||||
likeCount: 3,
|
likeCount: 6,
|
||||||
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-5647389f76c9a.jpg',
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-5647389f76c9a.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-555319062404d.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-555319062404d.jpg',
|
||||||
imageID: '163522',
|
imageID: '163522',
|
||||||
likeCount: 3,
|
likeCount: 6,
|
||||||
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-555319062404d.jpg',
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-555319062404d.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-535054d4e2e32.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-55cbdf80ef05c.jpg',
|
||||||
imageID: '118729',
|
imageID: '174560',
|
||||||
likeCount: 3,
|
likeCount: 6,
|
||||||
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-535054d4e2e32.jpg',
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-55cbdf80ef05c.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-53cf470795637.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-53cf470795637.jpg',
|
||||||
imageID: '130679',
|
imageID: '130679',
|
||||||
likeCount: 2,
|
likeCount: 5,
|
||||||
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-53cf470795637.jpg',
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-53cf470795637.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-59a82b1a7c6cd.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-59a82b1a7c6cd.jpg',
|
||||||
imageID: '225420',
|
imageID: '225420',
|
||||||
likeCount: 1,
|
likeCount: 5,
|
||||||
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-59a82b1a7c6cd.jpg',
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-59a82b1a7c6cd.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-59a82d31adb2f.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-59a82d31adb2f.jpg',
|
||||||
imageID: '225421',
|
imageID: '225421',
|
||||||
likeCount: 1,
|
likeCount: 4,
|
||||||
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-59a82d31adb2f.jpg',
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-59a82d31adb2f.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-55c4af535f7e9.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-55c4af535f7e9.jpg',
|
||||||
imageID: '173720',
|
imageID: '173720',
|
||||||
likeCount: 1,
|
likeCount: 4,
|
||||||
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-55c4af535f7e9.jpg',
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-55c4af535f7e9.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-55cbdf80ef05c.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-5e66d1295bad3.jpg',
|
||||||
imageID: '174560',
|
imageID: '289604',
|
||||||
likeCount: 1,
|
likeCount: 3,
|
||||||
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-55cbdf80ef05c.jpg',
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-5e66d1295bad3.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-59a31c7ad5931.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-603e7e8d5d50a.jpg',
|
||||||
imageID: '225172',
|
imageID: '329894',
|
||||||
likeCount: 0,
|
likeCount: 3,
|
||||||
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-59a31c7ad5931.jpg',
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-603e7e8d5d50a.jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-5c709e472b685.jpg',
|
||||||
|
imageID: '259037',
|
||||||
|
likeCount: 3,
|
||||||
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-5c709e472b685.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-59a48bfb008e3.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-59a48bfb008e3.jpg',
|
||||||
imageID: '225266',
|
imageID: '225266',
|
||||||
likeCount: 0,
|
likeCount: 3,
|
||||||
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-59a48bfb008e3.jpg',
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-59a48bfb008e3.jpg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-59a48df6822b4.jpg',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-59a48df6822b4.jpg',
|
||||||
imageID: '225267',
|
imageID: '225267',
|
||||||
likeCount: 0,
|
likeCount: 3,
|
||||||
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-59a48df6822b4.jpg',
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/albumcover/the-dark-side-of-the-moon-59a48df6822b4.jpg',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -312,6 +334,13 @@ Generated by [AVA](https://ava.li).
|
||||||
size: 1000,
|
size: 1000,
|
||||||
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-56047e045d648.png',
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-56047e045d648.png',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
discNumber: 1,
|
||||||
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-59a0791276643.png',
|
||||||
|
imageID: '225077',
|
||||||
|
size: 1000,
|
||||||
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-59a0791276643.png',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
discNumber: 3,
|
discNumber: 3,
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-50aa6521ef0f3.png',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-50aa6521ef0f3.png',
|
||||||
|
|
@ -319,6 +348,13 @@ Generated by [AVA](https://ava.li).
|
||||||
size: 1000,
|
size: 1000,
|
||||||
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-50aa6521ef0f3.png',
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-50aa6521ef0f3.png',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
discNumber: 1,
|
||||||
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-59a3237b3607a.png',
|
||||||
|
imageID: '225188',
|
||||||
|
size: 1000,
|
||||||
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-59a3237b3607a.png',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
discNumber: 1,
|
discNumber: 1,
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-56047e56b51f8.png',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-56047e56b51f8.png',
|
||||||
|
|
@ -333,13 +369,6 @@ Generated by [AVA](https://ava.li).
|
||||||
size: 1000,
|
size: 1000,
|
||||||
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-5957ea48b6728.png',
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-5957ea48b6728.png',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
discNumber: 1,
|
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-59a0791276643.png',
|
|
||||||
imageID: '225077',
|
|
||||||
size: 1000,
|
|
||||||
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-59a0791276643.png',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
discNumber: 1,
|
discNumber: 1,
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-501c2a49803a0.png',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-501c2a49803a0.png',
|
||||||
|
|
@ -396,13 +425,6 @@ Generated by [AVA](https://ava.li).
|
||||||
size: 1000,
|
size: 1000,
|
||||||
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-54c55c43410af.png',
|
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-54c55c43410af.png',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
discNumber: 1,
|
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-59a3237b3607a.png',
|
|
||||||
imageID: '225188',
|
|
||||||
size: 1000,
|
|
||||||
url: 'https://assets.fanart.tv/preview/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-59a3237b3607a.png',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
discNumber: 1,
|
discNumber: 1,
|
||||||
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-532569d57474d.png',
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/83d91898-7763-47d7-b03b-b92132375c47/cdart/the-dark-side-of-the-moon-532569d57474d.png',
|
||||||
|
|
@ -422,3 +444,32 @@ Generated by [AVA](https://ava.li).
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
## labels have a fanArt field and preview images
|
||||||
|
|
||||||
|
> Snapshot 1
|
||||||
|
|
||||||
|
{
|
||||||
|
lookup: {
|
||||||
|
label: {
|
||||||
|
fanArt: {
|
||||||
|
logos: [
|
||||||
|
{
|
||||||
|
color: null,
|
||||||
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/0cf56645-50ec-4411-aeb6-c9f4ce0f8edb/musiclabel/hardly-art-5c71e1b57f00c.png',
|
||||||
|
imageID: '259128',
|
||||||
|
likeCount: 0,
|
||||||
|
url: 'https://assets.fanart.tv/preview/music/0cf56645-50ec-4411-aeb6-c9f4ce0f8edb/musiclabel/hardly-art-5c71e1b57f00c.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: null,
|
||||||
|
fullSizeURL: 'https://assets.fanart.tv/fanart/music/0cf56645-50ec-4411-aeb6-c9f4ce0f8edb/musiclabel/hardly-art-5c71e1b586d3b.png',
|
||||||
|
imageID: '259129',
|
||||||
|
likeCount: 0,
|
||||||
|
url: 'https://assets.fanart.tv/preview/music/0cf56645-50ec-4411-aeb6-c9f4ce0f8edb/musiclabel/hardly-art-5c71e1b586d3b.png',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue