diff --git a/README.md b/README.md index 15008d5a..ff1ae849 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ We are looking into making this package usable in the browser as well. ## Before using this library -MusicBrainz asks that you [identifying your application](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2#User%20Data) by filling in the ['User-Agent' Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent). +MusicBrainz asks that you to [identify your application](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2#User%20Data) by filling in the ['User-Agent' Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent). By passing `appName`, `appVersion`, `appMail` musicbrainz-api takes care of that. ## Submitting metadata diff --git a/lib/digest-auth.ts b/lib/digest-auth.ts index 8d93cd54..81628623 100644 --- a/lib/digest-auth.ts +++ b/lib/digest-auth.ts @@ -14,7 +14,7 @@ export interface ICredentials { password: string; } -function md5(str) { +function md5(str: string): string { return crypto.createHash('md5').update(str).digest('hex'); // lgtm [js/insufficient-password-hash] } @@ -28,14 +28,14 @@ export class DigestAuth { * If the algorithm directive's value is "MD5-sess", then HA1 is * HA1=MD5(MD5(username:realm:password):nonce:cnonce) */ - public static ha1Compute(algorithm, user, realm, pass, nonce, cnonce) { + public static ha1Compute(algorithm: string, user: string, realm: string, pass: string, nonce: string, cnonce: string): string { const ha1 = md5(user + ':' + realm + ':' + pass); // lgtm [js/insufficient-password-hash] return algorithm && algorithm.toLowerCase() === 'md5-sess' ? md5(ha1 + ':' + nonce + ':' + cnonce) : ha1; } public hasAuth: boolean; public sentAuth: boolean; - public bearerToken: string; + public bearerToken: string | null; public constructor(private credentials: ICredentials) { this.hasAuth = false; @@ -60,13 +60,13 @@ export class DigestAuth { if (!match) { break; } - challenge[match[1]] = match[2] || match[3]; + (challenge as any)[match[1]] = match[2] || match[3]; } - const qop = /(^|,)\s*auth\s*($|,)/.test(challenge.qop) && 'auth'; + const qop = /(^|,)\s*auth\s*($|,)/.test(challenge.qop as string) && 'auth'; const nc = qop && '00000001'; const cnonce = qop && uuidv4().replace(/-/g, ''); - const ha1 = DigestAuth.ha1Compute(challenge.algorithm, this.credentials.username, challenge.realm, this.credentials.password, challenge.nonce, cnonce); + const ha1 = DigestAuth.ha1Compute(challenge.algorithm as string, this.credentials.username, challenge.realm as string, this.credentials.password, challenge.nonce as string, cnonce as string); const ha2 = md5(method + ':' + path); // lgtm [js/insufficient-password-hash] const digestResponse = qop ? md5(ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2) // lgtm [js/insufficient-password-hash] @@ -85,15 +85,16 @@ export class DigestAuth { }; const parts: string[] = []; - for (const k in authValues) { - if (authValues[k]) { - if (k === 'qop' || k === 'nc' || k === 'algorithm') { - parts.push(k + '=' + authValues[k]); + Object.entries(authValues).forEach(([key, value]) => { + if (value) { + if (key === 'qop' || key === 'nc' || key === 'algorithm') { + parts.push(key + '=' + value); } else { - parts.push(k + '="' + authValues[k] + '"'); + parts.push(key + '="' + value + '"'); } } - } + }); + authHeader = 'Digest ' + parts.join(', '); this.sentAuth = true; return authHeader; diff --git a/lib/musicbrainz-api.ts b/lib/musicbrainz-api.ts index 37abd79b..4cd6bd19 100644 --- a/lib/musicbrainz-api.ts +++ b/lib/musicbrainz-api.ts @@ -15,8 +15,10 @@ import { DigestAuth } from './digest-auth'; import { RateLimiter } from './rate-limiter'; import * as mb from './musicbrainz.types'; -import got, { Options } from 'got'; -import * as tough from 'tough-cookie'; +/* eslint-disable-next-line */ +import got, {type Options, type ToughCookieJar} from 'got'; + +import {type Cookie, CookieJar} from 'tough-cookie'; export * from './musicbrainz.types'; @@ -131,16 +133,14 @@ export type WorkIncludes = MiscIncludes | RelationsIncludes; export type UrlIncludes = RelationsIncludes; -const debug = Debug('musicbrainz-api'); +export type IFormData = {[key: string]: string | number}; -export interface IFormData { - [key: string]: string | number; -} +const debug = Debug('musicbrainz-api'); export interface IMusicBrainzConfig { - botAccount?: { - username: string, - password: string + botAccount: { + username?: string, + password?: string }, baseUrl?: string, @@ -158,17 +158,19 @@ export interface IMusicBrainzConfig { appContactInfo?: string } +export interface ICsrfSession { + sessionKey: string; + token: string; +} + export interface ISessionInformation { - csrf: { - sessionKey: string; - token: string; - } + csrf: ICsrfSession, loggedIn?: boolean; } export class MusicBrainzApi { - private static escapeText(text) { + private static escapeText(text: string): string { let str = ''; for (const chr of text) { // Escaping Special Characters: + - && || ! ( ) { } [ ] ^ " ~ * ? : \ / @@ -200,21 +202,22 @@ export class MusicBrainzApi { } public readonly config: IMusicBrainzConfig = { - baseUrl: 'https://musicbrainz.org' + baseUrl: 'https://musicbrainz.org', + botAccount: {} }; private rateLimiter: RateLimiter; private options: Options; - private session: ISessionInformation; + private session?: ISessionInformation; - public static fetchCsrf(html: string) { + public static fetchCsrf(html: string): ICsrfSession { return { - sessionKey: MusicBrainzApi.fetchValue(html, 'csrf_session_key'), - token: MusicBrainzApi.fetchValue(html, 'csrf_token') + sessionKey: MusicBrainzApi.fetchValue(html, 'csrf_session_key') as string, + token: MusicBrainzApi.fetchValue(html, 'csrf_token') as string }; } - private static fetchValue(html: string, key: string) { + private static fetchValue(html: string, key: string): string | undefined{ let pos = html.indexOf(`name="${key}"`); if (pos >= 0) { pos = html.indexOf('value="', pos + key.length + 7); @@ -227,13 +230,13 @@ export class MusicBrainzApi { } } - private getCookies: (currentUrl: string | URL) => Promise; + private getCookies: (currentUrl: string) => Promise; public constructor(_config?: IMusicBrainzConfig) { Object.assign(this.config, _config); - const cookieJar = new tough.CookieJar(); + const cookieJar: CookieJar = new CookieJar(); this.getCookies = promisify(cookieJar.getCookies.bind(cookieJar)); this.options = { @@ -242,7 +245,7 @@ export class MusicBrainzApi { headers: { 'User-Agent': `${this.config.appName}/${this.config.appVersion} ( ${this.config.appContactInfo} )` }, - cookieJar + cookieJar: cookieJar as ToughCookieJar }; this.rateLimiter = new RateLimiter(60, 50); @@ -544,7 +547,7 @@ export class MusicBrainzApi { const path = `ws/2/${entity}/`; // Get digest challenge - let digest: string = null; + let digest: string | undefined; let n = 1; const postData = xmlMetadata.toXml(); @@ -562,9 +565,9 @@ export class MusicBrainzApi { }); if (response.statusCode === HttpStatus.UNAUTHORIZED) { // Respond to digest challenge - const auth = new DigestAuth(this.config.botAccount); + const auth = new DigestAuth(this.config.botAccount as {username: string, password: string}); const relPath = Url.parse(response.requestUrl).path; // Ensure path is relative - digest = auth.digest(response.request.method, relPath, response.headers['www-authenticate']); + digest = auth.digest(response.request.method, relPath as string, response.headers['www-authenticate']); ++n; } else { break; @@ -578,13 +581,13 @@ export class MusicBrainzApi { assert.ok(this.config.botAccount.password, 'bot password should be set'); if (this.session && this.session.loggedIn) { - for (const cookie of await this.getCookies(this.options.prefixUrl)) { + for (const cookie of await this.getCookies(this.options.prefixUrl as string)) { if (cookie.key === 'remember_login') { return true; } } } - this.session = await this.getSession(this.config.baseUrl); + this.session = await this.getSession(); const redirectUri = '/success'; @@ -625,7 +628,7 @@ export class MusicBrainzApi { ...this.options }); const success = response.statusCode === HttpStatus.MOVED_TEMPORARILY && response.headers.location === redirectUri; - if (success) { + if (success && this.session) { this.session.loggedIn = true; } return success; @@ -641,7 +644,7 @@ export class MusicBrainzApi { await this.rateLimiter.limit(); - this.session = await this.getSession(this.config.baseUrl); + this.session = await this.getSession(); formData.csrf_session_key = this.session.csrf.sessionKey; formData.csrf_token = this.session.csrf.token; @@ -669,7 +672,7 @@ export class MusicBrainzApi { */ public async addUrlToRecording(recording: mb.IRecording, url2add: { linkTypeId: mb.LinkType, text: string }, editNote: string = '') { - const formData = {}; + const formData: {[key: string]: string | boolean | number} = {}; formData['edit-recording.name'] = recording.title; // Required formData['edit-recording.comment'] = recording.disambiguation; @@ -678,9 +681,9 @@ export class MusicBrainzApi { formData['edit-recording.url.0.link_type_id'] = url2add.linkTypeId; formData['edit-recording.url.0.text'] = url2add.text; - for (const i in recording.isrcs) { - formData[`edit-recording.isrcs.${i}`] = recording.isrcs[i]; - } + recording.isrcs?.forEach((isrcs, i) => { + formData[`edit-recording.isrcs.${i}`] = isrcs; + }); formData['edit-recording.edit_note'] = editNote; @@ -695,7 +698,7 @@ export class MusicBrainzApi { */ public async addIsrc(recording: mb.IRecording, isrc: string, editNote: string = '') { - const formData = {}; + const formData: IFormData = {}; formData[`edit-recording.name`] = recording.title; // Required @@ -775,7 +778,7 @@ export class MusicBrainzApi { return this.search('url', query); } - private async getSession(url: string): Promise { + private async getSession(): Promise { const response: any = await got.get('login', { followRedirect: false, // Disable redirects diff --git a/lib/musicbrainz.types.ts b/lib/musicbrainz.types.ts index b451fd38..b8f799fe 100644 --- a/lib/musicbrainz.types.ts +++ b/lib/musicbrainz.types.ts @@ -39,7 +39,7 @@ export interface IArtist extends IEntity { disambiguation: string; 'sort-name': string; 'type-id'?: string; - 'gender-id'?; + 'gender-id'?: string; 'life-span'?: IPeriod; country?: string; ipis?: any[]; // ToDo @@ -203,6 +203,7 @@ export interface IAreaList extends ISearchResult { export interface IReleaseList extends ISearchResult { releases: IReleaseMatch[]; + 'release-count': number; } export interface IReleaseGroupList extends ISearchResult { diff --git a/lib/rate-limiter.ts b/lib/rate-limiter.ts index 4cf83bf5..22192c50 100644 --- a/lib/rate-limiter.ts +++ b/lib/rate-limiter.ts @@ -4,7 +4,7 @@ const debug = Debug('musicbrainz-api:rate-limiter'); export class RateLimiter { - public static sleep(ms): Promise { + public static sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } diff --git a/package.json b/package.json index ea2576d5..32567f1b 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ }, "devDependencies": { "@types/chai": "^4.3.0", + "@types/jsontoxml": "^1.0.5", "@types/mocha": "^9.0.0", "@types/node": "^20.8.10", "@typescript-eslint/eslint-plugin": "^5.13.0", diff --git a/test/test-musicbrainz-api.ts b/test/test-musicbrainz-api.ts index 960b19fb..b95ecaa0 100644 --- a/test/test-musicbrainz-api.ts +++ b/test/test-musicbrainz-api.ts @@ -46,6 +46,7 @@ const testApiConfig: IMusicBrainzConfig = { const searchApiConfig: IMusicBrainzConfig = { baseUrl: 'https://musicbrainz.org', + botAccount: {}, /** * Enable proxy, like Fiddler @@ -206,7 +207,7 @@ describe('MusicBrainz-api', function () { const release = await mbApi.lookupRelease(mbid.release.Formidable, [inc.inc as any]); assert.strictEqual(release.id, mbid.release.Formidable); assert.strictEqual(release.title, 'Formidable'); - assert.isDefined(release[inc.key], `Should include '${inc.key}'`); + assert.isDefined((release as any)[inc.key], `Should include '${inc.key}'`); }); }); @@ -228,7 +229,7 @@ describe('MusicBrainz-api', function () { const group = await mbApi.lookupReleaseGroup(mbid.releaseGroup.Formidable, [inc.inc as any]); assert.strictEqual(group.id, mbid.releaseGroup.Formidable); assert.strictEqual(group.title, 'Formidable'); - assert.isDefined(group[inc.key], `Should include '${inc.key}'`); + assert.isDefined((group as any)[inc.key], `Should include '${inc.key}'`); }); }); @@ -262,7 +263,7 @@ describe('MusicBrainz-api', function () { const recording = await mbApi.lookupRecording(mbid.recording.Formidable, [inc.inc as any]); assert.strictEqual(recording.id, mbid.recording.Formidable); assert.strictEqual(recording.title, 'Formidable'); - assert.isDefined(recording[inc.key], `Should include '${inc.key}'`); + assert.isDefined((recording as any)[inc.key], `Should include '${inc.key}'`); }); }); @@ -292,7 +293,7 @@ describe('MusicBrainz-api', function () { const group = await mbApi.lookupReleaseGroup(mbid.releaseGroup.Formidable, [inc.inc as any]); assert.strictEqual(group.id, mbid.releaseGroup.Formidable); assert.strictEqual(group.title, 'Formidable'); - assert.isDefined(group[inc.key], `Should include '${inc.key}'`); + assert.isDefined((group as any)[inc.key], `Should include '${inc.key}'`); }); }); @@ -329,7 +330,7 @@ describe('MusicBrainz-api', function () { const event = await mbApi.lookupEvent(mbid.event.DireStraitsAlchemyLoveOverGold, [inc.inc as any]); assert.strictEqual(event.id, mbid.event.DireStraitsAlchemyLoveOverGold); assert.strictEqual(event.name, "Dire Straits - Love Over Gold"); - assert.isDefined(event[inc.key], `Should include '${inc.key}'`); + assert.isDefined((event as any)[inc.key], `Should include '${inc.key}'`); }); }); @@ -339,7 +340,7 @@ describe('MusicBrainz-api', function () { describe('Browse', () => { - function areBunchOf(entity: string, bunch) { + function areBunchOf(entity: string, bunch: any) { assert.isObject(bunch); assert.isNumber(bunch[entity + '-count']); assert.isNumber(bunch[entity + '-offset']); @@ -347,8 +348,8 @@ describe('MusicBrainz-api', function () { } describe('area', async () => { - function areBunchOfAreas(artists) { - areBunchOf('area', artists); + function areBunchOfAreas(areas : mb.IBrowseAreasResult) { + areBunchOf('area', areas); } it('by collection', async () => { diff --git a/tsconfig.json b/tsconfig.json index 6cc53c23..227c9643 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "inlineSources": false, "module": "commonjs", "moduleResolution": "node", - "target": "ES2017" + "target": "ES2017", + "strict": true } } diff --git a/yarn.lock b/yarn.lock index 0f8f6beb..9c1aad78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -494,6 +494,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/jsontoxml@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/jsontoxml/-/jsontoxml-1.0.5.tgz#732ca29c1bdf1cc1149d401be9ae0d4b074c20c8" + integrity sha512-gW4wQjeSyNFg8vR9ZNH1CbiHaYQLWTIVrg4ZPqoZlLeCO4Lh45NwWUevcTKdF+xKYOgCSAw59Syrpug7NDBdgQ== + "@types/keyv@*": version "3.1.1" resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7"