mirror of
https://github.com/BradNut/graphbrainz
synced 2025-09-08 17:40:32 +00:00
Compare commits
78 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94cf657f30 | ||
|
|
c020795b58 | ||
|
|
42f237d068 | ||
|
|
740a499377 | ||
|
|
f095cd4de7 | ||
|
|
24d53d4687 | ||
|
|
24ebfa1b51 | ||
|
|
cb2d2a1a3f | ||
|
|
a6693ed5a4 | ||
|
|
6d3d5a595b | ||
|
|
50d3268366 | ||
|
|
fc6f1337b6 | ||
|
|
01ebffad01 | ||
|
|
ba21977847 | ||
|
|
1499e07fa7 | ||
|
|
50c3223e6a | ||
|
|
d3bb6d2484 | ||
|
|
e696ffc70e | ||
|
|
3ed5ba12d7 | ||
|
|
505b679376 | ||
|
|
ff828699a0 | ||
|
|
d9c029ff71 | ||
|
|
98e80f1bd1 | ||
|
|
11afa8e4e3 | ||
|
|
80495a33f0 | ||
|
|
9f0172ba9d | ||
|
|
4768d2f359 | ||
|
|
f425082758 | ||
|
|
55176e6753 | ||
|
|
eec40c64a0 | ||
|
|
37e872b1e7 | ||
|
|
d8b8de2dce | ||
|
|
8aae9d5634 | ||
|
|
37447d1a41 | ||
|
|
6d22f0c5b6 | ||
|
|
9ddb4aab67 | ||
|
|
14487a747e | ||
|
|
2d0bd82c8b | ||
|
|
c3be2a2e98 | ||
|
|
2de2e60079 | ||
|
|
ccce751ccb | ||
|
|
f34ea2002f | ||
|
|
edeabeecac | ||
|
|
5c411ed79d | ||
|
|
8c0a9f44ef | ||
|
|
50888c9fb9 | ||
|
|
35f6cf63ea | ||
|
|
c21009b5c4 | ||
|
|
75e24c18bc | ||
|
|
e77143fbd7 | ||
|
|
62495c490d | ||
|
|
e4569607f4 | ||
|
|
7f49ccf117 | ||
|
|
d2f4d118fc | ||
|
|
03dc011934 | ||
|
|
46d16ebd13 | ||
|
|
be67d771ab | ||
|
|
a3fc3e97af | ||
|
|
dff11f76c8 | ||
|
|
bbe045a28f | ||
|
|
a7183cc15c | ||
|
|
51cc879363 | ||
|
|
086b7469e1 | ||
|
|
aa46d45419 | ||
|
|
898ec78a6f | ||
|
|
687ca43708 | ||
|
|
b5d0dcce91 | ||
|
|
bc2a5655d8 | ||
|
|
759310a2a6 | ||
|
|
698ba58492 | ||
|
|
35db26f8ce | ||
|
|
c9d9cb944b | ||
|
|
8447161f29 | ||
|
|
2353f9b6c2 | ||
|
|
1e535f203e | ||
|
|
8807d1e5b8 | ||
|
|
f6ca61233c | ||
|
|
fd86710fdb |
549 changed files with 48057 additions and 27009 deletions
10
.babelrc
10
.babelrc
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"presets": ["es2015", "stage-2"],
|
||||
"plugins": ["transform-runtime"],
|
||||
"only": ["scripts/**", "src/**", "test/helpers/**"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["istanbul"]
|
||||
}
|
||||
}
|
||||
}
|
||||
4
.eslintignore
Normal file
4
.eslintignore
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/.nyc_output
|
||||
/coverage
|
||||
/lib
|
||||
!.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,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -36,4 +36,5 @@ jspm_packages
|
|||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
.env
|
||||
lib
|
||||
|
|
|
|||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/.nyc_output
|
||||
/coverage
|
||||
/lib
|
||||
16
.travis.yml
16
.travis.yml
|
|
@ -1,10 +1,9 @@
|
|||
language: node_js
|
||||
|
||||
node_js:
|
||||
- "4"
|
||||
- "5"
|
||||
- "6"
|
||||
- "7"
|
||||
- '12'
|
||||
- '14'
|
||||
- '15'
|
||||
|
||||
# Use container-based Travis infrastructure.
|
||||
sudo: false
|
||||
|
|
@ -12,11 +11,16 @@ sudo: false
|
|||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^greenkeeper/.*$/
|
||||
- '/^greenkeeper/.*$/'
|
||||
|
||||
env:
|
||||
global:
|
||||
- secure: fyw/6iyz4skp/sdAb9wr9Pq/72j4z2TiIZIJLT6B4zZZUKclA9ihHYx2Qf4IK7rHe+mvg0dSKaJi+zgrEx04fg2bP05dV1f52mmR71sE/FDRc9nkGgDl6/2AeXfXh0Raf8lVsYuBWclI2G0GPQeG1xskY7EBEnlKBdRmnljXPDNu1kwSrcLoGkoP1sIMRIxnMAHPXlOO3QKstiHM/HMa3A4SsQ4VGVLfrYqDJJ3L3XOkSbqyaSdWhELxSXAxSEzEOYi28kPeKtztdaU+BQtOSamuIMrEPwpM6+bFtTrf/SxWcybCk2sqn4sUgDVE8658cEIECR6NC1U/gWvDZSVHEetT2caHkhe1NRjxQmKrRfx88a+qZPeqbv6ydpvK8XAnaMtpwZtytoPZ7gw4IF0wxfkeBhK8dgBb4Lsdpnv90tbzcIyHgrHJiKBgJLlghBr9McFo4Fe3W06eD4rBTNX1LCmURQCm0V6mfCPUPup/68tAuscgysVW54XT51P4fk1iR3MJzCjbxaWbxwxWdCEeD2ptIkZwXDGydVe2qL4eI7NpYyTyhHc8vITlXYQnDCj9ztwjSiSf+4jJQiN7mbNcrRymnF22ICubnpj0SHBxEkQFJ+f/Wf3Ksxtl8t8DLTBCPtSzv7rxbdtnKRR1QRWgeYnhos1QHcNaoDgrlWCkOIg=
|
||||
- secure: gS7EN+j/l9SDLjwM9YHCgOh6iPw0YTKvuQX9eXUI9YIEYoyhnBRQMfp77LJTuXdlucGgYj4v05YriLoxl4L7hmUmAKnDOVVEOUQqwYoAzg+h2GS3FQhoQRioxSAmTJc90nki0uhYAHW8FZ34+BjONCDSnew7r71TTU+UxRdRu4wOneFwXW1crPCNRtO5Ov/gkuiOWQaSPOoP6tzPOMP6FUueRQVqvf2GOoF3lBM8LnKeIq+m4H80DGsNjMtqdPJz9QQzAwO3VRNgsMAgr2wzVzjUnum7DfQC808AQDURlj9apg1HdYZDJH9bQyI/jIZ1gnHIcZ+kqqoq0l0QVNmITeMGrblFALU8xCFtGUyq70FGMjcmDGCanpveNOOhxjdqqfvvPKuqHEsfZtZkBJDovJdFtKmzf7DWb5xv8liPCzX26eBG/DPQgCEzyW0bl7IcB2LVMW/ObMRgixik2qiqYjN05pveWYsiO2iaiMo9ebm7FHXw/B0yfjM+CCGf7bjR9+RczXlN1x/gj+yjnYJ4K/nojSx0wms3QE7tJsXjmAMeguKUoZtaGGlF5yGbKtaZEVw/ZhYQDfLwwHQX2S5E424tYP/NAdi++CNCvyHJ7n0zel18FfsUNE1jIMaGb6aG6uOv6eOVI4+O09R71wxcYbQs40axdF+Y0A5f+CllNc8=
|
||||
|
||||
script:
|
||||
- yarn test
|
||||
|
||||
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)
|
||||
|
|
|
|||
185
README.md
185
README.md
|
|
@ -2,23 +2,37 @@
|
|||
|
||||
[](https://travis-ci.org/exogen/graphbrainz)
|
||||
[](https://codecov.io/gh/exogen/graphbrainz)
|
||||
[](https://greenkeeper.io/)
|
||||
[](https://www.npmjs.com/package/graphbrainz)
|
||||
[](https://github.com/exogen/graphbrainz/blob/master/LICENSE)
|
||||
|
||||
A [GraphQL][] schema, [Express][] server, and middleware for querying the
|
||||
[MusicBrainz][] API.
|
||||
[MusicBrainz][] API. It features an [extensible](./docs/extensions) schema to
|
||||
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
|
||||
npm install graphbrainz --save
|
||||
```
|
||||
|
||||
**[Try out the live demo!][demo]** :bulb: Use the “Docs” sidebar, the
|
||||
[schema][], or the [types][] docs to help construct your query.
|
||||
Install with Yarn:
|
||||
|
||||
```sh
|
||||
yarn add graphbrainz
|
||||
```
|
||||
|
||||
_GraphBrainz is written and distributed as native ECMAScript modules
|
||||
(ESM) and requires a compatible version of Node.js_
|
||||
|
||||
## Contents
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Contents
|
||||
|
||||
- [Usage](#usage)
|
||||
- [As a standalone server](#as-a-standalone-server)
|
||||
|
|
@ -30,6 +44,7 @@ npm install graphbrainz --save
|
|||
- [Pagination](#pagination)
|
||||
- [Questions](#questions)
|
||||
- [Schema](#schema)
|
||||
- [Extending the schema](#extending-the-schema)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
|
|
@ -40,8 +55,8 @@ middleware supplying a GraphQL endpoint.
|
|||
|
||||
### As a standalone server
|
||||
|
||||
Run the included `graphbrainz` executable to start the server. The server
|
||||
is configured using [environment variables](#environment-variables).
|
||||
Run the included `graphbrainz` executable to start the server. The server is
|
||||
configured using [environment variables](#environment-variables).
|
||||
|
||||
```sh
|
||||
$ graphbrainz
|
||||
|
|
@ -66,7 +81,7 @@ an endpoint, or you just want more customization, use the middleware.
|
|||
|
||||
```js
|
||||
import express from 'express';
|
||||
import graphbrainz from 'graphbrainz';
|
||||
import { middleware as graphbrainz } from 'graphbrainz';
|
||||
|
||||
const app = express();
|
||||
|
||||
|
|
@ -85,12 +100,12 @@ app.listen(3000);
|
|||
|
||||
The `graphbrainz` middleware function accepts the following options:
|
||||
|
||||
* **`client`**: A custom API client instance to use. See the
|
||||
[client submodule](src/api/client.js) for help with creating a custom instance.
|
||||
You probably only need to do this if you want to adjust the rate limit and retry
|
||||
behavior.
|
||||
* Any remaining options are passed along to the standard GraphQL middleware.
|
||||
See the [express-graphql][] documentation for more information.
|
||||
- **`client`**: A custom API client instance to use. See the
|
||||
[client submodule](src/api/client.js) for help with creating a custom
|
||||
instance. You probably only need to do this if you want to adjust the rate
|
||||
limit and retry behavior.
|
||||
- Any remaining options are passed along to the standard GraphQL middleware. See
|
||||
the [express-graphql][] documentation for more information.
|
||||
|
||||
### As a client
|
||||
|
||||
|
|
@ -101,48 +116,58 @@ GraphBrainz resolvers expect, like so:
|
|||
|
||||
```js
|
||||
import { graphql } from 'graphql';
|
||||
import { MusicBrainz, CoverArtArchive } from 'graphbrainz/api';
|
||||
import createLoaders from 'graphbrainz/loaders';
|
||||
import schema from 'graphbrainz/schema';
|
||||
import { MusicBrainz, createContext, baseSchema } from 'graphbrainz';
|
||||
|
||||
const client = new MusicBrainz();
|
||||
const coverArtClient = new CoverArtArchive();
|
||||
const loaders = createLoaders(client, coverArtClient);
|
||||
const context = { client, coverArtClient, loaders };
|
||||
const context = createContext({ client });
|
||||
|
||||
graphql(schema, `
|
||||
{
|
||||
lookup {
|
||||
releaseGroup(mbid: "99599db8-0e36-4a93-b0e8-350e9d7502a9") {
|
||||
title
|
||||
graphql(
|
||||
schema,
|
||||
`
|
||||
{
|
||||
lookup {
|
||||
releaseGroup(mbid: "99599db8-0e36-4a93-b0e8-350e9d7502a9") {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`, null, context).then(result => {
|
||||
const { releaseGroup } = result.data.lookup;
|
||||
console.log(`The album title is “${releaseGroup.title}”.`);
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
`,
|
||||
null,
|
||||
context
|
||||
)
|
||||
.then((result) => {
|
||||
const { releaseGroup } = result.data.lookup;
|
||||
console.log(`The album title is “${releaseGroup.title}”.`);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
* **`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/`.
|
||||
* **`GRAPHBRAINZ_PATH`**: The URL route at which to expose the GraphQL endpoint,
|
||||
- **`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/`.
|
||||
- **`GRAPHBRAINZ_PATH`**: The URL route at which to expose the GraphQL endpoint,
|
||||
if running the standalone server. Defaults to `/`.
|
||||
* **`GRAPHBRAINZ_CACHE_SIZE`**: The maximum number of REST API responses to
|
||||
- **`GRAPHBRAINZ_CORS_ORIGIN`**: The value of the `origin` option to pass to the
|
||||
[CORS][cors] middleware. Valid values are `true` to reflect the request
|
||||
origin, a specific origin string to allow, `*` to allow all origins, and
|
||||
`false` to disable CORS (the default).
|
||||
- **`GRAPHBRAINZ_CACHE_SIZE`**: The maximum number of REST API responses to
|
||||
cache. Increasing the cache size and TTL will greatly lower query execution
|
||||
time for complex queries involving frequently accessed entities. Defaults to
|
||||
`8192`.
|
||||
* **`GRAPHBRAINZ_CACHE_TTL`**: The maximum age of REST API responses in the
|
||||
- **`GRAPHBRAINZ_CACHE_TTL`**: The maximum age of REST API responses in the
|
||||
cache, in milliseconds. Responses older than this will be disposed of (and
|
||||
re-requested) the next time they are accessed. Defaults to `86400000` (one
|
||||
day).
|
||||
* **`GRAPHBRAINZ_GRAPHIQL`**: Set this to `true` if you want to force the
|
||||
- **`GRAPHBRAINZ_GRAPHIQL`**: Set this to `true` if you want to force the
|
||||
[GraphiQL][] interface to be available even in production mode.
|
||||
* **`PORT`**: Port number to use, if running the standalone server.
|
||||
- **`GRAPHBRAINZ_EXTENSIONS`**: A JSON array of module paths to load as
|
||||
[extensions](./docs/extensions).
|
||||
- **`PORT`**: Port number to use, if running the standalone server.
|
||||
|
||||
When running the standalone server, [dotenv][] is used to load these variables
|
||||
from a `.env` file, if one exists in the current working directory. This just
|
||||
|
|
@ -162,7 +187,8 @@ See the [debug][] package for more information.
|
|||
|
||||
## 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
|
||||
query NirvanaAlbumSingles {
|
||||
|
|
@ -198,7 +224,8 @@ query NirvanaAlbumSingles {
|
|||
|
||||
### 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
|
||||
query AppleLabels {
|
||||
|
|
@ -227,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
|
||||
query AppleLabels {
|
||||
|
|
@ -240,7 +268,7 @@ query AppleLabels {
|
|||
```
|
||||
|
||||
Who the members of the band on an Apple Records release married, and when
|
||||
([try it](https://graphbrainz.herokuapp.com/?query=query%20AppleRecordsMarriages%20%7B%0A%20%20search%20%7B%0A%20%20%20%20labels(query%3A%20%22Apple%20Records%22%2C%20first%3A%201)%20%7B%0A%20%20%20%20%20%20edges%20%7B%0A%20%20%20%20%20%20%20%20node%20%7B%0A%20%20%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%20%20%20%20disambiguation%0A%20%20%20%20%20%20%20%20%20%20country%0A%20%20%20%20%20%20%20%20%20%20releases(first%3A%201)%20%7B%0A%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%20node%20%7B%0A%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%20date%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20artists%20%7B%0A%20%20%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%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%20%20%20name%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20...bandMembers%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%0Afragment%20bandMembers%20on%20Artist%20%7B%0A%20%20relationships%20%7B%0A%20%20%20%20artists(direction%3A%20%22backward%22%2C%20type%3A%20%22member%20of%20band%22)%20%7B%0A%20%20%20%20%20%20edges%20%7B%0A%20%20%20%20%20%20%20%20node%20%7B%0A%20%20%20%20%20%20%20%20%20%20type%0A%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...%20on%20Artist%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20...marriages%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%0Afragment%20marriages%20on%20Artist%20%7B%0A%20%20relationships%20%7B%0A%20%20%20%20artists(type%3A%20%22married%22)%20%7B%0A%20%20%20%20%20%20edges%20%7B%0A%20%20%20%20%20%20%20%20node%20%7B%0A%20%20%20%20%20%20%20%20%20%20type%0A%20%20%20%20%20%20%20%20%20%20direction%0A%20%20%20%20%20%20%20%20%20%20begin%0A%20%20%20%20%20%20%20%20%20%20end%0A%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...%20on%20Artist%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%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=AppleRecordsMarriages)):
|
||||
([try it](<https://graphbrainz.herokuapp.com/?query=query%20AppleRecordsMarriages%20%7B%0A%20%20search%20%7B%0A%20%20%20%20labels(query%3A%20%22Apple%20Records%22%2C%20first%3A%201)%20%7B%0A%20%20%20%20%20%20edges%20%7B%0A%20%20%20%20%20%20%20%20node%20%7B%0A%20%20%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%20%20%20%20disambiguation%0A%20%20%20%20%20%20%20%20%20%20country%0A%20%20%20%20%20%20%20%20%20%20releases(first%3A%201)%20%7B%0A%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%20node%20%7B%0A%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%20date%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20artists%20%7B%0A%20%20%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%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%20%20%20name%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20...bandMembers%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%0Afragment%20bandMembers%20on%20Artist%20%7B%0A%20%20relationships%20%7B%0A%20%20%20%20artists(direction%3A%20%22backward%22%2C%20type%3A%20%22member%20of%20band%22)%20%7B%0A%20%20%20%20%20%20edges%20%7B%0A%20%20%20%20%20%20%20%20node%20%7B%0A%20%20%20%20%20%20%20%20%20%20type%0A%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...%20on%20Artist%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20...marriages%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%0Afragment%20marriages%20on%20Artist%20%7B%0A%20%20relationships%20%7B%0A%20%20%20%20artists(type%3A%20%22married%22)%20%7B%0A%20%20%20%20%20%20edges%20%7B%0A%20%20%20%20%20%20%20%20node%20%7B%0A%20%20%20%20%20%20%20%20%20%20type%0A%20%20%20%20%20%20%20%20%20%20direction%0A%20%20%20%20%20%20%20%20%20%20begin%0A%20%20%20%20%20%20%20%20%20%20end%0A%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...%20on%20Artist%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%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=AppleRecordsMarriages>)):
|
||||
|
||||
```graphql
|
||||
query AppleRecordsMarriages {
|
||||
|
|
@ -312,21 +340,54 @@ 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>)):
|
||||
|
||||
```graphql
|
||||
query TomPettyImages {
|
||||
lookup {
|
||||
artist(mbid: "5ca3f318-d028-4151-ac73-78e2b2d6cdcc") {
|
||||
name
|
||||
mediaWikiImages {
|
||||
url
|
||||
objectName
|
||||
descriptionHTML
|
||||
licenseShortName
|
||||
}
|
||||
fanArt {
|
||||
thumbnails {
|
||||
url
|
||||
likeCount
|
||||
}
|
||||
}
|
||||
theAudioDB {
|
||||
logo
|
||||
biography
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can find more example queries in the [schema tests][].
|
||||
|
||||
## Questions
|
||||
|
||||
**What’s with the cumbersome `edges`/`node` nesting? Why `first`/`after`
|
||||
instead of `limit`/`offset`? Why `mbid` instead of `id`?**
|
||||
**What’s with the cumbersome `edges`/`node` nesting? Why `first`/`after` instead
|
||||
of `limit`/`offset`? Why `mbid` instead of `id`?**
|
||||
|
||||
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
|
||||
end I decided that being compatible with Relay was a worthwhile feature. I
|
||||
agree, it’s ugly.
|
||||
|
||||
Don’t forget, though, that you can use [GraphQL aliases][aliases] to rename
|
||||
fields to your liking. For example, the following query renames `edges`, `node`,
|
||||
and `mbid` to `results`, `releaseGroup`, and `id`, respectively:
|
||||
The GraphBrainz schema includes an extra `nodes` field on every connection type.
|
||||
If you only want the nodes and no other fields on `edges`, you can use `nodes`
|
||||
as a shortcut.
|
||||
|
||||
Don’t forget that you can also use [GraphQL aliases][aliases] to rename fields
|
||||
to your liking. For example, the following query renames `edges`, `node`, and
|
||||
`mbid` to `results`, `releaseGroup`, and `id`, respectively:
|
||||
|
||||
```graphql
|
||||
query ChristmasAlbums {
|
||||
|
|
@ -347,9 +408,9 @@ query ChristmasAlbums {
|
|||
|
||||
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
|
||||
very hard to fetch only the data necessary, and with the smallest number of
|
||||
API requests, it is not 100% optimal (yet). Make sure you are only requesting
|
||||
the fields you need and a reasonable level of nested entities – unless you are
|
||||
very hard to fetch only the data necessary, and with the smallest number of API
|
||||
requests, it is not 100% optimal (yet). Make sure you are only requesting the
|
||||
fields you need and a reasonable level of nested entities – unless you are
|
||||
willing to wait.
|
||||
|
||||
You can also set up a [local MusicBrainz mirror][mirror] and configure
|
||||
|
|
@ -357,21 +418,29 @@ GraphBrainz to use that with no rate limiting.
|
|||
|
||||
## Schema
|
||||
|
||||
See the [GraphQL schema][schema] or the [types][] documentation.
|
||||
The [types][] document is the easiest to browse representation of the schema, or
|
||||
you can read the [schema in GraphQL syntax][schema].
|
||||
|
||||
### Extending the schema
|
||||
|
||||
The GraphBrainz schema can easily be extended to add integrations with
|
||||
third-party services. See the [Extensions](./docs/extensions) docs for more
|
||||
info.
|
||||
|
||||
[demo]: https://graphbrainz.herokuapp.com/
|
||||
[Express]: http://expressjs.com/
|
||||
[MusicBrainz]: https://musicbrainz.org/
|
||||
[GraphQL]: http://graphql.org/
|
||||
[express]: http://expressjs.com/
|
||||
[musicbrainz]: https://musicbrainz.org/
|
||||
[graphql]: http://graphql.org/
|
||||
[express-graphql]: https://www.npmjs.com/package/express-graphql
|
||||
[dotenv]: https://www.npmjs.com/package/dotenv
|
||||
[debug]: https://www.npmjs.com/package/debug
|
||||
[GraphiQL]: https://github.com/graphql/graphiql
|
||||
[graphiql]: https://github.com/graphql/graphiql
|
||||
[graphql-js]: https://github.com/graphql/graphql-js
|
||||
[Relay]: https://facebook.github.io/relay/
|
||||
[relay]: https://facebook.github.io/relay/
|
||||
[schema]: docs/schema.md
|
||||
[types]: docs/types.md
|
||||
[rate limiting]: https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting
|
||||
[mirror]: https://musicbrainz.org/doc/MusicBrainz_Server/Setup
|
||||
[aliases]: http://graphql.org/learn/queries/#aliases
|
||||
[schema tests]: test/schema.js
|
||||
[schema tests]: test/_schema.js
|
||||
[cors]: https://github.com/expressjs/cors
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
machine:
|
||||
node:
|
||||
version: 4.6.0
|
||||
|
||||
4
cli.js
Executable file
4
cli.js
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env node
|
||||
import { start } from './src/index.js';
|
||||
|
||||
start();
|
||||
255
docs/extensions/README.md
Normal file
255
docs/extensions/README.md
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
# Extensions
|
||||
|
||||
It is possible to extend the GraphBrainz schema to add integrations with
|
||||
third-party services that provide more information about MusicBrainz entities.
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
## Contents
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
|
||||
- [Loading Extensions](#loading-extensions)
|
||||
- [Built-in Extensions](#built-in-extensions)
|
||||
- [More Extensions](#more-extensions)
|
||||
- [Extension API](#extension-api)
|
||||
- [Properties](#properties)
|
||||
- [name](#name)
|
||||
- [description](#description)
|
||||
- [extendContext](#extendcontext)
|
||||
- [extendSchema](#extendschema)
|
||||
- [Example](#example)
|
||||
- [Extension Guidelines](#extension-guidelines)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Loading Extensions
|
||||
|
||||
The extensions to load are specified using the `extensions` option to the
|
||||
exported `middleware()` function. Each extension must be an object conforming to
|
||||
the [Extension API](#extension-api), or the path to a module to load via
|
||||
`require()` that exports such an object.
|
||||
|
||||
If you are running GraphBrainz as a standalone server, you may specify
|
||||
extensions via the `GRAPHBRAINZ_EXTENSIONS` environment variable, which will be
|
||||
parsed as a JSON array. For example:
|
||||
|
||||
```console
|
||||
$ export GRAPHBRAINZ_EXTENSIONS='["graphbrainz/extensions/fanart-tv"]'
|
||||
$ graphbrainz
|
||||
```
|
||||
|
||||
Note that some extensions may require additional configuration via extra options
|
||||
or environment variables. Check the documentation for each extension you use.
|
||||
|
||||
The default extensions configuration looks like this:
|
||||
|
||||
```js
|
||||
middleware({
|
||||
extensions: [
|
||||
'graphbrainz/extensions/cover-art-archive',
|
||||
'graphbrainz/extensions/fanart-tv',
|
||||
'graphbrainz/extensions/mediawiki',
|
||||
'graphbrainz/extensions/the-audio-db',
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Built-in Extensions
|
||||
|
||||
The following extensions are included with GraphBrainz and loaded by default.
|
||||
See their respective documentation pages for schema info and config options.
|
||||
|
||||
- [Cover Art Archive](./cover-art-archive.md): Retrieve cover art images for
|
||||
releases from the Cover Art Archive.
|
||||
- [fanart.tv](./fanart-tv.md): Retrieve high quality artwork for artists,
|
||||
releases, and labels from fanart.tv.
|
||||
- [MediaWiki](./mediawiki.md): Retrieve information from MediaWiki image pages,
|
||||
like the actual image file URL and EXIF metadata.
|
||||
- [TheAudioDB](./the-audio-db.md): Retrieve images and information about
|
||||
artists, releases, and recordings from TheAudioDB.com.
|
||||
|
||||
## More Extensions
|
||||
|
||||
The following extensions are published separately, but can easily be added to
|
||||
GraphBrainz by installing them:
|
||||
|
||||
- [Last.fm](https://github.com/exogen/graphbrainz-extension-lastfm): Retrieve
|
||||
artist, release, and recording information from
|
||||
[Last.fm](https://www.last.fm/).
|
||||
- [Discogs](https://github.com/exogen/graphbrainz-extension-discogs): Retrieve
|
||||
artist, label, release, and release group information from
|
||||
[Discogs](https://www.discogs.com/).
|
||||
- [Spotify](https://github.com/exogen/graphbrainz-extension-spotify): Retrieve
|
||||
artist, release, and recording information from
|
||||
[Spotify](https://www.spotify.com/).
|
||||
|
||||
## Extension API
|
||||
|
||||
The core idea behind extensions comes from the [schema stitching][] feature from
|
||||
[graphql-tools][], although GraphBrainz does not currently use the exact
|
||||
technique documented there. Instead, we call `parse` and `extendSchema` from
|
||||
[GraphQL.js][], followed by [addResolversToSchema][].
|
||||
|
||||
Extensions must export an object shaped like so:
|
||||
|
||||
```js
|
||||
type Extension = {
|
||||
name: string,
|
||||
description?: string,
|
||||
extendContext?: (context: Context, options: Options) => Context,
|
||||
extendSchema?:
|
||||
| { schemas: Array<string | DocumentNode>, resolvers: ResolverMap }
|
||||
| ((schema: GraphQLSchema, options: Options) => GraphQLSchema),
|
||||
};
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
#### name
|
||||
|
||||
The name of the extension.
|
||||
|
||||
#### description
|
||||
|
||||
A description of the functionality that the extension provides.
|
||||
|
||||
#### extendContext
|
||||
|
||||
An optional function that accepts a base context object (the `context` argument
|
||||
available to resolver functions) and returns a new context object. Extensions
|
||||
that access third-party APIs should add any API client instances they need here.
|
||||
The recommended way is to create a loader with [dataloader][] and add it onto
|
||||
`context.loaders`.
|
||||
|
||||
#### extendSchema
|
||||
|
||||
An optional object or function to extend the GraphBrainz schema.
|
||||
|
||||
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
|
||||
language) or a `DocumentNode` (if the type definitions have already been
|
||||
parsed). The `resolvers` object should contain a mapping of type fields to new
|
||||
resolver functions for those fields. See [addResolversToSchema][].
|
||||
|
||||
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
|
||||
logic. This may be necessary if you already have a `GraphQLSchema` instance and
|
||||
want to use [mergeSchemas][], for example. In most cases, you should keep it
|
||||
simple and use the object form.
|
||||
|
||||
### Example
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
name: 'Hello World',
|
||||
description: 'A simple example extension.',
|
||||
extendSchema: {
|
||||
schemas: [
|
||||
`
|
||||
extend type Query {
|
||||
helloWorld: String!
|
||||
}
|
||||
`,
|
||||
],
|
||||
resolvers: {
|
||||
Query: {
|
||||
helloWorld: {
|
||||
resolve: () => 'It worked!',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
This will allow the following query to be made:
|
||||
|
||||
```graphql
|
||||
{
|
||||
helloWorld
|
||||
}
|
||||
```
|
||||
|
||||
See the code for the [built-in extensions][] for more examples.
|
||||
|
||||
## Extension Guidelines
|
||||
|
||||
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
|
||||
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 for
|
||||
writing a good extension:
|
||||
|
||||
- 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.
|
||||
- Default to following the rate limiting rules of any APIs you use. If there are
|
||||
no guidelines on rate limiting, consider playing nice anyway and limiting your
|
||||
client to around 1 to 10 requests per second.
|
||||
- Use a [DataLoader][dataloader] instance to batch and cache requests. Even if
|
||||
the data source doesn’t support batching, DataLoader will help by deduping
|
||||
in-flight requests for the same key, preventing unnecessary requests.
|
||||
- Use a configurable cache and make sure you aren’t caching everything
|
||||
indefinitely by accident. The `cacheMap` option to DataLoader is a good place
|
||||
to put it.
|
||||
- Get as much configuration from environment variables as possible so that users
|
||||
can just run the standalone server instead of writing any code. If you need
|
||||
more complex configuration, use a single field on the `options` object as a
|
||||
namespace for your extension’s options.
|
||||
- Don’t hesitate to rename fields returned by third-party APIs when translating
|
||||
them to the GraphQL schema. Consistency with GraphQL conventions and the
|
||||
GraphBrainz schema is more desirable than consistency with the original API
|
||||
being used. Some general rules:
|
||||
- Match type names to the service they’re coming from (e.g. many services use
|
||||
the words “album” and “track” and the type names should reflect that), but
|
||||
match scalar field names to their MusicBrainz equivalents when possible
|
||||
(e.g. `name` for artists but `title` for releases and recordings).
|
||||
- Use camel case naming and capitalize acronyms (unless they are the only
|
||||
word), e.g. `id`, `url`, `artistID`, `pageURL`.
|
||||
- If it’s ambiguous whether a field refers to an object/list vs. a scalar
|
||||
summary of an object/list, consider clarifying the field name, e.g. `user` →
|
||||
`userID`, `members` → `memberCount`.
|
||||
- Don’t include fields that are already available in MusicBrainz (unless it’s
|
||||
possible to retrieve an entity that isn’t in MusicBrainz). Only include
|
||||
what’s relevant and useful.
|
||||
- Add descriptions for everything: types, fields, arguments, enum values, etc.
|
||||
– with Markdown links wherever they’d be helpful.
|
||||
- When extending the built-in types, prefer adding a single object field that
|
||||
serves as a namespace rather than adding many fields. That way it’s more
|
||||
obvious that the data source isn’t MusicBrainz itself, and you’re less likely
|
||||
to conflict with new MusicBrainz fields in the future.
|
||||
- Prefer using a [Relay][]-compliant schema for lists of objects that (1) have
|
||||
their own IDs and (2) are likely to be paginated. Feel free to add a `nodes`
|
||||
shortcut field to the Connection type (for users who want to skip over
|
||||
`edges`).
|
||||
- If you publish your extension, consider prefixing the package name with
|
||||
`graphbrainz-extension-` and having the default export of its `main` entry
|
||||
point be the extension object. That way, using it is as simple as adding the
|
||||
package name to the list of extensions.
|
||||
- Consider using [graphql-markdown][] to document the schema created by your
|
||||
extension; this will match how GraphBrainz itself is documented. You can use
|
||||
the [diffSchema][] function to document only the schema updates, see
|
||||
[scripts/build-extension-docs.js][build-extension-docs] for how this is done
|
||||
with the built-in extensions.
|
||||
|
||||
[graphql-tools]: http://dev.apollodata.com/tools/graphql-tools/index.html
|
||||
[schema stitching]:
|
||||
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
|
||||
[built-in extensions]: ../../src/extensions
|
||||
[client]: ../../src/api/client.js
|
||||
[graphql-markdown]: https://github.com/exogen/graphql-markdown
|
||||
[diffschema]:
|
||||
https://github.com/exogen/graphql-markdown#diffschemaoldschema-object-newschema-object-options-object
|
||||
[build-extension-docs]: ../../scripts/build-extension-docs.js
|
||||
[relay]: https://facebook.github.io/relay/
|
||||
[graphql.js]: http://graphql.org/graphql-js/
|
||||
[addresolverstoschema]:
|
||||
http://dev.apollodata.com/tools/graphql-tools/resolvers.html#addResolversToSchema
|
||||
382
docs/extensions/cover-art-archive.md
Normal file
382
docs/extensions/cover-art-archive.md
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
# Extension: Cover Art Archive
|
||||
|
||||
Retrieve cover art images for releases from the [Cover Art Archive](https://coverartarchive.org/).
|
||||
|
||||
This extension uses its own cache, separate from the MusicBrainz loader cache.
|
||||
|
||||
## Configuration
|
||||
|
||||
This extension can be configured using environment variables:
|
||||
|
||||
* **`COVER_ART_ARCHIVE_BASE_URL`**: The base URL at which to access the Cover
|
||||
Art Archive API. Defaults to `http://coverartarchive.org/`.
|
||||
* **`COVER_ART_ARCHIVE_CACHE_SIZE`**: The number of items to keep in the cache.
|
||||
Defaults to `GRAPHBRAINZ_CACHE_SIZE` if defined, or `8192`.
|
||||
* **`COVER_ART_ARCHIVE_CACHE_TTL`**: The number of seconds to keep items in the
|
||||
cache. Defaults to `GRAPHBRAINZ_CACHE_TTL` if defined, or `86400000` (one day).
|
||||
|
||||
<!-- START graphql-markdown -->
|
||||
|
||||
## Schema Types
|
||||
|
||||
<details>
|
||||
<summary><strong>Table of Contents</strong></summary>
|
||||
|
||||
* [Objects](#objects)
|
||||
* [CoverArtArchiveImage](#coverartarchiveimage)
|
||||
* [CoverArtArchiveImageThumbnails](#coverartarchiveimagethumbnails)
|
||||
* [CoverArtArchiveRelease](#coverartarchiverelease)
|
||||
* [Release](#release)
|
||||
* [ReleaseGroup](#releasegroup)
|
||||
* [Enums](#enums)
|
||||
* [CoverArtArchiveImageSize](#coverartarchiveimagesize)
|
||||
|
||||
</details>
|
||||
|
||||
### Objects
|
||||
|
||||
#### CoverArtArchiveImage
|
||||
|
||||
An individual piece of album artwork from the [Cover Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive).
|
||||
|
||||
<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>fileID</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a>!</td>
|
||||
<td>
|
||||
|
||||
The Internet Archive’s internal file ID for the image.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>image</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a>!</td>
|
||||
<td>
|
||||
|
||||
The URL at which the image can be found.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>thumbnails</strong></td>
|
||||
<td valign="top"><a href="#coverartarchiveimagethumbnails">CoverArtArchiveImageThumbnails</a>!</td>
|
||||
<td>
|
||||
|
||||
A set of thumbnails for the image.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>front</strong></td>
|
||||
<td valign="top"><a href="../types.md#boolean">Boolean</a>!</td>
|
||||
<td>
|
||||
|
||||
Whether this image depicts the “main front” of the release.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>back</strong></td>
|
||||
<td valign="top"><a href="../types.md#boolean">Boolean</a>!</td>
|
||||
<td>
|
||||
|
||||
Whether this image depicts the “main back” of the release.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>types</strong></td>
|
||||
<td valign="top">[<a href="../types.md#string">String</a>]!</td>
|
||||
<td>
|
||||
|
||||
A list of [image types](https://musicbrainz.org/doc/Cover_Art/Types)
|
||||
describing what part(s) of the release the image includes.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>edit</strong></td>
|
||||
<td valign="top"><a href="../types.md#int">Int</a></td>
|
||||
<td>
|
||||
|
||||
The MusicBrainz edit ID.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>approved</strong></td>
|
||||
<td valign="top"><a href="../types.md#boolean">Boolean</a></td>
|
||||
<td>
|
||||
|
||||
Whether the image was approved by the MusicBrainz edit system.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>comment</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
A free-text comment left for the image.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### CoverArtArchiveImageThumbnails
|
||||
|
||||
URLs for thumbnails of different sizes for a particular piece of cover art.
|
||||
|
||||
<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>small</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
The URL of a small version of the cover art, where the maximum dimension is
|
||||
250px.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>large</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
The URL of a large version of the cover art, where the maximum dimension is
|
||||
500px.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### CoverArtArchiveRelease
|
||||
|
||||
An object containing a list of the cover art images for a release obtained
|
||||
from the [Cover Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive),
|
||||
as well as a summary of what artwork is available.
|
||||
|
||||
<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>front</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
The URL of an image depicting the album cover or “main front” of the release,
|
||||
i.e. the front of the packaging of the audio recording (or in the case of a
|
||||
digital release, the image associated with it in a digital media store).
|
||||
|
||||
In the MusicBrainz schema, this field is a Boolean value indicating the
|
||||
presence of a front image, whereas here the value is the URL for the image
|
||||
itself if one exists. You can check for null if you just want to determine
|
||||
the presence of an image.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">size</td>
|
||||
<td valign="top"><a href="#coverartarchiveimagesize">CoverArtArchiveImageSize</a></td>
|
||||
<td>
|
||||
|
||||
The size of the image to retrieve. By default, the returned image will
|
||||
have its full original dimensions, but certain thumbnail sizes may be
|
||||
retrieved as well.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>back</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
The URL of an image depicting the “main back” of the release, i.e. the back
|
||||
of the packaging of the audio recording.
|
||||
|
||||
In the MusicBrainz schema, this field is a Boolean value indicating the
|
||||
presence of a back image, whereas here the value is the URL for the image
|
||||
itself. You can check for null if you just want to determine the presence of
|
||||
an image.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">size</td>
|
||||
<td valign="top"><a href="#coverartarchiveimagesize">CoverArtArchiveImageSize</a></td>
|
||||
<td>
|
||||
|
||||
The size of the image to retrieve. By default, the returned image will
|
||||
have its full original dimensions, but certain thumbnail sizes may be
|
||||
retrieved as well.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>images</strong></td>
|
||||
<td valign="top">[<a href="#coverartarchiveimage">CoverArtArchiveImage</a>]!</td>
|
||||
<td>
|
||||
|
||||
A list of images depicting the different sides and surfaces of a release’s
|
||||
media and packaging.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>artwork</strong></td>
|
||||
<td valign="top"><a href="../types.md#boolean">Boolean</a>!</td>
|
||||
<td>
|
||||
|
||||
Whether there is artwork present for this release.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>count</strong></td>
|
||||
<td valign="top"><a href="../types.md#int">Int</a>!</td>
|
||||
<td>
|
||||
|
||||
The number of artwork images present for this release.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>release</strong></td>
|
||||
<td valign="top"><a href="#release">Release</a></td>
|
||||
<td>
|
||||
|
||||
The particular release shown in the returned cover art.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### Release
|
||||
|
||||
:small_blue_diamond: *This type has been extended. See the [base schema](../types.md)
|
||||
for a description and additional fields.*
|
||||
|
||||
<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>coverArtArchive</strong></td>
|
||||
<td valign="top"><a href="#coverartarchiverelease">CoverArtArchiveRelease</a></td>
|
||||
<td>
|
||||
|
||||
An object containing a list and summary of the cover art images that are
|
||||
present for this release from the [Cover Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive).
|
||||
This field is provided by the Cover Art Archive extension.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### ReleaseGroup
|
||||
|
||||
:small_blue_diamond: *This type has been extended. See the [base schema](../types.md)
|
||||
for a description and additional fields.*
|
||||
|
||||
<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>coverArtArchive</strong></td>
|
||||
<td valign="top"><a href="#coverartarchiverelease">CoverArtArchiveRelease</a></td>
|
||||
<td>
|
||||
|
||||
The cover art for a release in the release group, obtained from the
|
||||
[Cover Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive). A
|
||||
release in the release group will be chosen as representative of the release
|
||||
group.
|
||||
This field is provided by the Cover Art Archive extension.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Enums
|
||||
|
||||
#### CoverArtArchiveImageSize
|
||||
|
||||
The image sizes that may be requested at the [Cover Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive).
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th align="left">Value</th>
|
||||
<th align="left">Description</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top"><strong>SMALL</strong></td>
|
||||
<td>
|
||||
|
||||
A maximum dimension of 250px.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign="top"><strong>LARGE</strong></td>
|
||||
<td>
|
||||
|
||||
A maximum dimension of 500px.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign="top"><strong>FULL</strong></td>
|
||||
<td>
|
||||
|
||||
The image’s original dimensions, with no maximum.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- END graphql-markdown -->
|
||||
479
docs/extensions/fanart-tv.md
Normal file
479
docs/extensions/fanart-tv.md
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
# Extension: fanart.tv
|
||||
|
||||
Retrieve high quality artwork for artists, releases, and labels from
|
||||
[fanart.tv](https://fanart.tv/).
|
||||
|
||||
This extension uses its own cache, separate from the MusicBrainz loader cache.
|
||||
|
||||
## Configuration
|
||||
|
||||
This extension can be configured using environment variables:
|
||||
|
||||
* **`FANART_API_KEY`**: The fanart.tv API key to use. This is required for any
|
||||
fields added by the extension to successfully resolve.
|
||||
* **`FANART_BASE_URL`**: The base URL at which to access the
|
||||
fanart.tv API. Defaults to `http://webservice.fanart.tv/v3/`.
|
||||
* **`FANART_CACHE_SIZE`**: The number of items to keep in the cache.
|
||||
Defaults to `GRAPHBRAINZ_CACHE_SIZE` if defined, or `8192`.
|
||||
* **`FANART_CACHE_TTL`**: The number of seconds to keep items in the
|
||||
cache. Defaults to `GRAPHBRAINZ_CACHE_TTL` if defined, or `86400000` (one day).
|
||||
|
||||
<!-- START graphql-markdown -->
|
||||
|
||||
## Schema Types
|
||||
|
||||
<details>
|
||||
<summary><strong>Table of Contents</strong></summary>
|
||||
|
||||
* [Objects](#objects)
|
||||
* [Artist](#artist)
|
||||
* [FanArtAlbum](#fanartalbum)
|
||||
* [FanArtArtist](#fanartartist)
|
||||
* [FanArtDiscImage](#fanartdiscimage)
|
||||
* [FanArtImage](#fanartimage)
|
||||
* [FanArtLabel](#fanartlabel)
|
||||
* [FanArtLabelImage](#fanartlabelimage)
|
||||
* [Label](#label)
|
||||
* [ReleaseGroup](#releasegroup)
|
||||
* [Enums](#enums)
|
||||
* [FanArtImageSize](#fanartimagesize)
|
||||
|
||||
</details>
|
||||
|
||||
### Objects
|
||||
|
||||
#### Artist
|
||||
|
||||
:small_blue_diamond: *This type has been extended. See the [base schema](../types.md)
|
||||
for a description and additional fields.*
|
||||
|
||||
<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>fanArt</strong></td>
|
||||
<td valign="top"><a href="#fanartartist">FanArtArtist</a></td>
|
||||
<td>
|
||||
|
||||
Images of the artist from [fanart.tv](https://fanart.tv/).
|
||||
This field is provided by the fanart.tv extension.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### FanArtAlbum
|
||||
|
||||
An object containing lists of the different types of release group images from
|
||||
[fanart.tv](https://fanart.tv/).
|
||||
|
||||
<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>albumCovers</strong></td>
|
||||
<td valign="top">[<a href="#fanartimage">FanArtImage</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of 1000x1000 JPG images of the cover artwork of the release group.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>discImages</strong></td>
|
||||
<td valign="top">[<a href="#fanartdiscimage">FanArtDiscImage</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of 1000x1000 PNG images of the physical disc media for the release
|
||||
group, with transparent backgrounds.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### FanArtArtist
|
||||
|
||||
An object containing lists of the different types of artist images from
|
||||
[fanart.tv](https://fanart.tv/).
|
||||
|
||||
<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>backgrounds</strong></td>
|
||||
<td valign="top">[<a href="#fanartimage">FanArtImage</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of 1920x1080 JPG images picturing the artist, suitable for use as
|
||||
backgrounds.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>banners</strong></td>
|
||||
<td valign="top">[<a href="#fanartimage">FanArtImage</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of 1000x185 JPG images containing the artist and their logo or name.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>logos</strong></td>
|
||||
<td valign="top">[<a href="#fanartimage">FanArtImage</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of 400x155 PNG images containing the artist’s logo or name, with
|
||||
transparent backgrounds.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>logosHD</strong></td>
|
||||
<td valign="top">[<a href="#fanartimage">FanArtImage</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of 800x310 PNG images containing the artist’s logo or name, with
|
||||
transparent backgrounds.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>thumbnails</strong></td>
|
||||
<td valign="top">[<a href="#fanartimage">FanArtImage</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of 1000x1000 JPG thumbnail images picturing the artist (usually
|
||||
containing every member of a band).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### FanArtDiscImage
|
||||
|
||||
A disc image from [fanart.tv](https://fanart.tv/).
|
||||
|
||||
<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>imageID</strong></td>
|
||||
<td valign="top"><a href="../types.md#id">ID</a></td>
|
||||
<td>
|
||||
|
||||
The ID of the image on fanart.tv.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>url</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
The URL of the image.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">size</td>
|
||||
<td valign="top"><a href="#fanartimagesize">FanArtImageSize</a></td>
|
||||
<td>
|
||||
|
||||
The size of the image to retrieve.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>likeCount</strong></td>
|
||||
<td valign="top"><a href="../types.md#int">Int</a></td>
|
||||
<td>
|
||||
|
||||
The number of likes the image has received by fanart.tv users.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>discNumber</strong></td>
|
||||
<td valign="top"><a href="../types.md#int">Int</a></td>
|
||||
<td>
|
||||
|
||||
The disc number.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>size</strong></td>
|
||||
<td valign="top"><a href="../types.md#int">Int</a></td>
|
||||
<td>
|
||||
|
||||
The width and height of the (square) disc image.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### FanArtImage
|
||||
|
||||
A single image from [fanart.tv](https://fanart.tv/).
|
||||
|
||||
<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>imageID</strong></td>
|
||||
<td valign="top"><a href="../types.md#id">ID</a></td>
|
||||
<td>
|
||||
|
||||
The ID of the image on fanart.tv.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>url</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
The URL of the image.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">size</td>
|
||||
<td valign="top"><a href="#fanartimagesize">FanArtImageSize</a></td>
|
||||
<td>
|
||||
|
||||
The size of the image to retrieve.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>likeCount</strong></td>
|
||||
<td valign="top"><a href="../types.md#int">Int</a></td>
|
||||
<td>
|
||||
|
||||
The number of likes the image has received by fanart.tv users.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### FanArtLabel
|
||||
|
||||
An object containing lists of the different types of label images from
|
||||
[fanart.tv](https://fanart.tv/).
|
||||
|
||||
<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>logos</strong></td>
|
||||
<td valign="top">[<a href="#fanartlabelimage">FanArtLabelImage</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of 400x270 PNG images containing the label’s logo. There will
|
||||
usually be a black version, a color version, and a white version, all with
|
||||
transparent backgrounds.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### FanArtLabelImage
|
||||
|
||||
A music label image from [fanart.tv](https://fanart.tv/).
|
||||
|
||||
<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>imageID</strong></td>
|
||||
<td valign="top"><a href="../types.md#id">ID</a></td>
|
||||
<td>
|
||||
|
||||
The ID of the image on fanart.tv.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>url</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
The URL of the image.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">size</td>
|
||||
<td valign="top"><a href="#fanartimagesize">FanArtImageSize</a></td>
|
||||
<td>
|
||||
|
||||
The size of the image to retrieve.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>likeCount</strong></td>
|
||||
<td valign="top"><a href="../types.md#int">Int</a></td>
|
||||
<td>
|
||||
|
||||
The number of likes the image has received by fanart.tv users.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>color</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The type of color content in the image (usually “white” or “colour”).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### Label
|
||||
|
||||
:small_blue_diamond: *This type has been extended. See the [base schema](../types.md)
|
||||
for a description and additional fields.*
|
||||
|
||||
<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>fanArt</strong></td>
|
||||
<td valign="top"><a href="#fanartlabel">FanArtLabel</a></td>
|
||||
<td>
|
||||
|
||||
Images of the label from [fanart.tv](https://fanart.tv/).
|
||||
This field is provided by the fanart.tv extension.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### ReleaseGroup
|
||||
|
||||
:small_blue_diamond: *This type has been extended. See the [base schema](../types.md)
|
||||
for a description and additional fields.*
|
||||
|
||||
<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>fanArt</strong></td>
|
||||
<td valign="top"><a href="#fanartalbum">FanArtAlbum</a></td>
|
||||
<td>
|
||||
|
||||
Images of the release group from [fanart.tv](https://fanart.tv/).
|
||||
This field is provided by the fanart.tv extension.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Enums
|
||||
|
||||
#### FanArtImageSize
|
||||
|
||||
The image sizes that may be requested at [fanart.tv](https://fanart.tv/).
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th align="left">Value</th>
|
||||
<th align="left">Description</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top"><strong>FULL</strong></td>
|
||||
<td>
|
||||
|
||||
The image’s full original dimensions.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign="top"><strong>PREVIEW</strong></td>
|
||||
<td>
|
||||
|
||||
A maximum dimension of 200px.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- END graphql-markdown -->
|
||||
411
docs/extensions/mediawiki.md
Normal file
411
docs/extensions/mediawiki.md
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
# Extension: MediaWiki
|
||||
|
||||
Retrieve information from MediaWiki image pages, like the actual image file URL
|
||||
and EXIF metadata.
|
||||
|
||||
On entities with [URL relationship types][relationships] that represent images,
|
||||
this extension will find those URLs that appear to be MediaWiki image pages, and
|
||||
use the [MediaWiki API][] to fetch information about the image. This information
|
||||
will include the actual file URL, so you can use it as the `src` in an `<img>`
|
||||
tag (for example).
|
||||
|
||||
MediaWiki image URLs are assumed to be those with a path that starts with
|
||||
`/wiki/Image:` or `/wiki/File:`.
|
||||
|
||||
This extension uses its own cache, separate from the MusicBrainz loader cache.
|
||||
|
||||
## Configuration
|
||||
|
||||
This extension can be configured using environment variables:
|
||||
|
||||
* **`MEDIAWIKI_CACHE_SIZE`**: The number of items to keep in the cache.
|
||||
Defaults to `GRAPHBRAINZ_CACHE_SIZE` if defined, or `8192`.
|
||||
* **`MEDIAWIKI_CACHE_TTL`**: The number of seconds to keep items in the
|
||||
cache. Defaults to `GRAPHBRAINZ_CACHE_TTL` if defined, or `86400000` (one day).
|
||||
|
||||
[relationships]: https://musicbrainz.org/relationships
|
||||
[MediaWiki API]: https://www.mediawiki.org/wiki/API:Main_page
|
||||
|
||||
<!-- START graphql-markdown -->
|
||||
|
||||
## Schema Types
|
||||
|
||||
<details>
|
||||
<summary><strong>Table of Contents</strong></summary>
|
||||
|
||||
* [Objects](#objects)
|
||||
* [Artist](#artist)
|
||||
* [Instrument](#instrument)
|
||||
* [Label](#label)
|
||||
* [MediaWikiImage](#mediawikiimage)
|
||||
* [MediaWikiImageMetadata](#mediawikiimagemetadata)
|
||||
* [Place](#place)
|
||||
|
||||
</details>
|
||||
|
||||
### Objects
|
||||
|
||||
#### Artist
|
||||
|
||||
:small_blue_diamond: *This type has been extended. See the [base schema](../types.md)
|
||||
for a description and additional fields.*
|
||||
|
||||
<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>mediaWikiImages</strong></td>
|
||||
<td valign="top">[<a href="#mediawikiimage">MediaWikiImage</a>]!</td>
|
||||
<td>
|
||||
|
||||
Artist images found at MediaWiki URLs in the artist’s URL relationships.
|
||||
Defaults to URL relationships with the type “image”.
|
||||
This field is provided by the MediaWiki extension.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">type</td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The type of URL relationship that will be selected to find images. See
|
||||
the possible [Artist-URL relationship types](https://musicbrainz.org/relationships/artist-url).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### Instrument
|
||||
|
||||
:small_blue_diamond: *This type has been extended. See the [base schema](../types.md)
|
||||
for a description and additional fields.*
|
||||
|
||||
<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>mediaWikiImages</strong></td>
|
||||
<td valign="top">[<a href="#mediawikiimage">MediaWikiImage</a>]!</td>
|
||||
<td>
|
||||
|
||||
Instrument images found at MediaWiki URLs in the instrument’s URL
|
||||
relationships. Defaults to URL relationships with the type “image”.
|
||||
This field is provided by the MediaWiki extension.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">type</td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The type of URL relationship that will be selected to find images. See the
|
||||
possible [Instrument-URL relationship types](https://musicbrainz.org/relationships/instrument-url).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### Label
|
||||
|
||||
:small_blue_diamond: *This type has been extended. See the [base schema](../types.md)
|
||||
for a description and additional fields.*
|
||||
|
||||
<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>mediaWikiImages</strong></td>
|
||||
<td valign="top">[<a href="#mediawikiimage">MediaWikiImage</a>]!</td>
|
||||
<td>
|
||||
|
||||
Label images found at MediaWiki URLs in the label’s URL relationships.
|
||||
Defaults to URL relationships with the type “logo”.
|
||||
This field is provided by the MediaWiki extension.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">type</td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The type of URL relationship that will be selected to find images. See the
|
||||
possible [Label-URL relationship types](https://musicbrainz.org/relationships/label-url).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### MediaWikiImage
|
||||
|
||||
An object describing various properties of an image stored on a MediaWiki
|
||||
server. The information comes the [MediaWiki imageinfo API](https://www.mediawiki.org/wiki/API:Imageinfo).
|
||||
|
||||
<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>url</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a>!</td>
|
||||
<td>
|
||||
|
||||
The URL of the actual image file.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>descriptionURL</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
The URL of the wiki page describing the image.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>user</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The user who uploaded the file.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>size</strong></td>
|
||||
<td valign="top"><a href="../types.md#int">Int</a></td>
|
||||
<td>
|
||||
|
||||
The size of the file in bytes.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>width</strong></td>
|
||||
<td valign="top"><a href="../types.md#int">Int</a></td>
|
||||
<td>
|
||||
|
||||
The pixel width of the image.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>height</strong></td>
|
||||
<td valign="top"><a href="../types.md#int">Int</a></td>
|
||||
<td>
|
||||
|
||||
The pixel height of the image.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>canonicalTitle</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The canonical title of the file.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>objectName</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The image title, brief description, or file name.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>descriptionHTML</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
A description of the image, potentially containing HTML.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>originalDateTimeHTML</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The original date of creation of the image. May be a description rather than
|
||||
a parseable timestamp, and may contain HTML.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>categories</strong></td>
|
||||
<td valign="top">[<a href="../types.md#string">String</a>]!</td>
|
||||
<td>
|
||||
|
||||
A list of the categories of the image.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>artistHTML</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The name of the image author, potentially containing HTML.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>creditHTML</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The source of the image, potentially containing HTML.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>licenseShortName</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
A short human-readable license name.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>licenseURL</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
A web address where the license is described.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>metadata</strong></td>
|
||||
<td valign="top">[<a href="#mediawikiimagemetadata">MediaWikiImageMetadata</a>]!</td>
|
||||
<td>
|
||||
|
||||
The full list of values in the `extmetadata` field.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### MediaWikiImageMetadata
|
||||
|
||||
An entry in the `extmetadata` field of a MediaWiki image file.
|
||||
|
||||
<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>name</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a>!</td>
|
||||
<td>
|
||||
|
||||
The name of the metadata field.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>value</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The value of the metadata field. All values will be converted to strings.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>source</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The source of the value.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### Place
|
||||
|
||||
:small_blue_diamond: *This type has been extended. See the [base schema](../types.md)
|
||||
for a description and additional fields.*
|
||||
|
||||
<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>mediaWikiImages</strong></td>
|
||||
<td valign="top">[<a href="#mediawikiimage">MediaWikiImage</a>]!</td>
|
||||
<td>
|
||||
|
||||
Place images found at MediaWiki URLs in the place’s URL relationships.
|
||||
Defaults to URL relationships with the type “image”.
|
||||
This field is provided by the MediaWiki extension.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">type</td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The type of URL relationship that will be selected to find images. See the
|
||||
possible [Place-URL relationship types](https://musicbrainz.org/relationships/place-url).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- END graphql-markdown -->
|
||||
776
docs/extensions/the-audio-db.md
Normal file
776
docs/extensions/the-audio-db.md
Normal file
|
|
@ -0,0 +1,776 @@
|
|||
# Extension: TheAudioDB
|
||||
|
||||
Retrieve images and information about artists, releases, and recordings from
|
||||
[TheAudioDB.com](http://www.theaudiodb.com/).
|
||||
|
||||
This extension uses its own cache, separate from the MusicBrainz loader cache.
|
||||
|
||||
## Configuration
|
||||
|
||||
This extension can be configured using environment variables:
|
||||
|
||||
* **`THEAUDIODB_API_KEY`**: TheAudioDB API key to use. This is required for any
|
||||
fields added by the extension to successfully resolve.
|
||||
* **`THEAUDIODB_BASE_URL`**: The base URL at which to access TheAudioDB API.
|
||||
Defaults to `http://www.theaudiodb.com/api/v1/json/`.
|
||||
* **`THEAUDIODB_CACHE_SIZE`**: The number of items to keep in the cache.
|
||||
Defaults to `GRAPHBRAINZ_CACHE_SIZE` if defined, or `8192`.
|
||||
* **`THEAUDIODB_CACHE_TTL`**: The number of seconds to keep items in the
|
||||
cache. Defaults to `GRAPHBRAINZ_CACHE_TTL` if defined, or `86400000` (one day).
|
||||
|
||||
<!-- START graphql-markdown -->
|
||||
|
||||
## Schema Types
|
||||
|
||||
<details>
|
||||
<summary><strong>Table of Contents</strong></summary>
|
||||
|
||||
* [Objects](#objects)
|
||||
* [Artist](#artist)
|
||||
* [Recording](#recording)
|
||||
* [ReleaseGroup](#releasegroup)
|
||||
* [TheAudioDBAlbum](#theaudiodbalbum)
|
||||
* [TheAudioDBArtist](#theaudiodbartist)
|
||||
* [TheAudioDBMusicVideo](#theaudiodbmusicvideo)
|
||||
* [TheAudioDBTrack](#theaudiodbtrack)
|
||||
* [Enums](#enums)
|
||||
* [TheAudioDBImageSize](#theaudiodbimagesize)
|
||||
|
||||
</details>
|
||||
|
||||
### Objects
|
||||
|
||||
#### Artist
|
||||
|
||||
:small_blue_diamond: *This type has been extended. See the [base schema](../types.md)
|
||||
for a description and additional fields.*
|
||||
|
||||
<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>theAudioDB</strong></td>
|
||||
<td valign="top"><a href="#theaudiodbartist">TheAudioDBArtist</a></td>
|
||||
<td>
|
||||
|
||||
Data about the artist from [TheAudioDB](http://www.theaudiodb.com/), a good
|
||||
source of biographical information and images.
|
||||
This field is provided by TheAudioDB extension.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### Recording
|
||||
|
||||
:small_blue_diamond: *This type has been extended. See the [base schema](../types.md)
|
||||
for a description and additional fields.*
|
||||
|
||||
<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>theAudioDB</strong></td>
|
||||
<td valign="top"><a href="#theaudiodbtrack">TheAudioDBTrack</a></td>
|
||||
<td>
|
||||
|
||||
Data about the recording from [TheAudioDB](http://www.theaudiodb.com/).
|
||||
This field is provided by TheAudioDB extension.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### ReleaseGroup
|
||||
|
||||
:small_blue_diamond: *This type has been extended. See the [base schema](../types.md)
|
||||
for a description and additional fields.*
|
||||
|
||||
<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>theAudioDB</strong></td>
|
||||
<td valign="top"><a href="#theaudiodbalbum">TheAudioDBAlbum</a></td>
|
||||
<td>
|
||||
|
||||
Data about the release group from [TheAudioDB](http://www.theaudiodb.com/),
|
||||
a good source of descriptive information, reviews, and images.
|
||||
This field is provided by TheAudioDB extension.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### TheAudioDBAlbum
|
||||
|
||||
An album on [TheAudioDB](http://www.theaudiodb.com/) corresponding with a
|
||||
MusicBrainz Release Group.
|
||||
|
||||
<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>albumID</strong></td>
|
||||
<td valign="top"><a href="../types.md#id">ID</a></td>
|
||||
<td>
|
||||
|
||||
TheAudioDB ID of the album.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>artistID</strong></td>
|
||||
<td valign="top"><a href="../types.md#id">ID</a></td>
|
||||
<td>
|
||||
|
||||
TheAudioDB ID of the artist who released the album.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>description</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
A description of the album, often available in several languages.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">lang</td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The two-letter code for the language in which to retrieve the biography.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>review</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
A review of the album.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>salesCount</strong></td>
|
||||
<td valign="top"><a href="../types.md#float">Float</a></td>
|
||||
<td>
|
||||
|
||||
The worldwide sales figure.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>score</strong></td>
|
||||
<td valign="top"><a href="../types.md#float">Float</a></td>
|
||||
<td>
|
||||
|
||||
The album’s rating as determined by user votes, out of 10.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>scoreVotes</strong></td>
|
||||
<td valign="top"><a href="../types.md#float">Float</a></td>
|
||||
<td>
|
||||
|
||||
The number of users who voted to determine the album’s score.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>discImage</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
An image of the physical disc media for the album.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">size</td>
|
||||
<td valign="top"><a href="#theaudiodbimagesize">TheAudioDBImageSize</a></td>
|
||||
<td>
|
||||
|
||||
The size of the image to retrieve.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>spineImage</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
An image of the spine of the album packaging.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">size</td>
|
||||
<td valign="top"><a href="#theaudiodbimagesize">TheAudioDBImageSize</a></td>
|
||||
<td>
|
||||
|
||||
The size of the image to retrieve.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>frontImage</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
An image of the front of the album packaging.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">size</td>
|
||||
<td valign="top"><a href="#theaudiodbimagesize">TheAudioDBImageSize</a></td>
|
||||
<td>
|
||||
|
||||
The size of the image to retrieve.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>backImage</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
An image of the back of the album packaging.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">size</td>
|
||||
<td valign="top"><a href="#theaudiodbimagesize">TheAudioDBImageSize</a></td>
|
||||
<td>
|
||||
|
||||
The size of the image to retrieve.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>genre</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The primary musical genre of the album (e.g. “Alternative Rock”).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>mood</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The primary musical mood of the album (e.g. “Sad”).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>style</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The primary musical style of the album (e.g. “Rock/Pop”).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>speed</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
A rough description of the primary musical speed of the album (e.g. “Medium”).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>theme</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The primary musical theme of the album (e.g. “In Love”).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### TheAudioDBArtist
|
||||
|
||||
An artist on [TheAudioDB](http://www.theaudiodb.com/).
|
||||
|
||||
<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>artistID</strong></td>
|
||||
<td valign="top"><a href="../types.md#id">ID</a></td>
|
||||
<td>
|
||||
|
||||
TheAudioDB ID of the artist.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>biography</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
A biography of the artist, often available in several languages.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">lang</td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The two-letter code for the language in which to retrieve the biography.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>memberCount</strong></td>
|
||||
<td valign="top"><a href="../types.md#int">Int</a></td>
|
||||
<td>
|
||||
|
||||
The number of members in the musical group, if applicable.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>banner</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
A 1000x185 JPG banner image containing the artist and their logo or name.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">size</td>
|
||||
<td valign="top"><a href="#theaudiodbimagesize">TheAudioDBImageSize</a></td>
|
||||
<td>
|
||||
|
||||
The size of the image to retrieve.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>fanArt</strong></td>
|
||||
<td valign="top">[<a href="../types.md#urlstring">URLString</a>]!</td>
|
||||
<td>
|
||||
|
||||
A list of 1280x720 or 1920x1080 JPG images depicting the artist.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">size</td>
|
||||
<td valign="top"><a href="#theaudiodbimagesize">TheAudioDBImageSize</a></td>
|
||||
<td>
|
||||
|
||||
The size of the images to retrieve.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>logo</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
A 400x155 PNG image containing the artist’s logo or name, with a transparent
|
||||
background.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">size</td>
|
||||
<td valign="top"><a href="#theaudiodbimagesize">TheAudioDBImageSize</a></td>
|
||||
<td>
|
||||
|
||||
The size of the image to retrieve.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>thumbnail</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
A 1000x1000 JPG thumbnail image picturing the artist (usually containing
|
||||
every member of a band).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">size</td>
|
||||
<td valign="top"><a href="#theaudiodbimagesize">TheAudioDBImageSize</a></td>
|
||||
<td>
|
||||
|
||||
The size of the image to retrieve.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>genre</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The primary musical genre of the artist (e.g. “Alternative Rock”).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>mood</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The primary musical mood of the artist (e.g. “Sad”).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>style</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The primary musical style of the artist (e.g. “Rock/Pop”).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### TheAudioDBMusicVideo
|
||||
|
||||
Details of a music video associated with a track on [TheAudioDB](http://www.theaudiodb.com/).
|
||||
|
||||
<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>url</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
The URL where the music video can be found.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>companyName</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The video production company of the music video.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>directorName</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The director of the music video.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>screenshots</strong></td>
|
||||
<td valign="top">[<a href="../types.md#urlstring">URLString</a>]!</td>
|
||||
<td>
|
||||
|
||||
A list of still images from the music video.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">size</td>
|
||||
<td valign="top"><a href="#theaudiodbimagesize">TheAudioDBImageSize</a></td>
|
||||
<td>
|
||||
|
||||
The size of the images to retrieve.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>viewCount</strong></td>
|
||||
<td valign="top"><a href="../types.md#float">Float</a></td>
|
||||
<td>
|
||||
|
||||
The number of views the video has received at the given URL. This will rarely
|
||||
be up to date, so use cautiously.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>likeCount</strong></td>
|
||||
<td valign="top"><a href="../types.md#float">Float</a></td>
|
||||
<td>
|
||||
|
||||
The number of likes the video has received at the given URL. This will rarely
|
||||
be up to date, so use cautiously.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>dislikeCount</strong></td>
|
||||
<td valign="top"><a href="../types.md#float">Float</a></td>
|
||||
<td>
|
||||
|
||||
The number of dislikes the video has received at the given URL. This will
|
||||
rarely be up to date, so use cautiously.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>commentCount</strong></td>
|
||||
<td valign="top"><a href="../types.md#float">Float</a></td>
|
||||
<td>
|
||||
|
||||
The number of comments the video has received at the given URL. This will
|
||||
rarely be up to date, so use cautiously.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
#### TheAudioDBTrack
|
||||
|
||||
A track on [TheAudioDB](http://www.theaudiodb.com/) corresponding with a
|
||||
MusicBrainz Recording.
|
||||
|
||||
<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>trackID</strong></td>
|
||||
<td valign="top"><a href="../types.md#id">ID</a></td>
|
||||
<td>
|
||||
|
||||
TheAudioDB ID of the track.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>albumID</strong></td>
|
||||
<td valign="top"><a href="../types.md#id">ID</a></td>
|
||||
<td>
|
||||
|
||||
TheAudioDB ID of the album on which the track appears.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>artistID</strong></td>
|
||||
<td valign="top"><a href="../types.md#id">ID</a></td>
|
||||
<td>
|
||||
|
||||
TheAudioDB ID of the artist who released the track.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>description</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
A description of the track.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">lang</td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The two-letter code for the language in which to retrieve the description.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>thumbnail</strong></td>
|
||||
<td valign="top"><a href="../types.md#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
A thumbnail image for the track.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">size</td>
|
||||
<td valign="top"><a href="#theaudiodbimagesize">TheAudioDBImageSize</a></td>
|
||||
<td>
|
||||
|
||||
The size of the image to retrieve.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>score</strong></td>
|
||||
<td valign="top"><a href="../types.md#float">Float</a></td>
|
||||
<td>
|
||||
|
||||
The track’s rating as determined by user votes, out of 10.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>scoreVotes</strong></td>
|
||||
<td valign="top"><a href="../types.md#float">Float</a></td>
|
||||
<td>
|
||||
|
||||
The number of users who voted to determine the album’s score.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>trackNumber</strong></td>
|
||||
<td valign="top"><a href="../types.md#int">Int</a></td>
|
||||
<td>
|
||||
|
||||
The track number of the song on the album.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>musicVideo</strong></td>
|
||||
<td valign="top"><a href="#theaudiodbmusicvideo">TheAudioDBMusicVideo</a></td>
|
||||
<td>
|
||||
|
||||
The official music video for the track.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>genre</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The primary musical genre of the track (e.g. “Alternative Rock”).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>mood</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The primary musical mood of the track (e.g. “Sad”).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>style</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The primary musical style of the track (e.g. “Rock/Pop”).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>theme</strong></td>
|
||||
<td valign="top"><a href="../types.md#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The primary musical theme of the track (e.g. “In Love”).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Enums
|
||||
|
||||
#### TheAudioDBImageSize
|
||||
|
||||
The image sizes that may be requested at [TheAudioDB](http://www.theaudiodb.com/).
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th align="left">Value</th>
|
||||
<th align="left">Description</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top"><strong>FULL</strong></td>
|
||||
<td>
|
||||
|
||||
The image’s full original dimensions.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign="top"><strong>PREVIEW</strong></td>
|
||||
<td>
|
||||
|
||||
A maximum dimension of 200px.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- END graphql-markdown -->
|
||||
4365
docs/schema.md
4365
docs/schema.md
File diff suppressed because it is too large
Load diff
664
docs/types.md
664
docs/types.md
|
|
@ -1,6 +1,9 @@
|
|||
# Schema Types
|
||||
|
||||
You may also be interested in reading the [schema in GraphQL syntax](schema.md).
|
||||
You may also be interested in reading the [schema in GraphQL syntax](schema.md)
|
||||
or the schemas provided by the [built-in extensions](extensions).
|
||||
|
||||
<!-- START graphql-markdown -->
|
||||
|
||||
<details>
|
||||
<summary><strong>Table of Contents</strong></summary>
|
||||
|
|
@ -20,8 +23,6 @@ You may also be interested in reading the [schema in GraphQL syntax](schema.md).
|
|||
* [CollectionConnection](#collectionconnection)
|
||||
* [CollectionEdge](#collectionedge)
|
||||
* [Coordinates](#coordinates)
|
||||
* [CoverArtImage](#coverartimage)
|
||||
* [CoverArtImageThumbnails](#coverartimagethumbnails)
|
||||
* [Disc](#disc)
|
||||
* [Event](#event)
|
||||
* [EventConnection](#eventconnection)
|
||||
|
|
@ -49,12 +50,10 @@ You may also be interested in reading the [schema in GraphQL syntax](schema.md).
|
|||
* [Relationships](#relationships)
|
||||
* [Release](#release)
|
||||
* [ReleaseConnection](#releaseconnection)
|
||||
* [ReleaseCoverArt](#releasecoverart)
|
||||
* [ReleaseEdge](#releaseedge)
|
||||
* [ReleaseEvent](#releaseevent)
|
||||
* [ReleaseGroup](#releasegroup)
|
||||
* [ReleaseGroupConnection](#releasegroupconnection)
|
||||
* [ReleaseGroupCoverArt](#releasegroupcoverart)
|
||||
* [ReleaseGroupEdge](#releasegroupedge)
|
||||
* [SearchQuery](#searchquery)
|
||||
* [Series](#series)
|
||||
|
|
@ -63,12 +62,12 @@ You may also be interested in reading the [schema in GraphQL syntax](schema.md).
|
|||
* [Tag](#tag)
|
||||
* [TagConnection](#tagconnection)
|
||||
* [TagEdge](#tagedge)
|
||||
* [Track](#track)
|
||||
* [URL](#url)
|
||||
* [Work](#work)
|
||||
* [WorkConnection](#workconnection)
|
||||
* [WorkEdge](#workedge)
|
||||
* [Enums](#enums)
|
||||
* [CoverArtImageSize](#coverartimagesize)
|
||||
* [ReleaseGroupType](#releasegrouptype)
|
||||
* [ReleaseStatus](#releasestatus)
|
||||
* [Scalars](#scalars)
|
||||
|
|
@ -78,6 +77,7 @@ You may also be interested in reading the [schema in GraphQL syntax](schema.md).
|
|||
* [Degrees](#degrees)
|
||||
* [DiscID](#discid)
|
||||
* [Duration](#duration)
|
||||
* [Float](#float)
|
||||
* [ID](#id)
|
||||
* [IPI](#ipi)
|
||||
* [ISNI](#isni)
|
||||
|
|
@ -95,7 +95,7 @@ You may also be interested in reading the [schema in GraphQL syntax](schema.md).
|
|||
|
||||
</details>
|
||||
|
||||
## Query
|
||||
## Query
|
||||
The query root, from which multiple types of MusicBrainz
|
||||
requests can be made.
|
||||
|
||||
|
|
@ -318,6 +318,36 @@ alternate names or misspellings.
|
|||
[ISO 3166 codes](https://en.wikipedia.org/wiki/ISO_3166) are
|
||||
the codes assigned by ISO to countries and subdivisions.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">standard</td>
|
||||
<td valign="top"><a href="#string">String</a></td>
|
||||
<td>
|
||||
|
||||
Specify the particular ISO standard codes to retrieve.
|
||||
Available ISO standards are 3166-1, 3166-2, and 3166-3.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>type</strong></td>
|
||||
<td valign="top"><a href="#string">String</a></td>
|
||||
<td>
|
||||
|
||||
The type of area (country, city, etc. – see the [possible
|
||||
values](https://musicbrainz.org/doc/Area)).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>typeID</strong></td>
|
||||
<td valign="top"><a href="#mbid">MBID</a></td>
|
||||
<td>
|
||||
|
||||
The MBID associated with the value of the `type`
|
||||
field.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -513,6 +543,16 @@ Information to aid in pagination.
|
|||
|
||||
A list of edges.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>nodes</strong></td>
|
||||
<td valign="top">[<a href="#area">Area</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of nodes in the connection (without going through the
|
||||
`edges` field).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -949,6 +989,16 @@ Information to aid in pagination.
|
|||
|
||||
A list of edges.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>nodes</strong></td>
|
||||
<td valign="top">[<a href="#artist">Artist</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of nodes in the connection (without going through the
|
||||
`edges` field).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -2052,6 +2102,16 @@ Information to aid in pagination.
|
|||
|
||||
A list of edges.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>nodes</strong></td>
|
||||
<td valign="top">[<a href="#collection">Collection</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of nodes in the connection (without going through the
|
||||
`edges` field).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -2147,143 +2207,6 @@ The east–west position of a point on the Earth’s surface.
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
### CoverArtImage
|
||||
|
||||
An individual piece of album artwork from the [Cover Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive).
|
||||
|
||||
<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>fileID</strong></td>
|
||||
<td valign="top"><a href="#string">String</a>!</td>
|
||||
<td>
|
||||
|
||||
The Internet Archive’s internal file ID for the image.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>image</strong></td>
|
||||
<td valign="top"><a href="#urlstring">URLString</a>!</td>
|
||||
<td>
|
||||
|
||||
The URL at which the image can be found.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>thumbnails</strong></td>
|
||||
<td valign="top"><a href="#coverartimagethumbnails">CoverArtImageThumbnails</a></td>
|
||||
<td>
|
||||
|
||||
A set of thumbnails for the image.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>front</strong></td>
|
||||
<td valign="top"><a href="#boolean">Boolean</a>!</td>
|
||||
<td>
|
||||
|
||||
Whether this image depicts the “main front” of the release.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>back</strong></td>
|
||||
<td valign="top"><a href="#boolean">Boolean</a>!</td>
|
||||
<td>
|
||||
|
||||
Whether this image depicts the “main back” of the release.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>types</strong></td>
|
||||
<td valign="top">[<a href="#string">String</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of [image types](https://musicbrainz.org/doc/Cover_Art/Types)
|
||||
describing what part(s) of the release the image includes.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>edit</strong></td>
|
||||
<td valign="top"><a href="#int">Int</a></td>
|
||||
<td>
|
||||
|
||||
The MusicBrainz edit ID.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>approved</strong></td>
|
||||
<td valign="top"><a href="#boolean">Boolean</a></td>
|
||||
<td>
|
||||
|
||||
Whether the image was approved by the MusicBrainz edit system.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>comment</strong></td>
|
||||
<td valign="top"><a href="#string">String</a></td>
|
||||
<td>
|
||||
|
||||
A free-text comment left for the image.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### CoverArtImageThumbnails
|
||||
|
||||
URLs for thumbnails of different sizes for a particular piece of
|
||||
cover art.
|
||||
|
||||
<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>small</strong></td>
|
||||
<td valign="top"><a href="#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
The URL of a small version of the cover art, where the
|
||||
maximum dimension is 250px.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>large</strong></td>
|
||||
<td valign="top"><a href="#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
The URL of a large version of the cover art, where the
|
||||
maximum dimension is 500px.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Disc
|
||||
|
||||
Information about the physical CD and releases associated with a
|
||||
|
|
@ -2575,6 +2498,16 @@ Information to aid in pagination.
|
|||
|
||||
A list of edges.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>nodes</strong></td>
|
||||
<td valign="top">[<a href="#event">Event</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of nodes in the connection (without going through the
|
||||
`edges` field).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -2808,6 +2741,16 @@ Information to aid in pagination.
|
|||
|
||||
A list of edges.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>nodes</strong></td>
|
||||
<td valign="top">[<a href="#instrument">Instrument</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of nodes in the connection (without going through the
|
||||
`edges` field).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -3135,6 +3078,16 @@ Information to aid in pagination.
|
|||
|
||||
A list of edges.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>nodes</strong></td>
|
||||
<td valign="top">[<a href="#label">Label</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of nodes in the connection (without going through the
|
||||
`edges` field).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -3592,6 +3545,15 @@ The number of audio tracks on 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>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
@ -3869,6 +3831,16 @@ Information to aid in pagination.
|
|||
|
||||
A list of edges.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>nodes</strong></td>
|
||||
<td valign="top">[<a href="#place">Place</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of nodes in the connection (without going through the
|
||||
`edges` field).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -3957,7 +3929,7 @@ The number of votes that have contributed to the rating.
|
|||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>value</strong></td>
|
||||
<td valign="top"><a href="#int">Int</a></td>
|
||||
<td valign="top"><a href="#float">Float</a></td>
|
||||
<td>
|
||||
|
||||
The average rating value based on the aggregated votes.
|
||||
|
|
@ -4240,6 +4212,16 @@ Information to aid in pagination.
|
|||
|
||||
A list of edges.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>nodes</strong></td>
|
||||
<td valign="top">[<a href="#recording">Recording</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of nodes in the connection (without going through the
|
||||
`edges` field).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -4454,6 +4436,16 @@ Information to aid in pagination.
|
|||
|
||||
A list of edges.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>nodes</strong></td>
|
||||
<td valign="top">[<a href="#relationship">Relationship</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of nodes in the connection (without going through the
|
||||
`edges` field).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -5356,16 +5348,6 @@ 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
|
||||
[EANs](https://en.wikipedia.org/wiki/International_Article_Number).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>coverArt</strong></td>
|
||||
<td valign="top"><a href="#releasecoverart">ReleaseCoverArt</a>!</td>
|
||||
<td>
|
||||
|
||||
A list and summary of the cover art images that are present
|
||||
for this release from the [Cover Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -5593,6 +5575,16 @@ Information to aid in pagination.
|
|||
|
||||
A list of edges.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>nodes</strong></td>
|
||||
<td valign="top">[<a href="#release">Release</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of nodes in the connection (without going through the
|
||||
`edges` field).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -5608,125 +5600,6 @@ ignoring pagination.
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
### ReleaseCoverArt
|
||||
|
||||
An object containing a list of the cover art images for a
|
||||
release obtained from the [Cover Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive),
|
||||
as well as a summary of what artwork is available.
|
||||
|
||||
<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>front</strong></td>
|
||||
<td valign="top"><a href="#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
The URL of an image depicting the album cover or “main
|
||||
front” of the release, i.e. the front of the packaging of the audio recording
|
||||
(or in the case of a digital release, the image associated with it in a digital
|
||||
media store).
|
||||
|
||||
In the MusicBrainz schema, this field is a Boolean value indicating the presence
|
||||
of a front image, whereas here the value is the URL for the image itself if one
|
||||
exists. You can check for null if you just want to determine the presence of an
|
||||
image.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">size</td>
|
||||
<td valign="top"><a href="#coverartimagesize">CoverArtImageSize</a></td>
|
||||
<td>
|
||||
|
||||
The size of the image to retrieve. By default, the returned
|
||||
image will have its full original dimensions, but certain thumbnail sizes may be
|
||||
retrieved as well.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>back</strong></td>
|
||||
<td valign="top"><a href="#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
The URL of an image depicting the “main back” of the
|
||||
release, i.e. the back of the packaging of the audio recording.
|
||||
|
||||
In the MusicBrainz schema, this field is a Boolean value indicating the presence
|
||||
of a back image, whereas here the value is the URL for the image itself. You can
|
||||
check for null if you just want to determine the presence of an image.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">size</td>
|
||||
<td valign="top"><a href="#coverartimagesize">CoverArtImageSize</a></td>
|
||||
<td>
|
||||
|
||||
The size of the image to retrieve. By default, the returned
|
||||
image will have its full original dimensions, but certain thumbnail sizes may be
|
||||
retrieved as well.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>images</strong></td>
|
||||
<td valign="top">[<a href="#coverartimage">CoverArtImage</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of images depicting the different sides and surfaces
|
||||
of a release’s media and packaging.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>artwork</strong></td>
|
||||
<td valign="top"><a href="#boolean">Boolean</a>!</td>
|
||||
<td>
|
||||
|
||||
Whether there is artwork present for this release.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>darkened</strong></td>
|
||||
<td valign="top"><a href="#boolean">Boolean</a>!</td>
|
||||
<td>
|
||||
|
||||
Whether the Cover Art Archive has received a take-down
|
||||
request for this release’s artwork, disallowing new uploads.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>count</strong></td>
|
||||
<td valign="top"><a href="#int">Int</a>!</td>
|
||||
<td>
|
||||
|
||||
The number of artwork images present for this release.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>release</strong></td>
|
||||
<td valign="top"><a href="#release">Release</a>!</td>
|
||||
<td>
|
||||
|
||||
The particular release shown in the returned cover art.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### ReleaseEdge
|
||||
|
||||
An edge in a connection.
|
||||
|
|
@ -5944,16 +5817,6 @@ that apply to this release group.
|
|||
The MBIDs associated with the values of the `secondaryTypes`
|
||||
field.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>coverArt</strong></td>
|
||||
<td valign="top"><a href="#releasegroupcoverart">ReleaseGroupCoverArt</a></td>
|
||||
<td>
|
||||
|
||||
The cover art for a release group, obtained from the [Cover
|
||||
Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -6101,6 +5964,16 @@ Information to aid in pagination.
|
|||
|
||||
A list of edges.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>nodes</strong></td>
|
||||
<td valign="top">[<a href="#releasegroup">ReleaseGroup</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of nodes in the connection (without going through the
|
||||
`edges` field).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -6116,78 +5989,6 @@ ignoring pagination.
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
### ReleaseGroupCoverArt
|
||||
|
||||
An object containing the cover art for a release group obtained
|
||||
from the [Cover Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive). For
|
||||
release groups, just the front cover of a particular release will be selected.
|
||||
|
||||
<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>front</strong></td>
|
||||
<td valign="top"><a href="#urlstring">URLString</a></td>
|
||||
<td>
|
||||
|
||||
The URL of an image depicting the album cover or “main
|
||||
front” of a release in the release group, i.e. the front of the packaging of the
|
||||
audio recording (or in the case of a digital release, the image associated with
|
||||
it in a digital media store).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="right" valign="top">size</td>
|
||||
<td valign="top"><a href="#coverartimagesize">CoverArtImageSize</a></td>
|
||||
<td>
|
||||
|
||||
The size of the image to retrieve. By default, the returned
|
||||
image will have its full original dimensions, but certain thumbnail sizes may be
|
||||
retrieved as well.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>images</strong></td>
|
||||
<td valign="top">[<a href="#coverartimage">CoverArtImage</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of images returned by the [Cover Art
|
||||
Archive](https://musicbrainz.org/doc/Cover_Art_Archive) for a release group. A
|
||||
particular release’s front image will be included in the list, and likely no
|
||||
others, even if other images are available.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>artwork</strong></td>
|
||||
<td valign="top"><a href="#boolean">Boolean</a>!</td>
|
||||
<td>
|
||||
|
||||
Whether there is artwork present for this release group.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>release</strong></td>
|
||||
<td valign="top"><a href="#release">Release</a>!</td>
|
||||
<td>
|
||||
|
||||
The particular release shown in the returned cover art.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### ReleaseGroupEdge
|
||||
|
||||
An edge in a connection.
|
||||
|
|
@ -6721,6 +6522,16 @@ Information to aid in pagination.
|
|||
|
||||
A list of edges.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>nodes</strong></td>
|
||||
<td valign="top">[<a href="#series">Series</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of nodes in the connection (without going through the
|
||||
`edges` field).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -6848,6 +6659,16 @@ Information to aid in pagination.
|
|||
|
||||
A list of edges.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>nodes</strong></td>
|
||||
<td valign="top">[<a href="#tag">Tag</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of nodes in the connection (without going through the
|
||||
`edges` field).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -6908,6 +6729,81 @@ these results were found through a search.
|
|||
</tbody>
|
||||
</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
|
||||
|
||||
A [URL](https://musicbrainz.org/doc/URL) pointing to a resource
|
||||
|
|
@ -7171,6 +7067,16 @@ Information to aid in pagination.
|
|||
|
||||
A list of edges.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" valign="top"><strong>nodes</strong></td>
|
||||
<td valign="top">[<a href="#work">Work</a>]</td>
|
||||
<td>
|
||||
|
||||
A list of nodes in the connection (without going through the
|
||||
`edges` field).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -7233,44 +7139,6 @@ these results were found through a search.
|
|||
|
||||
## Enums
|
||||
|
||||
### CoverArtImageSize
|
||||
|
||||
The image sizes that may be requested at the [Cover Art
|
||||
Archive](https://musicbrainz.org/doc/Cover_Art_Archive).
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<th align="left">Value</th>
|
||||
<th align="left">Description</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top"><strong>SMALL</strong></td>
|
||||
<td>
|
||||
|
||||
A maximum dimension of 250px.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign="top"><strong>LARGE</strong></td>
|
||||
<td>
|
||||
|
||||
A maximum dimension of 500px.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign="top"><strong>FULL</strong></td>
|
||||
<td>
|
||||
|
||||
The image’s original dimensions, with no maximum.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### ReleaseGroupType
|
||||
|
||||
A type used to describe release groups, e.g. album, single, EP,
|
||||
|
|
@ -7535,6 +7403,10 @@ offsets and hence the same disc ID.
|
|||
|
||||
A length of time, in milliseconds.
|
||||
|
||||
### Float
|
||||
|
||||
The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).
|
||||
|
||||
### ID
|
||||
|
||||
The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.
|
||||
|
|
@ -7572,7 +7444,7 @@ compositions.
|
|||
|
||||
### Int
|
||||
|
||||
The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.
|
||||
The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.
|
||||
|
||||
### Locale
|
||||
|
||||
|
|
@ -7650,3 +7522,5 @@ The id of the object.
|
|||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- END graphql-markdown -->
|
||||
|
|
|
|||
1
extensions/cover-art-archive.js
Normal file
1
extensions/cover-art-archive.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from '../src/extensions/cover-art-archive/index.js';
|
||||
1
extensions/fanart-tv.js
Normal file
1
extensions/fanart-tv.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from '../src/extensions/fanart-tv/index.js';
|
||||
1
extensions/mediawiki.js
Normal file
1
extensions/mediawiki.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from '../src/extensions/mediawiki/index.js';
|
||||
1
extensions/the-audio-db.js
Normal file
1
extensions/the-audio-db.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from '../src/extensions/the-audio-db/index.js';
|
||||
186
package.json
186
package.json
|
|
@ -1,47 +1,7 @@
|
|||
{
|
||||
"name": "graphbrainz",
|
||||
"version": "5.1.3",
|
||||
"description": "An Express server and middleware for querying the MusicBrainz API using GraphQL.",
|
||||
"main": "lib/index.js",
|
||||
"bin": "lib/index.js",
|
||||
"files": [
|
||||
"lib",
|
||||
"scripts",
|
||||
"Procfile",
|
||||
"schema.json",
|
||||
"yarn.lock"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.6.0",
|
||||
"npm": ">=3.8.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",
|
||||
"build:docs:readme": "doctoc --title \"## Contents\" 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 --require babel-register --prologue 'You may also be interested in reading the [schema in GraphQL syntax](schema.md).' ./src/schema.js > docs/types.md",
|
||||
"build:lib": "babel --out-dir lib src",
|
||||
"clean": "npm run clean:lib",
|
||||
"clean:lib": "rm -rf lib",
|
||||
"deploy": "./scripts/deploy.sh",
|
||||
"lint": "standard --verbose | snazzy",
|
||||
"lint:fix": "standard --verbose --fix",
|
||||
"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 --serial",
|
||||
"test:watch": "npm run test:only -- --watch",
|
||||
"update-schema": "npm run -s print-schema:json > schema.json"
|
||||
},
|
||||
"version": "9.0.0",
|
||||
"description": "A GraphQL schema, Express server, and middleware for querying the MusicBrainz.",
|
||||
"keywords": [
|
||||
"musicbrainz",
|
||||
"graphql",
|
||||
|
|
@ -54,71 +14,107 @@
|
|||
"author": {
|
||||
"name": "Brian Beck",
|
||||
"email": "exogen@gmail.com",
|
||||
"url": "http://brianbeck.com/"
|
||||
"url": "https://brianbeck.com/"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/exogen/graphbrainz.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.23.0",
|
||||
"compression": "^1.6.2",
|
||||
"dashify": "^0.2.2",
|
||||
"dataloader": "^1.3.0",
|
||||
"debug": "^2.6.6",
|
||||
"dotenv": "^4.0.0",
|
||||
"es6-error": "^4.0.2",
|
||||
"express": "^4.15.2",
|
||||
"express-graphql": "^0.6.4",
|
||||
"graphql": "^0.9.4",
|
||||
"graphql-relay": "^0.5.1",
|
||||
"lru-cache": "^4.0.1",
|
||||
"pascalcase": "^0.1.1",
|
||||
"qs": "^6.4.0",
|
||||
"request": "^2.81.0",
|
||||
"retry": "^0.10.1"
|
||||
"engines": {
|
||||
"node": ">=12.18.0",
|
||||
"npm": ">=6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ava": "^0.19.1",
|
||||
"babel-cli": "^6.24.1",
|
||||
"babel-eslint": "^7.2.3",
|
||||
"babel-plugin-istanbul": "^4.1.1",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-stage-2": "^6.24.1",
|
||||
"babel-register": "^6.24.1",
|
||||
"coveralls": "^2.13.1",
|
||||
"cross-env": "^4.0.0",
|
||||
"doctoc": "^1.3.0",
|
||||
"graphql-markdown": "^2.1.0",
|
||||
"nodemon": "^1.11.0",
|
||||
"nyc": "^10.2.0",
|
||||
"rimraf": "^2.6.1",
|
||||
"sepia": "^2.0.2",
|
||||
"sinon": "^2.1.0",
|
||||
"snazzy": "^7.0.0",
|
||||
"standard": "^10.0.2"
|
||||
"type": "module",
|
||||
"main": "./src/index.js",
|
||||
"exports": {
|
||||
".": "./src/index.js",
|
||||
"./extensions/cover-art-archive": "./extensions/cover-art-archive.js",
|
||||
"./extensions/fanart-tv": "./extensions/fanart-tv.js",
|
||||
"./extensions/mediawiki": "./extensions/mediawiki.js",
|
||||
"./extensions/the-audio-db": "./extensions/the-audio-db.js",
|
||||
"./package.json": "./package.json",
|
||||
"./schema.json": "./schema.json"
|
||||
},
|
||||
"standard": {
|
||||
"parser": "babel-eslint"
|
||||
"bin": "cli.js",
|
||||
"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": {
|
||||
"require": [
|
||||
"babel-register"
|
||||
"dotenv/config"
|
||||
]
|
||||
},
|
||||
"nyc": {
|
||||
"include": [
|
||||
"src/**"
|
||||
],
|
||||
"reporter": [
|
||||
"lcov",
|
||||
"text"
|
||||
],
|
||||
"all": true,
|
||||
"cache": true,
|
||||
"sourceMap": false,
|
||||
"instrument": false
|
||||
"ava-nock": {
|
||||
"fixtureDir": "fixtures",
|
||||
"pathFilter": [
|
||||
"(([?&]api_key=)(\\w+))|((/json/)(\\w+)(/[\\w-]+-mb\\.php))",
|
||||
"$2$5*$7"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@graphql-tools/schema": "^7.1.3",
|
||||
"compression": "^1.7.3",
|
||||
"cors": "^2.8.4",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21938
schema.json
21938
schema.json
File diff suppressed because it is too large
Load diff
62
scripts/build-extension-docs.js
Normal file
62
scripts/build-extension-docs.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import GraphQL from 'graphql';
|
||||
import GraphQLMarkdown from 'graphql-markdown';
|
||||
import { baseSchema, createSchema } from '../src/schema.js';
|
||||
|
||||
const { graphql, getIntrospectionQuery } = GraphQL;
|
||||
const { updateSchema, diffSchema } = GraphQLMarkdown;
|
||||
|
||||
async function getSchemaJSON(schema) {
|
||||
const result = await graphql(schema, getIntrospectionQuery());
|
||||
return result.data;
|
||||
}
|
||||
|
||||
async function buildExtensionDocs(extensionModules) {
|
||||
return Promise.all(
|
||||
extensionModules.map(async (extensionName) => {
|
||||
const extensionModule = await import(
|
||||
`../src/extensions/${extensionName}/index.js`
|
||||
);
|
||||
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, {
|
||||
processTypeDiff(type) {
|
||||
if (type.description === undefined) {
|
||||
type.description =
|
||||
':small_blue_diamond: *This type has been extended. See the ' +
|
||||
'[base schema](../types.md)\nfor a description and additional ' +
|
||||
'fields.*';
|
||||
}
|
||||
return type;
|
||||
},
|
||||
});
|
||||
const outputPath = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
`../docs/extensions/${extensionName}.md`
|
||||
);
|
||||
return updateSchema(outputPath, outputSchema, {
|
||||
unknownTypeURL: '../types.md',
|
||||
headingLevel: 2,
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
buildExtensionDocs([
|
||||
'cover-art-archive',
|
||||
'fanart-tv',
|
||||
'mediawiki',
|
||||
'the-audio-db',
|
||||
])
|
||||
.then((extensions) => {
|
||||
console.log(`Built docs for ${extensions.length} extension(s).`);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('Error:', err);
|
||||
});
|
||||
|
|
@ -7,7 +7,7 @@ RESET='\033[0m'
|
|||
# Fail if the `heroku` remote isn't there.
|
||||
git remote show heroku
|
||||
|
||||
git stash # Stash uncommitted changes.
|
||||
STASH_OUTPUT=$(git stash) # Stash uncommitted changes.
|
||||
git checkout -B deploy # Force branch creation/reset.
|
||||
npm run build
|
||||
git add -f lib # Force add ignored files.
|
||||
|
|
@ -17,6 +17,9 @@ git push -f heroku deploy:master
|
|||
git rm -r --cached lib # Otherwise switching branches will remove them.
|
||||
git checkout - # Switch back to whatever branch we came from.
|
||||
git branch -D deploy # Just to prevent someone accidentally pushing to GitHub.
|
||||
git stash pop --index || true # Restore uncommitted changes, OK if none.
|
||||
if [[ $STASH_OUTPUT != "No local changes"* ]]; then
|
||||
git stash pop --index # Restore uncommitted changes.
|
||||
fi
|
||||
|
||||
echo -e "\n${GREEN}✔︎ Successfully deployed.${RESET}"
|
||||
heroku open || true
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
import { graphql, introspectionQuery, printSchema } from 'graphql'
|
||||
import schema from '../src/schema'
|
||||
import GraphQL from 'graphql';
|
||||
import { baseSchema as schema } from '../src/schema.js';
|
||||
|
||||
const { graphql, getIntrospectionQuery, printSchema } = GraphQL;
|
||||
|
||||
if (process.argv[2] === '--json') {
|
||||
graphql(schema, introspectionQuery).then(result => {
|
||||
console.log(JSON.stringify(result.data, null, 2))
|
||||
}).catch(err => {
|
||||
console.error(err)
|
||||
})
|
||||
graphql(schema, getIntrospectionQuery())
|
||||
.then((result) => {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
} else {
|
||||
console.log(printSchema(schema))
|
||||
console.log(printSchema(schema));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,147 +1,88 @@
|
|||
import request from 'request'
|
||||
import retry from 'retry'
|
||||
import ExtendableError from 'es6-error'
|
||||
import RateLimit from '../rate-limit'
|
||||
import pkg from '../../package.json'
|
||||
import { fileURLToPath } from 'url';
|
||||
import createDebug from 'debug';
|
||||
import got from 'got';
|
||||
import { readPackageUpSync } from 'read-pkg-up';
|
||||
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
|
||||
// level than the HTTP response itself. If it's any of the following error
|
||||
// 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
|
||||
}
|
||||
}
|
||||
const { packageJson: pkg } = readPackageUpSync({
|
||||
cwd: fileURLToPath(import.meta.url),
|
||||
});
|
||||
|
||||
export default class Client {
|
||||
constructor ({
|
||||
baseURL = process.env.MUSICBRAINZ_BASE_URL || 'http://musicbrainz.org/ws/2/',
|
||||
constructor({
|
||||
baseURL,
|
||||
userAgent = `${pkg.name}/${pkg.version} ` +
|
||||
`( ${pkg.homepage || pkg.author.url || pkg.author.email} )`,
|
||||
extraHeaders = {},
|
||||
errorClass = ClientError,
|
||||
timeout = 60000,
|
||||
// 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
|
||||
// fulfill a query, we might as well make them all at once - as long as
|
||||
// we then wait a few seconds before making more. In practice this can
|
||||
// seemingly be set to about 5 requests every 5 seconds before we're
|
||||
// considered to exceed the rate limit.
|
||||
limit = 1,
|
||||
period = 1000,
|
||||
concurrency = 10,
|
||||
retries = 10,
|
||||
// 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
|
||||
retry,
|
||||
} = {}) {
|
||||
this.baseURL = baseURL
|
||||
this.userAgent = userAgent
|
||||
this.extraHeaders = extraHeaders
|
||||
this.errorClass = errorClass
|
||||
this.timeout = timeout
|
||||
this.limiter = new RateLimit({ limit, period, concurrency })
|
||||
this.retryOptions = {
|
||||
retries,
|
||||
minTimeout: retryDelayMin,
|
||||
maxTimeout: retryDelayMax,
|
||||
randomize: randomizeRetry
|
||||
}
|
||||
this.baseURL = baseURL;
|
||||
this.userAgent = userAgent;
|
||||
this.extraHeaders = extraHeaders;
|
||||
this.timeout = timeout;
|
||||
this.limiter = new RateLimit({ limit, period, concurrency });
|
||||
this.retryOptions = retry;
|
||||
}
|
||||
|
||||
parseErrorMessage(err) {
|
||||
return err;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we should retry the request based on the given error.
|
||||
* 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.
|
||||
* Send a request without any rate limiting.
|
||||
* Use `get` instead.
|
||||
*/
|
||||
_get (path, options = {}, info = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
options = {
|
||||
baseUrl: this.baseURL,
|
||||
url: path,
|
||||
gzip: true,
|
||||
timeout: this.timeout,
|
||||
...options,
|
||||
headers: {
|
||||
'User-Agent': this.userAgent,
|
||||
...this.extraHeaders,
|
||||
...options.headers
|
||||
}
|
||||
async _get(path, { searchParams, ...options } = {}) {
|
||||
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 = {
|
||||
responseType: 'json',
|
||||
timeout: this.timeout,
|
||||
retry: this.retryOptions,
|
||||
...options,
|
||||
headers: {
|
||||
'User-Agent': this.userAgent,
|
||||
...this.extraHeaders,
|
||||
...options.headers,
|
||||
},
|
||||
};
|
||||
|
||||
debug(`Sending request. url=${this.baseURL}${path} attempt=${info.currentAttempt}`)
|
||||
|
||||
request(options, (err, response, body) => {
|
||||
if (err) {
|
||||
debug(`Error: “${err}” url=${this.baseURL}${path}`)
|
||||
reject(err)
|
||||
} else if (response.statusCode >= 400) {
|
||||
const message = this.parseErrorMessage(response, body)
|
||||
debug(`Error: “${message}” url=${this.baseURL}${path}`)
|
||||
const ClientError = this.errorClass
|
||||
reject(new ClientError(message, response.statusCode))
|
||||
} else if (options.method === 'HEAD') {
|
||||
resolve(response.headers)
|
||||
} else {
|
||||
resolve(body)
|
||||
}
|
||||
})
|
||||
})
|
||||
let response;
|
||||
try {
|
||||
debug(`Sending request. url=%s`, url);
|
||||
response = await got(url.toString(), options);
|
||||
debug(`Success: %s url=%s`, response.statusCode, url);
|
||||
return response;
|
||||
} catch (err) {
|
||||
const parsedError = this.parseErrorMessage(err) || err;
|
||||
debug(`Error: “%s” url=%s`, parsedError, url);
|
||||
throw parsedError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request with retrying and rate limiting.
|
||||
* Send a request with rate limiting.
|
||||
*/
|
||||
get (path, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fn = this._get.bind(this)
|
||||
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)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
get(path, options = {}) {
|
||||
const fn = this._get.bind(this);
|
||||
return this.limiter.enqueue(fn, [path, options]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
import Client, { ClientError } from './client'
|
||||
|
||||
export class CoverArtArchiveError extends ClientError {}
|
||||
|
||||
export default class CoverArtArchive extends Client {
|
||||
constructor ({
|
||||
baseURL = process.env.COVER_ART_ARCHIVE_BASE_URL || 'http://coverartarchive.org/',
|
||||
errorClass = CoverArtArchiveError,
|
||||
limit = 10,
|
||||
period = 1000,
|
||||
...options
|
||||
} = {}) {
|
||||
super({ baseURL, errorClass, limit, period, ...options })
|
||||
}
|
||||
|
||||
parseErrorMessage (response, body) {
|
||||
if (typeof body === 'string' && body.startsWith('<!')) {
|
||||
const heading = /<h1>([^<]+)<\/h1>/i.exec(body)
|
||||
const message = /<p>([^<]+)<\/p>/i.exec(body)
|
||||
return `${heading ? heading[1] + ': ' : ''}${message ? message[1] : ''}`
|
||||
}
|
||||
return super.parseErrorMessage(response, body)
|
||||
}
|
||||
|
||||
getImagesURL (entity, mbid) {
|
||||
return `${entity}/${mbid}`
|
||||
}
|
||||
|
||||
images (entity, mbid) {
|
||||
const url = this.getImagesURL(entity, mbid)
|
||||
return this.get(url, { json: true })
|
||||
}
|
||||
|
||||
getImageURL (entity, mbid, typeOrID = 'front', size) {
|
||||
let url = `${entity}/${mbid}/${typeOrID}`
|
||||
if (size != null) {
|
||||
url += `-${size}`
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
imageURL (entity, mbid, typeOrID = 'front', size) {
|
||||
const url = this.getImageURL(entity, mbid, typeOrID, size)
|
||||
return this.get(url, { method: 'HEAD', followRedirect: false })
|
||||
.then(headers => headers.location)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,3 @@
|
|||
import MusicBrainz, { MusicBrainzError } from './musicbrainz'
|
||||
import CoverArtArchive, { CoverArtArchiveError } from './cover-art-archive'
|
||||
import MusicBrainz from './musicbrainz.js';
|
||||
|
||||
export {
|
||||
MusicBrainz as default,
|
||||
MusicBrainz,
|
||||
MusicBrainzError,
|
||||
CoverArtArchive,
|
||||
CoverArtArchiveError
|
||||
}
|
||||
export { MusicBrainz as default, MusicBrainz };
|
||||
|
|
|
|||
|
|
@ -1,83 +1,109 @@
|
|||
import qs from 'qs'
|
||||
import Client, { ClientError } from './client'
|
||||
import ExtendableError from 'es6-error';
|
||||
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 {
|
||||
constructor ({
|
||||
baseURL = process.env.MUSICBRAINZ_BASE_URL || 'http://musicbrainz.org/ws/2/',
|
||||
errorClass = MusicBrainzError,
|
||||
constructor({
|
||||
baseURL = process.env.MUSICBRAINZ_BASE_URL ||
|
||||
'http://musicbrainz.org/ws/2/',
|
||||
// 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
|
||||
// fulfill a query, we might as well make them all at once - as long as
|
||||
// we then wait a few seconds before making more. In practice this can
|
||||
// seemingly be set to about 5 requests every 5 seconds before we're
|
||||
// considered to exceed the rate limit.
|
||||
limit = 5,
|
||||
period = 5500,
|
||||
...options
|
||||
} = {}) {
|
||||
super({ baseURL, errorClass, limit, period, ...options })
|
||||
super({ baseURL, limit, period, ...options });
|
||||
}
|
||||
|
||||
parseErrorMessage (response, body) {
|
||||
if (body && body.error) {
|
||||
return body.error
|
||||
parseErrorMessage(err) {
|
||||
if (err.name === 'HTTPError') {
|
||||
const { body } = err.response;
|
||||
if (body && body.error) {
|
||||
return new MusicBrainzError(`${body.error}`, err.response);
|
||||
}
|
||||
}
|
||||
return super.parseErrorMessage(response, body)
|
||||
return super.parseErrorMessage(err);
|
||||
}
|
||||
|
||||
stringifyParams (params) {
|
||||
get(url, options = {}) {
|
||||
options = {
|
||||
resolveBodyOnly: true,
|
||||
...options,
|
||||
searchParams: {
|
||||
fmt: 'json',
|
||||
...options.searchParams,
|
||||
},
|
||||
};
|
||||
return super.get(url, options);
|
||||
}
|
||||
|
||||
stringifyParams(params) {
|
||||
if (Array.isArray(params.inc)) {
|
||||
params = {
|
||||
...params,
|
||||
inc: params.inc.join('+')
|
||||
}
|
||||
inc: params.inc.join('+'),
|
||||
};
|
||||
}
|
||||
if (Array.isArray(params.type)) {
|
||||
params = {
|
||||
...params,
|
||||
type: params.type.join('|')
|
||||
}
|
||||
type: params.type.join('|'),
|
||||
};
|
||||
}
|
||||
if (Array.isArray(params.status)) {
|
||||
params = {
|
||||
...params,
|
||||
status: params.status.join('|')
|
||||
}
|
||||
status: params.status.join('|'),
|
||||
};
|
||||
}
|
||||
return qs.stringify(params, {
|
||||
skipNulls: true,
|
||||
filter: (key, value) => value === '' ? undefined : value
|
||||
})
|
||||
return new URLSearchParams(
|
||||
filterObjectValues(params, (value) => value != null && value !== '')
|
||||
).toString();
|
||||
}
|
||||
|
||||
getURL (path, params) {
|
||||
const query = params ? this.stringifyParams(params) : ''
|
||||
return query ? `${path}?${query}` : path
|
||||
getURL(path, params) {
|
||||
const query = params ? this.stringifyParams(params) : '';
|
||||
return query ? `${path}?${query}` : path;
|
||||
}
|
||||
|
||||
getLookupURL (entity, id, params) {
|
||||
getLookupURL(entity, id, params) {
|
||||
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 = {}) {
|
||||
const url = this.getLookupURL(entity, id, params)
|
||||
return this.get(url, { json: true, qs: { fmt: 'json' } })
|
||||
lookup(entity, id, params = {}) {
|
||||
const url = this.getLookupURL(entity, id, params);
|
||||
return this.get(url);
|
||||
}
|
||||
|
||||
getBrowseURL (entity, params) {
|
||||
return this.getURL(entity, params)
|
||||
getBrowseURL(entity, params) {
|
||||
return this.getURL(entity, params);
|
||||
}
|
||||
|
||||
browse (entity, params = {}) {
|
||||
const url = this.getBrowseURL(entity, params)
|
||||
return this.get(url, { json: true, qs: { fmt: 'json' } })
|
||||
browse(entity, params = {}) {
|
||||
const url = this.getBrowseURL(entity, params);
|
||||
return this.get(url);
|
||||
}
|
||||
|
||||
getSearchURL (entity, query, params) {
|
||||
return this.getURL(entity, { ...params, query })
|
||||
getSearchURL(entity, query, params) {
|
||||
return this.getURL(entity, { ...params, query });
|
||||
}
|
||||
|
||||
search (entity, query, params = {}) {
|
||||
const url = this.getSearchURL(entity, query, params)
|
||||
return this.get(url, { json: true, qs: { fmt: 'json' } })
|
||||
search(entity, query, params = {}) {
|
||||
const url = this.getSearchURL(entity, query, params);
|
||||
return this.get(url);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
src/context.js
Normal file
30
src/context.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import createLoaders from './loaders.js';
|
||||
import createDebug from 'debug';
|
||||
|
||||
const debug = createDebug('graphbrainz:context');
|
||||
|
||||
export function extendContext(extension, context, options) {
|
||||
if (extension.extendContext) {
|
||||
if (typeof extension.extendContext === 'function') {
|
||||
debug(
|
||||
`Extending context via a function from the “${extension.name}” extension.`
|
||||
);
|
||||
context = extension.extendContext(context, options);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Extension “${extension.name}” contains an invalid \`extendContext\` ` +
|
||||
`value: ${extension.extendContext}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function createContext(options = {}) {
|
||||
const { client, extensions = [] } = options;
|
||||
const loaders = createLoaders(client);
|
||||
let context = { client, loaders };
|
||||
return extensions.reduce((context, extension) => {
|
||||
return extendContext(extension, context, options);
|
||||
}, context);
|
||||
}
|
||||
55
src/extensions/cover-art-archive/client.js
Normal file
55
src/extensions/cover-art-archive/client.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
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 {
|
||||
constructor({
|
||||
baseURL = process.env.COVER_ART_ARCHIVE_BASE_URL ||
|
||||
'http://coverartarchive.org/',
|
||||
limit = 10,
|
||||
period = 1000,
|
||||
...options
|
||||
} = {}) {
|
||||
super({ baseURL, limit, period, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sinfully attempt to parse HTML responses for the error message.
|
||||
*/
|
||||
parseErrorMessage(err) {
|
||||
if (err.name === 'HTTPError') {
|
||||
const { body } = err.response;
|
||||
if (typeof body === 'string' && body.startsWith('<!')) {
|
||||
const heading = /<h1>([^<]+)<\/h1>/i.exec(body);
|
||||
const message = /<p>([^<]+)<\/p>/i.exec(body);
|
||||
return new CoverArtArchiveError(
|
||||
`${heading ? heading[1] + ': ' : ''}${message ? message[1] : ''}`,
|
||||
err.response
|
||||
);
|
||||
}
|
||||
}
|
||||
return super.parseErrorMessage(err);
|
||||
}
|
||||
|
||||
images(entityType, mbid) {
|
||||
return this.get(`${entityType}/${mbid}`, { resolveBodyOnly: true });
|
||||
}
|
||||
|
||||
async imageURL(entityType, mbid, typeOrID = 'front', size) {
|
||||
let url = `${entityType}/${mbid}/${typeOrID}`;
|
||||
if (size != null) {
|
||||
url += `-${size}`;
|
||||
}
|
||||
const response = await this.get(url, {
|
||||
method: 'HEAD',
|
||||
followRedirect: false,
|
||||
});
|
||||
return response.headers.location;
|
||||
}
|
||||
}
|
||||
40
src/extensions/cover-art-archive/index.js
Normal file
40
src/extensions/cover-art-archive/index.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import schema from './schema.js';
|
||||
import resolvers from './resolvers.js';
|
||||
import createLoaders from './loaders.js';
|
||||
import CoverArtArchiveClient from './client.js';
|
||||
import { ONE_DAY } from '../../util.js';
|
||||
|
||||
export default {
|
||||
name: 'Cover Art Archive',
|
||||
description: `Retrieve cover art images for releases from the [Cover Art
|
||||
Archive](https://coverartarchive.org/).`,
|
||||
extendContext(context, { coverArtClient, coverArtArchive = {} } = {}) {
|
||||
const client = coverArtClient || new CoverArtArchiveClient(coverArtArchive);
|
||||
const cacheSize = parseInt(
|
||||
process.env.COVER_ART_ARCHIVE_CACHE_SIZE ||
|
||||
process.env.GRAPHBRAINZ_CACHE_SIZE ||
|
||||
8192,
|
||||
10
|
||||
);
|
||||
const cacheTTL = parseInt(
|
||||
process.env.COVER_ART_ARCHIVE_CACHE_TTL ||
|
||||
process.env.GRAPHBRAINZ_CACHE_TTL ||
|
||||
ONE_DAY,
|
||||
10
|
||||
);
|
||||
return {
|
||||
...context,
|
||||
// Add the client instance directly onto `context` for backwards
|
||||
// compatibility.
|
||||
coverArtClient: client,
|
||||
loaders: {
|
||||
...context.loaders,
|
||||
...createLoaders({ client, cacheSize, cacheTTL }),
|
||||
},
|
||||
};
|
||||
},
|
||||
extendSchema: {
|
||||
schemas: [schema],
|
||||
resolvers,
|
||||
},
|
||||
};
|
||||
68
src/extensions/cover-art-archive/loaders.js
Normal file
68
src/extensions/cover-art-archive/loaders.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import createDebug from 'debug';
|
||||
import DataLoader from 'dataloader';
|
||||
import LRUCache from 'lru-cache';
|
||||
|
||||
const debug = createDebug('graphbrainz:extensions/cover-art-archive');
|
||||
|
||||
export default function createLoaders(options) {
|
||||
const { client } = options;
|
||||
const cache = new LRUCache({
|
||||
max: options.cacheSize,
|
||||
maxAge: options.cacheTTL,
|
||||
dispose(key) {
|
||||
debug(`Removed from cache. key=${key}`);
|
||||
},
|
||||
});
|
||||
// Make the cache Map-like.
|
||||
cache.delete = cache.del;
|
||||
cache.clear = cache.reset;
|
||||
|
||||
return {
|
||||
coverArtArchive: new DataLoader(
|
||||
(keys) => {
|
||||
return Promise.all(
|
||||
keys.map((key) => {
|
||||
const [entityType, id] = key;
|
||||
return client
|
||||
.images(entityType, id)
|
||||
.catch((err) => {
|
||||
if (err.response.statusCode === 404) {
|
||||
return { images: [] };
|
||||
}
|
||||
throw err;
|
||||
})
|
||||
.then((coverArt) => ({
|
||||
...coverArt,
|
||||
_entityType: entityType,
|
||||
_id: id,
|
||||
_releaseID:
|
||||
coverArt.release && coverArt.release.split('/').pop(),
|
||||
}));
|
||||
})
|
||||
);
|
||||
},
|
||||
{
|
||||
cacheKeyFn: ([entityType, id]) => `${entityType}/${id}`,
|
||||
cacheMap: cache,
|
||||
}
|
||||
),
|
||||
coverArtArchiveURL: new DataLoader(
|
||||
(keys) => {
|
||||
return Promise.all(
|
||||
keys.map((key) => {
|
||||
const [entityType, id, type, size] = key;
|
||||
return client.imageURL(entityType, id, type, size);
|
||||
})
|
||||
);
|
||||
},
|
||||
{
|
||||
batch: false,
|
||||
cacheKeyFn: ([entityType, id, type, size]) => {
|
||||
const key = `${entityType}/${id}/${type}`;
|
||||
return size ? `${key}-${size}` : key;
|
||||
},
|
||||
cacheMap: cache,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
75
src/extensions/cover-art-archive/resolvers.js
Normal file
75
src/extensions/cover-art-archive/resolvers.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { resolveLookup } from '../../resolvers.js';
|
||||
|
||||
const SIZES = new Map([
|
||||
[null, null],
|
||||
[250, 250],
|
||||
[500, 500],
|
||||
['FULL', null],
|
||||
['SMALL', 250],
|
||||
['LARGE', 500],
|
||||
]);
|
||||
|
||||
function resolveImage(coverArt, args, { loaders }, info) {
|
||||
// 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
|
||||
// values.
|
||||
const size = SIZES.get(args.size);
|
||||
// Field should be `front` or `back`.
|
||||
const field = info.fieldName;
|
||||
if (coverArt.images) {
|
||||
const matches = coverArt.images.filter((image) => image[field]);
|
||||
if (!matches.length) {
|
||||
return null;
|
||||
} else if (matches.length === 1) {
|
||||
const match = matches[0];
|
||||
if (size === 250) {
|
||||
return match.thumbnails.small;
|
||||
} else if (size === 500) {
|
||||
return match.thumbnails.large;
|
||||
} else {
|
||||
return match.image;
|
||||
}
|
||||
}
|
||||
}
|
||||
const entityType = coverArt._entityType;
|
||||
const id = coverArt._id;
|
||||
const releaseID = coverArt._releaseID;
|
||||
if (entityType === 'release-group' && field === 'front') {
|
||||
// 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
|
||||
// release that the release group's cover art response points to.
|
||||
return loaders.coverArtArchiveURL.load(['release-group', id, field, size]);
|
||||
} else {
|
||||
return loaders.coverArtArchiveURL.load(['release', releaseID, field, size]);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
CoverArtArchiveImage: {
|
||||
fileID: (image) => image.id,
|
||||
},
|
||||
CoverArtArchiveRelease: {
|
||||
front: resolveImage,
|
||||
back: resolveImage,
|
||||
images: (coverArt) => coverArt.images,
|
||||
artwork: (coverArt) => coverArt.images.length > 0,
|
||||
count: (coverArt) => coverArt.images.length,
|
||||
release: (coverArt, args, context, info) => {
|
||||
const mbid = coverArt._releaseID;
|
||||
if (mbid) {
|
||||
return resolveLookup(coverArt, { mbid }, context, info);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
Release: {
|
||||
coverArtArchive: (release, args, { loaders }) => {
|
||||
return loaders.coverArtArchive.load(['release', release.id]);
|
||||
},
|
||||
},
|
||||
ReleaseGroup: {
|
||||
coverArtArchive: (releaseGroup, args, { loaders }) => {
|
||||
return loaders.coverArtArchive.load(['release-group', releaseGroup.id]);
|
||||
},
|
||||
},
|
||||
};
|
||||
176
src/extensions/cover-art-archive/schema.js
Normal file
176
src/extensions/cover-art-archive/schema.js
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import gql from '../../tag.js';
|
||||
|
||||
export default gql`
|
||||
"""
|
||||
An individual piece of album artwork from the [Cover Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive).
|
||||
"""
|
||||
type CoverArtArchiveImage {
|
||||
"""
|
||||
The Internet Archive’s internal file ID for the image.
|
||||
"""
|
||||
fileID: String!
|
||||
|
||||
"""
|
||||
The URL at which the image can be found.
|
||||
"""
|
||||
image: URLString!
|
||||
|
||||
"""
|
||||
A set of thumbnails for the image.
|
||||
"""
|
||||
thumbnails: CoverArtArchiveImageThumbnails!
|
||||
|
||||
"""
|
||||
Whether this image depicts the “main front” of the release.
|
||||
"""
|
||||
front: Boolean!
|
||||
|
||||
"""
|
||||
Whether this image depicts the “main back” of the release.
|
||||
"""
|
||||
back: Boolean!
|
||||
|
||||
"""
|
||||
A list of [image types](https://musicbrainz.org/doc/Cover_Art/Types)
|
||||
describing what part(s) of the release the image includes.
|
||||
"""
|
||||
types: [String]!
|
||||
|
||||
"""
|
||||
The MusicBrainz edit ID.
|
||||
"""
|
||||
edit: Int
|
||||
|
||||
"""
|
||||
Whether the image was approved by the MusicBrainz edit system.
|
||||
"""
|
||||
approved: Boolean
|
||||
|
||||
"""
|
||||
A free-text comment left for the image.
|
||||
"""
|
||||
comment: String
|
||||
}
|
||||
|
||||
"""
|
||||
The image sizes that may be requested at the [Cover Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive).
|
||||
"""
|
||||
enum CoverArtArchiveImageSize {
|
||||
"""
|
||||
A maximum dimension of 250px.
|
||||
"""
|
||||
SMALL
|
||||
|
||||
"""
|
||||
A maximum dimension of 500px.
|
||||
"""
|
||||
LARGE
|
||||
|
||||
"""
|
||||
The image’s original dimensions, with no maximum.
|
||||
"""
|
||||
FULL
|
||||
}
|
||||
|
||||
"""
|
||||
URLs for thumbnails of different sizes for a particular piece of cover art.
|
||||
"""
|
||||
type CoverArtArchiveImageThumbnails {
|
||||
"""
|
||||
The URL of a small version of the cover art, where the maximum dimension is
|
||||
250px.
|
||||
"""
|
||||
small: URLString
|
||||
|
||||
"""
|
||||
The URL of a large version of the cover art, where the maximum dimension is
|
||||
500px.
|
||||
"""
|
||||
large: URLString
|
||||
}
|
||||
|
||||
"""
|
||||
An object containing a list of the cover art images for a release obtained
|
||||
from the [Cover Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive),
|
||||
as well as a summary of what artwork is available.
|
||||
"""
|
||||
type CoverArtArchiveRelease {
|
||||
"""
|
||||
The URL of an image depicting the album cover or “main front” of the release,
|
||||
i.e. the front of the packaging of the audio recording (or in the case of a
|
||||
digital release, the image associated with it in a digital media store).
|
||||
|
||||
In the MusicBrainz schema, this field is a Boolean value indicating the
|
||||
presence of a front image, whereas here the value is the URL for the image
|
||||
itself if one exists. You can check for null if you just want to determine
|
||||
the presence of an image.
|
||||
"""
|
||||
front(
|
||||
"""
|
||||
The size of the image to retrieve. By default, the returned image will
|
||||
have its full original dimensions, but certain thumbnail sizes may be
|
||||
retrieved as well.
|
||||
"""
|
||||
size: CoverArtArchiveImageSize = FULL
|
||||
): URLString
|
||||
|
||||
"""
|
||||
The URL of an image depicting the “main back” of the release, i.e. the back
|
||||
of the packaging of the audio recording.
|
||||
|
||||
In the MusicBrainz schema, this field is a Boolean value indicating the
|
||||
presence of a back image, whereas here the value is the URL for the image
|
||||
itself. You can check for null if you just want to determine the presence of
|
||||
an image.
|
||||
"""
|
||||
back(
|
||||
"""
|
||||
The size of the image to retrieve. By default, the returned image will
|
||||
have its full original dimensions, but certain thumbnail sizes may be
|
||||
retrieved as well.
|
||||
"""
|
||||
size: CoverArtArchiveImageSize = FULL
|
||||
): URLString
|
||||
|
||||
"""
|
||||
A list of images depicting the different sides and surfaces of a release’s
|
||||
media and packaging.
|
||||
"""
|
||||
images: [CoverArtArchiveImage]!
|
||||
|
||||
"""
|
||||
Whether there is artwork present for this release.
|
||||
"""
|
||||
artwork: Boolean!
|
||||
|
||||
"""
|
||||
The number of artwork images present for this release.
|
||||
"""
|
||||
count: Int!
|
||||
|
||||
"""
|
||||
The particular release shown in the returned cover art.
|
||||
"""
|
||||
release: Release
|
||||
}
|
||||
|
||||
extend type Release {
|
||||
"""
|
||||
An object containing a list and summary of the cover art images that are
|
||||
present for this release from the [Cover Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive).
|
||||
This field is provided by the Cover Art Archive extension.
|
||||
"""
|
||||
coverArtArchive: CoverArtArchiveRelease
|
||||
}
|
||||
|
||||
extend type ReleaseGroup {
|
||||
"""
|
||||
The cover art for a release in the release group, obtained from the
|
||||
[Cover Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive). A
|
||||
release in the release group will be chosen as representative of the release
|
||||
group.
|
||||
This field is provided by the Cover Art Archive extension.
|
||||
"""
|
||||
coverArtArchive: CoverArtArchiveRelease
|
||||
}
|
||||
`;
|
||||
61
src/extensions/fanart-tv/client.js
Normal file
61
src/extensions/fanart-tv/client.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import ExtendableError from 'es6-error';
|
||||
import Client from '../../api/client.js';
|
||||
|
||||
export class FanArtError extends ExtendableError {}
|
||||
|
||||
export default class FanArtClient extends Client {
|
||||
constructor({
|
||||
apiKey = process.env.FANART_API_KEY,
|
||||
baseURL = process.env.FANART_BASE_URL || 'http://webservice.fanart.tv/v3/',
|
||||
limit = 10,
|
||||
period = 1000,
|
||||
...options
|
||||
} = {}) {
|
||||
super({ baseURL, limit, period, ...options });
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
get(path, options = {}) {
|
||||
if (!this.apiKey) {
|
||||
return Promise.reject(
|
||||
new FanArtError('No API key was configured for the fanart.tv client.')
|
||||
);
|
||||
}
|
||||
options = {
|
||||
resolveBodyOnly: true,
|
||||
...options,
|
||||
searchParams: {
|
||||
...options.searchParams,
|
||||
api_key: this.apiKey,
|
||||
},
|
||||
};
|
||||
return super.get(path, options);
|
||||
}
|
||||
|
||||
musicEntity(entityType, mbid) {
|
||||
switch (entityType) {
|
||||
case 'artist':
|
||||
return this.musicArtist(mbid);
|
||||
case 'label':
|
||||
return this.musicLabel(mbid);
|
||||
case 'release-group':
|
||||
return this.musicAlbum(mbid);
|
||||
default:
|
||||
return Promise.reject(
|
||||
new FanArtError(`Entity type unsupported: ${entityType}`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
musicArtist(mbid) {
|
||||
return this.get(`music/${mbid}`);
|
||||
}
|
||||
|
||||
musicAlbum(mbid) {
|
||||
return this.get(`music/albums/${mbid}`);
|
||||
}
|
||||
|
||||
musicLabel(mbid) {
|
||||
return this.get(`music/${mbid}`);
|
||||
}
|
||||
}
|
||||
37
src/extensions/fanart-tv/index.js
Normal file
37
src/extensions/fanart-tv/index.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import schema from './schema.js';
|
||||
import resolvers from './resolvers.js';
|
||||
import createLoader from './loader.js';
|
||||
import FanArtClient from './client.js';
|
||||
import { ONE_DAY } from '../../util.js';
|
||||
|
||||
export default {
|
||||
name: 'fanart.tv',
|
||||
description: `Retrieve high quality artwork for artists, releases, and labels
|
||||
from [fanart.tv](https://fanart.tv/).`,
|
||||
extendContext(context, { fanArt = {} } = {}) {
|
||||
const client = new FanArtClient(fanArt);
|
||||
const cacheSize = parseInt(
|
||||
process.env.FANART_CACHE_SIZE ||
|
||||
process.env.GRAPHBRAINZ_CACHE_SIZE ||
|
||||
8192,
|
||||
10
|
||||
);
|
||||
const cacheTTL = parseInt(
|
||||
process.env.FANART_CACHE_TTL ||
|
||||
process.env.GRAPHBRAINZ_CACHE_TTL ||
|
||||
ONE_DAY,
|
||||
10
|
||||
);
|
||||
return {
|
||||
...context,
|
||||
loaders: {
|
||||
...context.loaders,
|
||||
fanArt: createLoader({ client, cacheSize, cacheTTL }),
|
||||
},
|
||||
};
|
||||
},
|
||||
extendSchema: {
|
||||
schemas: [schema],
|
||||
resolvers,
|
||||
},
|
||||
};
|
||||
65
src/extensions/fanart-tv/loader.js
Normal file
65
src/extensions/fanart-tv/loader.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import createDebug from 'debug';
|
||||
import DataLoader from 'dataloader';
|
||||
import LRUCache from 'lru-cache';
|
||||
|
||||
const debug = createDebug('graphbrainz:extensions/fanart-tv');
|
||||
|
||||
export default function createLoader(options) {
|
||||
const { client } = options;
|
||||
const cache = new LRUCache({
|
||||
max: options.cacheSize,
|
||||
maxAge: options.cacheTTL,
|
||||
dispose(key) {
|
||||
debug(`Removed from cache. key=${key}`);
|
||||
},
|
||||
});
|
||||
// Make the cache Map-like.
|
||||
cache.delete = cache.del;
|
||||
cache.clear = cache.reset;
|
||||
|
||||
const loader = new DataLoader(
|
||||
(keys) => {
|
||||
return Promise.all(
|
||||
keys.map((key) => {
|
||||
const [entityType, id] = key;
|
||||
return client
|
||||
.musicEntity(entityType, id)
|
||||
.catch((err) => {
|
||||
if (err.statusCode === 404) {
|
||||
// 404s are OK, just return empty data.
|
||||
return {
|
||||
artistbackground: [],
|
||||
artistthumb: [],
|
||||
musiclogo: [],
|
||||
hdmusiclogo: [],
|
||||
musicbanner: [],
|
||||
musiclabel: [],
|
||||
albums: {},
|
||||
};
|
||||
}
|
||||
throw err;
|
||||
})
|
||||
.then((body) => {
|
||||
if (entityType === 'artist') {
|
||||
const releaseGroupIDs = Object.keys(body.albums || {});
|
||||
debug(
|
||||
`Priming album cache with ${releaseGroupIDs.length} album(s).`
|
||||
);
|
||||
releaseGroupIDs.forEach((key) =>
|
||||
loader.prime(['release-group', key], body)
|
||||
);
|
||||
}
|
||||
return body;
|
||||
});
|
||||
})
|
||||
);
|
||||
},
|
||||
{
|
||||
batch: false,
|
||||
cacheKeyFn: ([entityType, id]) => `${entityType}/${id}`,
|
||||
cacheMap: cache,
|
||||
}
|
||||
);
|
||||
|
||||
return loader;
|
||||
}
|
||||
64
src/extensions/fanart-tv/resolvers.js
Normal file
64
src/extensions/fanart-tv/resolvers.js
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
const imageResolvers = {
|
||||
imageID: (image) => image.id,
|
||||
url: (image, args) => {
|
||||
return args.size === 'PREVIEW'
|
||||
? image.url.replace('/fanart/', '/preview/')
|
||||
: image.url;
|
||||
},
|
||||
likeCount: (image) => image.likes,
|
||||
};
|
||||
|
||||
export default {
|
||||
FanArtImage: {
|
||||
...imageResolvers,
|
||||
},
|
||||
FanArtDiscImage: {
|
||||
...imageResolvers,
|
||||
discNumber: (image) => image.disc,
|
||||
},
|
||||
FanArtLabelImage: {
|
||||
...imageResolvers,
|
||||
color: (image) => image.colour,
|
||||
},
|
||||
FanArtArtist: {
|
||||
backgrounds: (artist) => {
|
||||
return artist.artistbackground || [];
|
||||
},
|
||||
thumbnails: (artist) => {
|
||||
return artist.artistthumb || [];
|
||||
},
|
||||
logos: (artist) => {
|
||||
return artist.musiclogo || [];
|
||||
},
|
||||
logosHD: (artist) => {
|
||||
return artist.hdmusiclogo || [];
|
||||
},
|
||||
banners: (artist) => {
|
||||
return artist.musicbanner || [];
|
||||
},
|
||||
},
|
||||
FanArtLabel: {
|
||||
logos: (label) => label.musiclabel || [],
|
||||
},
|
||||
FanArtAlbum: {
|
||||
albumCovers: (album) => album.albumcover || [],
|
||||
discImages: (album) => album.cdart || [],
|
||||
},
|
||||
Artist: {
|
||||
fanArt: (artist, args, context) => {
|
||||
return context.loaders.fanArt.load(['artist', artist.id]);
|
||||
},
|
||||
},
|
||||
Label: {
|
||||
fanArt: (label, args, context) => {
|
||||
return context.loaders.fanArt.load(['label', label.id]);
|
||||
},
|
||||
},
|
||||
ReleaseGroup: {
|
||||
fanArt: (releaseGroup, args, context) => {
|
||||
return context.loaders.fanArt
|
||||
.load(['release-group', releaseGroup.id])
|
||||
.then((artist) => artist.albums[releaseGroup.id]);
|
||||
},
|
||||
},
|
||||
};
|
||||
197
src/extensions/fanart-tv/schema.js
Normal file
197
src/extensions/fanart-tv/schema.js
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
import gql from '../../tag.js';
|
||||
|
||||
export default gql`
|
||||
"""
|
||||
The image sizes that may be requested at [fanart.tv](https://fanart.tv/).
|
||||
"""
|
||||
enum FanArtImageSize {
|
||||
"""
|
||||
The image’s full original dimensions.
|
||||
"""
|
||||
FULL
|
||||
|
||||
"""
|
||||
A maximum dimension of 200px.
|
||||
"""
|
||||
PREVIEW
|
||||
}
|
||||
|
||||
"""
|
||||
A single image from [fanart.tv](https://fanart.tv/).
|
||||
"""
|
||||
type FanArtImage {
|
||||
"""
|
||||
The ID of the image on fanart.tv.
|
||||
"""
|
||||
imageID: ID
|
||||
|
||||
"""
|
||||
The URL of the image.
|
||||
"""
|
||||
url(
|
||||
"""
|
||||
The size of the image to retrieve.
|
||||
"""
|
||||
size: FanArtImageSize = FULL
|
||||
): URLString
|
||||
|
||||
"""
|
||||
The number of likes the image has received by fanart.tv users.
|
||||
"""
|
||||
likeCount: Int
|
||||
}
|
||||
|
||||
"""
|
||||
A disc image from [fanart.tv](https://fanart.tv/).
|
||||
"""
|
||||
type FanArtDiscImage {
|
||||
"""
|
||||
The ID of the image on fanart.tv.
|
||||
"""
|
||||
imageID: ID
|
||||
|
||||
"""
|
||||
The URL of the image.
|
||||
"""
|
||||
url(
|
||||
"""
|
||||
The size of the image to retrieve.
|
||||
"""
|
||||
size: FanArtImageSize = FULL
|
||||
): URLString
|
||||
|
||||
"""
|
||||
The number of likes the image has received by fanart.tv users.
|
||||
"""
|
||||
likeCount: Int
|
||||
|
||||
"""
|
||||
The disc number.
|
||||
"""
|
||||
discNumber: Int
|
||||
|
||||
"""
|
||||
The width and height of the (square) disc image.
|
||||
"""
|
||||
size: Int
|
||||
}
|
||||
|
||||
"""
|
||||
A music label image from [fanart.tv](https://fanart.tv/).
|
||||
"""
|
||||
type FanArtLabelImage {
|
||||
"""
|
||||
The ID of the image on fanart.tv.
|
||||
"""
|
||||
imageID: ID
|
||||
|
||||
"""
|
||||
The URL of the image.
|
||||
"""
|
||||
url(
|
||||
"""
|
||||
The size of the image to retrieve.
|
||||
"""
|
||||
size: FanArtImageSize = FULL
|
||||
): URLString
|
||||
|
||||
"""
|
||||
The number of likes the image has received by fanart.tv users.
|
||||
"""
|
||||
likeCount: Int
|
||||
|
||||
"""
|
||||
The type of color content in the image (usually “white” or “colour”).
|
||||
"""
|
||||
color: String
|
||||
}
|
||||
|
||||
"""
|
||||
An object containing lists of the different types of artist images from
|
||||
[fanart.tv](https://fanart.tv/).
|
||||
"""
|
||||
type FanArtArtist {
|
||||
"""
|
||||
A list of 1920x1080 JPG images picturing the artist, suitable for use as
|
||||
backgrounds.
|
||||
"""
|
||||
backgrounds: [FanArtImage]
|
||||
|
||||
"""
|
||||
A list of 1000x185 JPG images containing the artist and their logo or name.
|
||||
"""
|
||||
banners: [FanArtImage]
|
||||
|
||||
"""
|
||||
A list of 400x155 PNG images containing the artist’s logo or name, with
|
||||
transparent backgrounds.
|
||||
"""
|
||||
logos: [FanArtImage]
|
||||
|
||||
"""
|
||||
A list of 800x310 PNG images containing the artist’s logo or name, with
|
||||
transparent backgrounds.
|
||||
"""
|
||||
logosHD: [FanArtImage]
|
||||
|
||||
"""
|
||||
A list of 1000x1000 JPG thumbnail images picturing the artist (usually
|
||||
containing every member of a band).
|
||||
"""
|
||||
thumbnails: [FanArtImage]
|
||||
}
|
||||
|
||||
"""
|
||||
An object containing lists of the different types of label images from
|
||||
[fanart.tv](https://fanart.tv/).
|
||||
"""
|
||||
type FanArtLabel {
|
||||
"""
|
||||
A list of 400x270 PNG images containing the label’s logo. There will
|
||||
usually be a black version, a color version, and a white version, all with
|
||||
transparent backgrounds.
|
||||
"""
|
||||
logos: [FanArtLabelImage]
|
||||
}
|
||||
|
||||
"""
|
||||
An object containing lists of the different types of release group images from
|
||||
[fanart.tv](https://fanart.tv/).
|
||||
"""
|
||||
type FanArtAlbum {
|
||||
"""
|
||||
A list of 1000x1000 JPG images of the cover artwork of the release group.
|
||||
"""
|
||||
albumCovers: [FanArtImage]
|
||||
|
||||
"""
|
||||
A list of 1000x1000 PNG images of the physical disc media for the release
|
||||
group, with transparent backgrounds.
|
||||
"""
|
||||
discImages: [FanArtDiscImage]
|
||||
}
|
||||
|
||||
extend type Artist {
|
||||
"""
|
||||
Images of the artist from [fanart.tv](https://fanart.tv/).
|
||||
This field is provided by the fanart.tv extension.
|
||||
"""
|
||||
fanArt: FanArtArtist
|
||||
}
|
||||
|
||||
extend type Label {
|
||||
"""
|
||||
Images of the label from [fanart.tv](https://fanart.tv/).
|
||||
This field is provided by the fanart.tv extension.
|
||||
"""
|
||||
fanArt: FanArtLabel
|
||||
}
|
||||
|
||||
extend type ReleaseGroup {
|
||||
"""
|
||||
Images of the release group from [fanart.tv](https://fanart.tv/).
|
||||
This field is provided by the fanart.tv extension.
|
||||
"""
|
||||
fanArt: FanArtAlbum
|
||||
}
|
||||
`;
|
||||
18
src/extensions/index.js
Normal file
18
src/extensions/index.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
export async function loadExtension(extensionModule) {
|
||||
let extension;
|
||||
if (typeof extensionModule === 'string') {
|
||||
extension = await import(extensionModule);
|
||||
} else {
|
||||
extension = extensionModule;
|
||||
}
|
||||
if (extension == null || typeof extension !== 'object') {
|
||||
throw new Error(
|
||||
`Expected ${extensionModule} to export an extension but instead ` +
|
||||
`got: ${extension}`
|
||||
);
|
||||
} else if (extension.default) {
|
||||
// ECMAScript module interop.
|
||||
extension = extension.default;
|
||||
}
|
||||
return extension;
|
||||
}
|
||||
54
src/extensions/mediawiki/client.js
Normal file
54
src/extensions/mediawiki/client.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import ExtendableError from 'es6-error';
|
||||
import Client from '../../api/client.js';
|
||||
|
||||
export class MediaWikiError extends ExtendableError {}
|
||||
|
||||
export default class MediaWikiClient extends Client {
|
||||
constructor({ limit = 10, period = 1000, ...options } = {}) {
|
||||
super({ limit, period, ...options });
|
||||
}
|
||||
|
||||
imageInfo(page) {
|
||||
const pageURL = new URL(page);
|
||||
|
||||
if (!pageURL.pathname.startsWith('/wiki/')) {
|
||||
return Promise.reject(
|
||||
new MediaWikiError(
|
||||
`MediaWiki page URL does not have the expected /wiki/ prefix: ${page}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const apiURL = new URL('/w/api.php', pageURL);
|
||||
apiURL.search = new URLSearchParams({
|
||||
action: 'query',
|
||||
titles: decodeURI(pageURL.pathname.slice(6)),
|
||||
prop: 'imageinfo',
|
||||
iiprop: 'url|size|canonicaltitle|user|extmetadata',
|
||||
format: 'json',
|
||||
}).toString();
|
||||
|
||||
return this.get(apiURL.toString(), { resolveBodyOnly: true }).then(
|
||||
(body) => {
|
||||
const pageIDs = Object.keys(body.query.pages);
|
||||
if (pageIDs.length !== 1) {
|
||||
throw new MediaWikiError(
|
||||
`Query returned multiple pages: [${pageIDs.join(', ')}]`
|
||||
);
|
||||
}
|
||||
if (pageIDs[0] === '-1') {
|
||||
throw new MediaWikiError(
|
||||
body.query.pages['-1'].invalidreason || 'Unknown error'
|
||||
);
|
||||
}
|
||||
const imageInfo = body.query.pages[pageIDs[0]].imageinfo;
|
||||
if (imageInfo.length !== 1) {
|
||||
throw new MediaWikiError(
|
||||
`Query returned info for ${imageInfo.length} images, expected 1.`
|
||||
);
|
||||
}
|
||||
return imageInfo[0];
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
37
src/extensions/mediawiki/index.js
Normal file
37
src/extensions/mediawiki/index.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import schema from './schema.js';
|
||||
import resolvers from './resolvers.js';
|
||||
import createLoader from './loader.js';
|
||||
import MediaWikiClient from './client.js';
|
||||
import { ONE_DAY } from '../../util.js';
|
||||
|
||||
export default {
|
||||
name: 'MediaWiki',
|
||||
description: `Retrieve information from MediaWiki image pages, like the actual
|
||||
image file URL and EXIF metadata.`,
|
||||
extendContext(context, { mediaWiki = {} } = {}) {
|
||||
const client = new MediaWikiClient(mediaWiki);
|
||||
const cacheSize = parseInt(
|
||||
process.env.MEDIAWIKI_CACHE_SIZE ||
|
||||
process.env.GRAPHBRAINZ_CACHE_SIZE ||
|
||||
8192,
|
||||
10
|
||||
);
|
||||
const cacheTTL = parseInt(
|
||||
process.env.MEDIAWIKI_CACHE_TTL ||
|
||||
process.env.GRAPHBRAINZ_CACHE_TTL ||
|
||||
ONE_DAY,
|
||||
10
|
||||
);
|
||||
return {
|
||||
...context,
|
||||
loaders: {
|
||||
...context.loaders,
|
||||
mediaWiki: createLoader({ client, cacheSize, cacheTTL }),
|
||||
},
|
||||
};
|
||||
},
|
||||
extendSchema: {
|
||||
schemas: [schema],
|
||||
resolvers,
|
||||
},
|
||||
};
|
||||
30
src/extensions/mediawiki/loader.js
Normal file
30
src/extensions/mediawiki/loader.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import createDebug from 'debug';
|
||||
import DataLoader from 'dataloader';
|
||||
import LRUCache from 'lru-cache';
|
||||
|
||||
const debug = createDebug('graphbrainz:extensions/mediawiki');
|
||||
|
||||
export default function createLoader(options) {
|
||||
const { client } = options;
|
||||
const cache = new LRUCache({
|
||||
max: options.cacheSize,
|
||||
maxAge: options.cacheTTL,
|
||||
dispose(key) {
|
||||
debug(`Removed from cache. key=${key}`);
|
||||
},
|
||||
});
|
||||
// Make the cache Map-like.
|
||||
cache.delete = cache.del;
|
||||
cache.clear = cache.reset;
|
||||
|
||||
return new DataLoader(
|
||||
(keys) => {
|
||||
return Promise.allSettled(
|
||||
keys.map((key) => client.imageInfo(key))
|
||||
).then((results) =>
|
||||
results.map((result) => result.reason || result.value)
|
||||
);
|
||||
},
|
||||
{ batch: false, cacheMap: cache }
|
||||
);
|
||||
}
|
||||
80
src/extensions/mediawiki/resolvers.js
Normal file
80
src/extensions/mediawiki/resolvers.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
async function resolveMediaWikiImages(source, args, { loaders }) {
|
||||
const isURL = (relation) => relation['target-type'] === 'url';
|
||||
let rels = source.relations ? source.relations.filter(isURL) : [];
|
||||
if (!rels.length) {
|
||||
rels = await loaders.lookup
|
||||
.load([source._type, source.id, { inc: 'url-rels' }])
|
||||
.then((source) => source.relations.filter(isURL));
|
||||
}
|
||||
const pages = rels
|
||||
.filter((rel) => {
|
||||
if (rel.type === args.type) {
|
||||
const url = new URL(rel.url.resource);
|
||||
if (url.pathname.match(/^\/wiki\/(File|Image):/)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map((rel) => rel.url.resource);
|
||||
return Promise.all(pages.map((page) => loaders.mediaWiki.load(page)));
|
||||
}
|
||||
|
||||
export default {
|
||||
MediaWikiImage: {
|
||||
descriptionURL: (imageInfo) => imageInfo.descriptionurl,
|
||||
canonicalTitle: (imageInfo) => imageInfo.canonicaltitle,
|
||||
objectName: (imageInfo) => {
|
||||
const data = imageInfo.extmetadata.ObjectName;
|
||||
return data ? data.value : null;
|
||||
},
|
||||
descriptionHTML: (imageInfo) => {
|
||||
const data = imageInfo.extmetadata.ImageDescription;
|
||||
return data ? data.value : null;
|
||||
},
|
||||
originalDateTimeHTML: (imageInfo) => {
|
||||
const data = imageInfo.extmetadata.DateTimeOriginal;
|
||||
return data ? data.value : null;
|
||||
},
|
||||
categories: (imageInfo) => {
|
||||
const data = imageInfo.extmetadata.Categories;
|
||||
return data ? data.value.split('|') : [];
|
||||
},
|
||||
artistHTML: (imageInfo) => {
|
||||
const data = imageInfo.extmetadata.Artist;
|
||||
return data ? data.value : null;
|
||||
},
|
||||
creditHTML: (imageInfo) => {
|
||||
const data = imageInfo.extmetadata.Credit;
|
||||
return data ? data.value : null;
|
||||
},
|
||||
licenseShortName: (imageInfo) => {
|
||||
const data = imageInfo.extmetadata.LicenseShortName;
|
||||
return data ? data.value : null;
|
||||
},
|
||||
licenseURL: (imageInfo) => {
|
||||
const data = imageInfo.extmetadata.LicenseUrl;
|
||||
return data ? data.value : null;
|
||||
},
|
||||
metadata: (imageInfo) =>
|
||||
Object.keys(imageInfo.extmetadata).map((key) => {
|
||||
const data = imageInfo.extmetadata[key];
|
||||
return { ...data, name: key };
|
||||
}),
|
||||
},
|
||||
MediaWikiImageMetadata: {
|
||||
value: (obj) => (obj.value == null ? obj.value : `${obj.value}`),
|
||||
},
|
||||
Artist: {
|
||||
mediaWikiImages: resolveMediaWikiImages,
|
||||
},
|
||||
Instrument: {
|
||||
mediaWikiImages: resolveMediaWikiImages,
|
||||
},
|
||||
Label: {
|
||||
mediaWikiImages: resolveMediaWikiImages,
|
||||
},
|
||||
Place: {
|
||||
mediaWikiImages: resolveMediaWikiImages,
|
||||
},
|
||||
};
|
||||
168
src/extensions/mediawiki/schema.js
Normal file
168
src/extensions/mediawiki/schema.js
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import gql from '../../tag.js';
|
||||
|
||||
export default gql`
|
||||
"""
|
||||
An object describing various properties of an image stored on a MediaWiki
|
||||
server. The information comes the [MediaWiki imageinfo API](https://www.mediawiki.org/wiki/API:Imageinfo).
|
||||
"""
|
||||
type MediaWikiImage {
|
||||
"""
|
||||
The URL of the actual image file.
|
||||
"""
|
||||
url: URLString!
|
||||
|
||||
"""
|
||||
The URL of the wiki page describing the image.
|
||||
"""
|
||||
descriptionURL: URLString
|
||||
|
||||
"""
|
||||
The user who uploaded the file.
|
||||
"""
|
||||
user: String
|
||||
|
||||
"""
|
||||
The size of the file in bytes.
|
||||
"""
|
||||
size: Int
|
||||
|
||||
"""
|
||||
The pixel width of the image.
|
||||
"""
|
||||
width: Int
|
||||
|
||||
"""
|
||||
The pixel height of the image.
|
||||
"""
|
||||
height: Int
|
||||
|
||||
"""
|
||||
The canonical title of the file.
|
||||
"""
|
||||
canonicalTitle: String
|
||||
|
||||
"""
|
||||
The image title, brief description, or file name.
|
||||
"""
|
||||
objectName: String
|
||||
|
||||
"""
|
||||
A description of the image, potentially containing HTML.
|
||||
"""
|
||||
descriptionHTML: String
|
||||
|
||||
"""
|
||||
The original date of creation of the image. May be a description rather than
|
||||
a parseable timestamp, and may contain HTML.
|
||||
"""
|
||||
originalDateTimeHTML: String
|
||||
|
||||
"""
|
||||
A list of the categories of the image.
|
||||
"""
|
||||
categories: [String]!
|
||||
|
||||
"""
|
||||
The name of the image author, potentially containing HTML.
|
||||
"""
|
||||
artistHTML: String
|
||||
|
||||
"""
|
||||
The source of the image, potentially containing HTML.
|
||||
"""
|
||||
creditHTML: String
|
||||
|
||||
"""
|
||||
A short human-readable license name.
|
||||
"""
|
||||
licenseShortName: String
|
||||
|
||||
"""
|
||||
A web address where the license is described.
|
||||
"""
|
||||
licenseURL: URLString
|
||||
|
||||
"""
|
||||
The full list of values in the \`extmetadata\` field.
|
||||
"""
|
||||
metadata: [MediaWikiImageMetadata]!
|
||||
}
|
||||
|
||||
"""
|
||||
An entry in the \`extmetadata\` field of a MediaWiki image file.
|
||||
"""
|
||||
type MediaWikiImageMetadata {
|
||||
"""
|
||||
The name of the metadata field.
|
||||
"""
|
||||
name: String!
|
||||
"""
|
||||
The value of the metadata field. All values will be converted to strings.
|
||||
"""
|
||||
value: String
|
||||
"""
|
||||
The source of the value.
|
||||
"""
|
||||
source: String
|
||||
}
|
||||
|
||||
extend type Artist {
|
||||
"""
|
||||
Artist images found at MediaWiki URLs in the artist’s URL relationships.
|
||||
Defaults to URL relationships with the type “image”.
|
||||
This field is provided by the MediaWiki extension.
|
||||
"""
|
||||
mediaWikiImages(
|
||||
"""
|
||||
The type of URL relationship that will be selected to find images. See
|
||||
the possible [Artist-URL relationship types](https://musicbrainz.org/relationships/artist-url).
|
||||
"""
|
||||
type: String = "image"
|
||||
): [MediaWikiImage]!
|
||||
}
|
||||
|
||||
extend type Instrument {
|
||||
"""
|
||||
Instrument images found at MediaWiki URLs in the instrument’s URL
|
||||
relationships. Defaults to URL relationships with the type “image”.
|
||||
This field is provided by the MediaWiki extension.
|
||||
"""
|
||||
mediaWikiImages(
|
||||
"""
|
||||
The type of URL relationship that will be selected to find images. See the
|
||||
possible [Instrument-URL relationship types](https://musicbrainz.org/relationships/instrument-url).
|
||||
"""
|
||||
type: String = "image"
|
||||
): [MediaWikiImage]!
|
||||
}
|
||||
|
||||
extend type Label {
|
||||
"""
|
||||
Label images found at MediaWiki URLs in the label’s URL relationships.
|
||||
Defaults to URL relationships with the type “logo”.
|
||||
This field is provided by the MediaWiki extension.
|
||||
"""
|
||||
mediaWikiImages(
|
||||
"""
|
||||
The type of URL relationship that will be selected to find images. See the
|
||||
possible [Label-URL relationship types](https://musicbrainz.org/relationships/label-url).
|
||||
"""
|
||||
type: String = "logo"
|
||||
): [MediaWikiImage]!
|
||||
}
|
||||
|
||||
extend type Place {
|
||||
"""
|
||||
Place images found at MediaWiki URLs in the place’s URL relationships.
|
||||
Defaults to URL relationships with the type “image”.
|
||||
This field is provided by the MediaWiki extension.
|
||||
"""
|
||||
mediaWikiImages(
|
||||
"""
|
||||
The type of URL relationship that will be selected to find images. See the
|
||||
possible [Place-URL relationship types](https://musicbrainz.org/relationships/place-url).
|
||||
"""
|
||||
type: String = "image"
|
||||
): [MediaWikiImage]!
|
||||
}
|
||||
`;
|
||||
78
src/extensions/the-audio-db/client.js
Normal file
78
src/extensions/the-audio-db/client.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import ExtendableError from 'es6-error';
|
||||
import Client from '../../api/client.js';
|
||||
|
||||
export class TheAudioDBError extends ExtendableError {}
|
||||
|
||||
export default class TheAudioDBClient extends Client {
|
||||
constructor({
|
||||
apiKey = process.env.THEAUDIODB_API_KEY,
|
||||
baseURL = process.env.THEAUDIODB_BASE_URL ||
|
||||
'https://www.theaudiodb.com/api/v1/json/',
|
||||
limit = 10,
|
||||
period = 1000,
|
||||
...options
|
||||
} = {}) {
|
||||
super({ baseURL, limit, period, ...options });
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
get(path, options = {}) {
|
||||
if (!this.apiKey) {
|
||||
return Promise.reject(
|
||||
new TheAudioDBError('No API key was configured for TheAudioDB client.')
|
||||
);
|
||||
}
|
||||
return super.get(`${this.apiKey}/${path}`, {
|
||||
resolveBodyOnly: true,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
entity(entityType, mbid) {
|
||||
switch (entityType) {
|
||||
case 'artist':
|
||||
return this.artist(mbid);
|
||||
case 'release-group':
|
||||
return this.album(mbid);
|
||||
case 'recording':
|
||||
return this.track(mbid);
|
||||
default:
|
||||
return Promise.reject(
|
||||
new TheAudioDBError(`Entity type unsupported: ${entityType}`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
artist(mbid) {
|
||||
return this.get('artist-mb.php', { searchParams: { i: mbid } }).then(
|
||||
(body) => {
|
||||
if (body.artists && body.artists.length === 1) {
|
||||
return body.artists[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
album(mbid) {
|
||||
return this.get('album-mb.php', { searchParams: { i: mbid } }).then(
|
||||
(body) => {
|
||||
if (body.album && body.album.length === 1) {
|
||||
return body.album[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
track(mbid) {
|
||||
return this.get('track-mb.php', { searchParams: { i: mbid } }).then(
|
||||
(body) => {
|
||||
if (body.track && body.track.length === 1) {
|
||||
return body.track[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
37
src/extensions/the-audio-db/index.js
Normal file
37
src/extensions/the-audio-db/index.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import schema from './schema.js';
|
||||
import resolvers from './resolvers.js';
|
||||
import createLoader from './loader.js';
|
||||
import TheAudioDBClient from './client.js';
|
||||
import { ONE_DAY } from '../../util.js';
|
||||
|
||||
export default {
|
||||
name: 'TheAudioDB',
|
||||
description: `Retrieve images and information about artists, releases, and
|
||||
recordings from [TheAudioDB.com](http://www.theaudiodb.com/).`,
|
||||
extendContext(context, { theAudioDB = {} } = {}) {
|
||||
const client = new TheAudioDBClient(theAudioDB);
|
||||
const cacheSize = parseInt(
|
||||
process.env.THEAUDIODB_CACHE_SIZE ||
|
||||
process.env.GRAPHBRAINZ_CACHE_SIZE ||
|
||||
8192,
|
||||
10
|
||||
);
|
||||
const cacheTTL = parseInt(
|
||||
process.env.THEAUDIODB_CACHE_TTL ||
|
||||
process.env.GRAPHBRAINZ_CACHE_TTL ||
|
||||
ONE_DAY,
|
||||
10
|
||||
);
|
||||
return {
|
||||
...context,
|
||||
loaders: {
|
||||
...context.loaders,
|
||||
theAudioDB: createLoader({ client, cacheSize, cacheTTL }),
|
||||
},
|
||||
};
|
||||
},
|
||||
extendSchema: {
|
||||
schemas: [schema],
|
||||
resolvers,
|
||||
},
|
||||
};
|
||||
35
src/extensions/the-audio-db/loader.js
Normal file
35
src/extensions/the-audio-db/loader.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import createDebug from 'debug';
|
||||
import DataLoader from 'dataloader';
|
||||
import LRUCache from 'lru-cache';
|
||||
|
||||
const debug = createDebug('graphbrainz:extensions/the-audio-db');
|
||||
|
||||
export default function createLoader(options) {
|
||||
const { client } = options;
|
||||
const cache = new LRUCache({
|
||||
max: options.cacheSize,
|
||||
maxAge: options.cacheTTL,
|
||||
dispose(key) {
|
||||
debug(`Removed from cache. key=${key}`);
|
||||
},
|
||||
});
|
||||
// Make the cache Map-like.
|
||||
cache.delete = cache.del;
|
||||
cache.clear = cache.reset;
|
||||
|
||||
return new DataLoader(
|
||||
(keys) => {
|
||||
return Promise.all(
|
||||
keys.map((key) => {
|
||||
const [entityType, id] = key;
|
||||
return client.entity(entityType, id);
|
||||
})
|
||||
);
|
||||
},
|
||||
{
|
||||
batch: false,
|
||||
cacheKeyFn: ([entityType, id]) => `${entityType}/${id}`,
|
||||
cacheMap: cache,
|
||||
}
|
||||
);
|
||||
}
|
||||
118
src/extensions/the-audio-db/resolvers.js
Normal file
118
src/extensions/the-audio-db/resolvers.js
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
function handleImageSize(resolver) {
|
||||
return (source, args, context, info) => {
|
||||
const getURL = (url) => (args.size === 'PREVIEW' ? `${url}/preview` : url);
|
||||
const url = resolver(source, args, context, info);
|
||||
if (!url) {
|
||||
return null;
|
||||
} else if (Array.isArray(url)) {
|
||||
return url.map(getURL);
|
||||
} else {
|
||||
return getURL(url);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
TheAudioDBArtist: {
|
||||
artistID: (artist) => artist.idArtist,
|
||||
biography: (artist, args) => {
|
||||
const lang = args.lang.toUpperCase();
|
||||
return artist[`strBiography${lang}`] || null;
|
||||
},
|
||||
memberCount: (artist) => artist.intMembers,
|
||||
banner: handleImageSize((artist) => artist.strArtistBanner),
|
||||
fanArt: handleImageSize((artist) => {
|
||||
return [
|
||||
artist.strArtistFanart,
|
||||
artist.strArtistFanart2,
|
||||
artist.strArtistFanart3,
|
||||
].filter(Boolean);
|
||||
}),
|
||||
logo: handleImageSize((artist) => artist.strArtistLogo),
|
||||
thumbnail: handleImageSize((artist) => artist.strArtistThumb),
|
||||
genre: (artist) => artist.strGenre || null,
|
||||
mood: (artist) => artist.strMood || null,
|
||||
style: (artist) => artist.strStyle || null,
|
||||
},
|
||||
TheAudioDBAlbum: {
|
||||
albumID: (album) => album.idAlbum,
|
||||
artistID: (album) => album.idArtist,
|
||||
description: (album, args) => {
|
||||
const lang = args.lang.toUpperCase();
|
||||
return album[`strDescription${lang}`] || null;
|
||||
},
|
||||
salesCount: (album) => album.intSales,
|
||||
score: (album) => album.intScore,
|
||||
scoreVotes: (album) => album.intScoreVotes,
|
||||
discImage: handleImageSize((album) => album.strAlbumCDart),
|
||||
spineImage: handleImageSize((album) => album.strAlbumSpine),
|
||||
frontImage: handleImageSize((album) => album.strAlbumThumb),
|
||||
backImage: handleImageSize((album) => album.strAlbumThumbBack),
|
||||
review: (album) => album.strReview || null,
|
||||
genre: (album) => album.strGenre || null,
|
||||
mood: (album) => album.strMood || null,
|
||||
style: (album) => album.strStyle || null,
|
||||
speed: (album) => album.strSpeed || null,
|
||||
theme: (album) => album.strTheme || null,
|
||||
},
|
||||
TheAudioDBTrack: {
|
||||
trackID: (track) => track.idTrack,
|
||||
albumID: (track) => track.idAlbum,
|
||||
artistID: (track) => track.idArtist,
|
||||
description: (track, args) => {
|
||||
const lang = args.lang.toUpperCase();
|
||||
return track[`strDescription${lang}`] || null;
|
||||
},
|
||||
thumbnail: handleImageSize((track) => track.strTrackThumb),
|
||||
score: (track) => track.intScore,
|
||||
scoreVotes: (track) => track.intScoreVotes,
|
||||
trackNumber: (track) => track.intTrackNumber,
|
||||
musicVideo: (track) => track,
|
||||
genre: (track) => track.strGenre || null,
|
||||
mood: (track) => track.strMood || null,
|
||||
style: (track) => track.strStyle || null,
|
||||
theme: (track) => track.strTheme || null,
|
||||
},
|
||||
TheAudioDBMusicVideo: {
|
||||
url: (track) => {
|
||||
let url = track.strMusicVid || null;
|
||||
// Many of these are missing the protocol and start with www, so add it
|
||||
// in that case.
|
||||
if (url && url.startsWith('www.')) {
|
||||
url = `https://${url}`;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
companyName: (track) => track.strMusicVidCompany || null,
|
||||
directorName: (track) => track.strMusicVidDirector || null,
|
||||
screenshots: handleImageSize((track) => {
|
||||
return [
|
||||
track.strMusicVidScreen1,
|
||||
track.strMusicVidScreen2,
|
||||
track.strMusicVidScreen3,
|
||||
].filter(Boolean);
|
||||
}),
|
||||
viewCount: (track) => track.intMusicVidViews,
|
||||
likeCount: (track) => track.intMusicVidLikes,
|
||||
dislikeCount: (track) => track.intMusicVidDislikes,
|
||||
commentCount: (track) => track.intMusicVidComments,
|
||||
},
|
||||
Artist: {
|
||||
theAudioDB: (artist, args, context) => {
|
||||
return context.loaders.theAudioDB.load(['artist', artist.id]);
|
||||
},
|
||||
},
|
||||
Recording: {
|
||||
theAudioDB: (recording, args, context) => {
|
||||
return context.loaders.theAudioDB.load(['recording', recording.id]);
|
||||
},
|
||||
},
|
||||
ReleaseGroup: {
|
||||
theAudioDB: (releaseGroup, args, context) => {
|
||||
return context.loaders.theAudioDB.load([
|
||||
'release-group',
|
||||
releaseGroup.id,
|
||||
]);
|
||||
},
|
||||
},
|
||||
};
|
||||
372
src/extensions/the-audio-db/schema.js
Normal file
372
src/extensions/the-audio-db/schema.js
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
import gql from '../../tag.js';
|
||||
|
||||
export default gql`
|
||||
"""
|
||||
The image sizes that may be requested at [TheAudioDB](http://www.theaudiodb.com/).
|
||||
"""
|
||||
enum TheAudioDBImageSize {
|
||||
"""
|
||||
The image’s full original dimensions.
|
||||
"""
|
||||
FULL
|
||||
|
||||
"""
|
||||
A maximum dimension of 200px.
|
||||
"""
|
||||
PREVIEW
|
||||
}
|
||||
|
||||
"""
|
||||
An artist on [TheAudioDB](http://www.theaudiodb.com/).
|
||||
"""
|
||||
type TheAudioDBArtist {
|
||||
"""
|
||||
TheAudioDB ID of the artist.
|
||||
"""
|
||||
artistID: ID
|
||||
|
||||
"""
|
||||
A biography of the artist, often available in several languages.
|
||||
"""
|
||||
biography(
|
||||
"""
|
||||
The two-letter code for the language in which to retrieve the biography.
|
||||
"""
|
||||
lang: String = "en"
|
||||
): String
|
||||
|
||||
"""
|
||||
The number of members in the musical group, if applicable.
|
||||
"""
|
||||
memberCount: Int
|
||||
|
||||
"""
|
||||
A 1000x185 JPG banner image containing the artist and their logo or name.
|
||||
"""
|
||||
banner(
|
||||
"""
|
||||
The size of the image to retrieve.
|
||||
"""
|
||||
size: TheAudioDBImageSize = FULL
|
||||
): URLString
|
||||
|
||||
"""
|
||||
A list of 1280x720 or 1920x1080 JPG images depicting the artist.
|
||||
"""
|
||||
fanArt(
|
||||
"""
|
||||
The size of the images to retrieve.
|
||||
"""
|
||||
size: TheAudioDBImageSize = FULL
|
||||
): [URLString]!
|
||||
|
||||
"""
|
||||
A 400x155 PNG image containing the artist’s logo or name, with a transparent
|
||||
background.
|
||||
"""
|
||||
logo(
|
||||
"""
|
||||
The size of the image to retrieve.
|
||||
"""
|
||||
size: TheAudioDBImageSize = FULL
|
||||
): URLString
|
||||
|
||||
"""
|
||||
A 1000x1000 JPG thumbnail image picturing the artist (usually containing
|
||||
every member of a band).
|
||||
"""
|
||||
thumbnail(
|
||||
"""
|
||||
The size of the image to retrieve.
|
||||
"""
|
||||
size: TheAudioDBImageSize = FULL
|
||||
): URLString
|
||||
|
||||
"""
|
||||
The primary musical genre of the artist (e.g. “Alternative Rock”).
|
||||
"""
|
||||
genre: String
|
||||
|
||||
"""
|
||||
The primary musical mood of the artist (e.g. “Sad”).
|
||||
"""
|
||||
mood: String
|
||||
|
||||
"""
|
||||
The primary musical style of the artist (e.g. “Rock/Pop”).
|
||||
"""
|
||||
style: String
|
||||
}
|
||||
|
||||
"""
|
||||
An album on [TheAudioDB](http://www.theaudiodb.com/) corresponding with a
|
||||
MusicBrainz Release Group.
|
||||
"""
|
||||
type TheAudioDBAlbum {
|
||||
"""
|
||||
TheAudioDB ID of the album.
|
||||
"""
|
||||
albumID: ID
|
||||
|
||||
"""
|
||||
TheAudioDB ID of the artist who released the album.
|
||||
"""
|
||||
artistID: ID
|
||||
|
||||
"""
|
||||
A description of the album, often available in several languages.
|
||||
"""
|
||||
description(
|
||||
"""
|
||||
The two-letter code for the language in which to retrieve the biography.
|
||||
"""
|
||||
lang: String = "en"
|
||||
): String
|
||||
|
||||
"""
|
||||
A review of the album.
|
||||
"""
|
||||
review: String
|
||||
|
||||
"""
|
||||
The worldwide sales figure.
|
||||
"""
|
||||
salesCount: Float
|
||||
|
||||
"""
|
||||
The album’s rating as determined by user votes, out of 10.
|
||||
"""
|
||||
score: Float
|
||||
|
||||
"""
|
||||
The number of users who voted to determine the album’s score.
|
||||
"""
|
||||
scoreVotes: Float
|
||||
|
||||
"""
|
||||
An image of the physical disc media for the album.
|
||||
"""
|
||||
discImage(
|
||||
"""
|
||||
The size of the image to retrieve.
|
||||
"""
|
||||
size: TheAudioDBImageSize = FULL
|
||||
): URLString
|
||||
|
||||
"""
|
||||
An image of the spine of the album packaging.
|
||||
"""
|
||||
spineImage(
|
||||
"""
|
||||
The size of the image to retrieve.
|
||||
"""
|
||||
size: TheAudioDBImageSize = FULL
|
||||
): URLString
|
||||
|
||||
"""
|
||||
An image of the front of the album packaging.
|
||||
"""
|
||||
frontImage(
|
||||
"""
|
||||
The size of the image to retrieve.
|
||||
"""
|
||||
size: TheAudioDBImageSize = FULL
|
||||
): URLString
|
||||
|
||||
"""
|
||||
An image of the back of the album packaging.
|
||||
"""
|
||||
backImage(
|
||||
"""
|
||||
The size of the image to retrieve.
|
||||
"""
|
||||
size: TheAudioDBImageSize = FULL
|
||||
): URLString
|
||||
|
||||
"""
|
||||
The primary musical genre of the album (e.g. “Alternative Rock”).
|
||||
"""
|
||||
genre: String
|
||||
|
||||
"""
|
||||
The primary musical mood of the album (e.g. “Sad”).
|
||||
"""
|
||||
mood: String
|
||||
|
||||
"""
|
||||
The primary musical style of the album (e.g. “Rock/Pop”).
|
||||
"""
|
||||
style: String
|
||||
|
||||
"""
|
||||
A rough description of the primary musical speed of the album (e.g. “Medium”).
|
||||
"""
|
||||
speed: String
|
||||
|
||||
"""
|
||||
The primary musical theme of the album (e.g. “In Love”).
|
||||
"""
|
||||
theme: String
|
||||
}
|
||||
|
||||
"""
|
||||
A track on [TheAudioDB](http://www.theaudiodb.com/) corresponding with a
|
||||
MusicBrainz Recording.
|
||||
"""
|
||||
type TheAudioDBTrack {
|
||||
"""
|
||||
TheAudioDB ID of the track.
|
||||
"""
|
||||
trackID: ID
|
||||
|
||||
"""
|
||||
TheAudioDB ID of the album on which the track appears.
|
||||
"""
|
||||
albumID: ID
|
||||
|
||||
"""
|
||||
TheAudioDB ID of the artist who released the track.
|
||||
"""
|
||||
artistID: ID
|
||||
|
||||
"""
|
||||
A description of the track.
|
||||
"""
|
||||
description(
|
||||
"""
|
||||
The two-letter code for the language in which to retrieve the description.
|
||||
"""
|
||||
lang: String = "en"
|
||||
): String
|
||||
|
||||
"""
|
||||
A thumbnail image for the track.
|
||||
"""
|
||||
thumbnail(
|
||||
"""
|
||||
The size of the image to retrieve.
|
||||
"""
|
||||
size: TheAudioDBImageSize = FULL
|
||||
): URLString
|
||||
|
||||
"""
|
||||
The track’s rating as determined by user votes, out of 10.
|
||||
"""
|
||||
score: Float
|
||||
|
||||
"""
|
||||
The number of users who voted to determine the album’s score.
|
||||
"""
|
||||
scoreVotes: Float
|
||||
|
||||
"""
|
||||
The track number of the song on the album.
|
||||
"""
|
||||
trackNumber: Int
|
||||
|
||||
"""
|
||||
The official music video for the track.
|
||||
"""
|
||||
musicVideo: TheAudioDBMusicVideo
|
||||
|
||||
"""
|
||||
The primary musical genre of the track (e.g. “Alternative Rock”).
|
||||
"""
|
||||
genre: String
|
||||
|
||||
"""
|
||||
The primary musical mood of the track (e.g. “Sad”).
|
||||
"""
|
||||
mood: String
|
||||
|
||||
"""
|
||||
The primary musical style of the track (e.g. “Rock/Pop”).
|
||||
"""
|
||||
style: String
|
||||
|
||||
"""
|
||||
The primary musical theme of the track (e.g. “In Love”).
|
||||
"""
|
||||
theme: String
|
||||
}
|
||||
|
||||
"""
|
||||
Details of a music video associated with a track on [TheAudioDB](http://www.theaudiodb.com/).
|
||||
"""
|
||||
type TheAudioDBMusicVideo {
|
||||
"""
|
||||
The URL where the music video can be found.
|
||||
"""
|
||||
url: URLString
|
||||
|
||||
"""
|
||||
The video production company of the music video.
|
||||
"""
|
||||
companyName: String
|
||||
|
||||
"""
|
||||
The director of the music video.
|
||||
"""
|
||||
directorName: String
|
||||
|
||||
"""
|
||||
A list of still images from the music video.
|
||||
"""
|
||||
screenshots(
|
||||
"""
|
||||
The size of the images to retrieve.
|
||||
"""
|
||||
size: TheAudioDBImageSize = FULL
|
||||
): [URLString]!
|
||||
|
||||
"""
|
||||
The number of views the video has received at the given URL. This will rarely
|
||||
be up to date, so use cautiously.
|
||||
"""
|
||||
viewCount: Float
|
||||
|
||||
"""
|
||||
The number of likes the video has received at the given URL. This will rarely
|
||||
be up to date, so use cautiously.
|
||||
"""
|
||||
likeCount: Float
|
||||
|
||||
"""
|
||||
The number of dislikes the video has received at the given URL. This will
|
||||
rarely be up to date, so use cautiously.
|
||||
"""
|
||||
dislikeCount: Float
|
||||
|
||||
"""
|
||||
The number of comments the video has received at the given URL. This will
|
||||
rarely be up to date, so use cautiously.
|
||||
"""
|
||||
commentCount: Float
|
||||
}
|
||||
|
||||
extend type Artist {
|
||||
"""
|
||||
Data about the artist from [TheAudioDB](http://www.theaudiodb.com/), a good
|
||||
source of biographical information and images.
|
||||
This field is provided by TheAudioDB extension.
|
||||
"""
|
||||
theAudioDB: TheAudioDBArtist
|
||||
}
|
||||
|
||||
extend type Recording {
|
||||
"""
|
||||
Data about the recording from [TheAudioDB](http://www.theaudiodb.com/).
|
||||
This field is provided by TheAudioDB extension.
|
||||
"""
|
||||
theAudioDB: TheAudioDBTrack
|
||||
}
|
||||
|
||||
extend type ReleaseGroup {
|
||||
"""
|
||||
Data about the release group from [TheAudioDB](http://www.theaudiodb.com/),
|
||||
a good source of descriptive information, reviews, and images.
|
||||
This field is provided by TheAudioDB extension.
|
||||
"""
|
||||
theAudioDB: TheAudioDBAlbum
|
||||
}
|
||||
`;
|
||||
129
src/index.js
129
src/index.js
|
|
@ -1,43 +1,104 @@
|
|||
import express from 'express'
|
||||
import graphqlHTTP from 'express-graphql'
|
||||
import compression from 'compression'
|
||||
import MusicBrainz, { CoverArtArchive } from './api'
|
||||
import schema from './schema'
|
||||
import createLoaders from './loaders'
|
||||
import express from 'express';
|
||||
import ExpressGraphQL from 'express-graphql';
|
||||
import compression from 'compression';
|
||||
import cors from 'cors';
|
||||
import MusicBrainz from './api/index.js';
|
||||
import Client from './api/client.js';
|
||||
import { baseSchema, createSchema } from './schema.js';
|
||||
import { createContext } from './context.js';
|
||||
import { loadExtension } from './extensions/index.js';
|
||||
import gql from './tag.js';
|
||||
|
||||
const { graphqlHTTP } = ExpressGraphQL;
|
||||
|
||||
const formatError = (err) => ({
|
||||
message: err.message,
|
||||
locations: err.locations,
|
||||
stack: err.stack
|
||||
})
|
||||
stack: err.stack,
|
||||
});
|
||||
|
||||
const middleware = ({
|
||||
const defaultExtensions = [
|
||||
'graphbrainz/extensions/cover-art-archive',
|
||||
'graphbrainz/extensions/fanart-tv',
|
||||
'graphbrainz/extensions/mediawiki',
|
||||
'graphbrainz/extensions/the-audio-db',
|
||||
];
|
||||
|
||||
function middleware({
|
||||
client = new MusicBrainz(),
|
||||
coverArtClient = new CoverArtArchive(),
|
||||
...options
|
||||
} = {}) => {
|
||||
const DEV = process.env.NODE_ENV !== 'production'
|
||||
const graphiql = DEV || process.env.GRAPHBRAINZ_GRAPHIQL === 'true'
|
||||
const loaders = createLoaders(client, coverArtClient)
|
||||
return graphqlHTTP({
|
||||
schema,
|
||||
context: { client, coverArtClient, loaders },
|
||||
pretty: DEV,
|
||||
graphiql,
|
||||
formatError: DEV ? formatError : undefined,
|
||||
...options
|
||||
})
|
||||
extensions = process.env.GRAPHBRAINZ_EXTENSIONS
|
||||
? JSON.parse(process.env.GRAPHBRAINZ_EXTENSIONS)
|
||||
: defaultExtensions,
|
||||
...middlewareOptions
|
||||
} = {}) {
|
||||
const DEV = process.env.NODE_ENV !== 'production';
|
||||
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({
|
||||
schema,
|
||||
context,
|
||||
pretty: DEV,
|
||||
graphiql,
|
||||
customFormatErrorFn: DEV ? formatError : undefined,
|
||||
...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
|
||||
|
||||
if (require.main === module) {
|
||||
require('dotenv').config({ silent: true })
|
||||
const app = express()
|
||||
const port = process.env.PORT || 3000
|
||||
const route = process.env.GRAPHBRAINZ_PATH || '/'
|
||||
app.use(compression())
|
||||
app.use(route, middleware())
|
||||
app.listen(port)
|
||||
console.log(`Listening on port ${port}.`)
|
||||
async function start(options) {
|
||||
const dotenv = await import('dotenv');
|
||||
dotenv.config({ silent: true });
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
const route = process.env.GRAPHBRAINZ_PATH || '/';
|
||||
const corsOptions = {
|
||||
origin: process.env.GRAPHBRAINZ_CORS_ORIGIN || false,
|
||||
methods: 'HEAD,GET,POST',
|
||||
};
|
||||
switch (corsOptions.origin) {
|
||||
case 'true':
|
||||
corsOptions.origin = true;
|
||||
break;
|
||||
case 'false':
|
||||
corsOptions.origin = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
app.use(compression());
|
||||
app.use(route, cors(corsOptions), middleware(options));
|
||||
app.listen(port);
|
||||
console.log(`Listening on port ${port}.`);
|
||||
}
|
||||
|
||||
export {
|
||||
Client,
|
||||
MusicBrainz,
|
||||
gql,
|
||||
baseSchema,
|
||||
createContext,
|
||||
createSchema,
|
||||
defaultExtensions,
|
||||
loadExtension,
|
||||
middleware,
|
||||
start,
|
||||
};
|
||||
|
|
|
|||
169
src/loaders.js
169
src/loaders.js
|
|
@ -1,105 +1,92 @@
|
|||
import DataLoader from 'dataloader'
|
||||
import LRUCache from 'lru-cache'
|
||||
import { toPlural } from './types/helpers'
|
||||
import createDebug from 'debug';
|
||||
import DataLoader from 'dataloader';
|
||||
import LRUCache from 'lru-cache';
|
||||
import { ONE_DAY, toPlural } from './util.js';
|
||||
|
||||
const debug = require('debug')('graphbrainz:loaders')
|
||||
const ONE_DAY = 24 * 60 * 60 * 1000
|
||||
const debug = createDebug('graphbrainz:loaders');
|
||||
|
||||
export default function createLoaders (client, coverArtClient) {
|
||||
export default function createLoaders(client) {
|
||||
// All loaders share a single LRU cache that will remember 8192 responses,
|
||||
// each cached for 1 day.
|
||||
const cache = LRUCache({
|
||||
const cache = new LRUCache({
|
||||
max: parseInt(process.env.GRAPHBRAINZ_CACHE_SIZE || 8192, 10),
|
||||
maxAge: parseInt(process.env.GRAPHBRAINZ_CACHE_TTL || ONE_DAY, 10),
|
||||
dispose (key) {
|
||||
debug(`Removed from cache. key=${key}`)
|
||||
}
|
||||
})
|
||||
dispose(key) {
|
||||
debug(`Removed from cache. key=${key}`);
|
||||
},
|
||||
});
|
||||
// Make the cache Map-like.
|
||||
cache.delete = cache.del
|
||||
cache.clear = cache.reset
|
||||
cache.delete = cache.del;
|
||||
cache.clear = cache.reset;
|
||||
|
||||
const lookup = new DataLoader(keys => {
|
||||
return Promise.all(keys.map(key => {
|
||||
const [ entityType, id, params = {} ] = key
|
||||
return client.lookup(entityType, id, params).then(entity => {
|
||||
if (entity) {
|
||||
// Store the entity type so we can determine what type of object this
|
||||
// is elsewhere in the code.
|
||||
entity._type = entityType
|
||||
}
|
||||
return entity
|
||||
})
|
||||
}))
|
||||
}, {
|
||||
cacheKeyFn: (key) => client.getLookupURL(...key),
|
||||
cacheMap: cache
|
||||
})
|
||||
|
||||
const browse = new DataLoader(keys => {
|
||||
return Promise.all(keys.map(key => {
|
||||
const [ entityType, params = {} ] = key
|
||||
return client.browse(entityType, params).then(list => {
|
||||
list[toPlural(entityType)].forEach(entity => {
|
||||
// Store the entity type so we can determine what type of object this
|
||||
// is elsewhere in the code.
|
||||
entity._type = entityType
|
||||
const lookup = new DataLoader(
|
||||
(keys) => {
|
||||
return Promise.all(
|
||||
keys.map((key) => {
|
||||
const [entityType, id, params = {}] = key;
|
||||
return client.lookup(entityType, id, params).then((entity) => {
|
||||
if (entity) {
|
||||
// Store the entity type so we can determine what type of object this
|
||||
// is elsewhere in the code.
|
||||
entity._type = entityType;
|
||||
}
|
||||
return entity;
|
||||
});
|
||||
})
|
||||
return list
|
||||
})
|
||||
}))
|
||||
}, {
|
||||
cacheKeyFn: (key) => client.getBrowseURL(...key),
|
||||
cacheMap: cache
|
||||
})
|
||||
);
|
||||
},
|
||||
{
|
||||
batch: false,
|
||||
cacheKeyFn: (key) => client.getLookupURL(...key),
|
||||
cacheMap: cache,
|
||||
}
|
||||
);
|
||||
|
||||
const search = new DataLoader(keys => {
|
||||
return Promise.all(keys.map(key => {
|
||||
const [ entityType, query, params = {} ] = key
|
||||
return client.search(entityType, query, params).then(list => {
|
||||
list[toPlural(entityType)].forEach(entity => {
|
||||
// Store the entity type so we can determine what type of object this
|
||||
// is elsewhere in the code.
|
||||
entity._type = entityType
|
||||
const browse = new DataLoader(
|
||||
(keys) => {
|
||||
return Promise.all(
|
||||
keys.map((key) => {
|
||||
const [entityType, params = {}] = key;
|
||||
return client.browse(entityType, params).then((list) => {
|
||||
list[toPlural(entityType)].forEach((entity) => {
|
||||
// Store the entity type so we can determine what type of object this
|
||||
// is elsewhere in the code.
|
||||
entity._type = entityType;
|
||||
});
|
||||
return list;
|
||||
});
|
||||
})
|
||||
return list
|
||||
})
|
||||
}))
|
||||
}, {
|
||||
cacheKeyFn: key => client.getSearchURL(...key),
|
||||
cacheMap: cache
|
||||
})
|
||||
);
|
||||
},
|
||||
{
|
||||
batch: false,
|
||||
cacheKeyFn: (key) => client.getBrowseURL(...key),
|
||||
cacheMap: cache,
|
||||
}
|
||||
);
|
||||
|
||||
const coverArt = new DataLoader(keys => {
|
||||
return Promise.all(keys.map(key => {
|
||||
const [ entityType, id ] = key
|
||||
return coverArtClient.images(...key).catch(err => {
|
||||
if (err.statusCode === 404) {
|
||||
return { images: [] }
|
||||
}
|
||||
throw err
|
||||
}).then(coverArt => {
|
||||
coverArt._parentType = entityType
|
||||
coverArt._parentID = id
|
||||
if (entityType === 'release') {
|
||||
coverArt._release = id
|
||||
} else {
|
||||
coverArt._release = coverArt.release && coverArt.release.split('/').pop()
|
||||
}
|
||||
return coverArt
|
||||
})
|
||||
}))
|
||||
}, {
|
||||
cacheKeyFn: key => `cover-art/${coverArtClient.getImagesURL(...key)}`,
|
||||
cacheMap: cache
|
||||
})
|
||||
const search = new DataLoader(
|
||||
(keys) => {
|
||||
return Promise.all(
|
||||
keys.map((key) => {
|
||||
const [entityType, query, params = {}] = key;
|
||||
return client.search(entityType, query, params).then((list) => {
|
||||
list[toPlural(entityType)].forEach((entity) => {
|
||||
// Store the entity type so we can determine what type of object this
|
||||
// is elsewhere in the code.
|
||||
entity._type = entityType;
|
||||
});
|
||||
return list;
|
||||
});
|
||||
})
|
||||
);
|
||||
},
|
||||
{
|
||||
batch: false,
|
||||
cacheKeyFn: (key) => client.getSearchURL(...key),
|
||||
cacheMap: cache,
|
||||
}
|
||||
);
|
||||
|
||||
const coverArtURL = new DataLoader(keys => {
|
||||
return Promise.all(keys.map(key => coverArtClient.imageURL(...key)))
|
||||
}, {
|
||||
cacheKeyFn: key => `cover-art/url/${coverArtClient.getImageURL(...key)}`,
|
||||
cacheMap: cache
|
||||
})
|
||||
|
||||
return { lookup, browse, search, coverArt, coverArtURL }
|
||||
return { lookup, browse, search };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { GraphQLObjectType, GraphQLString } from 'graphql'
|
||||
import { forwardConnectionArgs } from 'graphql-relay'
|
||||
import { resolveBrowse } from '../resolvers'
|
||||
import GraphQL from 'graphql';
|
||||
import GraphQLRelay from 'graphql-relay';
|
||||
import { resolveBrowse } from '../resolvers.js';
|
||||
import {
|
||||
MBID,
|
||||
AreaConnection,
|
||||
|
|
@ -15,62 +15,66 @@ import {
|
|||
RecordingConnection,
|
||||
ReleaseConnection,
|
||||
ReleaseGroupConnection,
|
||||
WorkConnection
|
||||
} from '../types'
|
||||
import { toWords, releaseGroupType, releaseStatus } from '../types/helpers'
|
||||
WorkConnection,
|
||||
} from '../types/index.js';
|
||||
import { releaseGroupType, releaseStatus } from '../types/helpers.js';
|
||||
import { toWords } from '../util.js';
|
||||
|
||||
const { GraphQLObjectType, GraphQLString } = GraphQL;
|
||||
const { forwardConnectionArgs } = GraphQLRelay;
|
||||
|
||||
const area = {
|
||||
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 = {
|
||||
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 = {
|
||||
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 = {
|
||||
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 = {
|
||||
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 = {
|
||||
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 = {
|
||||
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 = {
|
||||
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 = {
|
||||
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 = {
|
||||
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) {
|
||||
const typeName = toWords(connectionType.name.slice(0, -10))
|
||||
function createBrowseField(connectionType, args) {
|
||||
const typeName = toWords(connectionType.name.slice(0, -10));
|
||||
return {
|
||||
type: connectionType,
|
||||
description: `Browse ${typeName} entities linked to the given arguments.`,
|
||||
args: {
|
||||
...args,
|
||||
...forwardConnectionArgs
|
||||
...forwardConnectionArgs,
|
||||
},
|
||||
resolve: resolveBrowse
|
||||
}
|
||||
resolve: resolveBrowse,
|
||||
};
|
||||
}
|
||||
|
||||
export const BrowseQuery = new GraphQLObjectType({
|
||||
|
|
@ -79,7 +83,7 @@ export const BrowseQuery = new GraphQLObjectType({
|
|||
entity.`,
|
||||
fields: {
|
||||
areas: createBrowseField(AreaConnection, {
|
||||
collection
|
||||
collection,
|
||||
}),
|
||||
artists: createBrowseField(ArtistConnection, {
|
||||
area,
|
||||
|
|
@ -87,14 +91,14 @@ entity.`,
|
|||
recording,
|
||||
release,
|
||||
releaseGroup,
|
||||
work
|
||||
work,
|
||||
}),
|
||||
collections: createBrowseField(CollectionConnection, {
|
||||
area,
|
||||
artist,
|
||||
editor: {
|
||||
type: GraphQLString,
|
||||
description: 'The username of the editor who created the collection.'
|
||||
description: 'The username of the editor who created the collection.',
|
||||
},
|
||||
event,
|
||||
label,
|
||||
|
|
@ -102,22 +106,22 @@ entity.`,
|
|||
recording,
|
||||
release,
|
||||
releaseGroup,
|
||||
work
|
||||
work,
|
||||
}),
|
||||
events: createBrowseField(EventConnection, {
|
||||
area,
|
||||
artist,
|
||||
collection,
|
||||
place
|
||||
place,
|
||||
}),
|
||||
labels: createBrowseField(LabelConnection, {
|
||||
area,
|
||||
collection,
|
||||
release
|
||||
release,
|
||||
}),
|
||||
places: createBrowseField(PlaceConnection, {
|
||||
area,
|
||||
collection
|
||||
collection,
|
||||
}),
|
||||
recordings: createBrowseField(RecordingConnection, {
|
||||
artist,
|
||||
|
|
@ -125,9 +129,9 @@ entity.`,
|
|||
isrc: {
|
||||
type: 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, {
|
||||
area,
|
||||
|
|
@ -136,28 +140,28 @@ entity.`,
|
|||
discID: {
|
||||
type: DiscID,
|
||||
description: `A [disc ID](https://musicbrainz.org/doc/Disc_ID)
|
||||
associated with the release.`
|
||||
associated with the release.`,
|
||||
},
|
||||
label,
|
||||
recording,
|
||||
releaseGroup,
|
||||
track: {
|
||||
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: {
|
||||
type: MBID,
|
||||
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,
|
||||
status: releaseStatus
|
||||
status: releaseStatus,
|
||||
}),
|
||||
releaseGroups: createBrowseField(ReleaseGroupConnection, {
|
||||
artist,
|
||||
collection,
|
||||
release,
|
||||
type: releaseGroupType
|
||||
type: releaseGroupType,
|
||||
}),
|
||||
works: createBrowseField(WorkConnection, {
|
||||
artist,
|
||||
|
|
@ -165,18 +169,19 @@ release, but is not included in the credits for the release itself.`
|
|||
iswc: {
|
||||
type: ISWC,
|
||||
description: `The [International Standard Musical Work Code](https://musicbrainz.org/doc/ISWC)
|
||||
(ISWC) of the work.`
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
(ISWC) of the work.`,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export const browse = {
|
||||
type: BrowseQuery,
|
||||
description: 'Browse all MusicBrainz entities directly linked to another entity.',
|
||||
description:
|
||||
'Browse all MusicBrainz entities directly linked to another entity.',
|
||||
// We only have work to do once we know what entity types are being requested,
|
||||
// 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 { BrowseQuery, browse } from './browse'
|
||||
export { SearchQuery, search } from './search'
|
||||
export { LookupQuery, lookup } from './lookup.js';
|
||||
export { BrowseQuery, browse } from './browse.js';
|
||||
export { SearchQuery, search } from './search.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { GraphQLObjectType, GraphQLNonNull } from 'graphql'
|
||||
import { resolveLookup } from '../resolvers'
|
||||
import { mbid, toWords } from '../types/helpers'
|
||||
import GraphQL from 'graphql';
|
||||
import { resolveLookup } from '../resolvers.js';
|
||||
import { mbid } from '../types/helpers.js';
|
||||
import {
|
||||
Area,
|
||||
Artist,
|
||||
|
|
@ -18,17 +18,20 @@ import {
|
|||
Series,
|
||||
URL,
|
||||
URLString,
|
||||
Work
|
||||
} from '../types'
|
||||
Work,
|
||||
} from '../types/index.js';
|
||||
import { toWords } from '../util.js';
|
||||
|
||||
function createLookupField (entity, args) {
|
||||
const typeName = toWords(entity.name)
|
||||
const { GraphQLObjectType, GraphQLNonNull } = GraphQL;
|
||||
|
||||
function createLookupField(entity, args) {
|
||||
const typeName = toWords(entity.name);
|
||||
return {
|
||||
type: entity,
|
||||
description: `Look up a specific ${typeName} by its MBID.`,
|
||||
args: { mbid, ...args },
|
||||
resolve: resolveLookup
|
||||
}
|
||||
resolve: resolveLookup,
|
||||
};
|
||||
}
|
||||
|
||||
export const LookupQuery = new GraphQLObjectType({
|
||||
|
|
@ -45,12 +48,12 @@ export const LookupQuery = new GraphQLObjectType({
|
|||
discID: {
|
||||
type: new GraphQLNonNull(DiscID),
|
||||
description: `The [disc ID](https://musicbrainz.org/doc/Disc_ID)
|
||||
of the disc.`
|
||||
}
|
||||
of the disc.`,
|
||||
},
|
||||
},
|
||||
resolve: (root, { discID }, { loaders }, info) => {
|
||||
return loaders.lookup.load(['discid', discID])
|
||||
}
|
||||
return loaders.lookup.load(['discid', discID]);
|
||||
},
|
||||
},
|
||||
event: createLookupField(Event),
|
||||
instrument: createLookupField(Instrument),
|
||||
|
|
@ -65,23 +68,23 @@ of the disc.`
|
|||
...mbid,
|
||||
// Remove the non-null requirement that is usually on the `mbid` field
|
||||
// so that URLs can be looked up by `resource`.
|
||||
type: MBID
|
||||
type: MBID,
|
||||
},
|
||||
resource: {
|
||||
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 = {
|
||||
type: LookupQuery,
|
||||
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,
|
||||
// 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 { forwardConnectionArgs } from 'graphql-relay'
|
||||
import { resolveSearch } from '../resolvers'
|
||||
import GraphQL from 'graphql';
|
||||
import GraphQLRelay from 'graphql-relay';
|
||||
import { resolveSearch } from '../resolvers.js';
|
||||
import {
|
||||
AreaConnection,
|
||||
ArtistConnection,
|
||||
|
|
@ -12,12 +12,15 @@ import {
|
|||
ReleaseConnection,
|
||||
ReleaseGroupConnection,
|
||||
SeriesConnection,
|
||||
WorkConnection
|
||||
} from '../types'
|
||||
import { toWords } from '../types/helpers'
|
||||
WorkConnection,
|
||||
} from '../types/index.js';
|
||||
import { toWords } from '../util.js';
|
||||
|
||||
function createSearchField (connectionType) {
|
||||
const typeName = toWords(connectionType.name.slice(0, -10))
|
||||
const { GraphQLObjectType, GraphQLNonNull, GraphQLString } = GraphQL;
|
||||
const { forwardConnectionArgs } = GraphQLRelay;
|
||||
|
||||
function createSearchField(connectionType) {
|
||||
const typeName = toWords(connectionType.name.slice(0, -10));
|
||||
return {
|
||||
type: connectionType,
|
||||
description: `Search for ${typeName} entities matching the given query.`,
|
||||
|
|
@ -25,12 +28,12 @@ function createSearchField (connectionType) {
|
|||
query: {
|
||||
type: new GraphQLNonNull(GraphQLString),
|
||||
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({
|
||||
|
|
@ -47,16 +50,16 @@ export const SearchQuery = new GraphQLObjectType({
|
|||
releases: createSearchField(ReleaseConnection),
|
||||
releaseGroups: createSearchField(ReleaseGroupConnection),
|
||||
series: createSearchField(SeriesConnection),
|
||||
works: createSearchField(WorkConnection)
|
||||
}
|
||||
})
|
||||
works: createSearchField(WorkConnection),
|
||||
},
|
||||
});
|
||||
|
||||
export const search = {
|
||||
type: SearchQuery,
|
||||
description: 'Search for MusicBrainz entities using Lucene query syntax.',
|
||||
// We only have work to do once we know what entity types are being requested,
|
||||
// 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 {
|
||||
constructor ({
|
||||
constructor({
|
||||
limit = 1,
|
||||
period = 1000,
|
||||
concurrency = limit || 1,
|
||||
defaultPriority = 1
|
||||
defaultPriority = 1,
|
||||
} = {}) {
|
||||
this.limit = limit
|
||||
this.period = period
|
||||
this.defaultPriority = defaultPriority
|
||||
this.concurrency = concurrency
|
||||
this.queues = []
|
||||
this.numPending = 0
|
||||
this.periodStart = null
|
||||
this.periodCapacity = this.limit
|
||||
this.timer = null
|
||||
this.pendingFlush = false
|
||||
this.prevTaskID = null
|
||||
this.limit = limit;
|
||||
this.period = period;
|
||||
this.defaultPriority = defaultPriority;
|
||||
this.concurrency = concurrency;
|
||||
this.queues = [];
|
||||
this.numPending = 0;
|
||||
this.periodStart = null;
|
||||
this.periodCapacity = this.limit;
|
||||
this.timer = null;
|
||||
this.pendingFlush = false;
|
||||
this.prevTaskID = null;
|
||||
}
|
||||
|
||||
nextTaskID (prevTaskID = this.prevTaskID) {
|
||||
const id = (prevTaskID || 0) + 1
|
||||
this.prevTaskID = id
|
||||
return id
|
||||
nextTaskID(prevTaskID = this.prevTaskID) {
|
||||
const id = (prevTaskID || 0) + 1;
|
||||
this.prevTaskID = id;
|
||||
return id;
|
||||
}
|
||||
|
||||
enqueue (fn, args, priority = this.defaultPriority) {
|
||||
priority = Math.max(0, priority)
|
||||
enqueue(fn, args, priority = this.defaultPriority) {
|
||||
priority = Math.max(0, priority);
|
||||
return new Promise((resolve, reject) => {
|
||||
const queue = this.queues[priority] = this.queues[priority] || []
|
||||
const id = this.nextTaskID()
|
||||
debug(`Enqueuing task. id=${id} priority=${priority}`)
|
||||
queue.push({ fn, args, resolve, reject, id })
|
||||
const queue = (this.queues[priority] = this.queues[priority] || []);
|
||||
const id = this.nextTaskID();
|
||||
debug(`Enqueuing task. id=${id} priority=${priority}`);
|
||||
queue.push({ fn, args, resolve, reject, id });
|
||||
if (!this.pendingFlush) {
|
||||
this.pendingFlush = true
|
||||
this.pendingFlush = true;
|
||||
process.nextTick(() => {
|
||||
this.pendingFlush = false
|
||||
this.flush()
|
||||
})
|
||||
this.pendingFlush = false;
|
||||
this.flush();
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
dequeue () {
|
||||
let task
|
||||
dequeue() {
|
||||
let task;
|
||||
for (let i = this.queues.length - 1; i >= 0; i--) {
|
||||
const queue = this.queues[i]
|
||||
const queue = this.queues[i];
|
||||
if (queue && queue.length) {
|
||||
task = queue.shift()
|
||||
task = queue.shift();
|
||||
}
|
||||
if (!queue || !queue.length) {
|
||||
this.queues.length = i
|
||||
this.queues.length = i;
|
||||
}
|
||||
if (task) {
|
||||
break
|
||||
break;
|
||||
}
|
||||
}
|
||||
return task
|
||||
return task;
|
||||
}
|
||||
|
||||
flush () {
|
||||
flush() {
|
||||
if (this.numPending < this.concurrency && this.periodCapacity > 0) {
|
||||
const task = this.dequeue()
|
||||
const task = this.dequeue();
|
||||
if (task) {
|
||||
const { resolve, reject, fn, args, id } = task
|
||||
const { resolve, reject, fn, args, id } = task;
|
||||
if (this.timer == null) {
|
||||
const now = Date.now()
|
||||
let timeout = this.period
|
||||
const now = Date.now();
|
||||
let timeout = this.period;
|
||||
if (this.periodStart != null) {
|
||||
const delay = now - (this.periodStart + timeout)
|
||||
const delay = now - (this.periodStart + timeout);
|
||||
if (delay > 0 && delay <= timeout) {
|
||||
timeout -= delay
|
||||
timeout -= delay;
|
||||
}
|
||||
}
|
||||
this.periodStart = now
|
||||
this.periodStart = now;
|
||||
this.timer = setTimeout(() => {
|
||||
this.timer = null
|
||||
this.periodCapacity = this.limit
|
||||
this.flush()
|
||||
}, timeout)
|
||||
this.timer = null;
|
||||
this.periodCapacity = this.limit;
|
||||
this.flush();
|
||||
}, timeout);
|
||||
}
|
||||
this.numPending += 1
|
||||
this.periodCapacity -= 1
|
||||
this.numPending += 1;
|
||||
this.periodCapacity -= 1;
|
||||
const onResolve = (value) => {
|
||||
this.numPending -= 1
|
||||
resolve(value)
|
||||
this.flush()
|
||||
}
|
||||
this.numPending -= 1;
|
||||
resolve(value);
|
||||
this.flush();
|
||||
};
|
||||
const onReject = (err) => {
|
||||
this.numPending -= 1
|
||||
reject(err)
|
||||
this.flush()
|
||||
}
|
||||
debug(`Running task. id=${id}`)
|
||||
fn(...args).then(onResolve, onReject)
|
||||
this.flush()
|
||||
this.numPending -= 1;
|
||||
reject(err);
|
||||
this.flush();
|
||||
};
|
||||
debug(`Running task. id=${id}`);
|
||||
fn(...args).then(onResolve, onReject);
|
||||
this.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
275
src/resolvers.js
275
src/resolvers.js
|
|
@ -1,194 +1,216 @@
|
|||
import { toDashed, toSingular } from './types/helpers'
|
||||
import {
|
||||
import GraphQLRelay from 'graphql-relay';
|
||||
import { getFields, extendIncludes, toDashed, toSingular } from './util.js';
|
||||
|
||||
const {
|
||||
getOffsetWithDefault,
|
||||
connectionFromArray,
|
||||
connectionFromArraySlice
|
||||
} from 'graphql-relay'
|
||||
import { getFields, extendIncludes } from './util'
|
||||
connectionFromArraySlice,
|
||||
} = GraphQLRelay;
|
||||
|
||||
export function includeRelationships (params, info, fragments = info.fragments) {
|
||||
let fields = getFields(info, fragments)
|
||||
export function includeRelationships(params, info, fragments = info.fragments) {
|
||||
let fields = getFields(info, fragments);
|
||||
if (info.fieldName !== 'relationships') {
|
||||
if (fields.relationships) {
|
||||
fields = getFields(fields.relationships, fragments)
|
||||
fields = getFields(fields.relationships, fragments);
|
||||
} else {
|
||||
if (fields.edges) {
|
||||
fields = getFields(fields.edges, fragments)
|
||||
fields = getFields(fields.edges, fragments);
|
||||
if (fields.node) {
|
||||
return includeRelationships(params, fields.node, fragments)
|
||||
return includeRelationships(params, fields.node, fragments);
|
||||
}
|
||||
}
|
||||
return params
|
||||
return params;
|
||||
}
|
||||
}
|
||||
if (fields) {
|
||||
const relationships = Object.keys(fields)
|
||||
const includeRels = relationships.map(field => {
|
||||
return `${toDashed(toSingular(field))}-rels`
|
||||
})
|
||||
const relationships = Object.keys(fields);
|
||||
const includeRels = relationships.map((field) => {
|
||||
return `${toDashed(toSingular(field))}-rels`;
|
||||
});
|
||||
if (includeRels.length) {
|
||||
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) {
|
||||
const subqueryIncludes = {
|
||||
aliases: ['aliases'],
|
||||
artistCredit: ['artist-credits'],
|
||||
artistCredits: ['artist-credits'],
|
||||
isrcs: ['isrcs'],
|
||||
media: ['media', 'discids'],
|
||||
media: ['media'],
|
||||
'media.discs': ['discids'],
|
||||
'media.tracks': ['recordings'],
|
||||
rating: ['ratings'],
|
||||
tags: ['tags']
|
||||
}
|
||||
let fields = getFields(info, fragments)
|
||||
const include = []
|
||||
tags: ['tags'],
|
||||
};
|
||||
let fields = getFields(info, fragments, 1);
|
||||
const include = [];
|
||||
for (const key in subqueryIncludes) {
|
||||
if (fields[key]) {
|
||||
const value = subqueryIncludes[key]
|
||||
include.push(...value)
|
||||
const field = fields[key];
|
||||
if (field) {
|
||||
const value = subqueryIncludes[key];
|
||||
include.push(...value);
|
||||
}
|
||||
}
|
||||
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) {
|
||||
fields = getFields(fields.edges, fragments)
|
||||
if (fields.node) {
|
||||
params = includeSubqueries(params, fields.node, fragments)
|
||||
}
|
||||
}
|
||||
return params
|
||||
return params;
|
||||
}
|
||||
|
||||
export function resolveLookup (root, { mbid, ...params }, { loaders }, info) {
|
||||
export function resolveLookup(root, { mbid, ...params }, { loaders }, info) {
|
||||
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)
|
||||
params = includeSubqueries(params, info)
|
||||
params = includeRelationships(params, info)
|
||||
return loaders.lookup.load([entityType, mbid, params])
|
||||
const entityType = toDashed(info.fieldName);
|
||||
params = includeSubqueries(params, info);
|
||||
params = includeRelationships(params, info);
|
||||
return loaders.lookup.load([entityType, mbid, params]);
|
||||
}
|
||||
|
||||
export function resolveBrowse (root, {
|
||||
first,
|
||||
after,
|
||||
type = [],
|
||||
status = [],
|
||||
discID,
|
||||
isrc,
|
||||
iswc,
|
||||
...args
|
||||
}, { loaders }, info) {
|
||||
const pluralName = toDashed(info.fieldName)
|
||||
const singularName = toSingular(pluralName)
|
||||
export function resolveBrowse(
|
||||
root,
|
||||
{ first, after, type = [], status = [], discID, isrc, iswc, ...args },
|
||||
{ loaders },
|
||||
info
|
||||
) {
|
||||
const pluralName = toDashed(info.fieldName);
|
||||
const singularName = toSingular(pluralName);
|
||||
let params = {
|
||||
...args,
|
||||
type,
|
||||
status,
|
||||
limit: first,
|
||||
offset: getOffsetWithDefault(after, -1) + 1 || undefined
|
||||
}
|
||||
params = includeSubqueries(params, info)
|
||||
params = includeRelationships(params, info, info.fragments)
|
||||
const formatParam = value => value.toLowerCase().replace(/ /g, '')
|
||||
params.type = params.type.map(formatParam)
|
||||
params.status = params.status.map(formatParam)
|
||||
let request
|
||||
offset: getOffsetWithDefault(after, -1) + 1 || undefined,
|
||||
};
|
||||
params = includeSubqueries(params, info);
|
||||
params = includeRelationships(params, info, info.fragments);
|
||||
const formatParam = (value) => value.toLowerCase().replace(/ /g, '');
|
||||
params.type = params.type.map(formatParam);
|
||||
params.status = params.status.map(formatParam);
|
||||
let request;
|
||||
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`
|
||||
// and `discids` subqueries, and it is invalid to specify them.
|
||||
if (params.inc) {
|
||||
params.inc = params.inc.filter(value => {
|
||||
return value !== 'media' && value !== 'discids'
|
||||
})
|
||||
params.inc = params.inc.filter((value) => {
|
||||
return value !== 'media' && value !== 'discids';
|
||||
});
|
||||
}
|
||||
} 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) {
|
||||
request = loaders.lookup.load(['iswc', iswc, params])
|
||||
request = loaders.lookup.load(['iswc', iswc, params]);
|
||||
} 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
|
||||
// a Relay connection object.
|
||||
const {
|
||||
[pluralName]: arraySlice,
|
||||
[`${singularName}-offset`]: sliceStart = 0,
|
||||
[`${singularName}-count`]: arrayLength = arraySlice.length
|
||||
} = list
|
||||
const meta = { sliceStart, arrayLength }
|
||||
[`${singularName}-count`]: arrayLength = arraySlice.length,
|
||||
} = list;
|
||||
const meta = { sliceStart, arrayLength };
|
||||
const connection = connectionFromArraySlice(
|
||||
arraySlice,
|
||||
{ first, after },
|
||||
meta
|
||||
);
|
||||
return {
|
||||
nodes: connection.edges.map((edge) => edge.node),
|
||||
totalCount: arrayLength,
|
||||
...connectionFromArraySlice(arraySlice, { first, after }, meta)
|
||||
}
|
||||
})
|
||||
...connection,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveSearch (root, {
|
||||
after,
|
||||
first,
|
||||
query,
|
||||
...args
|
||||
}, { loaders }, info) {
|
||||
const pluralName = toDashed(info.fieldName)
|
||||
const singularName = toSingular(pluralName)
|
||||
export function resolveSearch(
|
||||
root,
|
||||
{ after, first, query, ...args },
|
||||
{ loaders },
|
||||
info
|
||||
) {
|
||||
const pluralName = toDashed(info.fieldName);
|
||||
const singularName = toSingular(pluralName);
|
||||
let params = {
|
||||
...args,
|
||||
limit: first,
|
||||
offset: getOffsetWithDefault(after, -1) + 1 || undefined
|
||||
}
|
||||
params = includeSubqueries(params, info)
|
||||
return loaders.search.load([singularName, query, params]).then(list => {
|
||||
offset: getOffsetWithDefault(after, -1) + 1 || undefined,
|
||||
};
|
||||
params = includeSubqueries(params, info);
|
||||
return loaders.search.load([singularName, query, params]).then((list) => {
|
||||
const {
|
||||
[pluralName]: arraySlice,
|
||||
offset: sliceStart,
|
||||
count: arrayLength
|
||||
} = list
|
||||
const meta = { sliceStart, arrayLength }
|
||||
const connection = {
|
||||
totalCount: arrayLength,
|
||||
...connectionFromArraySlice(arraySlice, { first, after }, meta)
|
||||
}
|
||||
count: arrayLength,
|
||||
} = list;
|
||||
const meta = { sliceStart, arrayLength };
|
||||
const connection = connectionFromArraySlice(
|
||||
arraySlice,
|
||||
{ first, after },
|
||||
meta
|
||||
);
|
||||
// Move the `score` field up to the edge object and make sure it's a
|
||||
// number (MusicBrainz returns a string).
|
||||
connection.edges.forEach(edge => { edge.score = +edge.node.score })
|
||||
return connection
|
||||
})
|
||||
const edges = connection.edges.map((edge) => ({
|
||||
...edge,
|
||||
score: +edge.node.score,
|
||||
}));
|
||||
const connectionWithExtras = {
|
||||
nodes: edges.map((edge) => edge.node),
|
||||
totalCount: arrayLength,
|
||||
...connection,
|
||||
edges,
|
||||
};
|
||||
return connectionWithExtras;
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveRelationship (rels, args, context, info) {
|
||||
const targetType = toDashed(toSingular(info.fieldName)).replace('-', '_')
|
||||
let matches = rels.filter(rel => rel['target-type'] === targetType)
|
||||
export function resolveRelationship(rels, args, context, info) {
|
||||
const targetType = toDashed(toSingular(info.fieldName)).replace('-', '_');
|
||||
let matches = rels.filter((rel) => rel['target-type'] === targetType);
|
||||
// There's no way to filter these at the API level, so do it here.
|
||||
if (args.direction != null) {
|
||||
matches = matches.filter(rel => rel.direction === args.direction)
|
||||
matches = matches.filter((rel) => rel.direction === args.direction);
|
||||
}
|
||||
if (args.type != null) {
|
||||
matches = matches.filter(rel => rel.type === args.type)
|
||||
matches = matches.filter((rel) => rel.type === args.type);
|
||||
}
|
||||
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);
|
||||
return {
|
||||
nodes: connection.edges.map((edge) => edge.node),
|
||||
totalCount: matches.length,
|
||||
...connectionFromArray(matches, args)
|
||||
}
|
||||
...connection,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveLinked (entity, args, context, info) {
|
||||
const parentEntity = toDashed(info.parentType.name)
|
||||
args = { ...args, [parentEntity]: entity.id }
|
||||
return resolveBrowse(entity, args, context, info)
|
||||
export function resolveLinked(entity, args, context, info) {
|
||||
const parentEntity = toDashed(info.parentType.name);
|
||||
args = { ...args, [parentEntity]: entity.id };
|
||||
return resolveBrowse(entity, args, context, info);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -196,29 +218,34 @@ export function resolveLinked (entity, args, context, info) {
|
|||
* for a particular field that's being requested, make another request to grab
|
||||
* it (after making sure it isn't already available).
|
||||
*/
|
||||
export function createSubqueryResolver ({ inc, key } = {}, handler = value => value) {
|
||||
export function createSubqueryResolver(
|
||||
{ inc, key } = {},
|
||||
handler = (value) => value
|
||||
) {
|
||||
return (entity, args, { loaders }, info) => {
|
||||
key = key || toDashed(info.fieldName)
|
||||
let promise
|
||||
key = key || toDashed(info.fieldName);
|
||||
let promise;
|
||||
if (key in entity) {
|
||||
promise = Promise.resolve(entity)
|
||||
promise = Promise.resolve(entity);
|
||||
} else {
|
||||
const { _type: entityType, id } = entity
|
||||
const params = { inc: [inc || key] }
|
||||
promise = loaders.lookup.load([entityType, id, params])
|
||||
const { _type: entityType, id } = entity;
|
||||
const params = { inc: [inc || key] };
|
||||
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) {
|
||||
const { releases } = disc
|
||||
export function resolveDiscReleases(disc, args, context, info) {
|
||||
const { releases } = disc;
|
||||
if (releases != null) {
|
||||
const connection = connectionFromArray(releases, args);
|
||||
return {
|
||||
nodes: connection.edges.map((edge) => edge.node),
|
||||
totalCount: releases.length,
|
||||
...connectionFromArray(releases, args)
|
||||
}
|
||||
...connection,
|
||||
};
|
||||
}
|
||||
args = { ...args, discID: disc.id }
|
||||
return resolveBrowse(disc, args, context, info)
|
||||
args = { ...args, discID: disc.id };
|
||||
return resolveBrowse(disc, args, context, info);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,62 @@
|
|||
import { GraphQLSchema, GraphQLObjectType } from 'graphql'
|
||||
import { lookup, browse, search } from './queries'
|
||||
import { nodeField } from './types/node'
|
||||
import createDebug from 'debug';
|
||||
import GraphQL from 'graphql';
|
||||
import GraphQLToolsSchema from '@graphql-tools/schema';
|
||||
import { lookup, browse, search } from './queries/index.js';
|
||||
import { nodeField } from './types/node.js';
|
||||
|
||||
export default new GraphQLSchema({
|
||||
const { GraphQLSchema, GraphQLObjectType, extendSchema, parse } = GraphQL;
|
||||
const { addResolversToSchema } = GraphQLToolsSchema;
|
||||
|
||||
const debug = createDebug('graphbrainz:schema');
|
||||
|
||||
export function applyExtension(extension, schema, options = {}) {
|
||||
let outputSchema = schema;
|
||||
if (extension.extendSchema) {
|
||||
if (typeof extension.extendSchema === 'object') {
|
||||
debug(
|
||||
`Extending schema via an object from the “${extension.name}” extension.`
|
||||
);
|
||||
const { schemas = [], resolvers } = extension.extendSchema;
|
||||
outputSchema = schemas.reduce((updatedSchema, extensionSchema) => {
|
||||
if (typeof extensionSchema === 'string') {
|
||||
extensionSchema = parse(extensionSchema);
|
||||
}
|
||||
return extendSchema(updatedSchema, extensionSchema);
|
||||
}, outputSchema);
|
||||
if (resolvers) {
|
||||
outputSchema = addResolversToSchema({
|
||||
schema: outputSchema,
|
||||
resolvers,
|
||||
});
|
||||
}
|
||||
} else if (typeof extension.extendSchema === 'function') {
|
||||
debug(
|
||||
`Extending schema via a function from the “${extension.name}” extension.`
|
||||
);
|
||||
outputSchema = extension.extendSchema(schema, options);
|
||||
} else {
|
||||
throw new Error(
|
||||
`The “${extension.name}” extension contains an invalid ` +
|
||||
`\`extendSchema\` value: ${extension.extendSchema}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fix for `graphql-tools` creating a new Query type with no description.
|
||||
if (outputSchema._queryType.description === undefined) {
|
||||
outputSchema._queryType.description = schema._queryType.description;
|
||||
}
|
||||
return outputSchema;
|
||||
}
|
||||
|
||||
export function createSchema(schema, options = {}) {
|
||||
const { extensions = [] } = options;
|
||||
return extensions.reduce((updatedSchema, extension) => {
|
||||
return applyExtension(extension, updatedSchema, options);
|
||||
}, schema);
|
||||
}
|
||||
|
||||
export const baseSchema = new GraphQLSchema({
|
||||
query: new GraphQLObjectType({
|
||||
name: 'Query',
|
||||
description: `The query root, from which multiple types of MusicBrainz
|
||||
|
|
@ -11,7 +65,7 @@ requests can be made.`,
|
|||
lookup,
|
||||
browse,
|
||||
search,
|
||||
node: nodeField
|
||||
})
|
||||
})
|
||||
})
|
||||
node: nodeField,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
17
src/tag.js
Normal file
17
src/tag.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* This module only exists because as of this writing, `graphql-tag` doesn't
|
||||
* support type, field, or argument descriptions. That's a bummer, so this
|
||||
* simple tag is provided instead. It doesn't support any type of interpolation
|
||||
* whatsoever, but will parse the GraphQL document, allow syntax highlighting,
|
||||
* and enable Prettier formatting.
|
||||
*/
|
||||
import GraphQL from 'graphql';
|
||||
|
||||
const { parse } = GraphQL;
|
||||
|
||||
export default function gql(literals, ...interpolations) {
|
||||
if (literals.length !== 1 || interpolations.length) {
|
||||
throw new Error('The gql template tag does not support interpolation.');
|
||||
}
|
||||
return parse(literals[0]);
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import {
|
||||
GraphQLObjectType,
|
||||
GraphQLBoolean
|
||||
} from 'graphql/type'
|
||||
import { Locale } from './scalars'
|
||||
import { name, sortName, fieldWithID } from './helpers'
|
||||
import GraphQL from 'graphql';
|
||||
import { Locale } from './scalars.js';
|
||||
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',
|
||||
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
|
||||
|
|
@ -13,22 +13,29 @@ entity will be given as a result – even if the actual name wouldn’t be.`,
|
|||
fields: () => ({
|
||||
name: {
|
||||
...name,
|
||||
description: 'The aliased name of the entity.'
|
||||
description: 'The aliased name of the entity.',
|
||||
},
|
||||
sortName,
|
||||
locale: {
|
||||
type: Locale,
|
||||
description: `The locale (language and/or country) in which the alias is
|
||||
used.`
|
||||
used.`,
|
||||
},
|
||||
primary: {
|
||||
type: GraphQLBoolean,
|
||||
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', {
|
||||
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,25 +1,29 @@
|
|||
import { GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql'
|
||||
import Node from './node'
|
||||
import Entity from './entity'
|
||||
import GraphQL from 'graphql';
|
||||
import { Node } from './node.js';
|
||||
import { Entity } from './entity.js';
|
||||
import {
|
||||
id,
|
||||
mbid,
|
||||
name,
|
||||
sortName,
|
||||
disambiguation,
|
||||
aliases,
|
||||
artists,
|
||||
events,
|
||||
labels,
|
||||
places,
|
||||
releases,
|
||||
relationships,
|
||||
collections,
|
||||
tags,
|
||||
connectionWithExtras
|
||||
} from './helpers'
|
||||
fieldWithID,
|
||||
connectionWithExtras,
|
||||
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',
|
||||
description: `[Areas](https://musicbrainz.org/doc/Area) are geographic regions
|
||||
or settlements (countries, cities, or the like).`,
|
||||
|
|
@ -35,8 +39,23 @@ or settlements (countries, cities, or the like).`,
|
|||
type: new GraphQLList(GraphQLString),
|
||||
description: `[ISO 3166 codes](https://en.wikipedia.org/wiki/ISO_3166) are
|
||||
the codes assigned by ISO to countries and subdivisions.`,
|
||||
resolve: data => data['iso-3166-1-codes']
|
||||
args: {
|
||||
standard: {
|
||||
type: GraphQLString,
|
||||
description: `Specify the particular ISO standard codes to retrieve.
|
||||
Available ISO standards are 3166-1, 3166-2, and 3166-3.`,
|
||||
defaultValue: '3166-1',
|
||||
},
|
||||
},
|
||||
resolve: (data, args) => {
|
||||
const { standard = '3166-1' } = args;
|
||||
return data[`iso-${standard}-codes`];
|
||||
},
|
||||
},
|
||||
...fieldWithID('type', {
|
||||
description: `The type of area (country, city, etc. – see the [possible
|
||||
values](https://musicbrainz.org/doc/Area)).`,
|
||||
}),
|
||||
artists,
|
||||
events,
|
||||
labels,
|
||||
|
|
@ -44,9 +63,10 @@ the codes assigned by ISO to countries and subdivisions.`,
|
|||
releases,
|
||||
relationships,
|
||||
collections,
|
||||
tags
|
||||
})
|
||||
})
|
||||
tags,
|
||||
}),
|
||||
});
|
||||
|
||||
export const AreaConnection = connectionWithExtras(Area)
|
||||
export default Area
|
||||
export const AreaConnection = connectionWithExtras(Area);
|
||||
|
||||
export const areas = linkedQuery(AreaConnection);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { GraphQLObjectType, GraphQLString } from 'graphql/type'
|
||||
import Artist from './artist'
|
||||
import GraphQL from 'graphql';
|
||||
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',
|
||||
description: `[Artist credits](https://musicbrainz.org/doc/Artist_Credits)
|
||||
indicate who is the main credited artist (or artists) for releases, release
|
||||
|
|
@ -14,23 +17,41 @@ track, etc., and join phrases between them.`,
|
|||
description: `The entity representing the artist referenced in the
|
||||
credits.`,
|
||||
resolve: (source) => {
|
||||
const { artist } = source
|
||||
const { artist } = source;
|
||||
if (artist) {
|
||||
artist._type = 'artist'
|
||||
artist._type = 'artist';
|
||||
}
|
||||
return artist
|
||||
}
|
||||
return artist;
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: GraphQLString,
|
||||
description: `The name of the artist as credited in the specific release,
|
||||
track, etc.`
|
||||
track, etc.`,
|
||||
},
|
||||
joinPhrase: {
|
||||
type: GraphQLString,
|
||||
description: `Join phrases might include words and/or punctuation to
|
||||
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 Node from './node'
|
||||
import Entity from './entity'
|
||||
import Area from './area'
|
||||
import { IPI, ISNI } from './scalars'
|
||||
import GraphQL from 'graphql';
|
||||
import { Node } from './node.js';
|
||||
import { Entity } from './entity.js';
|
||||
import { Area } from './area.js';
|
||||
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 {
|
||||
resolveWithFallback,
|
||||
fieldWithID,
|
||||
|
|
@ -11,20 +21,13 @@ import {
|
|||
name,
|
||||
sortName,
|
||||
disambiguation,
|
||||
aliases,
|
||||
lifeSpan,
|
||||
recordings,
|
||||
releases,
|
||||
releaseGroups,
|
||||
works,
|
||||
relationships,
|
||||
collections,
|
||||
rating,
|
||||
tags,
|
||||
connectionWithExtras
|
||||
} from './helpers'
|
||||
connectionWithExtras,
|
||||
linkedQuery,
|
||||
} from './helpers.js';
|
||||
|
||||
const Artist = new GraphQLObjectType({
|
||||
const { GraphQLObjectType, GraphQLString, GraphQLList } = GraphQL;
|
||||
|
||||
export const Artist = new GraphQLObjectType({
|
||||
name: 'Artist',
|
||||
description: `An [artist](https://musicbrainz.org/doc/Artist) is generally a
|
||||
musician, group of musicians, or other music professional (like a producer or
|
||||
|
|
@ -42,42 +45,42 @@ even a fictional character.`,
|
|||
country: {
|
||||
type: GraphQLString,
|
||||
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: {
|
||||
type: Area,
|
||||
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: {
|
||||
type: Area,
|
||||
description: `The area in which an artist began their career (or where
|
||||
they were born, if the artist is a person).`,
|
||||
resolve: resolveWithFallback(['begin-area', 'begin_area'])
|
||||
resolve: resolveWithFallback(['begin-area', 'begin_area']),
|
||||
},
|
||||
endArea: {
|
||||
type: Area,
|
||||
description: `The area in which an artist ended their career (or where
|
||||
they died, if the artist is a person).`,
|
||||
resolve: resolveWithFallback(['end-area', 'end_area'])
|
||||
resolve: resolveWithFallback(['end-area', 'end_area']),
|
||||
},
|
||||
lifeSpan,
|
||||
...fieldWithID('gender', {
|
||||
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', {
|
||||
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: {
|
||||
type: new GraphQLList(IPI),
|
||||
description: `List of [Interested Parties Information](https://musicbrainz.org/doc/IPI)
|
||||
(IPI) codes for the artist.`
|
||||
(IPI) codes for the artist.`,
|
||||
},
|
||||
isnis: {
|
||||
type: new GraphQLList(ISNI),
|
||||
description: `List of [International Standard Name Identifier](https://musicbrainz.org/doc/ISNI)
|
||||
(ISNI) codes for the artist.`
|
||||
(ISNI) codes for the artist.`,
|
||||
},
|
||||
recordings,
|
||||
releases,
|
||||
|
|
@ -86,9 +89,10 @@ neither. Groups do not have genders.`
|
|||
relationships,
|
||||
collections,
|
||||
rating,
|
||||
tags
|
||||
})
|
||||
})
|
||||
tags,
|
||||
}),
|
||||
});
|
||||
|
||||
export const ArtistConnection = connectionWithExtras(Artist)
|
||||
export default Artist
|
||||
export const ArtistConnection = connectionWithExtras(Artist);
|
||||
|
||||
export const artists = linkedQuery(ArtistConnection);
|
||||
|
|
|
|||
|
|
@ -1,32 +1,31 @@
|
|||
import {
|
||||
GraphQLObjectType,
|
||||
GraphQLNonNull,
|
||||
GraphQLString
|
||||
} from 'graphql/type'
|
||||
import Node from './node'
|
||||
import Entity from './entity'
|
||||
import GraphQL from 'graphql';
|
||||
import { Node } from './node.js';
|
||||
import { Entity } from './entity.js';
|
||||
import {
|
||||
id,
|
||||
mbid,
|
||||
name,
|
||||
areas,
|
||||
artists,
|
||||
events,
|
||||
instruments,
|
||||
labels,
|
||||
places,
|
||||
recordings,
|
||||
releases,
|
||||
releaseGroups,
|
||||
series,
|
||||
works,
|
||||
fieldWithID,
|
||||
resolveHyphenated,
|
||||
createCollectionField,
|
||||
connectionWithExtras
|
||||
} from './helpers'
|
||||
connectionWithExtras,
|
||||
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',
|
||||
description: `[Collections](https://musicbrainz.org/doc/Collections) are
|
||||
lists of entities that users can create.`,
|
||||
|
|
@ -37,15 +36,15 @@ lists of entities that users can create.`,
|
|||
name,
|
||||
editor: {
|
||||
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: {
|
||||
type: new GraphQLNonNull(GraphQLString),
|
||||
description: 'The type of entity listed in the collection.',
|
||||
resolve: resolveHyphenated
|
||||
resolve: resolveHyphenated,
|
||||
},
|
||||
...fieldWithID('type', {
|
||||
description: 'The type of collection.'
|
||||
description: 'The type of collection.',
|
||||
}),
|
||||
areas: createCollectionField(areas),
|
||||
artists: createCollectionField(artists),
|
||||
|
|
@ -57,9 +56,12 @@ lists of entities that users can create.`,
|
|||
releases: createCollectionField(releases),
|
||||
releaseGroups: createCollectionField(releaseGroups),
|
||||
series: createCollectionField(series),
|
||||
works: createCollectionField(works)
|
||||
})
|
||||
})
|
||||
works: createCollectionField(works),
|
||||
}),
|
||||
});
|
||||
|
||||
export const CollectionConnection = connectionWithExtras(Collection)
|
||||
export default Collection
|
||||
export const CollectionConnection = connectionWithExtras(Collection);
|
||||
|
||||
export const collections = linkedQuery(CollectionConnection, {
|
||||
description: 'A list of collections containing this entity.',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
import {
|
||||
GraphQLObjectType,
|
||||
GraphQLNonNull,
|
||||
GraphQLList,
|
||||
GraphQLBoolean,
|
||||
GraphQLString,
|
||||
GraphQLInt
|
||||
} from 'graphql/type'
|
||||
import { URLString } from './scalars'
|
||||
|
||||
export const CoverArtImageThumbnails = new GraphQLObjectType({
|
||||
name: 'CoverArtImageThumbnails',
|
||||
description: `URLs for thumbnails of different sizes for a particular piece of
|
||||
cover art.`,
|
||||
fields: () => ({
|
||||
small: {
|
||||
type: URLString,
|
||||
description: `The URL of a small version of the cover art, where the
|
||||
maximum dimension is 250px.`
|
||||
},
|
||||
large: {
|
||||
type: URLString,
|
||||
description: `The URL of a large version of the cover art, where the
|
||||
maximum dimension is 500px.`
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export default new GraphQLObjectType({
|
||||
name: 'CoverArtImage',
|
||||
description: 'An individual piece of album artwork from the [Cover Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive).',
|
||||
fields: () => ({
|
||||
fileID: {
|
||||
type: new GraphQLNonNull(GraphQLString),
|
||||
description: 'The Internet Archive’s internal file ID for the image.',
|
||||
resolve: image => image.id
|
||||
},
|
||||
image: {
|
||||
type: new GraphQLNonNull(URLString),
|
||||
description: 'The URL at which the image can be found.'
|
||||
},
|
||||
thumbnails: {
|
||||
type: CoverArtImageThumbnails,
|
||||
description: 'A set of thumbnails for the image.'
|
||||
},
|
||||
front: {
|
||||
type: new GraphQLNonNull(GraphQLBoolean),
|
||||
description: 'Whether this image depicts the “main front” of the release.'
|
||||
},
|
||||
back: {
|
||||
type: new GraphQLNonNull(GraphQLBoolean),
|
||||
description: 'Whether this image depicts the “main back” of the release.'
|
||||
},
|
||||
types: {
|
||||
type: new GraphQLList(GraphQLString),
|
||||
description: `A list of [image types](https://musicbrainz.org/doc/Cover_Art/Types)
|
||||
describing what part(s) of the release the image includes.`
|
||||
},
|
||||
edit: {
|
||||
type: GraphQLInt,
|
||||
description: 'The MusicBrainz edit ID.'
|
||||
},
|
||||
approved: {
|
||||
type: GraphQLBoolean,
|
||||
description: 'Whether the image was approved by the MusicBrainz edit system.'
|
||||
},
|
||||
comment: {
|
||||
type: GraphQLString,
|
||||
description: 'A free-text comment left for the image.'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
import {
|
||||
GraphQLObjectType,
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
GraphQLBoolean,
|
||||
GraphQLInt
|
||||
} from 'graphql/type'
|
||||
import CoverArtImage from './cover-art-image'
|
||||
import { CoverArtImageSize } from './enums'
|
||||
import Release from './release'
|
||||
import { URLString } from './scalars'
|
||||
import { resolveLookup } from '../resolvers'
|
||||
import { getFields } from '../util'
|
||||
|
||||
/**
|
||||
* Return a resolver that will call `resolveFn` only if the requested field on
|
||||
* the object is null or not present.
|
||||
*/
|
||||
function createFallbackResolver (resolveFn) {
|
||||
return function resolve (coverArt, args, context, info) {
|
||||
const value = coverArt[info.fieldName]
|
||||
if (value == null) {
|
||||
return resolveFn(coverArt, args, context, info)
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
function resolveImage (coverArt, { size }, { loaders }, info) {
|
||||
if (size === 'FULL') {
|
||||
size = null
|
||||
}
|
||||
const field = info.fieldName
|
||||
if (coverArt.images) {
|
||||
const matches = coverArt.images.filter(image => image[field])
|
||||
if (!matches.length) {
|
||||
return null
|
||||
} else if (matches.length === 1) {
|
||||
const match = matches[0]
|
||||
if (size === 250) {
|
||||
return match.thumbnails.small
|
||||
} else if (size === 500) {
|
||||
return match.thumbnails.large
|
||||
} else {
|
||||
return match.image
|
||||
}
|
||||
}
|
||||
}
|
||||
if (coverArt[field] !== false) {
|
||||
const {
|
||||
_parentType: entityType = 'release',
|
||||
_parentID: id = coverArt._release
|
||||
} = coverArt
|
||||
return loaders.coverArtURL.load([entityType, id, field, size])
|
||||
}
|
||||
}
|
||||
|
||||
const size = {
|
||||
type: CoverArtImageSize,
|
||||
description: `The size of the image to retrieve. By default, the returned
|
||||
image will have its full original dimensions, but certain thumbnail sizes may be
|
||||
retrieved as well.`,
|
||||
defaultValue: null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get around both the circular dependency between the release and cover art
|
||||
* types, and not have to define an identical `release` field twice on
|
||||
* `ReleaseCoverArt` and `ReleaseGroupCoverArt`.
|
||||
*/
|
||||
function createReleaseField () {
|
||||
return {
|
||||
type: new GraphQLNonNull(Release),
|
||||
description: 'The particular release shown in the returned cover art.',
|
||||
resolve: (coverArt, args, context, info) => {
|
||||
const id = coverArt._release
|
||||
const fields = Object.keys(getFields(info))
|
||||
if (fields.length > 1 || fields[0] !== 'mbid') {
|
||||
return resolveLookup(coverArt, { mbid: id }, context, info)
|
||||
}
|
||||
return { id }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This type combines two sets of data from different places. One is a *summary*
|
||||
// of the images available at the Cover Art Archive, found in the `cover-art-archive`
|
||||
// field on releases. The other is the actual list of images with their metadata,
|
||||
// fetched from the Cover Art Archive itself rather than MusicBrainz. Depending
|
||||
// on what fields are requested, we may only need to fetch one or the other, or
|
||||
// both. Much of the summary data can be reconstructed if we already fetched the
|
||||
// full image list, for example.
|
||||
export const ReleaseCoverArt = new GraphQLObjectType({
|
||||
name: 'ReleaseCoverArt',
|
||||
description: `An object containing a list of the cover art images for a
|
||||
release obtained from the [Cover Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive),
|
||||
as well as a summary of what artwork is available.`,
|
||||
fields: () => ({
|
||||
front: {
|
||||
type: URLString,
|
||||
description: `The URL of an image depicting the album cover or “main
|
||||
front” of the release, i.e. the front of the packaging of the audio recording
|
||||
(or in the case of a digital release, the image associated with it in a digital
|
||||
media store).
|
||||
|
||||
In the MusicBrainz schema, this field is a Boolean value indicating the presence
|
||||
of a front image, whereas here the value is the URL for the image itself if one
|
||||
exists. You can check for null if you just want to determine the presence of an
|
||||
image.`,
|
||||
args: { size },
|
||||
resolve: resolveImage
|
||||
},
|
||||
back: {
|
||||
type: URLString,
|
||||
description: `The URL of an image depicting the “main back” of the
|
||||
release, i.e. the back of the packaging of the audio recording.
|
||||
|
||||
In the MusicBrainz schema, this field is a Boolean value indicating the presence
|
||||
of a back image, whereas here the value is the URL for the image itself. You can
|
||||
check for null if you just want to determine the presence of an image.`,
|
||||
args: { size },
|
||||
resolve: resolveImage
|
||||
},
|
||||
images: {
|
||||
type: new GraphQLList(CoverArtImage),
|
||||
description: `A list of images depicting the different sides and surfaces
|
||||
of a release’s media and packaging.`,
|
||||
resolve: createFallbackResolver((coverArt, args, { loaders }) => {
|
||||
if (coverArt.count === 0) {
|
||||
return []
|
||||
}
|
||||
return loaders.coverArt.load(['release', coverArt._release])
|
||||
.then(coverArt => coverArt.images)
|
||||
})
|
||||
},
|
||||
artwork: {
|
||||
type: new GraphQLNonNull(GraphQLBoolean),
|
||||
description: 'Whether there is artwork present for this release.',
|
||||
resolve: createFallbackResolver(coverArt => coverArt.images.length > 0)
|
||||
},
|
||||
darkened: {
|
||||
type: new GraphQLNonNull(GraphQLBoolean),
|
||||
description: `Whether the Cover Art Archive has received a take-down
|
||||
request for this release’s artwork, disallowing new uploads.`,
|
||||
resolve: createFallbackResolver((coverArt, args, { loaders }) => {
|
||||
return loaders.lookup.load(['release', coverArt._release])
|
||||
.then(release => release['cover-art-archive'].darkened)
|
||||
})
|
||||
},
|
||||
count: {
|
||||
type: new GraphQLNonNull(GraphQLInt),
|
||||
description: 'The number of artwork images present for this release.',
|
||||
resolve: createFallbackResolver(coverArt => coverArt.images.length)
|
||||
},
|
||||
release: createReleaseField()
|
||||
})
|
||||
})
|
||||
|
||||
export const ReleaseGroupCoverArt = new GraphQLObjectType({
|
||||
name: 'ReleaseGroupCoverArt',
|
||||
description: `An object containing the cover art for a release group obtained
|
||||
from the [Cover Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive). For
|
||||
release groups, just the front cover of a particular release will be selected.`,
|
||||
fields: () => ({
|
||||
front: {
|
||||
type: URLString,
|
||||
description: `The URL of an image depicting the album cover or “main
|
||||
front” of a release in the release group, i.e. the front of the packaging of the
|
||||
audio recording (or in the case of a digital release, the image associated with
|
||||
it in a digital media store).`,
|
||||
args: { size },
|
||||
resolve: resolveImage
|
||||
},
|
||||
images: {
|
||||
type: new GraphQLList(CoverArtImage),
|
||||
description: `A list of images returned by the [Cover Art
|
||||
Archive](https://musicbrainz.org/doc/Cover_Art_Archive) for a release group. A
|
||||
particular release’s front image will be included in the list, and likely no
|
||||
others, even if other images are available.`
|
||||
},
|
||||
artwork: {
|
||||
type: new GraphQLNonNull(GraphQLBoolean),
|
||||
description: 'Whether there is artwork present for this release group.',
|
||||
resolve: createFallbackResolver(coverArt => coverArt.images.length > 0)
|
||||
},
|
||||
release: createReleaseField()
|
||||
})
|
||||
})
|
||||
|
|
@ -1,17 +1,15 @@
|
|||
import {
|
||||
GraphQLObjectType,
|
||||
GraphQLNonNull,
|
||||
GraphQLList,
|
||||
GraphQLInt
|
||||
} from 'graphql/type'
|
||||
import { forwardConnectionArgs } from 'graphql-relay'
|
||||
import Node from './node'
|
||||
import { DiscID } from './scalars'
|
||||
import { ReleaseConnection } from './release'
|
||||
import { resolveDiscReleases } from '../resolvers'
|
||||
import { id, resolveHyphenated } from './helpers'
|
||||
import GraphQL from 'graphql';
|
||||
import GraphQLRelay from 'graphql-relay';
|
||||
import { Node } from './node.js';
|
||||
import { DiscID } from './scalars.js';
|
||||
import { ReleaseConnection } from './release.js';
|
||||
import { resolveDiscReleases } from '../resolvers.js';
|
||||
import { id, resolveHyphenated } from './helpers.js';
|
||||
|
||||
export default new GraphQLObjectType({
|
||||
const { GraphQLObjectType, GraphQLNonNull, GraphQLList, GraphQLInt } = GraphQL;
|
||||
const { forwardConnectionArgs } = GraphQLRelay;
|
||||
|
||||
export const Disc = new GraphQLObjectType({
|
||||
name: 'Disc',
|
||||
description: `Information about the physical CD and releases associated with a
|
||||
particular [disc ID](https://musicbrainz.org/doc/Disc_ID).`,
|
||||
|
|
@ -21,26 +19,26 @@ particular [disc ID](https://musicbrainz.org/doc/Disc_ID).`,
|
|||
discID: {
|
||||
type: new GraphQLNonNull(DiscID),
|
||||
description: `The [disc ID](https://musicbrainz.org/doc/Disc_ID) of this disc.`,
|
||||
resolve: disc => disc.id
|
||||
resolve: (disc) => disc.id,
|
||||
},
|
||||
offsetCount: {
|
||||
type: new GraphQLNonNull(GraphQLInt),
|
||||
description: 'The number of offsets (tracks) on the disc.',
|
||||
resolve: resolveHyphenated
|
||||
resolve: resolveHyphenated,
|
||||
},
|
||||
offsets: {
|
||||
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: {
|
||||
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: {
|
||||
type: ReleaseConnection,
|
||||
description: 'The list of releases linked to this disc ID.',
|
||||
args: forwardConnectionArgs,
|
||||
resolve: resolveDiscReleases
|
||||
}
|
||||
})
|
||||
})
|
||||
resolve: resolveDiscReleases,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
import { GraphQLInterfaceType } from 'graphql'
|
||||
import { mbid, connectionWithExtras } from './helpers'
|
||||
import GraphQL from 'graphql';
|
||||
import { mbid, connectionWithExtras, resolveType } from './helpers.js';
|
||||
|
||||
const Entity = new GraphQLInterfaceType({
|
||||
const { GraphQLInterfaceType } = GraphQL;
|
||||
|
||||
export const Entity = new GraphQLInterfaceType({
|
||||
name: 'Entity',
|
||||
description: 'An entity in the MusicBrainz schema.',
|
||||
resolveType (value) {
|
||||
if (value._type && require.resolve(`./${value._type}`)) {
|
||||
return require(`./${value._type}`).default
|
||||
}
|
||||
},
|
||||
fields: () => ({ mbid })
|
||||
})
|
||||
resolveType,
|
||||
fields: () => ({ mbid }),
|
||||
});
|
||||
|
||||
export const EntityConnection = connectionWithExtras(Entity)
|
||||
export default Entity
|
||||
export const EntityConnection = connectionWithExtras(Entity);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { GraphQLEnumType } from 'graphql/type'
|
||||
import GraphQL from 'graphql';
|
||||
|
||||
const { GraphQLEnumType } = GraphQL;
|
||||
|
||||
export const ArtistType = new GraphQLEnumType({
|
||||
name: 'ArtistType',
|
||||
|
|
@ -8,36 +10,37 @@ etc.`,
|
|||
PERSON: {
|
||||
name: 'Person',
|
||||
description: 'This indicates an individual person.',
|
||||
value: 'Person'
|
||||
value: 'Person',
|
||||
},
|
||||
GROUP: {
|
||||
name: 'Group',
|
||||
description: `This indicates a group of people that may or may not have a
|
||||
distinctive name.`,
|
||||
value: 'Group'
|
||||
value: 'Group',
|
||||
},
|
||||
ORCHESTRA: {
|
||||
name: 'Orchestra',
|
||||
description: 'This indicates an orchestra (a large instrumental ensemble).',
|
||||
value: 'Orchestra'
|
||||
description:
|
||||
'This indicates an orchestra (a large instrumental ensemble).',
|
||||
value: 'Orchestra',
|
||||
},
|
||||
CHOIR: {
|
||||
name: 'Choir',
|
||||
description: 'This indicates a choir/chorus (a large vocal ensemble).',
|
||||
value: 'Choir'
|
||||
value: 'Choir',
|
||||
},
|
||||
CHARACTER: {
|
||||
name: 'Character',
|
||||
description: 'This indicates an individual fictional character.',
|
||||
value: 'Character'
|
||||
value: 'Character',
|
||||
},
|
||||
OTHER: {
|
||||
name: 'Other',
|
||||
description: 'An artist which does not fit into the other categories.',
|
||||
value: 'Other'
|
||||
}
|
||||
}
|
||||
})
|
||||
value: 'Other',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const CoverArtImageSize = new GraphQLEnumType({
|
||||
name: 'CoverArtImageSize',
|
||||
|
|
@ -47,20 +50,20 @@ Archive](https://musicbrainz.org/doc/Cover_Art_Archive).`,
|
|||
SMALL: {
|
||||
name: 'Small',
|
||||
description: 'A maximum dimension of 250px.',
|
||||
value: 250
|
||||
value: 250,
|
||||
},
|
||||
LARGE: {
|
||||
name: 'Large',
|
||||
description: 'A maximum dimension of 500px.',
|
||||
value: 500
|
||||
value: 500,
|
||||
},
|
||||
FULL: {
|
||||
name: 'Full',
|
||||
description: 'The image’s original dimensions, with no maximum.',
|
||||
value: null
|
||||
}
|
||||
}
|
||||
})
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const ReleaseStatus = new GraphQLEnumType({
|
||||
name: 'ReleaseStatus',
|
||||
|
|
@ -71,29 +74,29 @@ bootleg, etc.`,
|
|||
name: 'Official',
|
||||
description: `Any release officially sanctioned by the artist and/or their
|
||||
record company. (Most releases will fit into this category.)`,
|
||||
value: 'Official'
|
||||
value: 'Official',
|
||||
},
|
||||
PROMOTION: {
|
||||
name: 'Promotion',
|
||||
description: `A giveaway release or a release intended to promote an
|
||||
upcoming official release, e.g. prerelease albums or releases included with a
|
||||
magazine.`,
|
||||
value: 'Promotion'
|
||||
value: 'Promotion',
|
||||
},
|
||||
BOOTLEG: {
|
||||
name: 'Bootleg',
|
||||
description: `An unofficial/underground release that was not sanctioned by
|
||||
the artist and/or the record company.`,
|
||||
value: 'Bootleg'
|
||||
value: 'Bootleg',
|
||||
},
|
||||
PSEUDORELEASE: {
|
||||
name: 'Pseudo-Release',
|
||||
description: `A pseudo-release is a duplicate release for
|
||||
translation/transliteration purposes.`,
|
||||
value: 'Pseudo-Release'
|
||||
}
|
||||
}
|
||||
})
|
||||
value: 'Pseudo-Release',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const ReleaseGroupType = new GraphQLEnumType({
|
||||
name: 'ReleaseGroupType',
|
||||
|
|
@ -106,14 +109,14 @@ etc.`,
|
|||
release, generally consists of previously unreleased material (unless this type
|
||||
is combined with secondary types which change that, such as “Compilation”). This
|
||||
includes album re-issues, with or without bonus tracks.`,
|
||||
value: 'Album'
|
||||
value: 'Album',
|
||||
},
|
||||
SINGLE: {
|
||||
name: 'Single',
|
||||
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
|
||||
after its main song.`,
|
||||
value: 'Single'
|
||||
value: 'Single',
|
||||
},
|
||||
EP: {
|
||||
name: 'EP',
|
||||
|
|
@ -123,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
|
||||
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.`,
|
||||
value: 'EP'
|
||||
value: 'EP',
|
||||
},
|
||||
OTHER: {
|
||||
name: 'Other',
|
||||
description: 'Any release that does not fit any of the other categories.',
|
||||
value: 'Other'
|
||||
value: 'Other',
|
||||
},
|
||||
BROADCAST: {
|
||||
name: 'Broadcast',
|
||||
description: `An episodic release that was originally broadcast via radio,
|
||||
television, or the Internet, including podcasts.`,
|
||||
value: 'Broadcast'
|
||||
value: 'Broadcast',
|
||||
},
|
||||
COMPILATION: {
|
||||
name: 'Compilation',
|
||||
description: `A compilation is a collection of previously released tracks
|
||||
by one or more artists.`,
|
||||
value: 'Compilation'
|
||||
value: 'Compilation',
|
||||
},
|
||||
SOUNDTRACK: {
|
||||
name: 'Soundtrack',
|
||||
description: `A soundtrack is the musical score to a movie, TV series,
|
||||
stage show, computer game, etc.`,
|
||||
value: 'Soundtrack'
|
||||
value: 'Soundtrack',
|
||||
},
|
||||
SPOKENWORD: {
|
||||
name: 'Spoken Word',
|
||||
description: 'A non-music spoken word release.',
|
||||
value: 'Spoken Word'
|
||||
value: 'Spoken Word',
|
||||
},
|
||||
INTERVIEW: {
|
||||
name: 'Interview',
|
||||
description: `An interview release contains an interview, generally with
|
||||
an artist.`,
|
||||
value: 'Interview'
|
||||
value: 'Interview',
|
||||
},
|
||||
AUDIOBOOK: {
|
||||
name: 'Audiobook',
|
||||
description: 'An audiobook is a book read by a narrator without music.',
|
||||
value: 'Audiobook'
|
||||
value: 'Audiobook',
|
||||
},
|
||||
LIVE: {
|
||||
name: 'Live',
|
||||
description: 'A release that was recorded live.',
|
||||
value: 'Live'
|
||||
value: 'Live',
|
||||
},
|
||||
REMIX: {
|
||||
name: 'Remix',
|
||||
description: `A release that was (re)mixed from previously released
|
||||
material.`,
|
||||
value: 'Remix'
|
||||
value: 'Remix',
|
||||
},
|
||||
DJMIX: {
|
||||
name: 'DJ-mix',
|
||||
|
|
@ -182,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
|
||||
manner, and the DJ who does this modification is usually (although not always)
|
||||
credited in a fairly prominent way.`,
|
||||
value: 'DJ-mix'
|
||||
value: 'DJ-mix',
|
||||
},
|
||||
MIXTAPE: {
|
||||
name: 'Mixtape/Street',
|
||||
|
|
@ -198,18 +201,18 @@ significant proportion of new material, including original production or
|
|||
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
|
||||
to labels.`,
|
||||
value: 'Mixtape/Street'
|
||||
value: 'Mixtape/Street',
|
||||
},
|
||||
DEMO: {
|
||||
name: 'Demo',
|
||||
description: `A release that was recorded for limited circulation or
|
||||
reference use rather than for general public release.`,
|
||||
value: 'Demo'
|
||||
value: 'Demo',
|
||||
},
|
||||
NAT: {
|
||||
name: 'Non-Album Track',
|
||||
description: 'A non-album track (special case).',
|
||||
value: 'NAT'
|
||||
}
|
||||
}
|
||||
})
|
||||
value: 'NAT',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,23 +1,26 @@
|
|||
import { GraphQLObjectType, GraphQLString, GraphQLBoolean } from 'graphql/type'
|
||||
import Node from './node'
|
||||
import Entity from './entity'
|
||||
import { Time } from './scalars'
|
||||
import GraphQL from 'graphql';
|
||||
import { Node } from './node.js';
|
||||
import { Entity } from './entity.js';
|
||||
import { Time } from './scalars.js';
|
||||
import {
|
||||
fieldWithID,
|
||||
id,
|
||||
mbid,
|
||||
name,
|
||||
disambiguation,
|
||||
aliases,
|
||||
lifeSpan,
|
||||
relationships,
|
||||
collections,
|
||||
rating,
|
||||
tags,
|
||||
connectionWithExtras
|
||||
} from './helpers'
|
||||
connectionWithExtras,
|
||||
linkedQuery,
|
||||
} from './helpers.js';
|
||||
import { aliases } from './alias.js';
|
||||
import { collections } from './collection.js';
|
||||
import { lifeSpan } from './life-span.js';
|
||||
import { rating } from './rating.js';
|
||||
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',
|
||||
description: `An [event](https://musicbrainz.org/doc/Event) refers to an
|
||||
organised event which people can attend, and is relevant to MusicBrainz.
|
||||
|
|
@ -32,27 +35,29 @@ Generally this means live performances, like concerts and festivals.`,
|
|||
lifeSpan,
|
||||
time: {
|
||||
type: Time,
|
||||
description: 'The start time of the event.'
|
||||
description: 'The start time of the event.',
|
||||
},
|
||||
cancelled: {
|
||||
type: GraphQLBoolean,
|
||||
description: 'Whether or not the event took place.'
|
||||
description: 'Whether or not the event took place.',
|
||||
},
|
||||
setlist: {
|
||||
type: GraphQLString,
|
||||
description: `A list of songs performed, optionally including links to
|
||||
artists and works. See the [setlist documentation](https://musicbrainz.org/doc/Event/Setlist)
|
||||
for syntax and examples.`
|
||||
for syntax and examples.`,
|
||||
},
|
||||
...fieldWithID('type', {
|
||||
description: 'What kind of event the event is, e.g. concert, festival, etc.'
|
||||
description:
|
||||
'What kind of event the event is, e.g. concert, festival, etc.',
|
||||
}),
|
||||
relationships,
|
||||
collections,
|
||||
rating,
|
||||
tags
|
||||
})
|
||||
})
|
||||
tags,
|
||||
}),
|
||||
});
|
||||
|
||||
export const EventConnection = connectionWithExtras(Event)
|
||||
export default Event
|
||||
export const EventConnection = connectionWithExtras(Event);
|
||||
|
||||
export const events = linkedQuery(EventConnection);
|
||||
|
|
|
|||
|
|
@ -1,291 +1,156 @@
|
|||
import dashify from 'dashify'
|
||||
import pascalCase from 'pascalcase'
|
||||
import {
|
||||
GraphQLObjectType,
|
||||
GraphQLString,
|
||||
GraphQLInt,
|
||||
GraphQLList,
|
||||
GraphQLNonNull
|
||||
} from 'graphql'
|
||||
import {
|
||||
import GraphQL from 'graphql';
|
||||
import GraphQLRelay from 'graphql-relay';
|
||||
import { MBID } from './scalars.js';
|
||||
import { ReleaseGroupType, ReleaseStatus } from './enums.js';
|
||||
import { resolveLinked } from '../resolvers.js';
|
||||
import { toDashed, toPascal, toSingular, toPlural, toWords } from '../util.js';
|
||||
|
||||
const { GraphQLString, GraphQLInt, GraphQLList, GraphQLNonNull } = GraphQL;
|
||||
const {
|
||||
globalIdField,
|
||||
connectionArgs,
|
||||
connectionDefinitions,
|
||||
connectionFromArray,
|
||||
forwardConnectionArgs
|
||||
} 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'
|
||||
forwardConnectionArgs,
|
||||
} = GraphQLRelay;
|
||||
|
||||
export const toPascal = pascalCase
|
||||
export const toDashed = dashify
|
||||
const TYPE_NAMES = {
|
||||
discid: 'Disc',
|
||||
url: 'URL',
|
||||
};
|
||||
|
||||
export function toPlural (name) {
|
||||
return name.endsWith('s') ? name : name + 's'
|
||||
export function resolveType(value, context, info) {
|
||||
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 resolveHyphenated(obj, args, context, info) {
|
||||
const name = toDashed(info.fieldName);
|
||||
return obj[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) {
|
||||
const name = toDashed(info.fieldName)
|
||||
return obj[name]
|
||||
}
|
||||
|
||||
export function resolveWithFallback (keys) {
|
||||
export function resolveWithFallback(keys) {
|
||||
return (obj) => {
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i]
|
||||
const key = keys[i];
|
||||
if (key in obj) {
|
||||
return obj[key]
|
||||
return obj[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function fieldWithID (name, config = {}) {
|
||||
export function fieldWithID(name, config = {}) {
|
||||
config = {
|
||||
type: GraphQLString,
|
||||
resolve: resolveHyphenated,
|
||||
...config
|
||||
}
|
||||
const isPlural = config.type instanceof GraphQLList
|
||||
const singularName = isPlural ? toSingular(name) : name
|
||||
const idName = isPlural ? `${singularName}IDs` : `${name}ID`
|
||||
const s = isPlural ? 's' : ''
|
||||
...config,
|
||||
};
|
||||
const isPlural = config.type instanceof GraphQLList;
|
||||
const singularName = isPlural ? toSingular(name) : name;
|
||||
const idName = isPlural ? `${singularName}IDs` : `${name}ID`;
|
||||
const s = isPlural ? 's' : '';
|
||||
const idConfig = {
|
||||
type: isPlural ? new GraphQLList(MBID) : MBID,
|
||||
description: `The MBID${s} associated with the value${s} of the \`${name}\`
|
||||
field.`,
|
||||
resolve: resolveHyphenated
|
||||
}
|
||||
resolve: (entity, args, { loaders }) => {
|
||||
const fieldName = toDashed(idName);
|
||||
if (fieldName in entity) {
|
||||
return entity[fieldName];
|
||||
}
|
||||
return loaders.lookup
|
||||
.load([entity._type, entity.id])
|
||||
.then((data) => data[fieldName]);
|
||||
},
|
||||
};
|
||||
return {
|
||||
[name]: config,
|
||||
[idName]: idConfig
|
||||
}
|
||||
[idName]: idConfig,
|
||||
};
|
||||
}
|
||||
|
||||
export function createCollectionField (config) {
|
||||
const typeName = toPlural(toWords(config.type.name.slice(0, -10)))
|
||||
export function createCollectionField(config) {
|
||||
const typeName = toPlural(toWords(config.type.name.slice(0, -10)));
|
||||
return {
|
||||
...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 = {
|
||||
type: new GraphQLNonNull(MBID),
|
||||
description: 'The MBID of the entity.',
|
||||
resolve: entity => entity.id
|
||||
}
|
||||
resolve: (entity) => entity.id,
|
||||
};
|
||||
export const name = {
|
||||
type: GraphQLString,
|
||||
description: 'The official name of the entity.'
|
||||
}
|
||||
description: 'The official name of the entity.',
|
||||
};
|
||||
export const sortName = {
|
||||
type: GraphQLString,
|
||||
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
|
||||
the front).`,
|
||||
resolve: resolveHyphenated
|
||||
}
|
||||
resolve: resolveHyphenated,
|
||||
};
|
||||
export const title = {
|
||||
type: GraphQLString,
|
||||
description: 'The official title of the entity.'
|
||||
}
|
||||
description: 'The official title of the entity.',
|
||||
};
|
||||
export const disambiguation = {
|
||||
type: GraphQLString,
|
||||
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
|
||||
}
|
||||
description:
|
||||
'A comment used to help distinguish identically named entitites.',
|
||||
};
|
||||
|
||||
function linkedQuery (connectionType, { args, ...config } = {}) {
|
||||
const typeName = toPlural(toWords(connectionType.name.slice(0, -10)))
|
||||
export function linkedQuery(connectionType, { args, ...config } = {}) {
|
||||
const typeName = toPlural(toWords(connectionType.name.slice(0, -10)));
|
||||
return {
|
||||
type: connectionType,
|
||||
description: `A list of ${typeName} linked to this entity.`,
|
||||
args: {
|
||||
...args,
|
||||
...forwardConnectionArgs
|
||||
...forwardConnectionArgs,
|
||||
},
|
||||
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) => ({
|
||||
totalCount: value.length,
|
||||
...connectionFromArray(value, args)
|
||||
}))
|
||||
})
|
||||
export const works = linkedQuery(WorkConnection)
|
||||
|
||||
export const totalCount = {
|
||||
type: GraphQLInt,
|
||||
description: `A count of the total number of items in this connection,
|
||||
ignoring pagination.`
|
||||
}
|
||||
ignoring pagination.`,
|
||||
};
|
||||
|
||||
export const score = {
|
||||
type: GraphQLInt,
|
||||
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({
|
||||
nodeType,
|
||||
connectionFields: () => ({ totalCount }),
|
||||
edgeFields: () => ({ score })
|
||||
}).connectionType
|
||||
connectionFields: () => ({
|
||||
nodes: {
|
||||
type: new GraphQLList(nodeType),
|
||||
description: `A list of nodes in the connection (without going through the
|
||||
\`edges\` field).`,
|
||||
},
|
||||
totalCount,
|
||||
}),
|
||||
edgeFields: () => ({ score }),
|
||||
}).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,19 +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 { default as ReleaseGroup, ReleaseGroupConnection } from './release-group'
|
||||
export { default as Series, SeriesConnection } from './series'
|
||||
export { default as Tag, TagConnection } from './tag'
|
||||
export { default as URL, URLConnection } from './url'
|
||||
export { default as Work, WorkConnection } from './work'
|
||||
export {
|
||||
DateType,
|
||||
DiscID,
|
||||
IPI,
|
||||
ISRC,
|
||||
ISWC,
|
||||
MBID,
|
||||
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 Node from './node'
|
||||
import Entity from './entity'
|
||||
import GraphQL from 'graphql';
|
||||
import { Node } from './node.js';
|
||||
import { Entity } from './entity.js';
|
||||
import {
|
||||
fieldWithID,
|
||||
id,
|
||||
mbid,
|
||||
name,
|
||||
disambiguation,
|
||||
aliases,
|
||||
relationships,
|
||||
collections,
|
||||
tags,
|
||||
connectionWithExtras
|
||||
} from './helpers'
|
||||
connectionWithExtras,
|
||||
linkedQuery,
|
||||
} from './helpers.js';
|
||||
import { aliases } from './alias.js';
|
||||
import { collections } from './collection.js';
|
||||
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',
|
||||
description: `[Instruments](https://musicbrainz.org/doc/Instrument) are
|
||||
devices created or adapted to make musical sounds. Instruments are primarily
|
||||
|
|
@ -29,18 +32,19 @@ used in relationships between two other entities.`,
|
|||
description: {
|
||||
type: GraphQLString,
|
||||
description: `A brief description of the main characteristics of the
|
||||
instrument.`
|
||||
instrument.`,
|
||||
},
|
||||
...fieldWithID('type', {
|
||||
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)
|
||||
classification.`
|
||||
classification.`,
|
||||
}),
|
||||
relationships,
|
||||
collections,
|
||||
tags
|
||||
})
|
||||
})
|
||||
tags,
|
||||
}),
|
||||
});
|
||||
|
||||
export const InstrumentConnection = connectionWithExtras(Instrument)
|
||||
export default Instrument
|
||||
export const InstrumentConnection = connectionWithExtras(Instrument);
|
||||
|
||||
export const instruments = linkedQuery(InstrumentConnection);
|
||||
|
|
|
|||
|
|
@ -1,31 +1,29 @@
|
|||
import {
|
||||
GraphQLObjectType,
|
||||
GraphQLList,
|
||||
GraphQLString,
|
||||
GraphQLInt
|
||||
} from 'graphql/type'
|
||||
import Node from './node'
|
||||
import Entity from './entity'
|
||||
import { IPI } from './scalars'
|
||||
import Area from './area'
|
||||
import GraphQL from 'graphql';
|
||||
import { Node } from './node.js';
|
||||
import { Entity } from './entity.js';
|
||||
import { IPI } from './scalars.js';
|
||||
import { Area } from './area.js';
|
||||
import {
|
||||
id,
|
||||
mbid,
|
||||
name,
|
||||
sortName,
|
||||
disambiguation,
|
||||
aliases,
|
||||
lifeSpan,
|
||||
releases,
|
||||
relationships,
|
||||
collections,
|
||||
tags,
|
||||
rating,
|
||||
fieldWithID,
|
||||
connectionWithExtras
|
||||
} from './helpers'
|
||||
connectionWithExtras,
|
||||
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',
|
||||
description: `[Labels](https://musicbrainz.org/doc/Label) represent mostly
|
||||
(but not only) imprints. To a lesser extent, a label entity may be created to
|
||||
|
|
@ -40,34 +38,35 @@ represent a record company.`,
|
|||
aliases,
|
||||
country: {
|
||||
type: GraphQLString,
|
||||
description: 'The country of origin for the label.'
|
||||
description: 'The country of origin for the label.',
|
||||
},
|
||||
area: {
|
||||
type: Area,
|
||||
description: 'The area in which the label is based.'
|
||||
description: 'The area in which the label is based.',
|
||||
},
|
||||
lifeSpan,
|
||||
labelCode: {
|
||||
type: GraphQLInt,
|
||||
description: `The [“LC” code](https://musicbrainz.org/doc/Label/Label_Code)
|
||||
of the label.`
|
||||
of the label.`,
|
||||
},
|
||||
ipis: {
|
||||
type: new GraphQLList(IPI),
|
||||
description: `List of [Interested Parties Information](https://musicbrainz.org/doc/IPI)
|
||||
codes for the label.`
|
||||
codes for the label.`,
|
||||
},
|
||||
...fieldWithID('type', {
|
||||
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,
|
||||
relationships,
|
||||
collections,
|
||||
rating,
|
||||
tags
|
||||
})
|
||||
})
|
||||
tags,
|
||||
}),
|
||||
});
|
||||
|
||||
export const LabelConnection = connectionWithExtras(Label)
|
||||
export default Label
|
||||
export const LabelConnection = connectionWithExtras(Label);
|
||||
|
||||
export const labels = linkedQuery(LabelConnection);
|
||||
|
|
|
|||
|
|
@ -1,22 +1,32 @@
|
|||
import { GraphQLObjectType, GraphQLBoolean } from 'graphql/type'
|
||||
import { DateType } from './scalars'
|
||||
import GraphQL from 'graphql';
|
||||
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',
|
||||
description: `Fields indicating the begin and end date of an entity’s
|
||||
lifetime, including whether it has ended (even if the date is unknown).`,
|
||||
fields: () => ({
|
||||
begin: {
|
||||
type: DateType,
|
||||
description: 'The start date of the entity’s life span.'
|
||||
description: 'The start date of the entity’s life span.',
|
||||
},
|
||||
end: {
|
||||
type: DateType,
|
||||
description: 'The end date of the entity’s life span.'
|
||||
description: 'The end date of the entity’s life span.',
|
||||
},
|
||||
ended: {
|
||||
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 {
|
||||
GraphQLObjectType,
|
||||
GraphQLList,
|
||||
GraphQLString,
|
||||
GraphQLInt
|
||||
} from 'graphql/type'
|
||||
import Disc from './disc'
|
||||
import { resolveHyphenated, fieldWithID } from './helpers'
|
||||
import GraphQL from 'graphql';
|
||||
import { Disc } from './disc.js';
|
||||
import { Track } from './track.js';
|
||||
import { resolveHyphenated, fieldWithID } from './helpers.js';
|
||||
import { createSubqueryResolver } from '../resolvers.js';
|
||||
|
||||
export default new GraphQLObjectType({
|
||||
const { GraphQLObjectType, GraphQLList, GraphQLString, GraphQLInt } = GraphQL;
|
||||
|
||||
export const Media = new GraphQLObjectType({
|
||||
name: 'Medium',
|
||||
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
|
||||
|
|
@ -17,25 +16,34 @@ cassette) and can optionally also have a title.`,
|
|||
fields: () => ({
|
||||
title: {
|
||||
type: GraphQLString,
|
||||
description: 'The title of this particular medium.'
|
||||
description: 'The title of this particular medium.',
|
||||
},
|
||||
...fieldWithID('format', {
|
||||
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: {
|
||||
type: GraphQLInt,
|
||||
description: `The order of this medium in the release (for example, in a
|
||||
multi-disc release).`
|
||||
multi-disc release).`,
|
||||
},
|
||||
trackCount: {
|
||||
type: GraphQLInt,
|
||||
description: 'The number of audio tracks on this medium.',
|
||||
resolve: resolveHyphenated
|
||||
resolve: resolveHyphenated,
|
||||
},
|
||||
discs: {
|
||||
type: new GraphQLList(Disc),
|
||||
description: 'A list of physical discs and their disc IDs for this medium.'
|
||||
}
|
||||
})
|
||||
})
|
||||
description:
|
||||
'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,28 +1,17 @@
|
|||
import { nodeDefinitions, fromGlobalId } from 'graphql-relay'
|
||||
import { toDashed } from './helpers'
|
||||
import GraphQLRelay from 'graphql-relay';
|
||||
import { toDashed } from '../util.js';
|
||||
import { resolveType } from './helpers.js';
|
||||
|
||||
const debug = require('debug')('graphbrainz:types/node')
|
||||
|
||||
const TYPE_MODULES = {
|
||||
discid: 'disc'
|
||||
}
|
||||
const { nodeDefinitions, fromGlobalId } = GraphQLRelay;
|
||||
|
||||
const { nodeInterface, nodeField } = nodeDefinitions(
|
||||
(globalID, { loaders }) => {
|
||||
const { type, id } = fromGlobalId(globalID)
|
||||
const entityType = toDashed(type)
|
||||
return loaders.lookup.load([entityType, id])
|
||||
const { type, id } = fromGlobalId(globalID);
|
||||
const entityType = toDashed(type);
|
||||
return loaders.lookup.load([entityType, id]);
|
||||
},
|
||||
(obj) => {
|
||||
const type = TYPE_MODULES[obj._type] || obj._type
|
||||
try {
|
||||
return require(`./${type}`).default
|
||||
} catch (err) {
|
||||
debug(`Failed to load type: ${type}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
)
|
||||
resolveType
|
||||
);
|
||||
|
||||
export default nodeInterface
|
||||
export { nodeInterface, nodeField }
|
||||
export const Node = nodeInterface;
|
||||
export { nodeInterface, nodeField };
|
||||
|
|
|
|||
|
|
@ -1,22 +1,25 @@
|
|||
import { GraphQLObjectType, GraphQLString } from 'graphql/type'
|
||||
import Node from './node'
|
||||
import Entity from './entity'
|
||||
import { Degrees } from './scalars'
|
||||
import Area from './area'
|
||||
import GraphQL from 'graphql';
|
||||
import { Node } from './node.js';
|
||||
import { Entity } from './entity.js';
|
||||
import { Degrees } from './scalars.js';
|
||||
import { Area } from './area.js';
|
||||
import {
|
||||
id,
|
||||
mbid,
|
||||
name,
|
||||
disambiguation,
|
||||
aliases,
|
||||
lifeSpan,
|
||||
events,
|
||||
fieldWithID,
|
||||
relationships,
|
||||
collections,
|
||||
tags,
|
||||
connectionWithExtras
|
||||
} from './helpers'
|
||||
connectionWithExtras,
|
||||
linkedQuery,
|
||||
} from './helpers.js';
|
||||
import { aliases } from './alias.js';
|
||||
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({
|
||||
name: 'Coordinates',
|
||||
|
|
@ -24,16 +27,17 @@ export const Coordinates = new GraphQLObjectType({
|
|||
fields: () => ({
|
||||
latitude: {
|
||||
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: {
|
||||
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',
|
||||
description: `A [place](https://musicbrainz.org/doc/Place) is a venue, studio,
|
||||
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: {
|
||||
type: GraphQLString,
|
||||
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: {
|
||||
type: Area,
|
||||
description: `The area entity representing the area, such as the city, in
|
||||
which the place is located.`
|
||||
which the place is located.`,
|
||||
},
|
||||
coordinates: {
|
||||
type: Coordinates,
|
||||
description: 'The geographic coordinates of the place.'
|
||||
description: 'The geographic coordinates of the place.',
|
||||
},
|
||||
lifeSpan,
|
||||
...fieldWithID('type', {
|
||||
description: `The type categorises the place based on its primary
|
||||
function.`
|
||||
function.`,
|
||||
}),
|
||||
events,
|
||||
relationships,
|
||||
collections,
|
||||
tags
|
||||
})
|
||||
})
|
||||
tags,
|
||||
}),
|
||||
});
|
||||
|
||||
export const PlaceConnection = connectionWithExtras(Place)
|
||||
export default Place
|
||||
export const PlaceConnection = connectionWithExtras(Place);
|
||||
|
||||
export const places = linkedQuery(PlaceConnection);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import {
|
||||
GraphQLObjectType,
|
||||
GraphQLNonNull,
|
||||
GraphQLInt
|
||||
} from 'graphql/type'
|
||||
import GraphQL from 'graphql';
|
||||
import { createSubqueryResolver } from '../resolvers.js';
|
||||
|
||||
export default new GraphQLObjectType({
|
||||
const { GraphQLObjectType, GraphQLNonNull, GraphQLInt, GraphQLFloat } = GraphQL;
|
||||
|
||||
export const Rating = new GraphQLObjectType({
|
||||
name: 'Rating',
|
||||
description: `[Ratings](https://musicbrainz.org/doc/Rating_System) allow users
|
||||
to rate MusicBrainz entities. User may assign a value between 1 and 5; these
|
||||
|
|
@ -14,11 +13,17 @@ for the entity.`,
|
|||
voteCount: {
|
||||
type: new GraphQLNonNull(GraphQLInt),
|
||||
description: 'The number of votes that have contributed to the rating.',
|
||||
resolve: rating => rating['votes-count']
|
||||
resolve: (rating) => rating['votes-count'],
|
||||
},
|
||||
value: {
|
||||
type: GraphQLInt,
|
||||
description: 'The average rating value based on the aggregated votes.'
|
||||
}
|
||||
})
|
||||
})
|
||||
type: GraphQLFloat,
|
||||
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,29 +1,27 @@
|
|||
import {
|
||||
GraphQLObjectType,
|
||||
GraphQLList,
|
||||
GraphQLBoolean
|
||||
} from 'graphql/type'
|
||||
import Node from './node'
|
||||
import Entity from './entity'
|
||||
import { Duration, ISRC } from './scalars'
|
||||
import GraphQL from 'graphql';
|
||||
import { Node } from './node.js';
|
||||
import { Entity } from './entity.js';
|
||||
import { Duration, ISRC } from './scalars.js';
|
||||
import {
|
||||
id,
|
||||
mbid,
|
||||
title,
|
||||
disambiguation,
|
||||
aliases,
|
||||
artistCredit,
|
||||
artistCredits,
|
||||
artists,
|
||||
releases,
|
||||
relationships,
|
||||
collections,
|
||||
rating,
|
||||
tags,
|
||||
connectionWithExtras
|
||||
} from './helpers'
|
||||
connectionWithExtras,
|
||||
linkedQuery,
|
||||
} from './helpers.js';
|
||||
import { aliases } from './alias.js';
|
||||
import { artists } from './artist.js';
|
||||
import { artistCredit, artistCredits } from './artist-credit.js';
|
||||
import { collections } from './collection.js';
|
||||
import { tags } from './tag.js';
|
||||
import { rating } from './rating.js';
|
||||
import { relationships } from './relationship.js';
|
||||
import { releases } from './release.js';
|
||||
|
||||
const Recording = new GraphQLObjectType({
|
||||
const { GraphQLObjectType, GraphQLList, GraphQLBoolean } = GraphQL;
|
||||
|
||||
export const Recording = new GraphQLObjectType({
|
||||
name: 'Recording',
|
||||
description: `A [recording](https://musicbrainz.org/doc/Recording) is an
|
||||
entity in MusicBrainz which can be linked to tracks on releases. Each track must
|
||||
|
|
@ -49,25 +47,37 @@ or mixing.`,
|
|||
isrcs: {
|
||||
type: new GraphQLList(ISRC),
|
||||
description: `A list of [International Standard Recording Codes](https://musicbrainz.org/doc/ISRC)
|
||||
(ISRCs) for this recording.`
|
||||
(ISRCs) for this recording.`,
|
||||
resolve: (source, args, context) => {
|
||||
if (source.isrcs) {
|
||||
return source.isrcs;
|
||||
}
|
||||
// TODO: Add support for parent entities knowing to include this `inc`
|
||||
// parameter in their own calls by inspecting what fields are requested
|
||||
// or batching things at the loader level.
|
||||
return context.loaders.lookup
|
||||
.load(['recording', source.id, { inc: 'isrcs' }])
|
||||
.then((recording) => recording.isrcs);
|
||||
},
|
||||
},
|
||||
length: {
|
||||
type: Duration,
|
||||
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: {
|
||||
type: GraphQLBoolean,
|
||||
description: 'Whether this is a video recording.'
|
||||
description: 'Whether this is a video recording.',
|
||||
},
|
||||
artists,
|
||||
releases,
|
||||
relationships,
|
||||
collections,
|
||||
rating,
|
||||
tags
|
||||
})
|
||||
})
|
||||
tags,
|
||||
}),
|
||||
});
|
||||
|
||||
export const RecordingConnection = connectionWithExtras(Recording)
|
||||
export default Recording
|
||||
export const RecordingConnection = connectionWithExtras(Recording);
|
||||
|
||||
export const recordings = linkedQuery(RecordingConnection);
|
||||
|
|
|
|||
|
|
@ -1,19 +1,25 @@
|
|||
import GraphQL from 'graphql';
|
||||
import GraphQLRelay from 'graphql-relay';
|
||||
import { DateType } from './scalars.js';
|
||||
import { Entity } from './entity.js';
|
||||
import {
|
||||
resolveHyphenated,
|
||||
fieldWithID,
|
||||
connectionWithExtras,
|
||||
} from './helpers.js';
|
||||
import { resolveRelationship, includeRelationships } from '../resolvers.js';
|
||||
import { toDashed } from '../util.js';
|
||||
|
||||
const {
|
||||
GraphQLObjectType,
|
||||
GraphQLNonNull,
|
||||
GraphQLString,
|
||||
GraphQLList,
|
||||
GraphQLBoolean
|
||||
} from 'graphql/type'
|
||||
import { DateType } from './scalars'
|
||||
import Entity from './entity'
|
||||
import {
|
||||
resolveHyphenated,
|
||||
fieldWithID,
|
||||
connectionWithExtras
|
||||
} from './helpers'
|
||||
GraphQLBoolean,
|
||||
} = GraphQL;
|
||||
const { connectionArgs } = GraphQLRelay;
|
||||
|
||||
const Relationship = new GraphQLObjectType({
|
||||
export const Relationship = new GraphQLObjectType({
|
||||
name: 'Relationship',
|
||||
description: `[Relationships](https://musicbrainz.org/doc/Relationships) are a
|
||||
way to represent all the different ways in which entities are connected to each
|
||||
|
|
@ -22,61 +28,112 @@ other and to URLs outside MusicBrainz.`,
|
|||
target: {
|
||||
type: new GraphQLNonNull(Entity),
|
||||
description: 'The target entity.',
|
||||
resolve: source => {
|
||||
const targetType = source['target-type']
|
||||
const target = source[targetType]
|
||||
target._type = targetType.replace('_', '-')
|
||||
return target
|
||||
}
|
||||
resolve: (source) => {
|
||||
const targetType = source['target-type'];
|
||||
const target = source[targetType];
|
||||
target._type = targetType.replace('_', '-');
|
||||
return target;
|
||||
},
|
||||
},
|
||||
direction: {
|
||||
type: new GraphQLNonNull(GraphQLString),
|
||||
description: 'The direction of the relationship.'
|
||||
description: 'The direction of the relationship.',
|
||||
},
|
||||
targetType: {
|
||||
type: new GraphQLNonNull(GraphQLString),
|
||||
description: 'The type of entity on the receiving end of the relationship.',
|
||||
resolve: resolveHyphenated
|
||||
description:
|
||||
'The type of entity on the receiving end of the relationship.',
|
||||
resolve: resolveHyphenated,
|
||||
},
|
||||
sourceCredit: {
|
||||
type: GraphQLString,
|
||||
description: `How the source entity was actually credited, if different
|
||||
from its main (performance) name.`,
|
||||
resolve: resolveHyphenated
|
||||
resolve: resolveHyphenated,
|
||||
},
|
||||
targetCredit: {
|
||||
type: GraphQLString,
|
||||
description: `How the target entity was actually credited, if different
|
||||
from its main (performance) name.`,
|
||||
resolve: resolveHyphenated
|
||||
resolve: resolveHyphenated,
|
||||
},
|
||||
begin: {
|
||||
type: DateType,
|
||||
description: 'The date on which the relationship became applicable.'
|
||||
description: 'The date on which the relationship became applicable.',
|
||||
},
|
||||
end: {
|
||||
type: DateType,
|
||||
description: 'The date on which the relationship became no longer applicable.'
|
||||
description:
|
||||
'The date on which the relationship became no longer applicable.',
|
||||
},
|
||||
ended: {
|
||||
type: GraphQLBoolean,
|
||||
description: 'Whether the relationship still applies.'
|
||||
description: 'Whether the relationship still applies.',
|
||||
},
|
||||
attributes: {
|
||||
type: new GraphQLList(GraphQLString),
|
||||
description: `Attributes which modify the relationship. There is a [list
|
||||
of all attributes](https://musicbrainz.org/relationship-attributes), but 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`
|
||||
// field.
|
||||
// attributeValues: {},
|
||||
...fieldWithID('type', {
|
||||
description: 'The type of relationship.'
|
||||
})
|
||||
})
|
||||
})
|
||||
description: 'The type of relationship.',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const RelationshipConnection = connectionWithExtras(Relationship)
|
||||
export default Relationship
|
||||
export const RelationshipConnection = connectionWithExtras(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 { DateType } from './scalars'
|
||||
import Area from './area'
|
||||
import GraphQL from 'graphql';
|
||||
import { DateType } from './scalars.js';
|
||||
import { Area } from './area.js';
|
||||
|
||||
export default new GraphQLObjectType({
|
||||
const { GraphQLObjectType } = GraphQL;
|
||||
|
||||
export const ReleaseEvent = new GraphQLObjectType({
|
||||
name: 'ReleaseEvent',
|
||||
description: `The date on which a release was issued in a country/region with
|
||||
a particular label, catalog number, barcode, and format.`,
|
||||
fields: () => ({
|
||||
area: { type: Area },
|
||||
date: { type: DateType }
|
||||
})
|
||||
})
|
||||
date: { type: DateType },
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,29 +1,31 @@
|
|||
import { GraphQLObjectType, GraphQLList } from 'graphql/type'
|
||||
import Node from './node'
|
||||
import Entity from './entity'
|
||||
import { ReleaseGroupCoverArt } from './cover-art'
|
||||
import { DateType } from './scalars'
|
||||
import { ReleaseGroupType } from './enums'
|
||||
import GraphQL from 'graphql';
|
||||
import { Node } from './node.js';
|
||||
import { Entity } from './entity.js';
|
||||
import { DateType } from './scalars.js';
|
||||
import { ReleaseGroupType } from './enums.js';
|
||||
import {
|
||||
id,
|
||||
mbid,
|
||||
title,
|
||||
disambiguation,
|
||||
aliases,
|
||||
artistCredit,
|
||||
artistCredits,
|
||||
artists,
|
||||
releases,
|
||||
relationships,
|
||||
collections,
|
||||
rating,
|
||||
tags,
|
||||
fieldWithID,
|
||||
releaseGroupType,
|
||||
resolveHyphenated,
|
||||
connectionWithExtras
|
||||
} from './helpers'
|
||||
connectionWithExtras,
|
||||
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',
|
||||
description: `A [release group](https://musicbrainz.org/doc/Release_Group) is
|
||||
used to group several different releases into a single logical entity. Every
|
||||
|
|
@ -45,36 +47,33 @@ album – it doesn’t matter how many CDs or editions/versions it had.`,
|
|||
firstReleaseDate: {
|
||||
type: DateType,
|
||||
description: 'The date of the earliest release in the group.',
|
||||
resolve: resolveHyphenated
|
||||
resolve: resolveHyphenated,
|
||||
},
|
||||
...fieldWithID('primaryType', {
|
||||
type: ReleaseGroupType,
|
||||
description: `The [type](https://musicbrainz.org/doc/Release_Group/Type)
|
||||
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
|
||||
“main” type and an unspecified number of additional types.`
|
||||
“main” type and an unspecified number of additional types.`,
|
||||
}),
|
||||
...fieldWithID('secondaryTypes', {
|
||||
type: new GraphQLList(ReleaseGroupType),
|
||||
description: `Additional [types](https://musicbrainz.org/doc/Release_Group/Type)
|
||||
that apply to this release group.`
|
||||
that apply to this release group.`,
|
||||
}),
|
||||
coverArt: {
|
||||
type: ReleaseGroupCoverArt,
|
||||
description: `The cover art for a release group, obtained from the [Cover
|
||||
Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive).`,
|
||||
resolve: (releaseGroup, args, { loaders }) => {
|
||||
return loaders.coverArt.load(['release-group', releaseGroup.id])
|
||||
}
|
||||
},
|
||||
artists,
|
||||
releases,
|
||||
relationships,
|
||||
collections,
|
||||
rating,
|
||||
tags
|
||||
})
|
||||
})
|
||||
tags,
|
||||
}),
|
||||
});
|
||||
|
||||
export const ReleaseGroupConnection = connectionWithExtras(ReleaseGroup)
|
||||
export default ReleaseGroup
|
||||
export const ReleaseGroupConnection = connectionWithExtras(ReleaseGroup);
|
||||
|
||||
export const releaseGroups = linkedQuery(ReleaseGroupConnection, {
|
||||
args: {
|
||||
type: releaseGroupType,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,37 +1,35 @@
|
|||
import {
|
||||
GraphQLObjectType,
|
||||
GraphQLNonNull,
|
||||
GraphQLString,
|
||||
GraphQLList
|
||||
} from 'graphql/type'
|
||||
import Node from './node'
|
||||
import Entity from './entity'
|
||||
import { ASIN, DateType } from './scalars'
|
||||
import Media from './media'
|
||||
import { ReleaseCoverArt } from './cover-art'
|
||||
import { ReleaseStatus } from './enums'
|
||||
import ReleaseEvent from './release-event'
|
||||
import GraphQL from 'graphql';
|
||||
import { Node } from './node.js';
|
||||
import { Entity } from './entity.js';
|
||||
import { ASIN, DateType } from './scalars.js';
|
||||
import { Media } from './media.js';
|
||||
import { ReleaseStatus } from './enums.js';
|
||||
import { ReleaseEvent } from './release-event.js';
|
||||
import {
|
||||
id,
|
||||
mbid,
|
||||
title,
|
||||
disambiguation,
|
||||
aliases,
|
||||
artistCredit,
|
||||
artistCredits,
|
||||
artists,
|
||||
labels,
|
||||
recordings,
|
||||
releaseGroups,
|
||||
relationships,
|
||||
collections,
|
||||
tags,
|
||||
fieldWithID,
|
||||
releaseGroupType,
|
||||
releaseStatus,
|
||||
resolveHyphenated,
|
||||
connectionWithExtras
|
||||
} from './helpers'
|
||||
connectionWithExtras,
|
||||
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',
|
||||
description: `A [release](https://musicbrainz.org/doc/Release) represents the
|
||||
unique release (i.e. issuing) of a product on a specific date with specific
|
||||
|
|
@ -50,61 +48,48 @@ MusicBrainz as one release.`,
|
|||
releaseEvents: {
|
||||
type: new GraphQLList(ReleaseEvent),
|
||||
description: 'The release events for this release.',
|
||||
resolve: resolveHyphenated
|
||||
resolve: resolveHyphenated,
|
||||
},
|
||||
date: {
|
||||
type: DateType,
|
||||
description: `The [release date](https://musicbrainz.org/doc/Release/Date)
|
||||
is the date in which a release was made available through some sort of
|
||||
distribution mechanism.`
|
||||
distribution mechanism.`,
|
||||
},
|
||||
country: {
|
||||
type: GraphQLString,
|
||||
description: 'The country in which the release was issued.'
|
||||
description: 'The country in which the release was issued.',
|
||||
},
|
||||
asin: {
|
||||
type: ASIN,
|
||||
description: `The [Amazon Standard Identification Number](https://musicbrainz.org/doc/ASIN)
|
||||
of the release.`
|
||||
of the release.`,
|
||||
},
|
||||
barcode: {
|
||||
type: GraphQLString,
|
||||
description: `The [barcode](https://en.wikipedia.org/wiki/Barcode), if the
|
||||
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
|
||||
[EANs](https://en.wikipedia.org/wiki/International_Article_Number).`
|
||||
},
|
||||
coverArt: {
|
||||
type: new GraphQLNonNull(ReleaseCoverArt),
|
||||
description: `A list and summary of the cover art images that are present
|
||||
for this release from the [Cover Art Archive](https://musicbrainz.org/doc/Cover_Art_Archive).`,
|
||||
resolve: (release, args, { loaders }) => {
|
||||
const coverArt = release['cover-art-archive']
|
||||
if (coverArt) {
|
||||
coverArt._release = release.id
|
||||
return coverArt
|
||||
}
|
||||
return loaders.coverArt.load(['release', release.id])
|
||||
}
|
||||
[EANs](https://en.wikipedia.org/wiki/International_Article_Number).`,
|
||||
},
|
||||
...fieldWithID('status', {
|
||||
type: ReleaseStatus,
|
||||
description: 'The status describes how “official” a release is.'
|
||||
description: 'The status describes how “official” a release is.',
|
||||
}),
|
||||
...fieldWithID('packaging', {
|
||||
description: `The physical packaging that accompanies the release. See
|
||||
the [list of packaging](https://musicbrainz.org/doc/Release/Packaging) for more
|
||||
information.`
|
||||
information.`,
|
||||
}),
|
||||
quality: {
|
||||
type: GraphQLString,
|
||||
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
|
||||
[ratings](https://musicbrainz.org/doc/Rating_System).`
|
||||
[ratings](https://musicbrainz.org/doc/Rating_System).`,
|
||||
},
|
||||
media: {
|
||||
type: new GraphQLList(Media),
|
||||
description: 'The media on which the release was distributed.'
|
||||
description: 'The media on which the release was distributed.',
|
||||
},
|
||||
artists,
|
||||
labels,
|
||||
|
|
@ -112,9 +97,15 @@ It is not a mark of how good or bad the music itself is – for that, use
|
|||
releaseGroups,
|
||||
relationships,
|
||||
collections,
|
||||
tags
|
||||
})
|
||||
})
|
||||
tags,
|
||||
}),
|
||||
});
|
||||
|
||||
export const ReleaseConnection = connectionWithExtras(Release)
|
||||
export default Release
|
||||
export const ReleaseConnection = connectionWithExtras(Release);
|
||||
|
||||
export const releases = linkedQuery(ReleaseConnection, {
|
||||
args: {
|
||||
type: releaseGroupType,
|
||||
status: releaseStatus,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,69 +1,71 @@
|
|||
import { Kind } from 'graphql/language'
|
||||
import { GraphQLScalarType } from 'graphql/type'
|
||||
import GraphQL from 'graphql';
|
||||
|
||||
function createScalar (config) {
|
||||
const { Kind, GraphQLScalarType } = GraphQL;
|
||||
|
||||
function createScalar(config) {
|
||||
return new GraphQLScalarType({
|
||||
serialize: value => value,
|
||||
parseValue: value => value,
|
||||
parseLiteral (ast) {
|
||||
serialize: (value) => value,
|
||||
parseValue: (value) => value,
|
||||
parseLiteral(ast) {
|
||||
if (ast.kind === Kind.STRING) {
|
||||
return ast.value
|
||||
return ast.value;
|
||||
}
|
||||
return null
|
||||
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 locale = /^([a-z]{2})(_[A-Z]{2})?(\.[a-zA-Z0-9-]+)?$/
|
||||
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-]+)?$/;
|
||||
// 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)) {
|
||||
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) {
|
||||
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)) {
|
||||
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)) {
|
||||
return value
|
||||
return value;
|
||||
}
|
||||
throw new TypeError(`Malformed URL: ${value}`)
|
||||
throw new TypeError(`Malformed URL: ${value}`);
|
||||
}
|
||||
|
||||
export const ASIN = createScalar({
|
||||
name: 'ASIN',
|
||||
description: `An [Amazon Standard Identification Number](https://musicbrainz.org/doc/ASIN)
|
||||
(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({
|
||||
name: 'Date',
|
||||
description: 'Year, month (optional), and day (optional) in YYYY-MM-DD format.'
|
||||
})
|
||||
description:
|
||||
'Year, month (optional), and day (optional) in YYYY-MM-DD format.',
|
||||
});
|
||||
|
||||
export const Degrees = createScalar({
|
||||
name: 'Degrees',
|
||||
description: 'Decimal degrees, used for latitude and longitude.'
|
||||
})
|
||||
description: 'Decimal degrees, used for latitude and longitude.',
|
||||
});
|
||||
|
||||
export const DiscID = createScalar({
|
||||
name: 'DiscID',
|
||||
|
|
@ -79,35 +81,35 @@ Different pressing of a CD often have slightly different frame offsets, and
|
|||
hence different disc IDs.
|
||||
|
||||
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({
|
||||
name: 'Duration',
|
||||
description: 'A length of time, in milliseconds.',
|
||||
serialize: validatePositive,
|
||||
parseValue: validatePositive,
|
||||
parseLiteral (ast) {
|
||||
parseLiteral(ast) {
|
||||
if (ast.kind === Kind.INT) {
|
||||
return validatePositive(parseInt(ast.value, 10))
|
||||
return validatePositive(parseInt(ast.value, 10));
|
||||
}
|
||||
return null
|
||||
}
|
||||
})
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
export const IPI = createScalar({
|
||||
name: 'IPI',
|
||||
description: `An [Interested Parties Information](https://musicbrainz.org/doc/IPI)
|
||||
(IPI) code is an identifying number assigned by the CISAC database for musical
|
||||
rights management.`
|
||||
})
|
||||
rights management.`,
|
||||
});
|
||||
|
||||
export const ISNI = createScalar({
|
||||
name: 'ISNI',
|
||||
description: `The [International Standard Name Identifier](https://musicbrainz.org/doc/ISNI)
|
||||
(ISNI) is an ISO standard for uniquely identifying the public identities of
|
||||
contributors to media content.`
|
||||
})
|
||||
contributors to media content.`,
|
||||
});
|
||||
|
||||
export const ISRC = createScalar({
|
||||
name: 'ISRC',
|
||||
|
|
@ -120,28 +122,28 @@ not the song itself. Therefore, different recordings, edits, remixes and
|
|||
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.
|
||||
Songs are identified by analogous [International Standard Musical Work Codes](https://musicbrainz.org/doc/ISWC)
|
||||
(ISWCs).`
|
||||
})
|
||||
(ISWCs).`,
|
||||
});
|
||||
|
||||
export const ISWC = createScalar({
|
||||
name: '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 /
|
||||
compositions.`
|
||||
})
|
||||
compositions.`,
|
||||
});
|
||||
|
||||
export const Locale = createScalar({
|
||||
name: 'Locale',
|
||||
description: 'Language code, optionally with country and encoding.',
|
||||
serialize: validateLocale,
|
||||
parseValue: validateLocale,
|
||||
parseLiteral (ast) {
|
||||
parseLiteral(ast) {
|
||||
if (ast.kind === Kind.STRING) {
|
||||
return validateLocale(ast.value)
|
||||
return validateLocale(ast.value);
|
||||
}
|
||||
return null
|
||||
}
|
||||
})
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
export const MBID = createScalar({
|
||||
name: 'MBID',
|
||||
|
|
@ -149,28 +151,28 @@ export const MBID = createScalar({
|
|||
36-character UUIDs.`,
|
||||
serialize: validateMBID,
|
||||
parseValue: validateMBID,
|
||||
parseLiteral (ast) {
|
||||
parseLiteral(ast) {
|
||||
if (ast.kind === Kind.STRING) {
|
||||
return validateMBID(ast.value)
|
||||
return validateMBID(ast.value);
|
||||
}
|
||||
return null
|
||||
}
|
||||
})
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
export const Time = createScalar({
|
||||
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({
|
||||
name: 'URLString',
|
||||
description: 'A web address.',
|
||||
serialize: validateURL,
|
||||
parseValue: validateURL,
|
||||
parseLiteral (ast) {
|
||||
parseLiteral(ast) {
|
||||
if (ast.kind === Kind.STRING) {
|
||||
return validateURL(ast.value)
|
||||
return validateURL(ast.value);
|
||||
}
|
||||
return null
|
||||
}
|
||||
})
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,19 +1,22 @@
|
|||
import { GraphQLObjectType } from 'graphql/type'
|
||||
import Node from './node'
|
||||
import Entity from './entity'
|
||||
import GraphQL from 'graphql';
|
||||
import { Node } from './node.js';
|
||||
import { Entity } from './entity.js';
|
||||
import {
|
||||
id,
|
||||
mbid,
|
||||
name,
|
||||
disambiguation,
|
||||
relationships,
|
||||
collections,
|
||||
tags,
|
||||
fieldWithID,
|
||||
connectionWithExtras
|
||||
} from './helpers'
|
||||
connectionWithExtras,
|
||||
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',
|
||||
description: `A [series](https://musicbrainz.org/doc/Series) is a sequence of
|
||||
separate release groups, releases, recordings, works or events with a common
|
||||
|
|
@ -26,13 +29,14 @@ theme.`,
|
|||
disambiguation,
|
||||
...fieldWithID('type', {
|
||||
description: `The type primarily describes what type of entity the series
|
||||
contains.`
|
||||
contains.`,
|
||||
}),
|
||||
relationships,
|
||||
collections,
|
||||
tags
|
||||
})
|
||||
})
|
||||
tags,
|
||||
}),
|
||||
});
|
||||
|
||||
export const SeriesConnection = connectionWithExtras(Series)
|
||||
export default Series
|
||||
export const SeriesConnection = connectionWithExtras(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,
|
||||
GraphQLNonNull,
|
||||
GraphQLString,
|
||||
GraphQLInt
|
||||
} from 'graphql/type'
|
||||
import { connectionWithExtras } from './helpers'
|
||||
GraphQLInt,
|
||||
} = GraphQL;
|
||||
const { connectionFromArray } = GraphQLRelay;
|
||||
|
||||
const Tag = new GraphQLObjectType({
|
||||
export const Tag = new GraphQLObjectType({
|
||||
name: 'Tag',
|
||||
description: `[Tags](https://musicbrainz.org/tags) are a way to mark entities
|
||||
with extra information – for example, the genres that apply to an artist,
|
||||
|
|
@ -14,14 +19,24 @@ release, or recording.`,
|
|||
fields: () => ({
|
||||
name: {
|
||||
type: new GraphQLNonNull(GraphQLString),
|
||||
description: 'The tag label.'
|
||||
description: 'The tag label.',
|
||||
},
|
||||
count: {
|
||||
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 default Tag
|
||||
export const TagConnection = connectionWithExtras(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 Node from './node'
|
||||
import Entity from './entity'
|
||||
import { URLString } from './scalars'
|
||||
import { id, mbid, relationships, connectionWithExtras } from './helpers'
|
||||
import GraphQL from 'graphql';
|
||||
import { Node } from './node.js';
|
||||
import { Entity } from './entity.js';
|
||||
import { URLString } from './scalars.js';
|
||||
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',
|
||||
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
|
||||
|
|
@ -15,11 +18,10 @@ acquired, an entry in another database, etc.`,
|
|||
mbid,
|
||||
resource: {
|
||||
type: new GraphQLNonNull(URLString),
|
||||
description: 'The actual URL string.'
|
||||
description: 'The actual URL string.',
|
||||
},
|
||||
relationships
|
||||
})
|
||||
})
|
||||
relationships,
|
||||
}),
|
||||
});
|
||||
|
||||
export const URLConnection = connectionWithExtras(URL)
|
||||
export default URL
|
||||
export const URLConnection = connectionWithExtras(URL);
|
||||
|
|
|
|||
|
|
@ -1,22 +1,25 @@
|
|||
import { GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql/type'
|
||||
import Node from './node'
|
||||
import Entity from './entity'
|
||||
import GraphQL from 'graphql';
|
||||
import { Node } from './node.js';
|
||||
import { Entity } from './entity.js';
|
||||
import {
|
||||
id,
|
||||
mbid,
|
||||
title,
|
||||
disambiguation,
|
||||
aliases,
|
||||
artists,
|
||||
relationships,
|
||||
collections,
|
||||
rating,
|
||||
tags,
|
||||
fieldWithID,
|
||||
connectionWithExtras
|
||||
} from './helpers'
|
||||
connectionWithExtras,
|
||||
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',
|
||||
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
|
||||
|
|
@ -31,22 +34,23 @@ more audio recordings.`,
|
|||
iswcs: {
|
||||
type: new GraphQLList(GraphQLString),
|
||||
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: {
|
||||
type: GraphQLString,
|
||||
description: 'The language in which the work was originally written.'
|
||||
description: 'The language in which the work was originally written.',
|
||||
},
|
||||
...fieldWithID('type', {
|
||||
description: 'The type of work.'
|
||||
description: 'The type of work.',
|
||||
}),
|
||||
artists,
|
||||
relationships,
|
||||
collections,
|
||||
rating,
|
||||
tags
|
||||
})
|
||||
})
|
||||
tags,
|
||||
}),
|
||||
});
|
||||
|
||||
export const WorkConnection = connectionWithExtras(Work)
|
||||
export default Work
|
||||
export const WorkConnection = connectionWithExtras(Work);
|
||||
|
||||
export const works = linkedQuery(WorkConnection);
|
||||
|
|
|
|||
108
src/util.js
108
src/util.js
|
|
@ -1,45 +1,99 @@
|
|||
import util from 'util'
|
||||
import util from 'util';
|
||||
import dashify from 'dashify';
|
||||
import pascalCase from 'pascalcase';
|
||||
|
||||
export function getFields (info, fragments = info.fragments) {
|
||||
export const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
export function getFields(
|
||||
info,
|
||||
fragments = info.fragments,
|
||||
depth = 0,
|
||||
prefix = ''
|
||||
) {
|
||||
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) => {
|
||||
if (selection.kind === 'FragmentSpread') {
|
||||
const name = selection.name.value
|
||||
const fragment = fragments[name]
|
||||
const name = selection.name.value;
|
||||
const fragment = fragments[name];
|
||||
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') {
|
||||
selection.selectionSet.selections.reduce(reducer, fields);
|
||||
} 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 (obj, { depth = 5,
|
||||
colors = true,
|
||||
breakLength = 120 } = {}) {
|
||||
console.log(util.inspect(obj, { depth, colors, breakLength }))
|
||||
export function prettyPrint(
|
||||
obj,
|
||||
{ depth = 5, colors = true, breakLength = 120 } = {}
|
||||
) {
|
||||
console.log(util.inspect(obj, { depth, colors, breakLength }));
|
||||
}
|
||||
|
||||
export function toFilteredArray (obj) {
|
||||
return (Array.isArray(obj) ? obj : [obj]).filter(x => x)
|
||||
export function toFilteredArray(obj) {
|
||||
return (Array.isArray(obj) ? obj : [obj]).filter((x) => x);
|
||||
}
|
||||
|
||||
export function extendIncludes (includes, moreIncludes) {
|
||||
includes = toFilteredArray(includes)
|
||||
moreIncludes = toFilteredArray(moreIncludes)
|
||||
const seen = {}
|
||||
return includes.concat(moreIncludes).filter(x => {
|
||||
export function extendIncludes(includes, moreIncludes) {
|
||||
includes = toFilteredArray(includes);
|
||||
moreIncludes = toFilteredArray(moreIncludes);
|
||||
const seen = {};
|
||||
return includes.concat(moreIncludes).filter((x) => {
|
||||
if (seen[x]) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
seen[x] = true
|
||||
return true
|
||||
})
|
||||
seen[x] = 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);
|
||||
}
|
||||
|
|
|
|||
1475
test/_schema.js
Normal file
1475
test/_schema.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,9 +1,14 @@
|
|||
import test from 'ava'
|
||||
import Client from '../../src/api/client'
|
||||
import test from 'ava';
|
||||
import Client from '../../src/api/client.js';
|
||||
|
||||
test('parseErrorMessage() returns the body or status code', t => {
|
||||
const client = new Client()
|
||||
t.is(client.parseErrorMessage({ statusCode: 500 }, 'something went wrong'), 'something went wrong')
|
||||
t.is(client.parseErrorMessage({ statusCode: 500 }, ''), '500')
|
||||
t.is(client.parseErrorMessage({ statusCode: 404 }, {}), '404')
|
||||
})
|
||||
test('parseErrorMessage() returns the input error by default', (t) => {
|
||||
const client = new Client();
|
||||
const error = {
|
||||
name: 'HTTPError',
|
||||
response: {
|
||||
statusCode: 500,
|
||||
body: 'something went wrong',
|
||||
},
|
||||
};
|
||||
t.is(client.parseErrorMessage(error), error);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
import test from 'ava'
|
||||
import { CoverArtArchiveError } from '../../src/api'
|
||||
import client from '../helpers/client/cover-art-archive'
|
||||
|
||||
test('can retrieve a front image URL', t => {
|
||||
return client.imageURL('release', '76df3287-6cda-33eb-8e9a-044b5e15ffdd', 'front')
|
||||
.then(url => {
|
||||
t.is(url, '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', t => {
|
||||
return client.imageURL('release', '76df3287-6cda-33eb-8e9a-044b5e15ffdd', 'back')
|
||||
.then(url => {
|
||||
t.is(url, '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', t => {
|
||||
return client.images('release', '76df3287-6cda-33eb-8e9a-044b5e15ffdd')
|
||||
.then(data => {
|
||||
t.is(data.release, 'http://musicbrainz.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd')
|
||||
t.true(data.images.length >= 3)
|
||||
data.images.forEach(image => {
|
||||
t.true(image.approved)
|
||||
t.truthy(image.image)
|
||||
t.truthy(image.id)
|
||||
t.truthy(image.thumbnails.small)
|
||||
t.truthy(image.thumbnails.large)
|
||||
})
|
||||
t.true(data.images.some(image => image.front))
|
||||
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('Back') !== -1))
|
||||
t.true(data.images.some(image => image.types.indexOf('Medium') !== -1))
|
||||
})
|
||||
})
|
||||
|
||||
test('throws an error if given an invalid MBID', t => {
|
||||
return t.throws(client.images('release', 'xyz'), CoverArtArchiveError)
|
||||
})
|
||||
|
||||
test('uses the default error impementation if there is no HTML error', t => {
|
||||
t.is(client.parseErrorMessage({ statusCode: 501 }, 'yikes'), 'yikes')
|
||||
t.is(client.parseErrorMessage({ statusCode: 500 }, ''), '500')
|
||||
t.is(client.parseErrorMessage({ statusCode: 404 }, null), '404')
|
||||
})
|
||||
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,70 +1,87 @@
|
|||
import test from 'ava'
|
||||
import MusicBrainz, { MusicBrainzError } from '../../src/api'
|
||||
import client from '../helpers/client/musicbrainz'
|
||||
import test from 'ava';
|
||||
import MusicBrainz from '../../src/api/index.js';
|
||||
import client from '../helpers/client/musicbrainz.js';
|
||||
|
||||
test('getLookupURL() generates a lookup URL', t => {
|
||||
t.is(client.getLookupURL('artist', 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8', {
|
||||
inc: ['recordings', 'release-groups']
|
||||
}), 'artist/c8da2e40-bd28-4d4e-813a-bd2f51958ba8?inc=recordings%2Brelease-groups')
|
||||
})
|
||||
test('getLookupURL() generates a lookup URL', (t) => {
|
||||
t.is(
|
||||
client.getLookupURL('artist', 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8', {
|
||||
inc: ['recordings', 'release-groups'],
|
||||
}),
|
||||
'artist/c8da2e40-bd28-4d4e-813a-bd2f51958ba8?inc=recordings%2Brelease-groups'
|
||||
);
|
||||
});
|
||||
|
||||
test('getBrowseURL() generates a browse URL', t => {
|
||||
t.is(client.getBrowseURL('recording', {
|
||||
artist: 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8',
|
||||
limit: null,
|
||||
offset: 0
|
||||
}), 'recording?artist=c8da2e40-bd28-4d4e-813a-bd2f51958ba8&offset=0')
|
||||
})
|
||||
test('getBrowseURL() generates a browse URL', (t) => {
|
||||
t.is(
|
||||
client.getBrowseURL('recording', {
|
||||
artist: 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8',
|
||||
limit: null,
|
||||
offset: 0,
|
||||
}),
|
||||
'recording?artist=c8da2e40-bd28-4d4e-813a-bd2f51958ba8&offset=0'
|
||||
);
|
||||
});
|
||||
|
||||
test('getSearchURL() generates a search URL', t => {
|
||||
t.is(client.getSearchURL('artist', 'Lures', { inc: null }), 'artist?query=Lures')
|
||||
})
|
||||
test('getSearchURL() generates a search URL', (t) => {
|
||||
t.is(
|
||||
client.getSearchURL('artist', 'Lures', { inc: null }),
|
||||
'artist?query=Lures'
|
||||
);
|
||||
});
|
||||
|
||||
test('lookup() sends a lookup query', t => {
|
||||
return client.lookup('artist', 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8').then(response => {
|
||||
t.is(response.id, 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8')
|
||||
t.is(response.type, 'Group')
|
||||
})
|
||||
})
|
||||
test('lookup() sends a lookup query', async (t) => {
|
||||
const response = await client.lookup(
|
||||
'artist',
|
||||
'c8da2e40-bd28-4d4e-813a-bd2f51958ba8'
|
||||
);
|
||||
|
||||
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', {
|
||||
inc: ['foobar']
|
||||
})
|
||||
return t.throws(req, MusicBrainzError)
|
||||
})
|
||||
inc: ['foobar'],
|
||||
});
|
||||
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 => {
|
||||
t.true(client.shouldRetry(new MusicBrainzError('error', 500)))
|
||||
t.true(client.shouldRetry(new MusicBrainzError('error', 501)))
|
||||
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('rejects non-MusicBrainz errors', (t) => {
|
||||
const client = new MusicBrainz({ baseURL: '$!@#$' });
|
||||
return t.throwsAsync(
|
||||
client.get('artist/5b11f4ce-a62d-471e-81fc-a69a8278c7da'),
|
||||
{
|
||||
name: 'TypeError',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
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'), Error)
|
||||
})
|
||||
|
||||
test('uses the default error impementation if there is no JSON error', t => {
|
||||
t.is(client.parseErrorMessage({ statusCode: 501 }, 'yikes'), 'yikes')
|
||||
t.is(client.parseErrorMessage({ statusCode: 500 }, {}), '500')
|
||||
t.is(client.parseErrorMessage({ statusCode: 404 }, null), '404')
|
||||
})
|
||||
test('uses the default error impementation if there is no JSON error', (t) => {
|
||||
let error = {
|
||||
name: 'HTTPError',
|
||||
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);
|
||||
});
|
||||
|
|
|
|||
8
test/base-schema.js
Normal file
8
test/base-schema.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import test from 'ava';
|
||||
import { baseSchema } from '../src/schema.js';
|
||||
|
||||
test.before((t) => {
|
||||
t.context.schema = baseSchema;
|
||||
});
|
||||
|
||||
import './_schema.js';
|
||||
13
test/extended-schema.js
Normal file
13
test/extended-schema.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import test from 'ava';
|
||||
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';
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue