UNPKG

@xbibzlibrary/tiktokscrap

Version:

Powerful TikTok Scraper and Downloader Library

135 lines (109 loc) 4.08 kB
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;