@musicorum/lastfm
Version:
Fully typed [Last.fm](https://last.fm) api client library written and made for Typescript!
119 lines (101 loc) • 3.48 kB
text/typescript
/* eslint-disable @typescript-eslint/no-unused-vars */
import { LastfmError } from './error/LastfmError.js'
import { User } from './packages/User.js'
import { Track } from './packages/Track.js'
import { Album } from './packages/Album.js'
import { Artist } from './packages/Artist.js'
import { Auth } from './packages/Auth.js'
import { Utilities } from './packages/Utilities.js'
import type {
GetOriginalResponse,
LastfmApiMethod,
LastfmResponses
} from './types/responses'
import crypto from 'node:crypto'
export class LastClient {
private apiUrl = 'https://ws.audioscrobbler.com/2.0'
public user = new User(this)
public track = new Track(this)
public album = new Album(this)
public artist = new Artist(this)
public auth = new Auth(this)
public utilities = new Utilities(this)
private readonly headers: Record<string, string>
constructor(
public apiKey: string,
public apiSecret?: string,
appName?: string
) {
if (!apiKey) throw new Error('apiKey is required and is missing')
this.headers = {
'User-Agent': `${appName ?? 'Unspecified App'} (@musicorum/lastfm; github.com/musicorum-app/lastfm)`
}
}
onRequestStarted(
method: LastfmApiMethod | string,
params: Record<string, string>,
internalData: Record<string, never>
// eslint-disable-next-line @typescript-eslint/no-empty-function
) {}
onRequestFinished(
method: LastfmApiMethod | string,
params: Record<string, string>,
internalData: Record<string, never>,
response: Record<string, never>
// eslint-disable-next-line @typescript-eslint/no-empty-function
) {}
/**
* @todo implement signed requests
*/
async request<M extends string = LastfmApiMethod>(
method: M,
params?: Record<string, string | number | undefined>,
signed = false,
write = false
) {
if (signed && !this.apiSecret)
throw new Error('apiSecret is required for signed requests')
params = {
...params,
method,
api_key: this.apiKey,
format: 'json'
}
const cleanParams: { [p: string]: string } = Object.fromEntries(
Object.entries(params).filter(([_, v]) => !!v) as [string, string][]
)
const searchParams = new URLSearchParams(cleanParams)
if (signed) {
// order cleanParams alphabetically by key
const orderedParams = Object.fromEntries(
Object.entries(cleanParams).sort(([a], [b]) => a.localeCompare(b))
)
const signature =
Object.entries(orderedParams)
.filter(([k]) => k !== 'format')
.map(([k, v]) => `${k}${v}`)
.join('') + this.apiSecret
const hashedSignature = crypto
.createHash('md5')
.update(signature)
.digest('hex')
searchParams.set('api_sig', hashedSignature)
}
const queryString = searchParams.toString()
const internalData = {}
this.onRequestStarted(method, cleanParams, internalData)
const response = write
? await fetch(`${this.apiUrl}/?format=json`, {
method: 'POST',
headers: this.headers,
body: queryString
})
: await fetch(`${this.apiUrl}?${queryString}`, { headers: this.headers })
const data = await response.json()
this.onRequestFinished(method, cleanParams, internalData, data)
if (!response.ok) throw new LastfmError(data)
return data as M extends LastfmApiMethod
? GetOriginalResponse<LastfmResponses[M]>
: unknown
}
}