musicbrainz-api 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,71 +0,0 @@
1
- import { StatusCodes as HttpStatus } from 'http-status-codes';
2
- import Debug from 'debug';
3
- export { XmlMetadata } from './xml/xml-metadata.js';
4
- export { XmlIsrc } from './xml/xml-isrc.js';
5
- export { XmlIsrcList } from './xml/xml-isrc-list.js';
6
- export { XmlRecording } from './xml/xml-recording.js';
7
- import { HttpClientNode } from "./http-client-node.js";
8
- import { MusicBrainzApi as MusicBrainzApiDefault } from "./musicbrainz-api.js";
9
- export * from './musicbrainz.types.js';
10
- export * from './http-client.js';
11
- /*
12
- * https://musicbrainz.org/doc/Development/XML_Web_Service/Version_2#Subqueries
13
- */
14
- const debug = Debug('musicbrainz-api-node');
15
- export class MusicBrainzApi extends MusicBrainzApiDefault {
16
- initHttpClient() {
17
- return new HttpClientNode({
18
- baseUrl: this.config.baseUrl,
19
- timeout: 20 * 1000,
20
- userAgent: `${this.config.appName}/${this.config.appVersion} ( ${this.config.appContactInfo} )`
21
- });
22
- }
23
- async login() {
24
- if (!this.config.botAccount?.username)
25
- throw new Error('bot username should be set');
26
- if (!this.config.botAccount?.password)
27
- throw new Error('bot password should be set');
28
- if (this.session?.loggedIn) {
29
- const cookies = await this.httpClient.getCookies();
30
- return cookies.indexOf('musicbrainz_server_session') !== -1;
31
- }
32
- this.session = await this.getSession();
33
- const redirectUri = '/success';
34
- const formData = {
35
- username: this.config.botAccount.username,
36
- password: this.config.botAccount.password,
37
- csrf_session_key: this.session.csrf.sessionKey,
38
- csrf_token: this.session.csrf.token,
39
- remember_me: '1'
40
- };
41
- const response = await this.httpClient.postForm('login', formData, {
42
- query: {
43
- returnto: redirectUri
44
- },
45
- followRedirects: false
46
- });
47
- const success = response.status === HttpStatus.MOVED_TEMPORARILY && response.headers.get('location') === redirectUri;
48
- if (success) {
49
- this.session.loggedIn = true;
50
- }
51
- return success;
52
- }
53
- /**
54
- * Logout
55
- */
56
- async logout() {
57
- const redirectUri = '/success';
58
- const response = await this.httpClient.post('logout', {
59
- followRedirects: false,
60
- query: {
61
- returnto: redirectUri
62
- }
63
- });
64
- const success = response.status === HttpStatus.MOVED_TEMPORARILY && response.headers.get('location') === redirectUri;
65
- if (success && this.session) {
66
- this.session.loggedIn = true;
67
- }
68
- return success;
69
- }
70
- }
71
- //# sourceMappingURL=musicbrainz-api-node.js.map
@@ -1,180 +0,0 @@
1
- export { XmlMetadata } from './xml/xml-metadata.js';
2
- export { XmlIsrc } from './xml/xml-isrc.js';
3
- export { XmlIsrcList } from './xml/xml-isrc-list.js';
4
- export { XmlRecording } from './xml/xml-recording.js';
5
- import type { XmlMetadata } from './xml/xml-metadata.js';
6
- import { RateLimitThreshold } from 'rate-limit-threshold';
7
- import * as mb from './musicbrainz.types.js';
8
- import { HttpClient } from "./http-client.js";
9
- export * from './musicbrainz.types.js';
10
- export type RelationsIncludes = 'area-rels' | 'artist-rels' | 'event-rels' | 'instrument-rels' | 'label-rels' | 'place-rels' | 'recording-rels' | 'release-rels' | 'release-group-rels' | 'series-rels' | 'url-rels' | 'work-rels';
11
- export type SubQueryIncludes =
12
- /**
13
- * include discids for all media in the releases
14
- */
15
- 'discids'
16
- /**
17
- * include media for all releases, this includes the # of tracks on each medium and its format.
18
- */
19
- | 'media'
20
- /**
21
- * include isrcs for all recordings
22
- */
23
- | 'isrcs'
24
- /**
25
- * include artists credits for all releases and recordings
26
- */
27
- | 'artist-credits'
28
- /**
29
- * include only those releases where the artist appears on one of the tracks, only valid on artists in combination with `releases`
30
- */
31
- | 'various-artists';
32
- export type MiscIncludes = 'aliases' | 'annotation' | 'tags' | 'genres' | 'ratings' | 'media';
33
- export type AreaIncludes = MiscIncludes | RelationsIncludes;
34
- export type ArtistIncludes = MiscIncludes | RelationsIncludes | 'recordings' | 'releases' | 'release-groups' | 'works';
35
- export type CollectionIncludes = MiscIncludes | RelationsIncludes | 'user-collections';
36
- export type EventIncludes = MiscIncludes | RelationsIncludes;
37
- export type GenreIncludes = MiscIncludes;
38
- export type InstrumentIncludes = MiscIncludes | RelationsIncludes;
39
- export type LabelIncludes = MiscIncludes | RelationsIncludes | 'releases';
40
- export type PlaceIncludes = MiscIncludes | RelationsIncludes;
41
- export type RecordingIncludes = MiscIncludes | RelationsIncludes | SubQueryIncludes | 'artists' | 'releases' | 'isrcs';
42
- export type ReleaseIncludes = MiscIncludes | SubQueryIncludes | RelationsIncludes | 'artists' | 'collections' | 'labels' | 'recordings' | 'release-groups';
43
- export type ReleaseGroupIncludes = MiscIncludes | SubQueryIncludes | RelationsIncludes | 'artists' | 'releases';
44
- export type SeriesIncludes = MiscIncludes | RelationsIncludes;
45
- export type WorkIncludes = MiscIncludes | RelationsIncludes;
46
- export type UrlIncludes = RelationsIncludes;
47
- export type IFormData = {
48
- [key: string]: string | number;
49
- };
50
- export interface IMusicBrainzConfig {
51
- botAccount?: {
52
- username?: string;
53
- password?: string;
54
- };
55
- baseUrl?: string;
56
- appName?: string;
57
- appVersion?: string;
58
- /**
59
- * HTTP Proxy
60
- */
61
- proxy?: string;
62
- /**
63
- * User e-mail address or application URL
64
- */
65
- appContactInfo?: string;
66
- disableRateLimiting?: boolean;
67
- }
68
- interface IInternalConfig extends IMusicBrainzConfig {
69
- baseUrl: string;
70
- }
71
- export interface ICsrfSession {
72
- sessionKey: string;
73
- token: string;
74
- }
75
- export interface ISessionInformation {
76
- csrf: ICsrfSession;
77
- loggedIn?: boolean;
78
- }
79
- export declare class MusicBrainzApi {
80
- readonly config: IInternalConfig;
81
- protected rateLimiter: RateLimitThreshold;
82
- protected httpClient: HttpClient;
83
- protected session?: ISessionInformation;
84
- static fetchCsrf(html: string): ICsrfSession;
85
- private static fetchValue;
86
- constructor(_config?: IMusicBrainzConfig);
87
- protected initHttpClient(): HttpClient;
88
- restGet<T>(relUrl: string, query?: {
89
- [key: string]: string;
90
- }): Promise<T>;
91
- /**
92
- * Lookup entity
93
- * @param entity 'area', 'artist', collection', 'instrument', 'label', 'place', 'release', 'release-group', 'recording', 'series', 'work', 'url' or 'event'
94
- * @param mbid Entity MBID
95
- * @param inc Query, like: {<entity>: <MBID:}
96
- */
97
- lookup(entity: 'area', mbid: string, inc?: AreaIncludes[]): Promise<mb.IArea>;
98
- lookup(entity: 'artist', mbid: string, inc?: ArtistIncludes[]): Promise<mb.IArtist>;
99
- lookup(entity: 'collection', mbid: string, inc?: CollectionIncludes[]): Promise<mb.ICollection>;
100
- lookup(entity: 'instrument', mbid: string, inc?: InstrumentIncludes[]): Promise<mb.IInstrument>;
101
- lookup(entity: 'label', mbid: string, inc?: LabelIncludes[]): Promise<mb.ILabel>;
102
- lookup(entity: 'place', mbid: string, inc?: PlaceIncludes[]): Promise<mb.IPlace>;
103
- lookup(entity: 'release', mbid: string, inc?: ReleaseIncludes[]): Promise<mb.IRelease>;
104
- lookup(entity: 'release-group', mbid: string, inc?: ReleaseGroupIncludes[]): Promise<mb.IReleaseGroup>;
105
- lookup(entity: 'recording', mbid: string, inc?: RecordingIncludes[]): Promise<mb.IRecording>;
106
- lookup(entity: 'series', mbid: string, inc?: SeriesIncludes[]): Promise<mb.ISeries>;
107
- lookup(entity: 'work', mbid: string, inc?: WorkIncludes[]): Promise<mb.IWork>;
108
- lookup(entity: 'url', mbid: string, inc?: UrlIncludes[]): Promise<mb.IUrl>;
109
- lookup(entity: 'event', mbid: string, inc?: EventIncludes[]): Promise<mb.IEvent>;
110
- /**
111
- * Browse entity
112
- * https://wiki.musicbrainz.org/MusicBrainz_API#Browse
113
- * https://wiki.musicbrainz.org/MusicBrainz_API#Linked_entities
114
- * https://wiki.musicbrainz.org/Development/JSON_Web_Service#Browse_Requests
115
- * For example: http://musicbrainz.org/ws/2/release?label=47e718e1-7ee4-460c-b1cc-1192a841c6e5&offset=12&limit=2
116
- * @param entity MusicBrainz entity
117
- * @param query Query, like: {<entity>: <MBID:}
118
- */
119
- browse(entity: 'area', query?: mb.IBrowseAreasQuery): Promise<mb.IBrowseAreasResult>;
120
- browse(entity: 'artist', query?: mb.IBrowseArtistsQuery): Promise<mb.IBrowseArtistsResult>;
121
- browse(entity: 'collection', query?: mb.IBrowseCollectionsQuery): Promise<mb.IBrowseCollectionsResult>;
122
- browse(entity: 'event', query?: mb.IBrowseEventsQuery): Promise<mb.IBrowseEventsResult>;
123
- browse(entity: 'label', query?: mb.IBrowseLabelsQuery): Promise<mb.IBrowseLabelsResult>;
124
- browse(entity: 'instrument', query?: mb.IBrowseInstrumentsQuery): Promise<mb.IBrowseInstrumentsResult>;
125
- browse(entity: 'place', query?: mb.IBrowsePlacesQuery): Promise<mb.IBrowsePlacesResult>;
126
- browse(entity: 'recording', query?: mb.IBrowseRecordingsQuery): Promise<mb.IBrowseRecordingsResult>;
127
- browse(entity: 'release', query?: mb.IBrowseReleasesQuery): Promise<mb.IBrowseReleasesResult>;
128
- browse(entity: 'release-group', query?: mb.IBrowseReleaseGroupsQuery): Promise<mb.IBrowseReleaseGroupsResult>;
129
- browse(entity: 'series', query?: mb.IBrowseSeriesQuery): Promise<mb.IBrowseSeriesResult>;
130
- browse(entity: 'url', query?: mb.IBrowseUrlsQuery): Promise<mb.IUrl>;
131
- browse(entity: 'work', query?: mb.IBrowseWorksQuery): Promise<mb.IBrowseWorksResult>;
132
- /**
133
- * Search an entity using a search query
134
- * @param query e.g.: '" artist: Madonna, track: Like a virgin"' or object with search terms: {artist: Madonna}
135
- * @param entity e.g. 'recording'
136
- * @param query Arguments
137
- */
138
- search(entity: 'area', query: mb.ISearchQuery<AreaIncludes> & mb.ILinkedEntitiesArea): Promise<mb.IAreaList>;
139
- search(artist: 'artist', query: mb.ISearchQuery<ArtistIncludes> & mb.ILinkedEntitiesArea): Promise<mb.IArtistList>;
140
- search(artist: 'recording', query: mb.ISearchQuery<AreaIncludes> & mb.ILinkedEntitiesArea): Promise<mb.IRecordingList>;
141
- search(artist: 'release', query: mb.ISearchQuery<ReleaseIncludes> & mb.ILinkedEntitiesArea): Promise<mb.IReleaseList>;
142
- search(artist: 'release-group', query: mb.ISearchQuery<ReleaseGroupIncludes> & mb.ILinkedEntitiesArea): Promise<mb.IReleaseGroupList>;
143
- search(artist: 'url', query: mb.ISearchQuery<UrlIncludes> & mb.ILinkedEntitiesArea): Promise<mb.IUrlList>;
144
- postRecording(xmlMetadata: XmlMetadata): Promise<void>;
145
- post(entity: mb.EntityType, xmlMetadata: XmlMetadata): Promise<void>;
146
- /**
147
- * Submit entity
148
- * @param entity Entity type e.g. 'recording'
149
- * @param mbid
150
- * @param formData
151
- */
152
- editEntity(entity: mb.EntityType, mbid: string, formData: Record<string, any>): Promise<void>;
153
- /**
154
- * Set URL to recording
155
- * @param recording Recording to update
156
- * @param url2add URL to add to the recording
157
- * @param editNote Edit note
158
- */
159
- addUrlToRecording(recording: mb.IRecording, url2add: {
160
- linkTypeId: mb.LinkType;
161
- text: string;
162
- }, editNote?: string): Promise<void>;
163
- /**
164
- * Add ISRC to recording
165
- * @param recording Recording to update
166
- * @param isrc ISRC code to add
167
- */
168
- addIsrc(recording: mb.IRecording, isrc: string): Promise<void>;
169
- /**
170
- * Add Spotify-ID to MusicBrainz recording.
171
- * This function will automatically lookup the recording title, which is required to submit the recording URL
172
- * @param recording MBID of the recording
173
- * @param spotifyId Spotify ID
174
- * @param editNote Comment to add.
175
- */
176
- addSpotifyIdToRecording(recording: mb.IRecording, spotifyId: string, editNote: string): Promise<void>;
177
- protected getSession(): Promise<ISessionInformation>;
178
- protected applyRateLimiter(): Promise<void>;
179
- }
180
- export declare function makeAndQueryString(keyValuePairs: IFormData): string;
@@ -1,207 +0,0 @@
1
- import { StatusCodes as HttpStatus } from 'http-status-codes';
2
- import Debug from 'debug';
3
- export { XmlMetadata } from './xml/xml-metadata.js';
4
- export { XmlIsrc } from './xml/xml-isrc.js';
5
- export { XmlIsrcList } from './xml/xml-isrc-list.js';
6
- export { XmlRecording } from './xml/xml-recording.js';
7
- import { DigestAuth } from './digest-auth.js';
8
- import { RateLimitThreshold } from 'rate-limit-threshold';
9
- import * as mb from './musicbrainz.types.js';
10
- import { HttpClient } from "./http-client.js";
11
- export * from './musicbrainz.types.js';
12
- const debug = Debug('musicbrainz-api');
13
- export class MusicBrainzApi {
14
- static fetchCsrf(html) {
15
- return {
16
- sessionKey: MusicBrainzApi.fetchValue(html, 'csrf_session_key'),
17
- token: MusicBrainzApi.fetchValue(html, 'csrf_token')
18
- };
19
- }
20
- static fetchValue(html, key) {
21
- let pos = html.indexOf(`name="${key}"`);
22
- if (pos >= 0) {
23
- pos = html.indexOf('value="', pos + key.length + 7);
24
- if (pos >= 0) {
25
- pos += 7;
26
- const endValuePos = html.indexOf('"', pos);
27
- return html.substring(pos, endValuePos);
28
- }
29
- }
30
- }
31
- constructor(_config) {
32
- this.config = {
33
- ...{
34
- baseUrl: 'https://musicbrainz.org'
35
- },
36
- ..._config
37
- };
38
- this.httpClient = this.initHttpClient();
39
- this.rateLimiter = new RateLimitThreshold(15, 18);
40
- }
41
- initHttpClient() {
42
- return new HttpClient({
43
- baseUrl: this.config.baseUrl,
44
- timeout: 20 * 1000,
45
- userAgent: `${this.config.appName}/${this.config.appVersion} ( ${this.config.appContactInfo} )`
46
- });
47
- }
48
- async restGet(relUrl, query = {}) {
49
- query.fmt = 'json';
50
- await this.applyRateLimiter();
51
- const response = await this.httpClient.get(`ws/2${relUrl}`, {
52
- query,
53
- retryLimit: 10
54
- });
55
- return response.json();
56
- }
57
- lookup(entity, mbid, inc = []) {
58
- return this.restGet(`/${entity}/${mbid}`, { inc: inc.join(' ') });
59
- }
60
- browse(entity, query) {
61
- return this.restGet(`/${entity}`, query);
62
- }
63
- search(entity, query) {
64
- const urlQuery = { ...query };
65
- if (typeof query.query === 'object') {
66
- urlQuery.query = makeAndQueryString(query.query);
67
- }
68
- if (Array.isArray(query.inc)) {
69
- urlQuery.inc = urlQuery.inc.join(' ');
70
- }
71
- return this.restGet(`/${entity}/`, urlQuery);
72
- }
73
- // ---------------------------------------------------------------------------
74
- async postRecording(xmlMetadata) {
75
- return this.post('recording', xmlMetadata);
76
- }
77
- async post(entity, xmlMetadata) {
78
- if (!this.config.appName || !this.config.appVersion) {
79
- throw new Error("XML-Post requires the appName & appVersion to be defined");
80
- }
81
- const clientId = `${this.config.appName.replace(/-/g, '.')}-${this.config.appVersion}`;
82
- const path = `ws/2/${entity}/`;
83
- // Get digest challenge
84
- let digest = '';
85
- let n = 1;
86
- const postData = xmlMetadata.toXml();
87
- do {
88
- await this.applyRateLimiter();
89
- const response = await this.httpClient.post(path, {
90
- query: { client: clientId },
91
- headers: {
92
- authorization: digest,
93
- 'Content-Type': 'application/xml'
94
- },
95
- body: postData
96
- });
97
- if (response.statusCode === HttpStatus.UNAUTHORIZED) {
98
- // Respond to digest challenge
99
- const auth = new DigestAuth(this.config.botAccount);
100
- const relPath = response.requestUrl.pathname; // Ensure path is relative
101
- digest = auth.digest(response.request.method, relPath, response.headers['www-authenticate']);
102
- ++n;
103
- }
104
- else {
105
- break;
106
- }
107
- } while (n++ < 5);
108
- }
109
- /**
110
- * Submit entity
111
- * @param entity Entity type e.g. 'recording'
112
- * @param mbid
113
- * @param formData
114
- */
115
- async editEntity(entity, mbid, formData) {
116
- await this.applyRateLimiter();
117
- this.session = await this.getSession();
118
- formData.csrf_session_key = this.session.csrf.sessionKey;
119
- formData.csrf_token = this.session.csrf.token;
120
- formData.username = this.config.botAccount?.username;
121
- formData.password = this.config.botAccount?.password;
122
- formData.remember_me = 1;
123
- const response = await this.httpClient.postForm(`${entity}/${mbid}/edit`, formData, {
124
- followRedirects: false
125
- });
126
- if (response.status === HttpStatus.OK)
127
- throw new Error("Failed to submit form data");
128
- if (response.status === HttpStatus.MOVED_TEMPORARILY)
129
- return;
130
- throw new Error(`Unexpected status code: ${response.status}`);
131
- }
132
- /**
133
- * Set URL to recording
134
- * @param recording Recording to update
135
- * @param url2add URL to add to the recording
136
- * @param editNote Edit note
137
- */
138
- async addUrlToRecording(recording, url2add, editNote = '') {
139
- const formData = {};
140
- formData['edit-recording.name'] = recording.title; // Required
141
- formData['edit-recording.comment'] = recording.disambiguation;
142
- formData['edit-recording.make_votable'] = true;
143
- formData['edit-recording.url.0.link_type_id'] = url2add.linkTypeId;
144
- formData['edit-recording.url.0.text'] = url2add.text;
145
- recording.isrcs?.forEach((isrcs, i) => {
146
- formData[`edit-recording.isrcs.${i}`] = isrcs;
147
- });
148
- formData['edit-recording.edit_note'] = editNote;
149
- return this.editEntity('recording', recording.id, formData);
150
- }
151
- /**
152
- * Add ISRC to recording
153
- * @param recording Recording to update
154
- * @param isrc ISRC code to add
155
- */
156
- async addIsrc(recording, isrc) {
157
- const formData = {};
158
- formData["edit-recording.name"] = recording.title; // Required
159
- if (!recording.isrcs) {
160
- throw new Error('You must retrieve recording with existing ISRC values');
161
- }
162
- if (recording.isrcs.indexOf(isrc) === -1) {
163
- recording.isrcs.push(isrc);
164
- for (const i in recording.isrcs) {
165
- formData[`edit-recording.isrcs.${i}`] = recording.isrcs[i];
166
- }
167
- return this.editEntity('recording', recording.id, formData);
168
- }
169
- }
170
- // -----------------------------------------------------------------------------------------------------------------
171
- // Helper functions
172
- // -----------------------------------------------------------------------------------------------------------------
173
- /**
174
- * Add Spotify-ID to MusicBrainz recording.
175
- * This function will automatically lookup the recording title, which is required to submit the recording URL
176
- * @param recording MBID of the recording
177
- * @param spotifyId Spotify ID
178
- * @param editNote Comment to add.
179
- */
180
- addSpotifyIdToRecording(recording, spotifyId, editNote) {
181
- if (spotifyId.length !== 22) {
182
- throw new Error('Invalid Spotify ID length');
183
- }
184
- return this.addUrlToRecording(recording, {
185
- linkTypeId: mb.LinkType.stream_for_free,
186
- text: `https://open.spotify.com/track/${spotifyId}`
187
- }, editNote);
188
- }
189
- async getSession() {
190
- const response = await this.httpClient.get('login', {
191
- followRedirects: false
192
- });
193
- return {
194
- csrf: MusicBrainzApi.fetchCsrf(await response.text())
195
- };
196
- }
197
- async applyRateLimiter() {
198
- if (!this.config.disableRateLimiting) {
199
- const delay = await this.rateLimiter.limit();
200
- debug(`Client side rate limiter activated: cool down for ${Math.round(delay / 100) / 10} s...`);
201
- }
202
- }
203
- }
204
- export function makeAndQueryString(keyValuePairs) {
205
- return Object.keys(keyValuePairs).map(key => `${key}:"${keyValuePairs[key]}"`).join(' AND ');
206
- }
207
- //# sourceMappingURL=musicbrainz-api.js.map