UNPKG

@xbibzlibrary/tiktokscrap

Version:

Powerful TikTok Scraper and Downloader Library

174 lines (147 loc) 5.97 kB
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import { CookieJar } from 'tough-cookie'; import { wrapper } from 'axios-cookiejar-support'; import UserAgent from 'user-agents'; import Logger from './logger'; import { NetworkError, RateLimitError, AuthenticationError, ForbiddenError, NotFoundError, ServerError } from '../errors'; import { TikTokScrapOptions } from '../types'; export class HttpClient { private client: AxiosInstance; private options: TikTokScrapOptions; private logger = Logger; constructor(options: TikTokScrapOptions = {}) { this.options = { timeout: 30000, retries: 3, userAgent: new UserAgent().toString(), ...options }; const jar = new CookieJar(); this.client = wrapper(axios.create({ jar, timeout: this.options.timeout, headers: { 'User-Agent': this.options.userAgent, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'Accept-Language': 'en-US,en;q=0.9', 'Accept-Encoding': 'gzip, deflate, br', 'Connection': 'keep-alive', 'Upgrade-Insecure-Requests': '1', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'none', 'Cache-Control': 'max-age=0', ...this.options.headers } })); if (this.options.proxy) { this.client.defaults.proxy = this.options.proxy; } this.setupInterceptors(); } private setupInterceptors(): void { this.client.interceptors.request.use( (config) => { this.logger.debug(`Making request to ${config.url}`); return config; }, (error) => { this.logger.error(`Request error: ${error.message}`); return Promise.reject(error); } ); this.client.interceptors.response.use( (response) => { this.logger.debug(`Received response from ${response.config.url} with status ${response.status}`); return response; }, (error) => { const { response, request, message } = error; if (response) { const { status, data } = response; this.logger.error(`Response error: ${status} - ${message}`); switch (status) { case 401: return Promise.reject(new AuthenticationError(data?.message || 'Authentication failed')); case 403: return Promise.reject(new ForbiddenError(data?.message || 'Access forbidden')); case 404: return Promise.reject(new NotFoundError(data?.message || 'Resource not found')); case 429: const retryAfter = response.headers['retry-after'] || 60; return Promise.reject(new RateLimitError(data?.message || 'Rate limit exceeded', parseInt(retryAfter))); case 500: case 502: case 503: case 504: return Promise.reject(new ServerError(data?.message || 'Server error')); default: return Promise.reject(new NetworkError(data?.message || message, status, data)); } } else if (request) { this.logger.error(`Network error: ${message}`); return Promise.reject(new NetworkError(message)); } else { this.logger.error(`Request setup error: ${message}`); return Promise.reject(new NetworkError(message)); } } ); } public async get<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> { return this.retryRequest(() => this.client.get<T>(url, config)); } public async post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> { return this.retryRequest(() => this.client.post<T>(url, data, config)); } public async put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> { return this.retryRequest(() => this.client.put<T>(url, data, config)); } public async delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> { return this.retryRequest(() => this.client.delete<T>(url, config)); } private async retryRequest<T>(requestFn: () => Promise<T>): Promise<T> { let lastError: Error; for (let attempt = 1; attempt <= this.options.retries!; attempt++) { try { return await requestFn(); } catch (error) { lastError = error as Error; if (error instanceof RateLimitError && attempt < this.options.retries!) { const retryAfter = error.details?.retryAfter || 60; this.logger.warn(`Rate limited. Retrying in ${retryAfter} seconds...`); await this.delay(retryAfter * 1000); continue; } if (attempt < this.options.retries!) { const delay = Math.pow(2, attempt) * 1000; this.logger.warn(`Request failed. Retrying in ${delay / 1000} seconds... (${attempt}/${this.options.retries})`); await this.delay(delay); } } } throw lastError!; } private delay(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } public updateOptions(options: Partial<TikTokScrapOptions>): void { this.options = { ...this.options, ...options }; if (options.timeout) { this.client.defaults.timeout = options.timeout; } if (options.userAgent) { this.client.defaults.headers['User-Agent'] = options.userAgent; } if (options.headers) { this.client.defaults.headers = { ...this.client.defaults.headers, ...options.headers }; } if (options.proxy) { this.client.defaults.proxy = options.proxy; } } public getOptions(): TikTokScrapOptions { return { ...this.options }; } } export default HttpClient;