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.
- package/LICENSE.txt +9 -0
- package/README.md +151 -31
- package/package.json +6 -5
- package/lib/coverartarchive-api.d.ts +0 -48
- package/lib/coverartarchive-api.js +0 -60
- package/lib/digest-auth.d.ts +0 -21
- package/lib/digest-auth.js +0 -77
- package/lib/entry-default.d.ts +0 -2
- package/lib/entry-default.js +0 -3
- package/lib/entry-node.d.ts +0 -2
- package/lib/entry-node.js +0 -3
- package/lib/http-client-node.d.ts +0 -11
- package/lib/http-client-node.js +0 -19
- package/lib/http-client.d.ts +0 -27
- package/lib/http-client.js +0 -50
- package/lib/musicbrainz-api-node.d.ts +0 -16
- package/lib/musicbrainz-api-node.js +0 -71
- package/lib/musicbrainz-api.d.ts +0 -180
- package/lib/musicbrainz-api.js +0 -207
- package/lib/musicbrainz.types.d.ts +0 -606
- package/lib/musicbrainz.types.js +0 -14
- package/lib/xml/xml-isrc-list.d.ts +0 -17
- package/lib/xml/xml-isrc-list.js +0 -19
- package/lib/xml/xml-isrc.d.ts +0 -10
- package/lib/xml/xml-isrc.js +0 -14
- package/lib/xml/xml-metadata.d.ts +0 -6
- package/lib/xml/xml-metadata.js +0 -26
- package/lib/xml/xml-recording.d.ts +0 -24
- package/lib/xml/xml-recording.js +0 -17
|
@@ -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
|
package/lib/musicbrainz-api.d.ts
DELETED
|
@@ -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;
|
package/lib/musicbrainz-api.js
DELETED
|
@@ -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
|