@xbibzlibrary/tiktokscrap
Version:
Powerful TikTok Scraper and Downloader Library
135 lines (109 loc) • 4.08 kB
text/typescript
import fs from 'fs-extra';
import path from 'path';
import fetch from 'node-fetch';
import { promisify } from 'util';
import stream from 'stream';
import { TikTokDownloadOptions, TikTokScrapResult } from '../types';
import { DownloadError, ValidationError } from '../errors';
import Logger from '../utils/logger';
const pipeline = promisify(stream.pipeline);
export abstract class BaseDownloader {
protected logger = Logger;
protected async executeDownload<T>(
downloadFn: () => Promise<T>,
errorMessage: string
): Promise<TikTokScrapResult<T>> {
try {
this.logger.info(`Starting download: ${errorMessage}`);
const data = await downloadFn();
this.logger.success(`Download completed successfully: ${errorMessage}`);
return {
success: true,
data,
message: 'Success'
};
} catch (error) {
this.logger.error(`Download failed: ${errorMessage} - ${(error as Error).message}`);
if (error instanceof DownloadError || error instanceof ValidationError) {
return {
success: false,
error: (error as Error).message,
message: error instanceof DownloadError ? 'DOWNLOAD_ERROR' : 'VALIDATION_ERROR'
};
}
return {
success: false,
error: (error as Error).message,
message: 'UNKNOWN_ERROR'
};
}
}
protected async downloadFile(
url: string,
outputPath: string,
options: TikTokDownloadOptions = {}
): Promise<string> {
if (!url || typeof url !== 'string') {
throw new ValidationError('URL is required and must be a string');
}
if (!outputPath || typeof outputPath !== 'string') {
throw new ValidationError('Output path is required and must be a string');
}
// Ensure output directory exists
const dir = path.dirname(outputPath);
await fs.ensureDir(dir);
// Check if file already exists
if (await fs.pathExists(outputPath)) {
this.logger.warn(`File already exists: ${outputPath}`);
return outputPath;
}
try {
const response = await fetch(url);
if (!response.ok) {
throw new DownloadError(`Failed to download file: ${response.statusText}`);
}
const contentLength = response.headers.get('content-length');
const totalBytes = contentLength ? parseInt(contentLength, 10) : 0;
let downloadedBytes = 0;
return new Promise((resolve, reject) => {
const fileStream = fs.createWriteStream(outputPath);
response.body?.on('data', (chunk) => {
downloadedBytes += chunk.length;
if (options.progressCallback && totalBytes > 0) {
const progress = Math.round((downloadedBytes / totalBytes) * 100);
options.progressCallback(progress);
}
});
response.body?.pipe(fileStream);
fileStream.on('finish', () => {
fileStream.close();
resolve(outputPath);
});
fileStream.on('error', (error) => {
fs.unlink(outputPath).catch(() => {});
reject(new DownloadError(`Failed to write file: ${error.message}`));
});
});
} catch (error) {
// Clean up partial file if download failed
if (await fs.pathExists(outputPath)) {
await fs.unlink(outputPath);
}
throw new DownloadError(`Download failed: ${(error as Error).message}`);
}
}
protected generateFilename(url: string, extension: string, options: TikTokDownloadOptions = {}): string {
const { filename } = options;
if (filename) {
return filename.endsWith(extension) ? filename : `${filename}${extension}`;
}
// Extract ID from URL as default filename
const urlParts = url.split('/');
const id = urlParts[urlParts.length - 1].split('?')[0];
return `${id}${extension}`;
}
protected getOutputPath(outputDir: string, filename: string): string {
return path.join(outputDir, filename);
}
}
export default BaseDownloader;