lastfm-api-lib
Version:
A TypeScript library for interacting with the Last.fm API
151 lines (129 loc) • 3.55 kB
text/typescript
/**
* @file httpClient.ts
* @description HTTP client for making requests to Last.fm API
* @author Caleb Price
* @version 1.0.0
* @date 2025-07-23
*/
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
export interface RequestOptions {
method?: 'GET' | 'POST'
params?: Record<string, any>
headers?: Record<string, string>
retries?: number
}
export interface LastFmApiError extends Error {
status?: number
response?: any
code?: number
}
// HTTP status codes that should be retried
const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504])
export class LastFmHttpClient {
private client: AxiosInstance
private apiKey: string
private readonly baseURL = 'https://ws.audioscrobbler.com/2.0/'
constructor(apiKey?: string) {
this.apiKey = apiKey || ''
this.client = axios.create({
baseURL: this.baseURL,
timeout: 10000,
headers: {
'User-Agent': 'lastfm-api-lib/1.0.0',
'Accept': 'application/json',
},
})
// Add response interceptor for error handling
this.client.interceptors.response.use(
(response) => response,
(error) => this.handleResponseError(error)
)
}
/**
* Set API key for requests
*/
public setApiKey(apiKey: string): void {
this.apiKey = apiKey
}
/**
* Clear the API key
*/
public clearApiKey(): void {
this.apiKey = ''
}
/**
* Make a request to the Last.fm API
*/
public async request(
method: string,
params: Record<string, any> = {},
options: RequestOptions = {}
): Promise<any> {
if (!this.apiKey) {
throw new Error('API key is required')
}
const requestParams = {
method,
api_key: this.apiKey,
format: 'json',
...params,
}
const config: AxiosRequestConfig = {
method: options.method || 'GET',
params: requestParams,
headers: options.headers,
}
try {
const response = await this.client.request(config)
// Check for Last.fm API errors in response
if (response.data?.error) {
const error = new Error(response.data.message || 'Last.fm API error') as LastFmApiError
error.code = response.data.error
error.status = response.status
throw error
}
return response.data
} catch (error: any) {
if (error.isAxiosError) {
const lastFmError = new Error(
error.response?.data?.message || error.message || 'Request failed'
) as LastFmApiError
lastFmError.status = error.response?.status
lastFmError.response = error.response?.data
throw lastFmError
}
throw error
}
}
/**
* Handle response errors with retries for certain status codes
*/
private async handleResponseError(error: any): Promise<any> {
if (
error.response?.status &&
RETRYABLE_STATUS_CODES.has(error.response.status) &&
error.config &&
!error.config._retryCount
) {
error.config._retryCount = 1
// Add delay for rate limiting
if (error.response.status === 429) {
await this.delay(1000)
}
return this.client.request(error.config)
}
return Promise.reject(error)
}
/**
* Delay helper for rate limiting
*/
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
/**
* Get the HTTP client instance
*/
public getHttpClient(): AxiosInstance {
return this.client
}
}