Skip to content

Commit

Permalink
Introduce strict TypeScript
Browse files Browse the repository at this point in the history
  • Loading branch information
Borewit committed Nov 7, 2023
1 parent f390469 commit e954c29
Show file tree
Hide file tree
Showing 9 changed files with 73 additions and 60 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 13 additions & 12 deletions lib/digest-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}

Expand All @@ -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;
Expand All @@ -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]
Expand All @@ -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;
Expand Down
75 changes: 39 additions & 36 deletions lib/musicbrainz-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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,

Expand All @@ -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: + - && || ! ( ) { } [ ] ^ " ~ * ? : \ /
Expand Down Expand Up @@ -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);
Expand All @@ -227,13 +230,13 @@ export class MusicBrainzApi {
}
}

private getCookies: (currentUrl: string | URL) => Promise<tough.Cookie[]>;
private getCookies: (currentUrl: string) => Promise<Cookie[]>;

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 = {
Expand All @@ -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);
Expand Down Expand Up @@ -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();

Expand All @@ -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;
Expand All @@ -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';

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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

Expand Down Expand Up @@ -775,7 +778,7 @@ export class MusicBrainzApi {
return this.search<mb.IUrlList, UrlIncludes>('url', query);
}

private async getSession(url: string): Promise<ISessionInformation> {
private async getSession(): Promise<ISessionInformation> {

const response: any = await got.get('login', {
followRedirect: false, // Disable redirects
Expand Down
3 changes: 2 additions & 1 deletion lib/musicbrainz.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -203,6 +203,7 @@ export interface IAreaList extends ISearchResult {

export interface IReleaseList extends ISearchResult {
releases: IReleaseMatch[];
'release-count': number;
}

export interface IReleaseGroupList extends ISearchResult {
Expand Down
2 changes: 1 addition & 1 deletion lib/rate-limiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const debug = Debug('musicbrainz-api:rate-limiter');

export class RateLimiter {

public static sleep(ms): Promise<void> {
public static sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 9 additions & 8 deletions test/test-musicbrainz-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const testApiConfig: IMusicBrainzConfig = {
const searchApiConfig: IMusicBrainzConfig = {

baseUrl: 'https://musicbrainz.org',
botAccount: {},

/**
* Enable proxy, like Fiddler
Expand Down Expand Up @@ -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}'`);
});
});

Expand All @@ -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}'`);
});
});

Expand Down Expand Up @@ -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}'`);
});
});

Expand Down Expand Up @@ -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}'`);
});
});

Expand Down Expand Up @@ -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}'`);
});
});

Expand All @@ -339,16 +340,16 @@ 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']);
assert.isArray(bunch[entity.endsWith('s') ? entity : (entity + 's')]);
}

describe('area', async () => {
function areBunchOfAreas(artists) {
areBunchOf('area', artists);
function areBunchOfAreas(areas : mb.IBrowseAreasResult) {
areBunchOf('area', areas);
}

it('by collection', async () => {
Expand Down
Loading

0 comments on commit e954c29

Please sign in to comment.