@xbibzlibrary/tiktokscrap
Version:
Powerful TikTok Scraper and Downloader Library
174 lines (147 loc) • 5.97 kB
text/typescript
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;