Media tracks (#87)

* Add support for getting tracks on release objects

* Fix track number type, update docs

* Stop snapshotting tracks query because it blows up AVA

* Fix lint issue
This commit is contained in:
Brian Beck 2018-09-16 21:53:15 -07:00 committed by GitHub
parent a6693ed5a4
commit cb2d2a1a3f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1239 additions and 1298 deletions

View file

@ -21,6 +21,7 @@ npm install graphbrainz --save
<!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Usage](#usage) - [Usage](#usage)
- [As a standalone server](#as-a-standalone-server) - [As a standalone server](#as-a-standalone-server)
- [As middleware](#as-middleware) - [As middleware](#as-middleware)

View file

@ -1261,6 +1261,9 @@ type Medium {
"""A list of physical discs and their disc IDs for this medium.""" """A list of physical discs and their disc IDs for this medium."""
discs: [Disc] discs: [Disc]
"""The list of tracks on the given media."""
tracks: [Track]
} }
"""An object with an ID""" """An object with an ID"""
@ -2542,6 +2545,37 @@ type TagEdge {
"""A time of day, in 24-hour hh:mm notation.""" """A time of day, in 24-hour hh:mm notation."""
scalar Time scalar Time
"""
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.
"""
type Track implements Entity {
"""The MBID of the entity."""
mbid: MBID!
"""The official title of the entity."""
title: String
"""
The tracks position on the overall release (including all
tracks from all discs).
"""
position: Int
"""
The track number, which may include information about the
disc or side it appears on, e.g. “A1” or “B3”.
"""
number: String
"""The length of the track."""
length: Duration
"""The recording that appears on the track."""
recording: Recording
}
""" """
A [URL](https://musicbrainz.org/doc/URL) pointing to a resource A [URL](https://musicbrainz.org/doc/URL) pointing to a resource
external to MusicBrainz, i.e. an official homepage, a site where music can be external to MusicBrainz, i.e. an official homepage, a site where music can be

View file

@ -62,6 +62,7 @@ or the schemas provided by the [built-in extensions](extensions).
* [Tag](#tag) * [Tag](#tag)
* [TagConnection](#tagconnection) * [TagConnection](#tagconnection)
* [TagEdge](#tagedge) * [TagEdge](#tagedge)
* [Track](#track)
* [URL](#url) * [URL](#url)
* [Work](#work) * [Work](#work)
* [WorkConnection](#workconnection) * [WorkConnection](#workconnection)
@ -3544,6 +3545,15 @@ The number of audio tracks on this medium.
A list of physical discs and their disc IDs for this medium. A list of physical discs and their disc IDs for this medium.
</td>
</tr>
<tr>
<td colspan="2" valign="top"><strong>tracks</strong></td>
<td valign="top">[<a href="#track">Track</a>]</td>
<td>
The list of tracks on the given media.
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -6719,6 +6729,81 @@ these results were found through a search.
</tbody> </tbody>
</table> </table>
### Track
A track is the way a recording is represented on a particular
release (or, more exactly, on a particular medium). Every track has a title
(see the guidelines for titles) and is credited to one or more artists.
<table>
<thead>
<tr>
<th align="left">Field</th>
<th align="right">Argument</th>
<th align="left">Type</th>
<th align="left">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2" valign="top"><strong>mbid</strong></td>
<td valign="top"><a href="#mbid">MBID</a>!</td>
<td>
The MBID of the entity.
</td>
</tr>
<tr>
<td colspan="2" valign="top"><strong>title</strong></td>
<td valign="top"><a href="#string">String</a></td>
<td>
The official title of the entity.
</td>
</tr>
<tr>
<td colspan="2" valign="top"><strong>position</strong></td>
<td valign="top"><a href="#int">Int</a></td>
<td>
The tracks position on the overall release (including all
tracks from all discs).
</td>
</tr>
<tr>
<td colspan="2" valign="top"><strong>number</strong></td>
<td valign="top"><a href="#string">String</a></td>
<td>
The track number, which may include information about the
disc or side it appears on, e.g. “A1” or “B3”.
</td>
</tr>
<tr>
<td colspan="2" valign="top"><strong>length</strong></td>
<td valign="top"><a href="#duration">Duration</a></td>
<td>
The length of the track.
</td>
</tr>
<tr>
<td colspan="2" valign="top"><strong>recording</strong></td>
<td valign="top"><a href="#recording">Recording</a></td>
<td>
The recording that appears on the track.
</td>
</tr>
</tbody>
</table>
### URL ### URL
A [URL](https://musicbrainz.org/doc/URL) pointing to a resource A [URL](https://musicbrainz.org/doc/URL) pointing to a resource

View file

@ -33,8 +33,7 @@
"format": "npm run lint:fix", "format": "npm run lint:fix",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint --fix .", "lint:fix": "eslint --fix .",
"postinstall": "postinstall-build lib --script build:lib", "prepare": "npm run clean:lib && npm run build:lib",
"prepublish": "npm run clean:lib && npm run build:lib",
"preversion": "npm run update-schema && npm run build:docs && git add schema.json docs", "preversion": "npm run update-schema && npm run build:docs && git add schema.json docs",
"print-schema": "babel-node scripts/print-schema.js", "print-schema": "babel-node scripts/print-schema.js",
"print-schema:json": "npm run print-schema -- --json", "print-schema:json": "npm run print-schema -- --json",
@ -84,7 +83,6 @@
"graphql-tools": "^3.1.1", "graphql-tools": "^3.1.1",
"lru-cache": "^4.1.3", "lru-cache": "^4.1.3",
"pascalcase": "^0.1.1", "pascalcase": "^0.1.1",
"postinstall-build": "^5.0.1",
"qs": "^6.5.2", "qs": "^6.5.2",
"request": "^2.87.0", "request": "^2.87.0",
"retry": "^0.12.0" "retry": "^0.12.0"

View file

@ -1068,6 +1068,11 @@
"name": "Release", "name": "Release",
"ofType": null "ofType": null
}, },
{
"kind": "OBJECT",
"name": "Track",
"ofType": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Label", "name": "Label",
@ -3432,6 +3437,22 @@
}, },
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
},
{
"name": "tracks",
"description": "The list of tracks on the given media.",
"args": [],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Track",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
} }
], ],
"inputFields": null, "inputFields": null,
@ -3579,6 +3600,99 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "Track",
"description": "A track is the way a recording is represented on a particular\n release (or, more exactly, on a particular medium). Every track has a title\n (see the guidelines for titles) and is credited to one or more artists.",
"fields": [
{
"name": "mbid",
"description": "The MBID of the entity.",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "MBID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "title",
"description": "The official title of the entity.",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "position",
"description": "The tracks position on the overall release (including all\ntracks from all discs).",
"args": [],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "number",
"description": "The track number, which may include information about the\ndisc or side it appears on, e.g. “A1” or “B3”.",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "length",
"description": "The length of the track.",
"args": [],
"type": {
"kind": "SCALAR",
"name": "Duration",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "recording",
"description": "The recording that appears on the track.",
"args": [],
"type": {
"kind": "OBJECT",
"name": "Recording",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
{
"kind": "INTERFACE",
"name": "Entity",
"ofType": null
}
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "LabelConnection", "name": "LabelConnection",

View file

@ -42,14 +42,17 @@ export function includeSubqueries(params, info, fragments = info.fragments) {
artistCredit: ['artist-credits'], artistCredit: ['artist-credits'],
artistCredits: ['artist-credits'], artistCredits: ['artist-credits'],
isrcs: ['isrcs'], isrcs: ['isrcs'],
media: ['media', 'discids'], media: ['media'],
'media.discs': ['discids'],
'media.tracks': ['recordings'],
rating: ['ratings'], rating: ['ratings'],
tags: ['tags'] tags: ['tags']
} }
let fields = getFields(info, fragments) let fields = getFields(info, fragments, 1)
const include = [] const include = []
for (const key in subqueryIncludes) { for (const key in subqueryIncludes) {
if (fields[key]) { const field = fields[key]
if (field) {
const value = subqueryIncludes[key] const value = subqueryIncludes[key]
include.push(...value) include.push(...value)
} }
@ -58,11 +61,8 @@ export function includeSubqueries(params, info, fragments = info.fragments) {
...params, ...params,
inc: extendIncludes(params.inc, include) inc: extendIncludes(params.inc, include)
} }
if (fields.edges) { if (fields['edges.node']) {
fields = getFields(fields.edges, fragments) params = includeSubqueries(params, fields['edges.node'], fragments)
if (fields.node) {
params = includeSubqueries(params, fields.node, fragments)
}
} }
return params return params
} }

View file

@ -5,7 +5,9 @@ import {
GraphQLInt GraphQLInt
} from 'graphql/type' } from 'graphql/type'
import Disc from './disc' import Disc from './disc'
import Track from './track'
import { resolveHyphenated, fieldWithID } from './helpers' import { resolveHyphenated, fieldWithID } from './helpers'
import { createSubqueryResolver } from '../resolvers'
export default new GraphQLObjectType({ export default new GraphQLObjectType({
name: 'Medium', name: 'Medium',
@ -37,6 +39,14 @@ multi-disc release).`
type: new GraphQLList(Disc), type: new GraphQLList(Disc),
description: description:
'A list of physical discs and their disc IDs for this medium.' 'A list of physical discs and their disc IDs for this medium.'
},
tracks: {
type: new GraphQLList(Track),
description: 'The list of tracks on the given media.',
resolve: createSubqueryResolver({
inc: 'recordings',
key: 'tracks'
})
} }
}) })
}) })

42
src/types/track.js Normal file
View file

@ -0,0 +1,42 @@
import { GraphQLObjectType, GraphQLInt, GraphQLString } from 'graphql/type'
import Entity from './entity'
import { Duration } from './scalars'
import Recording from './recording'
import { mbid, title } from './helpers'
export default 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 tracks 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
}
}
})
})

View file

@ -2,7 +2,12 @@ import util from 'util'
export const ONE_DAY = 24 * 60 * 60 * 1000 export const ONE_DAY = 24 * 60 * 60 * 1000
export function getFields(info, fragments = info.fragments) { export function getFields(
info,
fragments = info.fragments,
depth = 0,
prefix = ''
) {
if (info.kind !== 'Field') { if (info.kind !== 'Field') {
info = info.fieldNodes[0] info = info.fieldNodes[0]
} }
@ -18,7 +23,17 @@ export function getFields(info, fragments = info.fragments) {
} else if (selection.kind === 'InlineFragment') { } else if (selection.kind === 'InlineFragment') {
selection.selectionSet.selections.reduce(reducer, fields) selection.selectionSet.selections.reduce(reducer, fields)
} else { } else {
fields[selection.name.value] = selection const prefixedName = prefix + selection.name.value
fields[prefixedName] = selection
if (depth > 0 && selection.selectionSet) {
const subFields = getFields(
selection,
fragments,
depth - 1,
`${prefixedName}.`
)
Object.assign(fields, subFields)
}
} }
return fields return fields
} }

View file

@ -1408,3 +1408,40 @@ test(
t.true(artists[0].tags.edges.some(edge => edge.node.count > 0)) t.true(artists[0].tags.edges.some(edge => edge.node.count > 0))
} }
) )
test(
'releases can include tracks',
testData,
`
{
lookup {
release(mbid: "fba5f8fe-c6c8-4511-8562-c9febf482674") {
media {
trackCount
position
formatID
format
tracks {
mbid
title
position
number
length
recording {
title
}
}
}
}
}
}
`,
(t, data) => {
t.true(data.lookup.release.media.every(media => media.tracks.length > 0))
t.true(
data.lookup.release.media.every(media =>
media.tracks.every(track => track.recording)
)
)
}
)

View file

@ -0,0 +1 @@
{"packaging":"Cassette Case","title":"OK Computer","status":"Official","text-representation":{"script":"Latn","language":"eng"},"country":"GB","date":"1997-06-16","barcode":"724385522949","asin":"B000002UJR","media":[{"format-id":"f5e6e254-8f39-331c-936b-9c69d686dc47","format":"Cassette","track-offset":0,"title":"","position":1,"track-count":12,"tracks":[{"recording":{"title":"Airbag","video":false,"disambiguation":"","id":"4a7fea2e-545b-4c63-bc9a-9943cc3a29d7","length":284400},"position":1,"length":284400,"id":"a31f27a1-dfd8-38cc-958a-07f3af52fc25","number":"A1","title":"Airbag"},{"number":"A2","title":"Paranoid Android","recording":{"id":"9f9cf187-d6f9-437f-9d98-d59cdbd52757","length":383493,"title":"Paranoid Android","video":false,"disambiguation":""},"length":383493,"position":2,"id":"26ca3f84-46d8-3627-b894-b1eb8599a110"},{"id":"66ee1e73-a67c-3988-9ab5-e74721be5258","length":267706,"recording":{"length":267706,"id":"bd82738d-163c-4b1a-bfaf-7acffe30e68a","video":false,"title":"Subterranean Homesick Alien","disambiguation":""},"position":3,"title":"Subterranean Homesick Alien","number":"A3"},{"id":"db7b6f2d-18b8-39b2-826d-eb9e82f35f3a","position":4,"recording":{"disambiguation":"","video":false,"title":"Exit Music (for a Film)","length":264800,"id":"23c3c36b-9449-4484-9040-6ef2125999aa"},"length":264800,"title":"Exit Music (for a Film)","number":"A4"},{"title":"Let Down","number":"A5","id":"5fe3d1b0-2f09-327e-b7e7-9fcff3158a8a","recording":{"length":299266,"id":"47b02a82-c3bf-4647-b894-dd1c8f608e7f","disambiguation":"","video":false,"title":"Let Down"},"length":299266,"position":5},{"number":"A6","title":"Karma Police","length":261626,"recording":{"length":262000,"id":"9e2ad5bc-c6f9-40d2-a36f-3122ee2072a3","disambiguation":"","title":"Karma Police","video":false},"position":6,"id":"944a6d7d-a244-3295-ad6c-1440a52998e8"},{"title":"Fitter Happier","number":"B1","id":"7feb7034-4809-39c0-8576-2ea7a83d118e","position":7,"recording":{"video":false,"title":"Fitter Happier","disambiguation":"","id":"5838f978-0822-4e28-874f-e1511324ec3a","length":117333},"length":117333},{"recording":{"video":false,"title":"Electioneering","disambiguation":"","id":"ba0a796c-bd1f-4d4b-85a8-918f217a204a","length":230640},"position":8,"length":230640,"id":"0481f01e-b351-3c42-b851-748fa741ca74","number":"B2","title":"Electioneering"},{"id":"52ac1a5e-a231-37b4-a82d-7dbc0c74bd72","length":285200,"recording":{"disambiguation":"","video":false,"title":"Climbing Up the Walls","id":"c7225576-001e-423c-adc1-58f0985dcb27","length":285200},"position":9,"title":"Climbing Up the Walls","number":"B3"},{"title":"No Surprises","number":"B4","id":"94cede1d-3eb0-3978-8235-3747bb616cc0","position":10,"recording":{"length":228533,"id":"980a426e-623e-4ea5-98c7-008d037a0508","video":false,"title":"No Surprises","disambiguation":""},"length":228533},{"number":"B5","title":"Lucky","recording":{"disambiguation":"","title":"Lucky","video":false,"id":"79047824-f821-4b1a-9893-e0cea1c947dd","length":259626},"length":259626,"position":11,"id":"34a59962-7e37-3fd9-8a74-f80528ffa1a0"},{"number":"B6","title":"The Tourist","recording":{"length":324533,"id":"610c0012-6eb4-42a0-b759-3a2532ce0f15","title":"The Tourist","video":false,"disambiguation":""},"length":324533,"position":12,"id":"9f537c38-29dc-3114-9ce3-8c99d37e743f"}]}],"status-id":"4e304316-386d-3409-af2e-78857eec5cfe","disambiguation":"","packaging-id":"c70b737a-0114-39a9-88f7-82843e54f906","cover-art-archive":{"count":1,"artwork":true,"darkened":false,"back":false,"front":true},"release-events":[{"area":{"iso-3166-1-codes":["GB"],"disambiguation":"","sort-name":"United Kingdom","name":"United Kingdom","id":"8a754a16-0027-3a29-b6d7-2b40ea0481ed"},"date":"1997-06-16"}],"quality":"normal","id":"fba5f8fe-c6c8-4511-8562-c9febf482674"}

View file

@ -0,0 +1,28 @@
{
"statusCode": 200,
"headers": {
"date": "Mon, 17 Sep 2018 04:12:07 GMT",
"content-type": "application/json; charset=utf-8",
"transfer-encoding": "chunked",
"connection": "keep-alive",
"keep-alive": "timeout=15",
"vary": "Accept-Encoding",
"x-ratelimit-limit": "1200",
"x-ratelimit-remaining": "909",
"x-ratelimit-reset": "1537157527",
"server": "Plack::Handler::Starlet",
"etag": "W/\"cb65dbe98571f1f412e000ada8a9c1e9\"",
"access-control-allow-origin": "*"
},
"url": "http://musicbrainz.org:80/ws/2/release/fba5f8fe-c6c8-4511-8562-c9febf482674?inc=media%2Brecordings&fmt=json",
"time": 469,
"request": {
"method": "GET",
"headers": {
"User-Agent": "graphbrainz/8.0.4 ( https://github.com/exogen/graphbrainz )",
"host": "musicbrainz.org",
"accept-encoding": "gzip, deflate",
"accept": "application/json"
}
}
}

View file

@ -0,0 +1 @@
{"disambiguation":"","barcode":"606949047320","media":[{"track-count":12,"format":"CD","position":1,"title":"Left","format-id":"9712d52a-4509-3d4b-a1a2-67c88c643e31"},{"format-id":"9712d52a-4509-3d4b-a1a2-67c88c643e31","title":"Right","track-count":11,"position":2,"format":"CD"}],"country":"US","release-events":[{"area":{"name":"United States","sort-name":"United States","disambiguation":"","id":"489ce91b-6658-3307-9877-795b68554c98","iso-3166-1-codes":["US"]},"date":"1999-09-21"}],"status":"Official","date":"1999-09-21","packaging":"Digipak","asin":"B00001P4TH","status-id":"4e304316-386d-3409-af2e-78857eec5cfe","id":"a4864e94-6d75-4ade-bc93-0dabf3521453","cover-art-archive":{"front":true,"artwork":true,"count":2,"darkened":false,"back":true},"title":"The Fragile","text-representation":{"script":"Latn","language":"eng"},"packaging-id":"8f931351-d2e2-310f-afc6-37b89ddba246","quality":"normal"}

View file

@ -0,0 +1,28 @@
{
"statusCode": 200,
"headers": {
"date": "Mon, 17 Sep 2018 04:11:56 GMT",
"content-type": "application/json; charset=utf-8",
"transfer-encoding": "chunked",
"connection": "keep-alive",
"keep-alive": "timeout=15",
"vary": "Accept-Encoding",
"x-ratelimit-limit": "1200",
"x-ratelimit-remaining": "1151",
"x-ratelimit-reset": "1537157517",
"server": "Plack::Handler::Starlet",
"etag": "W/\"dc6ae505140f505a493e772ea6af4426\"",
"access-control-allow-origin": "*"
},
"url": "http://musicbrainz.org:80/ws/2/release/a4864e94-6d75-4ade-bc93-0dabf3521453?inc=media&fmt=json",
"time": 408,
"request": {
"method": "GET",
"headers": {
"User-Agent": "graphbrainz/8.0.4 ( https://github.com/exogen/graphbrainz )",
"host": "musicbrainz.org",
"accept-encoding": "gzip, deflate",
"accept": "application/json"
}
}
}

2117
yarn.lock

File diff suppressed because it is too large Load diff