tiktok decode
This commit is contained in:
parent
0bbd4822cb
commit
f6dc781495
6
packages/media/ref/yt-dlp/.gitignore
vendored
6
packages/media/ref/yt-dlp/.gitignore
vendored
@ -1,6 +0,0 @@
|
||||
/node_modules
|
||||
/coverage
|
||||
*.log
|
||||
.DS_Store
|
||||
clear_history.sh
|
||||
./tests/assets
|
||||
@ -1,383 +0,0 @@
|
||||
# yt-dlp-wrapper
|
||||
|
||||
A TypeScript wrapper library for [yt-dlp](https://github.com/yt-dlp/yt-dlp), a powerful command-line video downloader.
|
||||
|
||||
[](https://www.npmjs.com/package/yt-dlp-wrapper)
|
||||
[](https://github.com/yourusername/yt-dlp-wrapper/blob/main/LICENSE)
|
||||
|
||||
## Features
|
||||
|
||||
- 🔄 **Full TypeScript support** with comprehensive type definitions
|
||||
- 🧩 **Modular architecture** for easy integration into your projects
|
||||
- 🔍 **Zod validation** for reliable input/output handling
|
||||
- 📊 **Structured logging** with TSLog
|
||||
- 🛠️ **Command-line interface** built with yargs
|
||||
- ⚡ **Promise-based API** for easy async operations
|
||||
- 🔧 **Highly configurable** with sensible defaults
|
||||
- 🔄 **Progress tracking** for downloads
|
||||
- 💼 **Error handling** with clear error messages
|
||||
- 🎵 **Audio extraction** with MP3 conversion support
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using this library, ensure you have:
|
||||
|
||||
1. **Node.js** (v14 or higher)
|
||||
2. **yt-dlp** installed on your system:
|
||||
- **Linux/macOS**: `brew install yt-dlp` or `pip install yt-dlp`
|
||||
- **Windows**: Download from [yt-dlp GitHub releases](https://github.com/yt-dlp/yt-dlp/releases)
|
||||
3. **ffmpeg** (required for MP3 conversion):
|
||||
- **Linux/macOS**: `brew install ffmpeg` or `apt install ffmpeg`
|
||||
- **Windows**: Download from [ffmpeg.org](https://ffmpeg.org/download.html)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Using npm
|
||||
npm install yt-dlp-wrapper
|
||||
|
||||
# Using yarn
|
||||
yarn add yt-dlp-wrapper
|
||||
|
||||
# Using pnpm
|
||||
pnpm add yt-dlp-wrapper
|
||||
```
|
||||
|
||||
## CLI Usage
|
||||
|
||||
The library includes a command-line interface for common operations:
|
||||
|
||||
### Download a video
|
||||
|
||||
```bash
|
||||
# Basic download
|
||||
npx yt-dlp-wrapper download https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
||||
|
||||
# Download with specific format
|
||||
npx yt-dlp-wrapper download https://www.tiktok.com/@businessblurb/video/7479849082844892458 -f "best[height<=720]"
|
||||
|
||||
# Download to specific directory
|
||||
npx yt-dlp-wrapper download https://youtu.be/dQw4w9WgXcQ --output-dir "./downloads"
|
||||
|
||||
# Download as MP3 audio only
|
||||
npx yt-dlp-wrapper download https://youtu.be/dQw4w9WgXcQ --mp3
|
||||
```
|
||||
|
||||
### Get video information
|
||||
|
||||
```bash
|
||||
# Basic info
|
||||
npx yt-dlp-wrapper info https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
||||
|
||||
# With JSON output
|
||||
npx yt-dlp-wrapper info https://www.tiktok.com/@businessblurb/video/7479849082844892458 --dump-json
|
||||
```
|
||||
|
||||
### List available formats
|
||||
|
||||
```bash
|
||||
npx yt-dlp-wrapper formats https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
||||
```
|
||||
|
||||
### Help
|
||||
|
||||
```bash
|
||||
# General help
|
||||
npx yt-dlp-wrapper --help
|
||||
|
||||
# Command-specific help
|
||||
npx yt-dlp-wrapper download --help
|
||||
```
|
||||
|
||||
## API Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
import { YtDlp } from 'yt-dlp-wrapper';
|
||||
|
||||
// Create a new instance with default options
|
||||
const ytdlp = new YtDlp();
|
||||
|
||||
// Download a video
|
||||
async function downloadVideo() {
|
||||
try {
|
||||
const filePath = await ytdlp.downloadVideo('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
|
||||
console.log(`Video downloaded to: ${filePath}`);
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
downloadVideo();
|
||||
```
|
||||
|
||||
### Download with Options
|
||||
|
||||
```typescript
|
||||
import { YtDlp } from 'yt-dlp-wrapper';
|
||||
|
||||
const ytdlp = new YtDlp();
|
||||
|
||||
async function downloadWithOptions() {
|
||||
try {
|
||||
const filePath = await ytdlp.downloadVideo('https://www.tiktok.com/@businessblurb/video/7479849082844892458', {
|
||||
format: 'bestvideo[height<=720]+bestaudio/best[height<=720]',
|
||||
format: 'bestvideo[height<=720]+bestaudio/best[height<=720]',
|
||||
outputDir: './downloads',
|
||||
filename: 'tiktok-video.mp4',
|
||||
subtitles: true,
|
||||
audioOnly: false,
|
||||
// Add any other options as needed
|
||||
|
||||
console.log(`Video downloaded to: ${filePath}`);
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
downloadWithOptions();
|
||||
```
|
||||
|
||||
### Get Video Information
|
||||
|
||||
```typescript
|
||||
import { YtDlp } from 'yt-dlp-wrapper';
|
||||
|
||||
const ytdlp = new YtDlp();
|
||||
|
||||
async function getVideoInfo() {
|
||||
try {
|
||||
const info = await ytdlp.getVideoInfo('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
|
||||
console.log('Video title:', info.title);
|
||||
console.log('Duration:', info.duration);
|
||||
console.log('Uploader:', info.uploader);
|
||||
// Access other properties as needed
|
||||
} catch (error) {
|
||||
console.error('Failed to get video info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
getVideoInfo();
|
||||
```
|
||||
|
||||
### List Available Formats
|
||||
|
||||
```typescript
|
||||
import { YtDlp } from 'yt-dlp-wrapper';
|
||||
|
||||
const ytdlp = new YtDlp();
|
||||
|
||||
async function listFormats() {
|
||||
try {
|
||||
const formats = await ytdlp.listFormats('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
|
||||
|
||||
// Display all formats
|
||||
formats.forEach(format => {
|
||||
console.log(`Format ID: ${format.formatId}`);
|
||||
console.log(`Resolution: ${format.resolution}`);
|
||||
console.log(`Extension: ${format.extension}`);
|
||||
console.log(`File size: ${format.filesize}`);
|
||||
console.log('---');
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to list formats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
listFormats();
|
||||
```
|
||||
|
||||
### Custom Configuration
|
||||
|
||||
```typescript
|
||||
import { YtDlp } from 'yt-dlp-wrapper';
|
||||
|
||||
// Create a new instance with custom options
|
||||
const ytdlp = new YtDlp({
|
||||
executablePath: '/usr/local/bin/yt-dlp', // Custom path to yt-dlp executable
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
verbose: true,
|
||||
// Add other global options as needed
|
||||
});
|
||||
|
||||
// Use as normal
|
||||
ytdlp.downloadVideo('https://www.youtube.com/watch?v=dQw4w9WgXcQ')
|
||||
.then(filePath => console.log(`Video downloaded to: ${filePath}`))
|
||||
.catch(error => console.error('Download failed:', error));
|
||||
```
|
||||
|
||||
## MP3 Downloads
|
||||
|
||||
The library supports extracting audio from videos and converting to MP3 format. This functionality requires ffmpeg to be installed on your system.
|
||||
|
||||
### MP3 Downloads via CLI
|
||||
|
||||
```bash
|
||||
# Basic MP3 download
|
||||
npx yt-dlp-wrapper download https://www.youtube.com/watch?v=dQw4w9WgXcQ --mp3
|
||||
|
||||
# MP3 download with specific output directory
|
||||
npx yt-dlp-wrapper download https://youtu.be/dQw4w9WgXcQ --mp3 --output-dir "./music"
|
||||
|
||||
# MP3 download with custom filename
|
||||
npx yt-dlp-wrapper download https://youtu.be/dQw4w9WgXcQ --mp3 --filename "my-song.mp3"
|
||||
```
|
||||
|
||||
### MP3 Downloads via API
|
||||
|
||||
```typescript
|
||||
import { YtDlp } from 'yt-dlp-wrapper';
|
||||
|
||||
const ytdlp = new YtDlp();
|
||||
|
||||
async function downloadAsMp3() {
|
||||
try {
|
||||
const filePath = await ytdlp.downloadVideo('https://www.youtube.com/watch?v=dQw4w9WgXcQ', {
|
||||
audioOnly: true,
|
||||
audioFormat: 'mp3',
|
||||
outputDir: './music',
|
||||
filename: 'audio-track.mp3'
|
||||
});
|
||||
|
||||
console.log(`Audio downloaded to: ${filePath}`);
|
||||
} catch (error) {
|
||||
console.error('Audio download failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
downloadAsMp3();
|
||||
```
|
||||
|
||||
### Notes on MP3 Conversion
|
||||
|
||||
- MP3 conversion requires ffmpeg to be installed on your system
|
||||
- If ffmpeg is not found, an error message will be shown
|
||||
- The audio quality defaults to the best available
|
||||
- Progress for both download and conversion will be displayed
|
||||
|
||||
## Advanced Examples
|
||||
|
||||
### Download Playlist
|
||||
|
||||
```typescript
|
||||
import { YtDlp } from 'yt-dlp-wrapper';
|
||||
|
||||
const ytdlp = new YtDlp();
|
||||
|
||||
async function downloadPlaylist() {
|
||||
try {
|
||||
const result = await ytdlp.downloadVideo('https://www.youtube.com/playlist?list=PLexamplelistID', {
|
||||
outputDir: './playlists',
|
||||
playlistItems: '1-5', // Only download the first 5 videos
|
||||
limit: 5,
|
||||
format: 'best[height<=480]' // Lower quality to save space
|
||||
});
|
||||
|
||||
console.log(`Playlist downloaded to: ${result}`);
|
||||
} catch (error) {
|
||||
console.error('Playlist download failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
downloadPlaylist();
|
||||
```
|
||||
|
||||
### Progress Tracking
|
||||
|
||||
```typescript
|
||||
import { YtDlp } from 'yt-dlp-wrapper';
|
||||
|
||||
const ytdlp = new YtDlp();
|
||||
|
||||
async function downloadWithProgress() {
|
||||
try {
|
||||
const filePath = await ytdlp.downloadVideo('https://www.youtube.com/watch?v=dQw4w9WgXcQ', {
|
||||
onProgress: (progress) => {
|
||||
console.log(`Download progress: ${progress.percent}%`);
|
||||
console.log(`Speed: ${progress.speed}`);
|
||||
console.log(`ETA: ${progress.eta}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Video downloaded to: ${filePath}`);
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
downloadWithProgress();
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The library provides detailed error information:
|
||||
|
||||
```typescript
|
||||
import { YtDlp, YtDlpError } from 'yt-dlp-wrapper';
|
||||
|
||||
const ytdlp = new YtDlp();
|
||||
|
||||
async function handleErrors() {
|
||||
try {
|
||||
await ytdlp.downloadVideo('https://invalid-url.com/video');
|
||||
} catch (error) {
|
||||
if (error instanceof YtDlpError) {
|
||||
console.error(`YtDlp Error: ${error.message}`);
|
||||
console.error(`Error Code: ${error.code}`);
|
||||
console.error(`Command: ${error.command}`);
|
||||
} else {
|
||||
console.error('Unknown error:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleErrors();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### YtDlp Class
|
||||
|
||||
#### Constructor
|
||||
|
||||
```typescript
|
||||
new YtDlp(options?: YtDlpOptions)
|
||||
```
|
||||
|
||||
**YtDlpOptions:**
|
||||
|
||||
| Option | Type | Description | Default |
|
||||
|--------|------|-------------|---------|
|
||||
| executablePath | string | Path to yt-dlp executable | 'yt-dlp' |
|
||||
| userAgent | string | User agent to use for requests | Default browser UA |
|
||||
| verbose | boolean | Enable verbose output | false |
|
||||
| quiet | boolean | Suppress output | false |
|
||||
|
||||
#### Methods
|
||||
|
||||
**downloadVideo(url, options?)**
|
||||
Downloads a video from the given URL.
|
||||
|
||||
**getVideoInfo(url, options?)**
|
||||
Gets information about a video.
|
||||
|
||||
**listFormats(url, options?)**
|
||||
Lists available formats for a video.
|
||||
|
||||
**checkInstallation()**
|
||||
Verifies that yt-dlp is installed and working.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) - The amazing tool this library wraps
|
||||
- All the contributors to the open-source libraries used in this project
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
export {};
|
||||
//# sourceMappingURL=mp3.test.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"mp3.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/mp3.test.ts"],"names":[],"mappings":""}
|
||||
@ -1,71 +0,0 @@
|
||||
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
|
||||
import { YtDlp } from '../ytdlp.js';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
describe('MP3 Extraction Tests', () => {
|
||||
const testOutputDir = path.join(process.cwd(), 'test-downloads/mp3-test');
|
||||
const ytdlp = new YtDlp({
|
||||
output: '%(title)s.%(ext)s'
|
||||
});
|
||||
// Short YouTube video by YouTube co-founder
|
||||
const videoUrl = 'https://www.youtube.com/watch?v=jNQXAC9IVRw';
|
||||
let downloadedFiles = [];
|
||||
beforeAll(() => {
|
||||
// Create the output directory if it doesn't exist
|
||||
if (!fs.existsSync(testOutputDir)) {
|
||||
fs.mkdirSync(testOutputDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
afterAll(() => {
|
||||
// Clean up downloaded files after tests
|
||||
downloadedFiles.forEach(file => {
|
||||
if (fs.existsSync(file)) {
|
||||
fs.unlinkSync(file);
|
||||
console.log(`Cleaned up test file: ${file}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
it('should download video and extract audio as MP3', async () => {
|
||||
// Download the video with MP3 extraction
|
||||
const downloadOptions = {
|
||||
outputDir: testOutputDir,
|
||||
audioOnly: true,
|
||||
audioFormat: 'mp3',
|
||||
audioQuality: '0', // best quality
|
||||
verbose: true
|
||||
};
|
||||
const filePath = await ytdlp.downloadVideo(videoUrl, downloadOptions);
|
||||
downloadedFiles.push(filePath);
|
||||
console.log(`Downloaded MP3 file: ${filePath}`);
|
||||
// Verify the file exists
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
// Verify it has an MP3 extension
|
||||
expect(path.extname(filePath)).toBe('.mp3');
|
||||
// Verify the file has content (not empty)
|
||||
const stats = fs.statSync(filePath);
|
||||
expect(stats.size).toBeGreaterThan(0);
|
||||
console.log(`MP3 file size: ${stats.size} bytes`);
|
||||
// Log file info for debugging
|
||||
console.log(`File details:
|
||||
- Path: ${filePath}
|
||||
- Size: ${stats.size} bytes
|
||||
- Created: ${stats.birthtime}
|
||||
- Modified: ${stats.mtime}
|
||||
`);
|
||||
}, 60000); // Increase timeout to 60 seconds for download to complete
|
||||
it('should have proper MP3 metadata', async () => {
|
||||
// Get the most recently downloaded file (from previous test)
|
||||
const mp3File = downloadedFiles[0];
|
||||
expect(mp3File).toBeDefined();
|
||||
// Ensure the file still exists
|
||||
expect(fs.existsSync(mp3File)).toBe(true);
|
||||
// Check basic file properties to verify it's a valid audio file
|
||||
const stats = fs.statSync(mp3File);
|
||||
// MP3 files should have some minimum size (a few KB at least)
|
||||
expect(stats.size).toBeGreaterThan(10000); // At least 10KB
|
||||
// We could do more detailed checks with a media info library
|
||||
// but that would require additional dependencies
|
||||
console.log(`Verified MP3 file: ${mp3File} (${stats.size} bytes)`);
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=mp3.test.js.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"mp3.test.js","sourceRoot":"","sources":["../../src/__tests__/mp3.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACnE,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,yBAAyB,CAAC,CAAC;IAC1E,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC;QACtB,MAAM,EAAE,mBAAmB;KAC5B,CAAC,CAAC;IACH,4CAA4C;IAC5C,MAAM,QAAQ,GAAG,6CAA6C,CAAC;IAC/D,IAAI,eAAe,GAAa,EAAE,CAAC;IAEnC,SAAS,CAAC,GAAG,EAAE;QACb,kDAAkD;QAClD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;YAClC,EAAE,CAAC,SAAS,CAAC,aAAa,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACnD,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,GAAG,EAAE;QACZ,wCAAwC;QACxC,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;YAC7B,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;gBACxB,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;gBACpB,OAAO,CAAC,GAAG,CAAC,yBAAyB,IAAI,EAAE,CAAC,CAAC;YAC/C,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,KAAK,IAAI,EAAE;QAC9D,yCAAyC;QACzC,MAAM,eAAe,GAAG;YACtB,SAAS,EAAE,aAAa;YACxB,SAAS,EAAE,IAAI;YACf,WAAW,EAAE,KAAK;YAClB,YAAY,EAAE,GAAG,EAAE,eAAe;YAClC,OAAO,EAAE,IAAI;SACd,CAAC;QAEF,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,aAAa,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;QACtE,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAE/B,OAAO,CAAC,GAAG,CAAC,wBAAwB,QAAQ,EAAE,CAAC,CAAC;QAEhD,yBAAyB;QACzB,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAE3C,iCAAiC;QACjC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAE5C,0CAA0C;QAC1C,MAAM,KAAK,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACpC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,kBAAkB,KAAK,CAAC,IAAI,QAAQ,CAAC,CAAC;QAElD,8BAA8B;QAC9B,OAAO,CAAC,GAAG,CAAC;gBACA,QAAQ;gBACR,KAAK,CAAC,IAAI;mBACP,KAAK,CAAC,SAAS;oBACd,KAAK,CAAC,KAAK;KAC1B,CAAC,CAAC;IACL,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,0DAA0D;IAErE,EAAE,CAAC,iCAAiC,EAAE,KAAK,IAAI,EAAE;QAC/C,6DAA6D;QAC7D,MAAM,OAAO,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;QACnC,MAAM,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;QAE9B,+BAA+B;QAC/B,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAE1C,gEAAgE;QAChE,MAAM,KAAK,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAEnC,8DAA8D;QAC9D,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,gBAAgB;QAE3D,6DAA6D;QAC7D,iDAAiD;QAEjD,OAAO,CAAC,GAAG,CAAC,sBAAsB,OAAO,KAAK,KAAK,CAAC,IAAI,SAAS,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
||||
@ -1,2 +0,0 @@
|
||||
export {};
|
||||
//# sourceMappingURL=tiktok.test.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"tiktok.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/tiktok.test.ts"],"names":[],"mappings":""}
|
||||
@ -1,80 +0,0 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
||||
import { existsSync, statSync } from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { YtDlp } from '../ytdlp.js';
|
||||
describe('TikTok Download Tests', () => {
|
||||
// TikTok URL to test
|
||||
const tiktokUrl = 'https://www.tiktok.com/@woman.power.quote/video/7476910372121971970';
|
||||
// Temporary output directory for test downloads
|
||||
const outputDir = path.join(process.cwd(), 'test-downloads');
|
||||
// Instance of YtDlp
|
||||
let ytdlp;
|
||||
// Path to the downloaded file (will be set during test)
|
||||
let downloadedFilePath;
|
||||
beforeAll(() => {
|
||||
// Initialize YtDlp instance with test options
|
||||
ytdlp = new YtDlp({
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
});
|
||||
// Set up spy for console.log to track progress messages
|
||||
vi.spyOn(console, 'log').mockImplementation(() => { });
|
||||
});
|
||||
afterAll(async () => {
|
||||
// Clean up downloaded files if they exist
|
||||
if (downloadedFilePath && existsSync(downloadedFilePath)) {
|
||||
try {
|
||||
//await unlink(downloadedFilePath);
|
||||
console.log(`Test cleanup: Deleted ${downloadedFilePath}`);
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Failed to delete test file: ${error}`);
|
||||
}
|
||||
}
|
||||
// Restore console.log
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
it('should download a TikTok video successfully', async () => {
|
||||
// Define download options
|
||||
const options = {
|
||||
format: 'best',
|
||||
outputDir,
|
||||
// Add a timestamp to ensure unique filenames across test runs
|
||||
outputTemplate: `tiktok-test-${Date.now()}.%(ext)s`,
|
||||
};
|
||||
// Download the video
|
||||
downloadedFilePath = await ytdlp.downloadVideo(tiktokUrl, options);
|
||||
// Verify the download was successful
|
||||
expect(downloadedFilePath).toBeTruthy();
|
||||
expect(existsSync(downloadedFilePath)).toBe(true);
|
||||
// Check file has some content (not empty)
|
||||
const stats = statSync(downloadedFilePath).size;
|
||||
expect(stats).toBeGreaterThan(0);
|
||||
console.log(`Downloaded TikTok video to: ${downloadedFilePath}`);
|
||||
}, 60000); // Increase timeout for download to complete
|
||||
it('should get video info from TikTok URL', async () => {
|
||||
// Get video info
|
||||
const videoInfo = await ytdlp.getVideoInfo(tiktokUrl);
|
||||
// Verify basic video information
|
||||
expect(videoInfo).toBeTruthy();
|
||||
expect(videoInfo.id).toBeTruthy();
|
||||
expect(videoInfo.title).toBeTruthy();
|
||||
expect(videoInfo.uploader).toBeTruthy();
|
||||
console.log(`TikTok Video Title: ${videoInfo.title}`);
|
||||
console.log(`TikTok Video Uploader: ${videoInfo.uploader}`);
|
||||
}, 30000); // Increase timeout for API response
|
||||
it('should list available formats for TikTok video', async () => {
|
||||
// List available formats
|
||||
const formats = await ytdlp.listFormats(tiktokUrl);
|
||||
// Verify formats are returned
|
||||
expect(formats).toBeInstanceOf(Array);
|
||||
expect(formats.length).toBeGreaterThan(0);
|
||||
// At least one format should have a format_id
|
||||
expect(formats[0].format_id).toBeTruthy();
|
||||
// Log some useful information for debugging
|
||||
console.log(`Found ${formats.length} formats for TikTok video`);
|
||||
if (formats.length > 0) {
|
||||
console.log('First format:', JSON.stringify(formats[0], null, 2));
|
||||
}
|
||||
}, 30000); // Increase timeout for format listing
|
||||
});
|
||||
//# sourceMappingURL=tiktok.test.js.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"tiktok.test.js","sourceRoot":"","sources":["../../src/__tests__/tiktok.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,qBAAqB;IACrB,MAAM,SAAS,GAAG,qEAAqE,CAAC;IAExF,gDAAgD;IAChD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,gBAAgB,CAAC,CAAC;IAE7D,oBAAoB;IACpB,IAAI,KAAY,CAAC;IAEjB,wDAAwD;IACxD,IAAI,kBAA0B,CAAC;IAE/B,SAAS,CAAC,GAAG,EAAE;QACb,8CAA8C;QAC9C,KAAK,GAAG,IAAI,KAAK,CAAC;YAChB,SAAS,EAAE,qHAAqH;SACjI,CAAC,CAAC;QAEH,wDAAwD;QACxD,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,kBAAkB,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,KAAK,IAAI,EAAE;QAClB,0CAA0C;QAC1C,IAAI,kBAAkB,IAAI,UAAU,CAAC,kBAAkB,CAAC,EAAE,CAAC;YACzD,IAAI,CAAC;gBACH,mCAAmC;gBACnC,OAAO,CAAC,GAAG,CAAC,yBAAyB,kBAAkB,EAAE,CAAC,CAAC;YAC7D,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,KAAK,CAAC,+BAA+B,KAAK,EAAE,CAAC,CAAC;YACxD,CAAC;QACH,CAAC;QAED,sBAAsB;QACtB,EAAE,CAAC,eAAe,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;QAC3D,0BAA0B;QAC1B,MAAM,OAAO,GAAG;YACd,MAAM,EAAE,MAAM;YACd,SAAS;YACT,8DAA8D;YAC9D,cAAc,EAAE,eAAe,IAAI,CAAC,GAAG,EAAE,UAAU;SACpD,CAAC;QAEF,qBAAqB;QACrB,kBAAkB,GAAG,MAAM,KAAK,CAAC,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAEnE,qCAAqC;QACrC,MAAM,CAAC,kBAAkB,CAAC,CAAC,UAAU,EAAE,CAAC;QACxC,MAAM,CAAC,UAAU,CAAC,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAElD,0CAA0C;QAC1C,MAAM,KAAK,GAAG,QAAQ,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC;QAChD,MAAM,CAAC,KAAK,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QAEjC,OAAO,CAAC,GAAG,CAAC,+BAA+B,kBAAkB,EAAE,CAAC,CAAC;IACnE,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,4CAA4C;IAEvD,EAAE,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;QACrD,iBAAiB;QACjB,MAAM,SAAS,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;QAEtD,iCAAiC;QACjC,MAAM,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,CAAC;QAC/B,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC;QAClC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,UAAU,EAAE,CAAC;QACrC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,UAAU,EAAE,CAAC;QAExC,OAAO,CAAC,GAAG,CAAC,uBAAuB,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC;QACtD,OAAO,CAAC,GAAG,CAAC,0BAA0B,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC9D,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,oCAAoC;IAE/C,EAAE,CAAC,gDAAgD,EAAE,KAAK,IAAI,EAAE;QAC9D,yBAAyB;QACzB,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;QAEnD,8BAA8B;QAC9B,MAAM,CAAC,OAAO,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;QACtC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QAE1C,8CAA8C;QAC9C,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,CAAC;QAE1C,4CAA4C;QAC5C,OAAO,CAAC,GAAG,CAAC,SAAS,OAAO,CAAC,MAAM,2BAA2B,CAAC,CAAC;QAChE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QACpE,CAAC;IACH,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,sCAAsC;AACnD,CAAC,CAAC,CAAC"}
|
||||
@ -1,2 +0,0 @@
|
||||
export {};
|
||||
//# sourceMappingURL=youtube.test.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"youtube.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/youtube.test.ts"],"names":[],"mappings":""}
|
||||
@ -1,64 +0,0 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { YtDlp } from '../ytdlp.js';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
// Create a test directory for downloads
|
||||
const TEST_DIR = path.join(process.cwd(), 'test-downloads');
|
||||
const YOUTUBE_URL = 'https://www.youtube.com/watch?v=_oVI0GW-Xd4';
|
||||
describe('YouTube Video Download', () => {
|
||||
// Ensure the test directory exists
|
||||
if (!fs.existsSync(TEST_DIR)) {
|
||||
fs.mkdirSync(TEST_DIR, { recursive: true });
|
||||
}
|
||||
let downloadedFiles = [];
|
||||
// Clean up after tests
|
||||
afterEach(() => {
|
||||
// Delete any downloaded files
|
||||
downloadedFiles.forEach(file => {
|
||||
const fullPath = path.resolve(file);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
//fs.unlinkSync(fullPath);
|
||||
console.log(`Cleaned up test file: ${fullPath}`);
|
||||
}
|
||||
});
|
||||
downloadedFiles = [];
|
||||
});
|
||||
it('should successfully download a YouTube video', async () => {
|
||||
// Create a new YtDlp instance
|
||||
const ytdlp = new YtDlp();
|
||||
// Check if yt-dlp is installed
|
||||
const isInstalled = await ytdlp.isInstalled();
|
||||
expect(isInstalled).toBe(true);
|
||||
// Download the video with specific options to keep the test fast
|
||||
// Use a lower quality format to speed up the test
|
||||
const downloadOptions = {
|
||||
outputDir: TEST_DIR,
|
||||
format: 'worst[ext=mp4]', // Use lowest quality for faster test
|
||||
outputTemplate: 'youtube-test-%(id)s.%(ext)s'
|
||||
};
|
||||
// Execute the download
|
||||
const filePath = await ytdlp.downloadVideo(YOUTUBE_URL, downloadOptions);
|
||||
console.log(`Downloaded file: ${filePath}`);
|
||||
// Add to cleanup list
|
||||
downloadedFiles.push(filePath);
|
||||
// Assert that the file exists
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
// Assert that the file has content (not empty)
|
||||
const stats = fs.statSync(filePath);
|
||||
expect(stats.size).toBeGreaterThan(0);
|
||||
}, 60000); // Increase timeout to 60 seconds as downloads may take time
|
||||
it('should retrieve video information correctly', async () => {
|
||||
const ytdlp = new YtDlp();
|
||||
// Get video info
|
||||
const videoInfo = await ytdlp.getVideoInfo(YOUTUBE_URL);
|
||||
// Assert video properties
|
||||
expect(videoInfo).toBeDefined();
|
||||
expect(videoInfo.id).toBeDefined();
|
||||
expect(videoInfo.title).toBeDefined();
|
||||
expect(videoInfo.webpage_url).toBeDefined();
|
||||
// Verify the video ID matches the expected ID from the URL
|
||||
const expectedVideoId = '_oVI0GW-Xd4';
|
||||
expect(videoInfo.id).toBe(expectedVideoId);
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=youtube.test.js.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"youtube.test.js","sourceRoot":"","sources":["../../src/__tests__/youtube.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,wCAAwC;AACxC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,gBAAgB,CAAC,CAAC;AAC5D,MAAM,WAAW,GAAG,6CAA6C,CAAC;AAElE,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACtC,mCAAmC;IACnC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,CAAC;IAED,IAAI,eAAe,GAAa,EAAE,CAAC;IAEnC,uBAAuB;IACvB,SAAS,CAAC,GAAG,EAAE;QACb,8BAA8B;QAC9B,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;YAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YACpC,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC5B,0BAA0B;gBAC1B,OAAO,CAAC,GAAG,CAAC,yBAAyB,QAAQ,EAAE,CAAC,CAAC;YACnD,CAAC;QACH,CAAC,CAAC,CAAC;QACH,eAAe,GAAG,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;QAC5D,8BAA8B;QAC9B,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAE1B,+BAA+B;QAC/B,MAAM,WAAW,GAAG,MAAM,KAAK,CAAC,WAAW,EAAE,CAAC;QAC9C,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAE/B,iEAAiE;QACjE,kDAAkD;QAClD,MAAM,eAAe,GAAG;YACtB,SAAS,EAAE,QAAQ;YACnB,MAAM,EAAE,gBAAgB,EAAE,qCAAqC;YAC/D,cAAc,EAAE,6BAA6B;SAC9C,CAAC;QAEF,uBAAuB;QACvB,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,aAAa,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC;QACzE,OAAO,CAAC,GAAG,CAAC,oBAAoB,QAAQ,EAAE,CAAC,CAAC;QAE5C,sBAAsB;QACtB,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAE/B,8BAA8B;QAC9B,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAE3C,+CAA+C;QAC/C,MAAM,KAAK,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACpC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IACxC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,4DAA4D;IAEvE,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;QAC3D,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAE1B,iBAAiB;QACjB,MAAM,SAAS,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;QAExD,0BAA0B;QAC1B,MAAM,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,CAAC;QAChC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QACnC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QACtC,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,WAAW,EAAE,CAAC;QAE5C,2DAA2D;QAC3D,MAAM,eAAe,GAAG,aAAa,CAAC;QACtC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
||||
11525
packages/media/ref/yt-dlp/dist/cli-bundle.js
vendored
11525
packages/media/ref/yt-dlp/dist/cli-bundle.js
vendored
File diff suppressed because it is too large
Load Diff
3
packages/media/ref/yt-dlp/dist/cli.d.ts
vendored
3
packages/media/ref/yt-dlp/dist/cli.d.ts
vendored
@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
export {};
|
||||
//# sourceMappingURL=cli.d.ts.map
|
||||
1
packages/media/ref/yt-dlp/dist/cli.d.ts.map
vendored
1
packages/media/ref/yt-dlp/dist/cli.d.ts.map
vendored
@ -1 +0,0 @@
|
||||
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
||||
234
packages/media/ref/yt-dlp/dist/cli.js
vendored
234
packages/media/ref/yt-dlp/dist/cli.js
vendored
@ -1,234 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import { YtDlp } from './ytdlp.js';
|
||||
import { logger } from './logger.js';
|
||||
import { FormatOptionsSchema, VideoInfoOptionsSchema, DownloadOptionsSchema } from './types.js';
|
||||
import { z } from 'zod';
|
||||
import { exec } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
const execAsync = promisify(exec);
|
||||
const ytdlp = new YtDlp();
|
||||
// Function to generate user-friendly help message
|
||||
function printHelp() {
|
||||
console.log("\n======= yt-dlp TypeScript Wrapper =======");
|
||||
console.log("A TypeScript wrapper for the yt-dlp video downloader\n");
|
||||
console.log("USAGE:");
|
||||
console.log(" ytdlp-ts <command> [options] <url>\n");
|
||||
console.log("COMMANDS:");
|
||||
console.log(" download [url] Download a video from the specified URL");
|
||||
console.log(" info [url] Get information about a video");
|
||||
console.log(" formats [url] List available formats for a video");
|
||||
console.log(" tiktok:meta [url] Scrape metadata from a TikTok video and save as JSON\n");
|
||||
console.log("DOWNLOAD OPTIONS:");
|
||||
console.log(" --format, -f Specify video format code");
|
||||
console.log(" --output, -o Specify output filename template");
|
||||
console.log(" --quiet, -q Activate quiet mode");
|
||||
console.log(" --verbose, -v Print various debugging information");
|
||||
console.log(" --mp3 Download only the audio in MP3 format\n");
|
||||
console.log("INFO OPTIONS:");
|
||||
console.log(" --dump-json Output JSON information");
|
||||
console.log(" --flat-playlist Flat playlist output\n");
|
||||
console.log("FORMATS OPTIONS:");
|
||||
console.log(" --all Show all available formats\n");
|
||||
console.log("EXAMPLES:");
|
||||
console.log(" ytdlp-ts download https://www.tiktok.com/@woman.power.quote/video/7476910372121970");
|
||||
console.log(" ytdlp-ts download https://www.youtube.com/watch?v=_oVI0GW-Xd4 -f \"bestvideo[height<=1080]+bestaudio/best[height<=1080]\"");
|
||||
console.log(" ytdlp-ts download https://www.youtube.com/watch?v=_oVI0GW-Xd4 --mp3");
|
||||
console.log(" ytdlp-ts info https://www.tiktok.com/@woman.power.quote/video/7476910372121970 --dump-json");
|
||||
console.log(" ytdlp-ts formats https://www.youtube.com/watch?v=_oVI0GW-Xd4 --all\n");
|
||||
console.log("For more information, visit https://github.com/yt-dlp/yt-dlp");
|
||||
}
|
||||
/**
|
||||
* Checks if ffmpeg is installed on the system
|
||||
* @returns {Promise<boolean>} True if ffmpeg is installed, false otherwise
|
||||
*/
|
||||
async function isFFmpegInstalled() {
|
||||
try {
|
||||
// Try to execute ffmpeg -version command
|
||||
await execAsync('ffmpeg -version');
|
||||
return true;
|
||||
}
|
||||
catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Check for help flags directly in process.argv
|
||||
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
// Create a simple yargs CLI with clear command structure
|
||||
yargs(hideBin(process.argv))
|
||||
.scriptName('yt-dlp-wrapper')
|
||||
.usage('$0 <cmd> [args]')
|
||||
.command('download [url]', 'Download a video', (yargs) => {
|
||||
return yargs
|
||||
.positional('url', {
|
||||
type: 'string',
|
||||
describe: 'URL of the video to download',
|
||||
demandOption: true,
|
||||
})
|
||||
.option('format', {
|
||||
type: 'string',
|
||||
describe: 'Video format code',
|
||||
alias: 'f',
|
||||
})
|
||||
.option('output', {
|
||||
type: 'string',
|
||||
describe: 'Output filename template',
|
||||
alias: 'o',
|
||||
})
|
||||
.option('quiet', {
|
||||
type: 'boolean',
|
||||
describe: 'Activate quiet mode',
|
||||
alias: 'q',
|
||||
default: false,
|
||||
})
|
||||
.option('verbose', {
|
||||
type: 'boolean',
|
||||
describe: 'Print various debugging information',
|
||||
alias: 'v',
|
||||
default: false,
|
||||
})
|
||||
.option('mp3', {
|
||||
type: 'boolean',
|
||||
describe: 'Download only the audio in MP3 format',
|
||||
default: false,
|
||||
});
|
||||
}, async (argv) => {
|
||||
try {
|
||||
logger.info(`Starting download process for: ${argv.url}`);
|
||||
logger.debug(`Download options: mp3=${argv.mp3}, format=${argv.format}, output=${argv.output}`);
|
||||
// Check if ffmpeg is installed when MP3 option is specified
|
||||
if (argv.mp3) {
|
||||
logger.info('MP3 option detected. Checking for ffmpeg installation...');
|
||||
const ffmpegInstalled = await isFFmpegInstalled();
|
||||
if (!ffmpegInstalled) {
|
||||
logger.error('\x1b[31mError: ffmpeg is not installed or not found in PATH\x1b[0m');
|
||||
logger.error('\nTo download videos as MP3, ffmpeg is required. Please install ffmpeg:');
|
||||
logger.error('\n • Windows: https://ffmpeg.org/download.html or install via Chocolatey/Scoop');
|
||||
logger.error(' • macOS: brew install ffmpeg');
|
||||
logger.error(' • Linux: apt install ffmpeg / yum install ffmpeg / etc. (depending on your distribution)');
|
||||
logger.error('\nAfter installing, make sure ffmpeg is in your PATH and try again.');
|
||||
process.exit(1);
|
||||
}
|
||||
logger.info('ffmpeg is installed. Proceeding with MP3 download...');
|
||||
}
|
||||
// Parse and validate options using Zod
|
||||
const options = DownloadOptionsSchema.parse({
|
||||
format: argv.format,
|
||||
output: argv.output,
|
||||
quiet: argv.quiet,
|
||||
verbose: argv.verbose,
|
||||
audioOnly: argv.mp3 ? true : undefined,
|
||||
audioFormat: argv.mp3 ? 'mp3' : undefined,
|
||||
});
|
||||
logger.debug(`Parsed download options: ${JSON.stringify(options)}`);
|
||||
logger.info(`Starting download with options: ${JSON.stringify({
|
||||
url: argv.url,
|
||||
mp3: argv.mp3,
|
||||
format: argv.format,
|
||||
output: argv.output
|
||||
})}`);
|
||||
const downloadedFile = await ytdlp.downloadVideo(argv.url, options);
|
||||
logger.info(`Download completed successfully: ${downloadedFile}`);
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.error('Invalid options:', error.errors);
|
||||
}
|
||||
else {
|
||||
logger.error('Failed to download video:', error);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
.command('info [url]', 'Get video information', (yargs) => {
|
||||
return yargs
|
||||
.positional('url', {
|
||||
type: 'string',
|
||||
describe: 'URL of the video',
|
||||
demandOption: true,
|
||||
})
|
||||
.option('dump-json', {
|
||||
type: 'boolean',
|
||||
describe: 'Output JSON information',
|
||||
default: false,
|
||||
})
|
||||
.option('flat-playlist', {
|
||||
type: 'boolean',
|
||||
describe: 'Flat playlist output',
|
||||
default: false,
|
||||
});
|
||||
}, async (argv) => {
|
||||
try {
|
||||
logger.info(`Starting info retrieval for: ${argv.url}`);
|
||||
// Parse and validate options using Zod
|
||||
const options = VideoInfoOptionsSchema.parse({
|
||||
dumpJson: argv.dumpJson,
|
||||
flatPlaylist: argv.flatPlaylist,
|
||||
});
|
||||
const info = await ytdlp.getVideoInfo(argv.url, options);
|
||||
logger.info(`Info retrieval completed successfully`);
|
||||
console.log(JSON.stringify(info, null, 2));
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.error('Invalid options:', error.errors);
|
||||
}
|
||||
else {
|
||||
logger.error('Failed to get video info:', error);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
.command('formats [url]', 'List available formats of a video', (yargs) => {
|
||||
return yargs
|
||||
.positional('url', {
|
||||
type: 'string',
|
||||
describe: 'URL of the video',
|
||||
demandOption: true,
|
||||
})
|
||||
.option('all', {
|
||||
type: 'boolean',
|
||||
describe: 'Show all available formats',
|
||||
default: false,
|
||||
});
|
||||
}, async (argv) => {
|
||||
try {
|
||||
logger.info(`Getting available formats for video: ${argv.url}`);
|
||||
// Parse and validate options using Zod
|
||||
const options = FormatOptionsSchema.parse({
|
||||
all: argv.all,
|
||||
});
|
||||
const formats = await ytdlp.listFormats(argv.url, options);
|
||||
logger.info(`Format listing completed successfully`);
|
||||
console.log(formats);
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.error('Invalid options:', error.errors);
|
||||
}
|
||||
else {
|
||||
logger.error('Failed to list formats:', error);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
.example('$0 download https://www.tiktok.com/@woman.power.quote/video/7476910372121971970', 'Download a TikTok video with default settings')
|
||||
.example('$0 download https://www.youtube.com/watch?v=_oVI0GW-Xd4 -f "bestvideo[height<=1080]+bestaudio/best[height<=1080]"', 'Download a YouTube video in 1080p or lower quality')
|
||||
.example('$0 download https://www.youtube.com/watch?v=_oVI0GW-Xd4 --mp3', 'Extract and download only the audio in MP3 format')
|
||||
.example('$0 info https://www.tiktok.com/@woman.power.quote/video/7476910372121971970 --dump-json', 'Retrieve and display detailed video metadata in JSON format')
|
||||
.example('$0 formats https://www.youtube.com/watch?v=_oVI0GW-Xd4 --all', 'List all available video and audio formats for a YouTube video')
|
||||
.example('$0 tiktok:meta https://www.tiktok.com/@username/video/1234567890 -o metadata.json', 'Scrape metadata from a TikTok video and save it to metadata.json')
|
||||
.demandCommand(1, 'You need to specify a command')
|
||||
.strict()
|
||||
.help()
|
||||
.alias('h', 'help')
|
||||
.version()
|
||||
.alias('V', 'version')
|
||||
.wrap(800) // Fixed width value instead of yargs.terminalWidth() which isn't compatible with ESM
|
||||
.showHelpOnFail(true)
|
||||
.parse();
|
||||
//# sourceMappingURL=cli.js.map
|
||||
1
packages/media/ref/yt-dlp/dist/cli.js.map
vendored
1
packages/media/ref/yt-dlp/dist/cli.js.map
vendored
File diff suppressed because one or more lines are too long
4
packages/media/ref/yt-dlp/dist/index.d.ts
vendored
4
packages/media/ref/yt-dlp/dist/index.d.ts
vendored
@ -1,4 +0,0 @@
|
||||
export { YtDlp } from './ytdlp.js';
|
||||
export { YtDlpOptions, DownloadOptions, FormatOptions, VideoInfoOptions, VideoFormat, VideoInfo, YtDlpOptionsSchema, DownloadOptionsSchema, FormatOptionsSchema, VideoInfoOptionsSchema, VideoFormatSchema, VideoInfoSchema, } from './types.js';
|
||||
export { logger } from './logger.js';
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAEL,YAAY,EACZ,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,SAAS,EAGT,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,GAChB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC"}
|
||||
9
packages/media/ref/yt-dlp/dist/index.js
vendored
9
packages/media/ref/yt-dlp/dist/index.js
vendored
@ -1,9 +0,0 @@
|
||||
// Export YtDlp class
|
||||
export { YtDlp } from './ytdlp.js';
|
||||
// Export all types and schemas
|
||||
export {
|
||||
// Zod schemas
|
||||
YtDlpOptionsSchema, DownloadOptionsSchema, FormatOptionsSchema, VideoInfoOptionsSchema, VideoFormatSchema, VideoInfoSchema, } from './types.js';
|
||||
// Export logger
|
||||
export { logger } from './logger.js';
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
packages/media/ref/yt-dlp/dist/index.js.map
vendored
1
packages/media/ref/yt-dlp/dist/index.js.map
vendored
@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qBAAqB;AACrB,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,+BAA+B;AAC/B,OAAO;AASL,cAAc;AACd,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,GAChB,MAAM,YAAY,CAAC;AAEpB,gBAAgB;AAChB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC"}
|
||||
13
packages/media/ref/yt-dlp/dist/logger.d.ts
vendored
13
packages/media/ref/yt-dlp/dist/logger.d.ts
vendored
@ -1,13 +0,0 @@
|
||||
import { Logger } from 'tslog';
|
||||
export declare enum LogLevel {
|
||||
SILLY = "silly",
|
||||
TRACE = "trace",
|
||||
DEBUG = "debug",
|
||||
INFO = "info",
|
||||
WARN = "warn",
|
||||
ERROR = "error",
|
||||
FATAL = "fatal"
|
||||
}
|
||||
export declare const logger: Logger<unknown>;
|
||||
export default logger;
|
||||
//# sourceMappingURL=logger.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAW,MAAM,OAAO,CAAC;AAGxC,oBAAY,QAAQ;IAClB,KAAK,UAAU;IACf,KAAK,UAAU;IACf,KAAK,UAAU;IACf,IAAI,SAAS;IACb,IAAI,SAAS;IACb,KAAK,UAAU;IACf,KAAK,UAAU;CAChB;AAyBD,eAAO,MAAM,MAAM,iBAEjB,CAAC;AAgBH,eAAe,MAAM,CAAC"}
|
||||
51
packages/media/ref/yt-dlp/dist/logger.js
vendored
51
packages/media/ref/yt-dlp/dist/logger.js
vendored
@ -1,51 +0,0 @@
|
||||
import { Logger } from 'tslog';
|
||||
// Configure log levels
|
||||
export var LogLevel;
|
||||
(function (LogLevel) {
|
||||
LogLevel["SILLY"] = "silly";
|
||||
LogLevel["TRACE"] = "trace";
|
||||
LogLevel["DEBUG"] = "debug";
|
||||
LogLevel["INFO"] = "info";
|
||||
LogLevel["WARN"] = "warn";
|
||||
LogLevel["ERROR"] = "error";
|
||||
LogLevel["FATAL"] = "fatal";
|
||||
})(LogLevel || (LogLevel = {}));
|
||||
// Mapping from string LogLevel to numeric values expected by tslog
|
||||
const logLevelToTsLogLevel = {
|
||||
[LogLevel.SILLY]: 0,
|
||||
[LogLevel.TRACE]: 1,
|
||||
[LogLevel.DEBUG]: 2,
|
||||
[LogLevel.INFO]: 3,
|
||||
[LogLevel.WARN]: 4,
|
||||
[LogLevel.ERROR]: 5,
|
||||
[LogLevel.FATAL]: 6
|
||||
};
|
||||
// Convert a LogLevel string to its corresponding numeric value
|
||||
const getNumericLogLevel = (level) => {
|
||||
return logLevelToTsLogLevel[level];
|
||||
};
|
||||
// Custom transport for logs if needed
|
||||
const logToTransport = (logObject) => {
|
||||
// Here you can implement custom transport like file or external service
|
||||
// For example, log to file or send to a log management service
|
||||
// console.log("Custom transport:", JSON.stringify(logObject));
|
||||
};
|
||||
// Create the logger instance
|
||||
export const logger = new Logger({
|
||||
name: "yt-dlp-wrapper"
|
||||
});
|
||||
// Add transport if needed
|
||||
// logger.attachTransport(
|
||||
// {
|
||||
// silly: logToTransport,
|
||||
// debug: logToTransport,
|
||||
// trace: logToTransport,
|
||||
// info: logToTransport,
|
||||
// warn: logToTransport,
|
||||
// error: logToTransport,
|
||||
// fatal: logToTransport,
|
||||
// },
|
||||
// LogLevel.INFO
|
||||
// );
|
||||
export default logger;
|
||||
//# sourceMappingURL=logger.js.map
|
||||
1
packages/media/ref/yt-dlp/dist/logger.js.map
vendored
1
packages/media/ref/yt-dlp/dist/logger.js.map
vendored
@ -1 +0,0 @@
|
||||
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAW,MAAM,OAAO,CAAC;AAExC,uBAAuB;AACvB,MAAM,CAAN,IAAY,QAQX;AARD,WAAY,QAAQ;IAClB,2BAAe,CAAA;IACf,2BAAe,CAAA;IACf,2BAAe,CAAA;IACf,yBAAa,CAAA;IACb,yBAAa,CAAA;IACb,2BAAe,CAAA;IACf,2BAAe,CAAA;AACjB,CAAC,EARW,QAAQ,KAAR,QAAQ,QAQnB;AAED,mEAAmE;AACnE,MAAM,oBAAoB,GAA6B;IACrD,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;IACnB,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;IACnB,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;IACnB,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IAClB,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IAClB,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;IACnB,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;CACpB,CAAC;AAEF,+DAA+D;AAC/D,MAAM,kBAAkB,GAAG,CAAC,KAAe,EAAU,EAAE;IACrD,OAAO,oBAAoB,CAAC,KAAK,CAAC,CAAC;AACrC,CAAC,CAAC;AACF,sCAAsC;AACtC,MAAM,cAAc,GAAG,CAAC,SAAkB,EAAE,EAAE;IAC5C,wEAAwE;IACxE,+DAA+D;IAC/D,+DAA+D;AACjE,CAAC,CAAC;AAEF,6BAA6B;AAC7B,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC;IAC/B,IAAI,EAAE,gBAAgB;CACvB,CAAC,CAAC;AAEH,0BAA0B;AAC1B,0BAA0B;AAC1B,MAAM;AACN,6BAA6B;AAC7B,6BAA6B;AAC7B,6BAA6B;AAC7B,4BAA4B;AAC5B,4BAA4B;AAC5B,6BAA6B;AAC7B,6BAA6B;AAC7B,OAAO;AACP,kBAAkB;AAClB,KAAK;AAEL,eAAe,MAAM,CAAC"}
|
||||
5862
packages/media/ref/yt-dlp/dist/main.js
vendored
5862
packages/media/ref/yt-dlp/dist/main.js
vendored
File diff suppressed because it is too large
Load Diff
@ -1,2 +0,0 @@
|
||||
export {};
|
||||
//# sourceMappingURL=tiktok-scraper.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"tiktok-scraper.d.ts","sourceRoot":"","sources":["../src/tiktok-scraper.ts"],"names":[],"mappings":""}
|
||||
@ -1,2 +0,0 @@
|
||||
export {};
|
||||
//# sourceMappingURL=tiktok-scraper.js.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"tiktok-scraper.js","sourceRoot":"","sources":["../src/tiktok-scraper.ts"],"names":[],"mappings":""}
|
||||
1072
packages/media/ref/yt-dlp/dist/types.d.ts
vendored
1072
packages/media/ref/yt-dlp/dist/types.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@ -1 +0,0 @@
|
||||
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0D7B,CAAC;AAGH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAG9D,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4C1B,CAAC;AAEH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAGxD,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAQ/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAGlE,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;EAM9B,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAGhE,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;EAS/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAGlE,oBAAY,cAAc;IACxB,aAAa,kBAAkB;IAC/B,gBAAgB,qBAAqB;IACrC,cAAc,mBAAmB;IACjC,eAAe,oBAAoB;IACnC,aAAa,kBAAkB;CAChC;AAGD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;EAK3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAG1D,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUhC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAGpE,eAAO,MAAM,mBAAmB;;;;;;EAE9B,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAGhE,eAAO,MAAM,sBAAsB;;;;;;;;;EAGjC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAItE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkB5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAG5D,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;EAQtC,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAGhF,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuE/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
|
||||
257
packages/media/ref/yt-dlp/dist/types.js
vendored
257
packages/media/ref/yt-dlp/dist/types.js
vendored
@ -1,257 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
// Basic YouTube DLP options schema
|
||||
export const YtDlpOptionsSchema = z.object({
|
||||
// Path to the yt-dlp executable
|
||||
executablePath: z.string().optional(),
|
||||
// Output options
|
||||
output: z.string().optional(),
|
||||
format: z.string().optional(),
|
||||
formatSort: z.string().optional(),
|
||||
mergeOutputFormat: z.enum(['mp4', 'flv', 'webm', 'mkv', 'avi']).optional(),
|
||||
// Download options
|
||||
limit: z.number().int().positive().optional(),
|
||||
maxFilesize: z.string().optional(),
|
||||
minFilesize: z.string().optional(),
|
||||
// Filesystem options
|
||||
noOverwrites: z.boolean().optional(),
|
||||
continue: z.boolean().optional(),
|
||||
noPart: z.boolean().optional(),
|
||||
// Thumbnail options
|
||||
writeThumbnail: z.boolean().optional(),
|
||||
writeAllThumbnails: z.boolean().optional(),
|
||||
// Subtitles options
|
||||
writeSubtitles: z.boolean().optional(),
|
||||
writeAutoSubtitles: z.boolean().optional(),
|
||||
subLang: z.string().optional(),
|
||||
// Authentication options
|
||||
username: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
// Video selection options
|
||||
playlistStart: z.number().int().positive().optional(),
|
||||
playlistEnd: z.number().int().positive().optional(),
|
||||
playlistItems: z.string().optional(),
|
||||
// Post-processing options
|
||||
extractAudio: z.boolean().optional(),
|
||||
audioFormat: z.enum(['best', 'aac', 'flac', 'mp3', 'm4a', 'opus', 'vorbis', 'wav']).optional(),
|
||||
audioQuality: z.string().optional(),
|
||||
remuxVideo: z.enum(['mp4', 'mkv', 'flv', 'webm', 'mov', 'avi']).optional(),
|
||||
recodeVideo: z.enum(['mp4', 'flv', 'webm', 'mkv', 'avi']).optional(),
|
||||
// Verbosity and simulation options
|
||||
quiet: z.boolean().optional(),
|
||||
verbose: z.boolean().optional(),
|
||||
noWarnings: z.boolean().optional(),
|
||||
simulate: z.boolean().optional(),
|
||||
// Workarounds
|
||||
noCheckCertificates: z.boolean().optional(),
|
||||
preferInsecure: z.boolean().optional(),
|
||||
userAgent: z.string().optional(),
|
||||
// Extra arguments as string array
|
||||
extraArgs: z.array(z.string()).optional(),
|
||||
});
|
||||
// Video information schema
|
||||
export const VideoInfoSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
formats: z.array(z.object({
|
||||
format_id: z.string(),
|
||||
format: z.string(),
|
||||
ext: z.string(),
|
||||
resolution: z.string().optional(),
|
||||
fps: z.number().optional(),
|
||||
filesize: z.number().optional(),
|
||||
tbr: z.number().optional(),
|
||||
protocol: z.string(),
|
||||
vcodec: z.string(),
|
||||
acodec: z.string(),
|
||||
})),
|
||||
thumbnails: z.array(z.object({
|
||||
url: z.string(),
|
||||
height: z.number().optional(),
|
||||
width: z.number().optional(),
|
||||
})).optional(),
|
||||
description: z.string().optional(),
|
||||
upload_date: z.string().optional(),
|
||||
uploader: z.string().optional(),
|
||||
uploader_id: z.string().optional(),
|
||||
uploader_url: z.string().optional(),
|
||||
channel_id: z.string().optional(),
|
||||
channel_url: z.string().optional(),
|
||||
duration: z.number().optional(),
|
||||
view_count: z.number().optional(),
|
||||
like_count: z.number().optional(),
|
||||
dislike_count: z.number().optional(),
|
||||
average_rating: z.number().optional(),
|
||||
age_limit: z.number().optional(),
|
||||
webpage_url: z.string(),
|
||||
categories: z.array(z.string()).optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
is_live: z.boolean().optional(),
|
||||
was_live: z.boolean().optional(),
|
||||
playable_in_embed: z.boolean().optional(),
|
||||
availability: z.string().optional(),
|
||||
});
|
||||
// Download result schema
|
||||
export const DownloadResultSchema = z.object({
|
||||
videoInfo: VideoInfoSchema,
|
||||
filePath: z.string(),
|
||||
downloadedBytes: z.number().optional(),
|
||||
elapsedTime: z.number().optional(),
|
||||
averageSpeed: z.number().optional(), // in bytes/s
|
||||
success: z.boolean(),
|
||||
error: z.string().optional(),
|
||||
});
|
||||
// Command execution result schema
|
||||
export const CommandResultSchema = z.object({
|
||||
command: z.string(),
|
||||
stdout: z.string(),
|
||||
stderr: z.string(),
|
||||
success: z.boolean(),
|
||||
exitCode: z.number(),
|
||||
});
|
||||
// Progress update schema for download progress events
|
||||
export const ProgressUpdateSchema = z.object({
|
||||
videoId: z.string(),
|
||||
percent: z.number().min(0).max(100),
|
||||
totalSize: z.number().optional(),
|
||||
downloadedBytes: z.number(),
|
||||
speed: z.number(), // in bytes/s
|
||||
eta: z.number().optional(), // in seconds
|
||||
status: z.enum(['downloading', 'finished', 'error']),
|
||||
message: z.string().optional(),
|
||||
});
|
||||
// Error types
|
||||
export var YtDlpErrorType;
|
||||
(function (YtDlpErrorType) {
|
||||
YtDlpErrorType["PROCESS_ERROR"] = "PROCESS_ERROR";
|
||||
YtDlpErrorType["VALIDATION_ERROR"] = "VALIDATION_ERROR";
|
||||
YtDlpErrorType["DOWNLOAD_ERROR"] = "DOWNLOAD_ERROR";
|
||||
YtDlpErrorType["UNSUPPORTED_URL"] = "UNSUPPORTED_URL";
|
||||
YtDlpErrorType["NETWORK_ERROR"] = "NETWORK_ERROR";
|
||||
})(YtDlpErrorType || (YtDlpErrorType = {}));
|
||||
// Custom error schema
|
||||
export const YtDlpErrorSchema = z.object({
|
||||
type: z.nativeEnum(YtDlpErrorType),
|
||||
message: z.string(),
|
||||
details: z.record(z.any()).optional(),
|
||||
command: z.string().optional(),
|
||||
});
|
||||
// Download options schema
|
||||
export const DownloadOptionsSchema = z.object({
|
||||
outputDir: z.string().optional(),
|
||||
format: z.string().optional(),
|
||||
outputTemplate: z.string().optional(),
|
||||
audioOnly: z.boolean().optional(),
|
||||
audioFormat: z.string().optional(),
|
||||
subtitles: z.union([z.boolean(), z.array(z.string())]).optional(),
|
||||
maxFileSize: z.number().optional(),
|
||||
rateLimit: z.string().optional(),
|
||||
additionalArgs: z.array(z.string()).optional(),
|
||||
});
|
||||
// Format options schema for listing video formats
|
||||
export const FormatOptionsSchema = z.object({
|
||||
all: z.boolean().optional().default(false),
|
||||
});
|
||||
// Video info options schema
|
||||
export const VideoInfoOptionsSchema = z.object({
|
||||
dumpJson: z.boolean().optional().default(false),
|
||||
flatPlaylist: z.boolean().optional().default(false),
|
||||
});
|
||||
// Video format schema representing a single format option returned by yt-dlp
|
||||
// Video format schema representing a single format option returned by yt-dlp
|
||||
export const VideoFormatSchema = z.object({
|
||||
format_id: z.string(),
|
||||
format: z.string(),
|
||||
ext: z.string(),
|
||||
resolution: z.string().optional(),
|
||||
fps: z.number().optional(),
|
||||
filesize: z.number().optional(),
|
||||
tbr: z.number().optional(),
|
||||
protocol: z.string(),
|
||||
vcodec: z.string(),
|
||||
acodec: z.string(),
|
||||
width: z.number().optional(),
|
||||
height: z.number().optional(),
|
||||
url: z.string().optional(),
|
||||
format_note: z.string().optional(),
|
||||
container: z.string().optional(),
|
||||
quality: z.number().optional(),
|
||||
preference: z.number().optional(),
|
||||
});
|
||||
// TikTok metadata options schema
|
||||
export const TikTokMetadataOptionsSchema = z.object({
|
||||
url: z.string().url(),
|
||||
outputPath: z.string(),
|
||||
format: z.enum(['json', 'pretty']).optional().default('json'),
|
||||
includeComments: z.boolean().optional().default(false),
|
||||
timeout: z.number().int().positive().optional().default(30000),
|
||||
userAgent: z.string().optional(),
|
||||
proxy: z.string().optional(),
|
||||
});
|
||||
// TikTok metadata schema
|
||||
export const TikTokMetadataSchema = z.object({
|
||||
id: z.string(),
|
||||
url: z.string().url(),
|
||||
timestamp: z.number(),
|
||||
scrapedAt: z.string().datetime(),
|
||||
author: z.object({
|
||||
id: z.string(),
|
||||
uniqueId: z.string(), // username
|
||||
nickname: z.string(),
|
||||
avatarUrl: z.string().url().optional(),
|
||||
verified: z.boolean().optional(),
|
||||
secUid: z.string().optional(),
|
||||
following: z.number().optional(),
|
||||
followers: z.number().optional(),
|
||||
likes: z.number().optional(),
|
||||
profileUrl: z.string().url().optional(),
|
||||
}),
|
||||
video: z.object({
|
||||
id: z.string(),
|
||||
description: z.string(),
|
||||
createTime: z.number(),
|
||||
width: z.number(),
|
||||
height: z.number(),
|
||||
duration: z.number(),
|
||||
ratio: z.string().optional(),
|
||||
coverUrl: z.string().url().optional(),
|
||||
playUrl: z.string().url().optional(),
|
||||
downloadUrl: z.string().url().optional(),
|
||||
shareCount: z.number().optional(),
|
||||
commentCount: z.number().optional(),
|
||||
likeCount: z.number().optional(),
|
||||
viewCount: z.number().optional(),
|
||||
hashTags: z.array(z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
title: z.string().optional(),
|
||||
})).optional(),
|
||||
mentions: z.array(z.string()).optional(),
|
||||
musicInfo: z.object({
|
||||
id: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
authorName: z.string().optional(),
|
||||
coverUrl: z.string().url().optional(),
|
||||
playUrl: z.string().url().optional(),
|
||||
duration: z.number().optional(),
|
||||
}).optional(),
|
||||
}),
|
||||
comments: z.array(z.object({
|
||||
id: z.string(),
|
||||
text: z.string(),
|
||||
createTime: z.number(),
|
||||
likeCount: z.number(),
|
||||
authorId: z.string(),
|
||||
authorName: z.string(),
|
||||
authorAvatar: z.string().url().optional(),
|
||||
replies: z.array(z.object({
|
||||
id: z.string(),
|
||||
text: z.string(),
|
||||
createTime: z.number(),
|
||||
likeCount: z.number(),
|
||||
authorId: z.string(),
|
||||
authorName: z.string(),
|
||||
authorAvatar: z.string().url().optional(),
|
||||
})).optional(),
|
||||
})).optional(),
|
||||
});
|
||||
//# sourceMappingURL=types.js.map
|
||||
1
packages/media/ref/yt-dlp/dist/types.js.map
vendored
1
packages/media/ref/yt-dlp/dist/types.js.map
vendored
File diff suppressed because one or more lines are too long
63
packages/media/ref/yt-dlp/dist/ytdlp.d.ts
vendored
63
packages/media/ref/yt-dlp/dist/ytdlp.d.ts
vendored
@ -1,63 +0,0 @@
|
||||
import { VideoInfo, DownloadOptions, YtDlpOptions, FormatOptions, VideoInfoOptions, VideoFormat } from './types.js';
|
||||
/**
|
||||
* A wrapper class for the yt-dlp command line tool
|
||||
*/
|
||||
export declare class YtDlp {
|
||||
private options;
|
||||
private executable;
|
||||
/**
|
||||
* Create a new YtDlp instance
|
||||
* @param options Configuration options for yt-dlp
|
||||
*/
|
||||
constructor(options?: YtDlpOptions);
|
||||
/**
|
||||
* Check if yt-dlp is installed and accessible
|
||||
* @returns Promise resolving to true if yt-dlp is installed, false otherwise
|
||||
*/
|
||||
isInstalled(): Promise<boolean>;
|
||||
/**
|
||||
* Download a video from a given URL
|
||||
* @param url The URL of the video to download
|
||||
* @param options Download options
|
||||
* @returns Promise resolving to the path of the downloaded file
|
||||
*/
|
||||
downloadVideo(url: string, options?: DownloadOptions): Promise<string>;
|
||||
/**
|
||||
* Search for possible filenames in yt-dlp output
|
||||
* @param stdout The stdout output from yt-dlp
|
||||
* @param outputDir The output directory
|
||||
* @returns Array of possible filenames found
|
||||
*/
|
||||
private searchPossibleFilenames;
|
||||
/**
|
||||
* Get information about a video without downloading it
|
||||
* @param url The URL of the video to get information for
|
||||
* @returns Promise resolving to video information
|
||||
*/
|
||||
/**
|
||||
* Escapes a string for shell use based on the current platform
|
||||
* @param str The string to escape
|
||||
* @returns The escaped string
|
||||
*/
|
||||
private escapeShellArg;
|
||||
getVideoInfo(url: string, options?: VideoInfoOptions): Promise<VideoInfo>;
|
||||
/**
|
||||
* List available formats for a video
|
||||
* @param url The URL of the video to get formats for
|
||||
* @returns Promise resolving to an array of VideoFormat objects
|
||||
*/
|
||||
listFormats(url: string, options?: FormatOptions): Promise<VideoFormat[]>;
|
||||
/**
|
||||
* Parse the format list output from yt-dlp into an array of VideoFormat objects
|
||||
* @param output The raw output from yt-dlp format listing
|
||||
* @returns Array of VideoFormat objects
|
||||
*/
|
||||
private parseFormatOutput;
|
||||
/**
|
||||
* Set the path to the yt-dlp executable
|
||||
* @param path Path to the yt-dlp executable
|
||||
*/
|
||||
setExecutablePath(path: string): void;
|
||||
}
|
||||
export default YtDlp;
|
||||
//# sourceMappingURL=ytdlp.d.ts.map
|
||||
@ -1 +0,0 @@
|
||||
{"version":3,"file":"ytdlp.d.ts","sourceRoot":"","sources":["../src/ytdlp.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,WAAW,EAAqB,MAAM,YAAY,CAAC;AAKvI;;GAEG;AACH,qBAAa,KAAK;IAOJ,OAAO,CAAC,OAAO;IAN3B,OAAO,CAAC,UAAU,CAAoB;IAEtC;;;OAGG;gBACiB,OAAO,GAAE,YAAiB;IAQ9C;;;OAGG;IACG,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAWrC;;;;;OAKG;IACG,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,MAAM,CAAC;IAsShF;;;;;OAKG;IACH,OAAO,CAAC,uBAAuB;IA+B/B;;;;OAIG;IACH;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAYhB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,gBAA2D,GAAG,OAAO,CAAC,SAAS,CAAC;IAyCzH;;;;OAIG;IACG,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,aAA8B,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAwB/F;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IA+IzB;;;OAGG;IACH,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;CAOtC;AAED,eAAe,KAAK,CAAC"}
|
||||
577
packages/media/ref/yt-dlp/dist/ytdlp.js
vendored
577
packages/media/ref/yt-dlp/dist/ytdlp.js
vendored
@ -1,577 +0,0 @@
|
||||
import { exec, spawn } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { logger } from './logger.js';
|
||||
const execAsync = promisify(exec);
|
||||
/**
|
||||
* A wrapper class for the yt-dlp command line tool
|
||||
*/
|
||||
export class YtDlp {
|
||||
/**
|
||||
* Create a new YtDlp instance
|
||||
* @param options Configuration options for yt-dlp
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.options = options;
|
||||
this.executable = 'yt-dlp';
|
||||
if (options.executablePath) {
|
||||
this.executable = options.executablePath;
|
||||
}
|
||||
logger.debug('YtDlp initialized with options:', options);
|
||||
}
|
||||
/**
|
||||
* Check if yt-dlp is installed and accessible
|
||||
* @returns Promise resolving to true if yt-dlp is installed, false otherwise
|
||||
*/
|
||||
async isInstalled() {
|
||||
try {
|
||||
const { stdout } = await execAsync(`${this.executable} --version`);
|
||||
logger.debug(`yt-dlp version: ${stdout.trim()}`);
|
||||
return true;
|
||||
}
|
||||
catch (error) {
|
||||
logger.warn('yt-dlp is not installed or not found in PATH');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Download a video from a given URL
|
||||
* @param url The URL of the video to download
|
||||
* @param options Download options
|
||||
* @returns Promise resolving to the path of the downloaded file
|
||||
*/
|
||||
async downloadVideo(url, options = {}) {
|
||||
if (!url) {
|
||||
logger.error('Download failed: No URL provided');
|
||||
throw new Error('URL is required');
|
||||
}
|
||||
logger.info(`Downloading video from: ${url}`);
|
||||
logger.debug(`Download options: ${JSON.stringify({
|
||||
...options,
|
||||
// Redact any sensitive information
|
||||
userAgent: this.options.userAgent ? '[REDACTED]' : undefined
|
||||
}, null, 2)}`);
|
||||
// Prepare output directory
|
||||
const outputDir = options.outputDir || '.';
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
logger.debug(`Creating output directory: ${outputDir}`);
|
||||
try {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
logger.debug(`Output directory created successfully`);
|
||||
}
|
||||
catch (error) {
|
||||
logger.error(`Failed to create output directory: ${error.message}`);
|
||||
throw new Error(`Failed to create output directory ${outputDir}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.debug(`Output directory already exists: ${outputDir}`);
|
||||
}
|
||||
// Build command arguments
|
||||
const args = [];
|
||||
// Add user agent if specified in global options
|
||||
if (this.options.userAgent) {
|
||||
args.push('--user-agent', this.options.userAgent);
|
||||
}
|
||||
// Format selection
|
||||
if (options.format) {
|
||||
args.push('-f', options.format);
|
||||
}
|
||||
// Output template
|
||||
const outputTemplate = options.outputTemplate || '%(title)s.%(ext)s';
|
||||
args.push('-o', path.join(outputDir, outputTemplate));
|
||||
// Add other options
|
||||
if (options.audioOnly) {
|
||||
logger.debug(`Audio-only mode enabled, extracting audio with format: ${options.audioFormat || 'default'}`);
|
||||
logger.info(`Extracting audio in ${options.audioFormat || 'best available'} format`);
|
||||
// Add extract audio flag
|
||||
args.push('-x');
|
||||
// Handle audio format
|
||||
if (options.audioFormat) {
|
||||
logger.debug(`Setting audio format to: ${options.audioFormat}`);
|
||||
args.push('--audio-format', options.audioFormat);
|
||||
// For MP3 format, ensure we're using the right audio quality
|
||||
if (options.audioFormat.toLowerCase() === 'mp3') {
|
||||
logger.debug('MP3 format requested, setting audio quality');
|
||||
args.push('--audio-quality', '0'); // 0 is best quality
|
||||
// Ensure ffmpeg installed message for MP3 conversion
|
||||
logger.debug('MP3 conversion requires ffmpeg to be installed');
|
||||
logger.info('Note: MP3 conversion requires ffmpeg to be installed on your system');
|
||||
}
|
||||
}
|
||||
logger.debug(`Audio extraction command arguments: ${args.join(' ')}`);
|
||||
}
|
||||
if (options.subtitles) {
|
||||
args.push('--write-subs');
|
||||
if (Array.isArray(options.subtitles)) {
|
||||
args.push('--sub-lang', options.subtitles.join(','));
|
||||
}
|
||||
}
|
||||
if (options.maxFileSize) {
|
||||
args.push('--max-filesize', options.maxFileSize.toString());
|
||||
}
|
||||
if (options.rateLimit) {
|
||||
args.push('--limit-rate', options.rateLimit);
|
||||
}
|
||||
// Add custom arguments if provided
|
||||
if (options.additionalArgs) {
|
||||
args.push(...options.additionalArgs);
|
||||
}
|
||||
// Add the URL
|
||||
args.push(url);
|
||||
// Create a copy of args with sensitive information redacted for logging
|
||||
const logSafeArgs = [...args].map(arg => {
|
||||
if (arg === this.options.userAgent && this.options.userAgent) {
|
||||
return '[REDACTED]';
|
||||
}
|
||||
return arg;
|
||||
});
|
||||
// Log the command for debugging
|
||||
logger.debug(`Executing download command: ${this.executable} ${logSafeArgs.join(' ')}`);
|
||||
logger.debug(`Download configuration: audioOnly=${!!options.audioOnly}, format=${options.format || 'default'}, audioFormat=${options.audioFormat || 'n/a'}`);
|
||||
// Add additional debugging for audio extraction
|
||||
if (options.audioOnly) {
|
||||
logger.debug(`Audio extraction details: format=${options.audioFormat || 'auto'}, mp3=${options.audioFormat?.toLowerCase() === 'mp3'}`);
|
||||
logger.info(`Audio extraction mode: ${options.audioFormat || 'best quality'}`);
|
||||
}
|
||||
// Print to console for user feedback
|
||||
if (options.audioOnly) {
|
||||
logger.info(`Downloading audio in ${options.audioFormat || 'best'} format from: ${url}`);
|
||||
}
|
||||
else {
|
||||
logger.info(`Downloading video from: ${url}`);
|
||||
}
|
||||
logger.debug('Executing download command');
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.debug('Spawning yt-dlp process');
|
||||
try {
|
||||
const ytdlpProcess = spawn(this.executable, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let downloadedFile = null;
|
||||
let progressData = {
|
||||
percent: 0,
|
||||
totalSize: '0',
|
||||
downloadedSize: '0',
|
||||
speed: '0',
|
||||
eta: '0'
|
||||
};
|
||||
ytdlpProcess.stdout.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
stdout += output;
|
||||
logger.debug(`yt-dlp output: ${output.trim()}`);
|
||||
// Try to extract the output filename
|
||||
const destinationMatch = output.match(/Destination: (.+)/);
|
||||
if (destinationMatch && destinationMatch[1]) {
|
||||
downloadedFile = destinationMatch[1].trim();
|
||||
logger.debug(`Detected output filename: ${downloadedFile}`);
|
||||
}
|
||||
// Alternative method to extract the output filename
|
||||
const alreadyDownloadedMatch = output.match(/\[download\] (.+) has already been downloaded/);
|
||||
if (alreadyDownloadedMatch && alreadyDownloadedMatch[1]) {
|
||||
downloadedFile = alreadyDownloadedMatch[1].trim();
|
||||
logger.debug(`Video was already downloaded: ${downloadedFile}`);
|
||||
}
|
||||
// Track download progress
|
||||
const progressMatch = output.match(/\[download\]\s+(\d+\.?\d*)%\s+of\s+~?(\S+)\s+at\s+(\S+)\s+ETA\s+(\S+)/);
|
||||
if (progressMatch) {
|
||||
progressData = {
|
||||
percent: parseFloat(progressMatch[1]),
|
||||
totalSize: progressMatch[2],
|
||||
downloadedSize: (parseFloat(progressMatch[1]) * parseFloat(progressMatch[2]) / 100).toFixed(2) + 'MB',
|
||||
speed: progressMatch[3],
|
||||
eta: progressMatch[4]
|
||||
};
|
||||
// Log progress more frequently for better user feedback
|
||||
if (Math.floor(progressData.percent) % 5 === 0) {
|
||||
logger.info(`Download progress: ${progressData.percent.toFixed(1)}% (${progressData.downloadedSize}/${progressData.totalSize}) at ${progressData.speed} (ETA: ${progressData.eta})`);
|
||||
}
|
||||
}
|
||||
// Capture the file after extraction (important for audio conversions)
|
||||
const extractingMatch = output.match(/\[ExtractAudio\] Destination: (.+)/);
|
||||
if (extractingMatch && extractingMatch[1]) {
|
||||
downloadedFile = extractingMatch[1].trim();
|
||||
logger.debug(`Audio extraction destination: ${downloadedFile}`);
|
||||
// Log audio conversion information
|
||||
if (options.audioOnly) {
|
||||
logger.info(`Converting to ${options.audioFormat || 'best format'}: ${downloadedFile}`);
|
||||
}
|
||||
}
|
||||
// Also capture ffmpeg conversion progress for audio
|
||||
const ffmpegMatch = output.match(/\[ffmpeg\] Destination: (.+)/);
|
||||
if (ffmpegMatch && ffmpegMatch[1]) {
|
||||
downloadedFile = ffmpegMatch[1].trim();
|
||||
logger.debug(`ffmpeg conversion destination: ${downloadedFile}`);
|
||||
if (options.audioOnly && options.audioFormat) {
|
||||
logger.info(`Converting to ${options.audioFormat}: ${downloadedFile}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
ytdlpProcess.stderr.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
stderr += output;
|
||||
logger.error(`yt-dlp error: ${output.trim()}`);
|
||||
// Try to detect common error types for better error reporting
|
||||
if (output.includes('No such file or directory')) {
|
||||
logger.error('yt-dlp executable not found. Please make sure it is installed and in your PATH.');
|
||||
}
|
||||
else if (output.includes('HTTP Error 403: Forbidden')) {
|
||||
logger.error('Access forbidden - the server denied access. This might be due to rate limiting or geo-restrictions.');
|
||||
}
|
||||
else if (output.includes('HTTP Error 404: Not Found')) {
|
||||
logger.error('The requested video was not found. It might have been deleted or made private.');
|
||||
}
|
||||
else if (output.includes('Unsupported URL')) {
|
||||
logger.error('The URL is not supported by yt-dlp. Check if the website is supported or if you need a newer version of yt-dlp.');
|
||||
}
|
||||
});
|
||||
ytdlpProcess.on('close', (code) => {
|
||||
logger.debug(`yt-dlp process exited with code ${code}`);
|
||||
if (code === 0) {
|
||||
if (downloadedFile) {
|
||||
// Verify the file actually exists
|
||||
try {
|
||||
const stats = fs.statSync(downloadedFile);
|
||||
logger.info(`Successfully downloaded: ${downloadedFile} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
|
||||
resolve(downloadedFile);
|
||||
}
|
||||
catch (error) {
|
||||
logger.warn(`File reported as downloaded but not found on disk: ${downloadedFile}`);
|
||||
logger.debug(`File access error: ${error.message}`);
|
||||
// Try to find an alternative filename
|
||||
const possibleFiles = this.searchPossibleFilenames(stdout, outputDir);
|
||||
if (possibleFiles.length > 0) {
|
||||
logger.info(`Found alternative downloaded file: ${possibleFiles[0]}`);
|
||||
resolve(possibleFiles[0]);
|
||||
}
|
||||
else {
|
||||
logger.info('Download reported as successful, but could not locate the output file');
|
||||
resolve('Download completed');
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Try to find the downloaded file from stdout if it wasn't captured
|
||||
logger.debug('No downloadedFile captured, searching stdout for filename');
|
||||
const possibleFiles = this.searchPossibleFilenames(stdout, outputDir);
|
||||
if (possibleFiles.length > 0) {
|
||||
logger.info(`Successfully downloaded: ${possibleFiles[0]}`);
|
||||
resolve(possibleFiles[0]);
|
||||
}
|
||||
else {
|
||||
logger.info('Download successful, but could not determine the output file name');
|
||||
logger.debug('Full stdout for debugging:');
|
||||
logger.debug(stdout);
|
||||
resolve('Download completed');
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.error(`Download failed with exit code ${code}`);
|
||||
// Provide more context based on the error code
|
||||
let errorContext = '';
|
||||
if (code === 1) {
|
||||
errorContext = 'General error - check URL and network connection';
|
||||
}
|
||||
else if (code === 2) {
|
||||
errorContext = 'Network error - check your internet connection';
|
||||
}
|
||||
else if (code === 3) {
|
||||
errorContext = 'File system error - check permissions and disk space';
|
||||
}
|
||||
reject(new Error(`yt-dlp exited with code ${code} (${errorContext}): ${stderr}`));
|
||||
}
|
||||
});
|
||||
ytdlpProcess.on('error', (err) => {
|
||||
logger.error(`Failed to start yt-dlp process: ${err.message}`);
|
||||
logger.debug(`Error details: ${JSON.stringify(err)}`);
|
||||
// Check for common spawn errors and provide helpful messages
|
||||
if (err.message.includes('ENOENT')) {
|
||||
reject(new Error(`yt-dlp executable not found. Make sure it's installed and available in your PATH. Error: ${err.message}`));
|
||||
}
|
||||
else if (err.message.includes('EACCES')) {
|
||||
reject(new Error(`Permission denied when executing yt-dlp. Check file permissions. Error: ${err.message}`));
|
||||
}
|
||||
else {
|
||||
reject(new Error(`Failed to start yt-dlp: ${err.message}`));
|
||||
}
|
||||
});
|
||||
// Handle unexpected termination
|
||||
process.on('SIGINT', () => {
|
||||
logger.warn('Process interrupted, terminating download');
|
||||
ytdlpProcess.kill('SIGINT');
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
logger.error(`Exception during process spawn: ${error.message}`);
|
||||
reject(new Error(`Failed to start download process: ${error.message}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Search for possible filenames in yt-dlp output
|
||||
* @param stdout The stdout output from yt-dlp
|
||||
* @param outputDir The output directory
|
||||
* @returns Array of possible filenames found
|
||||
*/
|
||||
searchPossibleFilenames(stdout, outputDir) {
|
||||
const possibleFiles = [];
|
||||
// Various regex patterns to find filenames in the yt-dlp output
|
||||
const patterns = [
|
||||
/\[download\] (.+?) has already been downloaded/,
|
||||
/\[download\] Destination: (.+)/,
|
||||
/\[ffmpeg\] Destination: (.+)/,
|
||||
/\[ExtractAudio\] Destination: (.+)/,
|
||||
/\[Merger\] Merging formats into "(.+)"/,
|
||||
/\[Merger\] Merged into (.+)/
|
||||
];
|
||||
// Try each pattern
|
||||
for (const pattern of patterns) {
|
||||
const matches = stdout.matchAll(new RegExp(pattern, 'g'));
|
||||
for (const match of matches) {
|
||||
if (match[1]) {
|
||||
const filePath = match[1].trim();
|
||||
// Check if it's a relative or absolute path
|
||||
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(outputDir, filePath);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
logger.debug(`Found output file: ${fullPath}`);
|
||||
possibleFiles.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return possibleFiles;
|
||||
}
|
||||
/**
|
||||
* Get information about a video without downloading it
|
||||
* @param url The URL of the video to get information for
|
||||
* @returns Promise resolving to video information
|
||||
*/
|
||||
/**
|
||||
* Escapes a string for shell use based on the current platform
|
||||
* @param str The string to escape
|
||||
* @returns The escaped string
|
||||
*/
|
||||
escapeShellArg(str) {
|
||||
if (os.platform() === 'win32') {
|
||||
// Windows: Double quotes need to be escaped with backslash
|
||||
// and the whole string wrapped in double quotes
|
||||
return `"${str.replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
else {
|
||||
// Unix-like: Single quotes provide the strongest escaping
|
||||
// Double any existing single quotes and wrap in single quotes
|
||||
return `'${str.replace(/'/g, "'\\''")}'`;
|
||||
}
|
||||
}
|
||||
async getVideoInfo(url, options = { dumpJson: false, flatPlaylist: false }) {
|
||||
if (!url) {
|
||||
throw new Error('URL is required');
|
||||
}
|
||||
logger.info(`Getting video info for: ${url}`);
|
||||
try {
|
||||
// Build command with options
|
||||
const args = ['--dump-json'];
|
||||
// Add user agent if specified in global options
|
||||
if (this.options.userAgent) {
|
||||
args.push('--user-agent', this.options.userAgent);
|
||||
}
|
||||
// Add VideoInfoOptions flags
|
||||
if (options.flatPlaylist) {
|
||||
args.push('--flat-playlist');
|
||||
}
|
||||
args.push(url);
|
||||
// Properly escape arguments for the exec call
|
||||
const escapedArgs = args.map(arg => {
|
||||
// Only escape arguments that need escaping (contains spaces or special characters)
|
||||
return /[\s"'$&()<>`|;]/.test(arg) ? this.escapeShellArg(arg) : arg;
|
||||
});
|
||||
const { stdout } = await execAsync(`${this.executable} ${escapedArgs.join(' ')}`);
|
||||
const videoInfo = JSON.parse(stdout);
|
||||
logger.debug('Video info retrieved successfully');
|
||||
return videoInfo;
|
||||
}
|
||||
catch (error) {
|
||||
logger.error('Failed to get video info:', error);
|
||||
throw new Error(`Failed to get video info: ${error.message}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* List available formats for a video
|
||||
* @param url The URL of the video to get formats for
|
||||
* @returns Promise resolving to an array of VideoFormat objects
|
||||
*/
|
||||
async listFormats(url, options = { all: false }) {
|
||||
if (!url) {
|
||||
throw new Error('URL is required');
|
||||
}
|
||||
logger.info(`Listing formats for: ${url}`);
|
||||
try {
|
||||
// Build command with options
|
||||
const formatFlag = options.all ? '--list-formats-all' : '-F';
|
||||
// Properly escape URL if needed
|
||||
const escapedUrl = /[\s"'$&()<>`|;]/.test(url) ? this.escapeShellArg(url) : url;
|
||||
const { stdout } = await execAsync(`${this.executable} ${formatFlag} ${escapedUrl}`);
|
||||
logger.debug('Format list retrieved successfully');
|
||||
// Parse the output to extract format information
|
||||
return this.parseFormatOutput(stdout);
|
||||
}
|
||||
catch (error) {
|
||||
logger.error('Failed to list formats:', error);
|
||||
throw new Error(`Failed to list formats: ${error.message}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Parse the format list output from yt-dlp into an array of VideoFormat objects
|
||||
* @param output The raw output from yt-dlp format listing
|
||||
* @returns Array of VideoFormat objects
|
||||
*/
|
||||
parseFormatOutput(output) {
|
||||
const formats = [];
|
||||
const lines = output.split('\n');
|
||||
// Find the line with table headers to determine where the format list starts
|
||||
let formatListStartIndex = 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].includes('format code') || lines[i].includes('ID')) {
|
||||
formatListStartIndex = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Regular expressions to match various format components
|
||||
const formatIdRegex = /^(\S+)/;
|
||||
const extensionRegex = /(\w+)\s+/;
|
||||
const resolutionRegex = /(\d+x\d+|\d+p)/;
|
||||
const fpsRegex = /(\d+)fps/;
|
||||
const filesizeRegex = /(\d+(\.\d+)?)(K|M|G|T)iB/;
|
||||
const bitrateRegex = /(\d+(\.\d+)?)(k|m)bps/;
|
||||
const codecRegex = /(mp4|webm|m4a|mp3|opus|vorbis)\s+([\w.]+)/i;
|
||||
const formatNoteRegex = /(audio only|video only|tiny|small|medium|large|best)/i;
|
||||
// Process each line that contains format information
|
||||
for (let i = formatListStartIndex; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line || line.includes('----'))
|
||||
continue; // Skip empty lines or separators
|
||||
// Extract format ID - typically the first part of the line
|
||||
const formatIdMatch = line.match(formatIdRegex);
|
||||
if (!formatIdMatch)
|
||||
continue;
|
||||
const formatId = formatIdMatch[1];
|
||||
// Create a base format object
|
||||
const format = {
|
||||
format_id: formatId,
|
||||
format: line, // Use the full line as the format description
|
||||
ext: 'unknown',
|
||||
protocol: 'https',
|
||||
vcodec: 'unknown',
|
||||
acodec: 'unknown'
|
||||
};
|
||||
// Try to extract format components
|
||||
// Extract extension
|
||||
const extMatch = line.substring(formatId.length).match(extensionRegex);
|
||||
if (extMatch) {
|
||||
format.ext = extMatch[1];
|
||||
}
|
||||
// Extract resolution
|
||||
const resMatch = line.match(resolutionRegex);
|
||||
if (resMatch) {
|
||||
format.resolution = resMatch[1];
|
||||
// If resolution is in the form of "1280x720", extract width and height
|
||||
const dimensions = format.resolution.split('x');
|
||||
if (dimensions.length === 2) {
|
||||
format.width = parseInt(dimensions[0], 10);
|
||||
format.height = parseInt(dimensions[1], 10);
|
||||
}
|
||||
else if (format.resolution.endsWith('p')) {
|
||||
// If resolution is like "720p", extract height
|
||||
format.height = parseInt(format.resolution.replace('p', ''), 10);
|
||||
}
|
||||
}
|
||||
// Extract FPS
|
||||
const fpsMatch = line.match(fpsRegex);
|
||||
if (fpsMatch) {
|
||||
format.fps = parseInt(fpsMatch[1], 10);
|
||||
}
|
||||
// Extract filesize
|
||||
const sizeMatch = line.match(filesizeRegex);
|
||||
if (sizeMatch) {
|
||||
let size = parseFloat(sizeMatch[1]);
|
||||
const unit = sizeMatch[3];
|
||||
// Convert to bytes
|
||||
if (unit === 'K')
|
||||
size *= 1024;
|
||||
else if (unit === 'M')
|
||||
size *= 1024 * 1024;
|
||||
else if (unit === 'G')
|
||||
size *= 1024 * 1024 * 1024;
|
||||
else if (unit === 'T')
|
||||
size *= 1024 * 1024 * 1024 * 1024;
|
||||
format.filesize = Math.round(size);
|
||||
}
|
||||
// Extract bitrate
|
||||
const bitrateMatch = line.match(bitrateRegex);
|
||||
if (bitrateMatch) {
|
||||
let bitrate = parseFloat(bitrateMatch[1]);
|
||||
const unit = bitrateMatch[3];
|
||||
// Convert to Kbps
|
||||
if (unit === 'm')
|
||||
bitrate *= 1000;
|
||||
format.tbr = bitrate;
|
||||
}
|
||||
// Extract format note
|
||||
const noteMatch = line.match(formatNoteRegex);
|
||||
if (noteMatch) {
|
||||
format.format_note = noteMatch[1];
|
||||
}
|
||||
// Determine audio/video codec
|
||||
if (line.includes('audio only')) {
|
||||
format.vcodec = 'none';
|
||||
// Try to get audio codec
|
||||
const codecMatch = line.match(codecRegex);
|
||||
if (codecMatch) {
|
||||
format.acodec = codecMatch[2] || format.acodec;
|
||||
}
|
||||
}
|
||||
else if (line.includes('video only')) {
|
||||
format.acodec = 'none';
|
||||
// Try to get video codec
|
||||
const codecMatch = line.match(codecRegex);
|
||||
if (codecMatch) {
|
||||
format.vcodec = codecMatch[2] || format.vcodec;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Both audio and video
|
||||
const codecMatch = line.match(codecRegex);
|
||||
if (codecMatch) {
|
||||
format.container = codecMatch[1];
|
||||
if (codecMatch[2]) {
|
||||
if (line.includes('video')) {
|
||||
format.vcodec = codecMatch[2];
|
||||
}
|
||||
else if (line.includes('audio')) {
|
||||
format.acodec = codecMatch[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add the format to our result array
|
||||
formats.push(format);
|
||||
}
|
||||
return formats;
|
||||
}
|
||||
/**
|
||||
* Set the path to the yt-dlp executable
|
||||
* @param path Path to the yt-dlp executable
|
||||
*/
|
||||
setExecutablePath(path) {
|
||||
if (!path) {
|
||||
throw new Error('Executable path cannot be empty');
|
||||
}
|
||||
this.executable = path;
|
||||
logger.debug(`yt-dlp executable path set to: ${path}`);
|
||||
}
|
||||
}
|
||||
export default YtDlp;
|
||||
//# sourceMappingURL=ytdlp.js.map
|
||||
1
packages/media/ref/yt-dlp/dist/ytdlp.js.map
vendored
1
packages/media/ref/yt-dlp/dist/ytdlp.js.map
vendored
File diff suppressed because one or more lines are too long
4984
packages/media/ref/yt-dlp/package-lock.json
generated
4984
packages/media/ref/yt-dlp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,58 +0,0 @@
|
||||
{
|
||||
"name": "yt-dlp-wrapper",
|
||||
"version": "1.0.0",
|
||||
"description": "TypeScript wrapper for yt-dlp with CLI interface",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"bin": {
|
||||
"yt-dlp-wrapper": "dist/cli.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build:webpack": "webpack --mode production",
|
||||
"watch": "tsc --watch",
|
||||
"start": "node dist/cli.js",
|
||||
"start:webpack": "node dist/cli-bundle.js",
|
||||
"dev": "ts-node --esm src/cli.ts",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:ui": "vitest --ui"
|
||||
},
|
||||
"keywords": [
|
||||
"yt-dlp",
|
||||
"video",
|
||||
"downloader",
|
||||
"cli"
|
||||
],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"puppeteer": "^24.4.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslog": "^4.9.3",
|
||||
"typescript": "^5.8.2",
|
||||
"yargs": "^17.7.2",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/coverage-v8": "^3.0.8",
|
||||
"@vitest/ui": "^3.0.8",
|
||||
"ts-loader": "^9.5.2",
|
||||
"vitest": "^3.0.8",
|
||||
"webpack": "^5.98.0",
|
||||
"webpack-cli": "^6.0.1"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,87 +0,0 @@
|
||||
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
|
||||
import { YtDlp } from '../ytdlp.js';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
describe('MP3 Extraction Tests', () => {
|
||||
const testOutputDir = path.join(process.cwd(), 'test-downloads/mp3-test');
|
||||
const ytdlp = new YtDlp({
|
||||
output: '%(title)s.%(ext)s'
|
||||
});
|
||||
// Short YouTube video by YouTube co-founder
|
||||
const videoUrl = 'https://www.youtube.com/watch?v=jNQXAC9IVRw';
|
||||
let downloadedFiles: string[] = [];
|
||||
|
||||
beforeAll(() => {
|
||||
// Create the output directory if it doesn't exist
|
||||
if (!fs.existsSync(testOutputDir)) {
|
||||
fs.mkdirSync(testOutputDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Clean up downloaded files after tests
|
||||
downloadedFiles.forEach(file => {
|
||||
if (fs.existsSync(file)) {
|
||||
fs.unlinkSync(file);
|
||||
console.log(`Cleaned up test file: ${file}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should download video and extract audio as MP3', async () => {
|
||||
// Download the video with MP3 extraction
|
||||
const downloadOptions = {
|
||||
outputDir: testOutputDir,
|
||||
audioOnly: true,
|
||||
audioFormat: 'mp3',
|
||||
audioQuality: '0', // best quality
|
||||
verbose: true
|
||||
};
|
||||
|
||||
const filePath = await ytdlp.downloadVideo(videoUrl, downloadOptions);
|
||||
downloadedFiles.push(filePath);
|
||||
|
||||
console.log(`Downloaded MP3 file: ${filePath}`);
|
||||
|
||||
// Verify the file exists
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
|
||||
// Verify it has an MP3 extension
|
||||
expect(path.extname(filePath)).toBe('.mp3');
|
||||
|
||||
// Verify the file has content (not empty)
|
||||
const stats = fs.statSync(filePath);
|
||||
expect(stats.size).toBeGreaterThan(0);
|
||||
console.log(`MP3 file size: ${stats.size} bytes`);
|
||||
|
||||
// Log file info for debugging
|
||||
console.log(`File details:
|
||||
- Path: ${filePath}
|
||||
- Size: ${stats.size} bytes
|
||||
- Created: ${stats.birthtime}
|
||||
- Modified: ${stats.mtime}
|
||||
`);
|
||||
}, 60000); // Increase timeout to 60 seconds for download to complete
|
||||
|
||||
it('should have proper MP3 metadata', async () => {
|
||||
// Get the most recently downloaded file (from previous test)
|
||||
const mp3File = downloadedFiles[0];
|
||||
expect(mp3File).toBeDefined();
|
||||
|
||||
// Ensure the file still exists
|
||||
expect(fs.existsSync(mp3File)).toBe(true);
|
||||
|
||||
// Check basic file properties to verify it's a valid audio file
|
||||
const stats = fs.statSync(mp3File);
|
||||
|
||||
// MP3 files should have some minimum size (a few KB at least)
|
||||
expect(stats.size).toBeGreaterThan(10000); // At least 10KB
|
||||
|
||||
// We could do more detailed checks with a media info library
|
||||
// but that would require additional dependencies
|
||||
|
||||
console.log(`Verified MP3 file: ${mp3File} (${stats.size} bytes)`);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,100 +0,0 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
||||
import { existsSync, statSync } from 'node:fs';
|
||||
import { unlink } from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import { YtDlp } from '../ytdlp.js';
|
||||
|
||||
describe('TikTok Download Tests', () => {
|
||||
// TikTok URL to test
|
||||
const tiktokUrl = 'https://www.tiktok.com/@woman.power.quote/video/7476910372121971970';
|
||||
|
||||
// Temporary output directory for test downloads
|
||||
const outputDir = path.join(process.cwd(), 'test-downloads');
|
||||
|
||||
// Instance of YtDlp
|
||||
let ytdlp: YtDlp;
|
||||
|
||||
// Path to the downloaded file (will be set during test)
|
||||
let downloadedFilePath: string;
|
||||
|
||||
beforeAll(() => {
|
||||
// Initialize YtDlp instance with test options
|
||||
ytdlp = new YtDlp({
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
});
|
||||
|
||||
// Set up spy for console.log to track progress messages
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up downloaded files if they exist
|
||||
if (downloadedFilePath && existsSync(downloadedFilePath)) {
|
||||
try {
|
||||
//await unlink(downloadedFilePath);
|
||||
console.log(`Test cleanup: Deleted ${downloadedFilePath}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete test file: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore console.log
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should download a TikTok video successfully', async () => {
|
||||
// Define download options
|
||||
const options = {
|
||||
format: 'best',
|
||||
outputDir,
|
||||
// Add a timestamp to ensure unique filenames across test runs
|
||||
outputTemplate: `tiktok-test-${Date.now()}.%(ext)s`,
|
||||
};
|
||||
|
||||
// Download the video
|
||||
downloadedFilePath = await ytdlp.downloadVideo(tiktokUrl, options);
|
||||
|
||||
// Verify the download was successful
|
||||
expect(downloadedFilePath).toBeTruthy();
|
||||
expect(existsSync(downloadedFilePath)).toBe(true);
|
||||
|
||||
// Check file has some content (not empty)
|
||||
const stats = statSync(downloadedFilePath).size;
|
||||
expect(stats).toBeGreaterThan(0);
|
||||
|
||||
console.log(`Downloaded TikTok video to: ${downloadedFilePath}`);
|
||||
}, 60000); // Increase timeout for download to complete
|
||||
|
||||
it('should get video info from TikTok URL', async () => {
|
||||
// Get video info
|
||||
const videoInfo = await ytdlp.getVideoInfo(tiktokUrl);
|
||||
|
||||
// Verify basic video information
|
||||
expect(videoInfo).toBeTruthy();
|
||||
expect(videoInfo.id).toBeTruthy();
|
||||
expect(videoInfo.title).toBeTruthy();
|
||||
expect(videoInfo.uploader).toBeTruthy();
|
||||
|
||||
console.log(`TikTok Video Title: ${videoInfo.title}`);
|
||||
console.log(`TikTok Video Uploader: ${videoInfo.uploader}`);
|
||||
}, 30000); // Increase timeout for API response
|
||||
|
||||
it('should list available formats for TikTok video', async () => {
|
||||
// List available formats
|
||||
const formats = await ytdlp.listFormats(tiktokUrl);
|
||||
|
||||
// Verify formats are returned
|
||||
expect(formats).toBeInstanceOf(Array);
|
||||
expect(formats.length).toBeGreaterThan(0);
|
||||
|
||||
// At least one format should have a format_id
|
||||
expect(formats[0].format_id).toBeTruthy();
|
||||
|
||||
// Log some useful information for debugging
|
||||
console.log(`Found ${formats.length} formats for TikTok video`);
|
||||
if (formats.length > 0) {
|
||||
console.log('First format:', JSON.stringify(formats[0], null, 2));
|
||||
}
|
||||
}, 30000); // Increase timeout for format listing
|
||||
});
|
||||
|
||||
@ -1,79 +0,0 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { YtDlp } from '../ytdlp.js';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
// Create a test directory for downloads
|
||||
const TEST_DIR = path.join(process.cwd(), 'test-downloads');
|
||||
const YOUTUBE_URL = 'https://www.youtube.com/watch?v=_oVI0GW-Xd4';
|
||||
|
||||
describe('YouTube Video Download', () => {
|
||||
// Ensure the test directory exists
|
||||
if (!fs.existsSync(TEST_DIR)) {
|
||||
fs.mkdirSync(TEST_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
let downloadedFiles: string[] = [];
|
||||
|
||||
// Clean up after tests
|
||||
afterEach(() => {
|
||||
// Delete any downloaded files
|
||||
downloadedFiles.forEach(file => {
|
||||
const fullPath = path.resolve(file);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
//fs.unlinkSync(fullPath);
|
||||
console.log(`Cleaned up test file: ${fullPath}`);
|
||||
}
|
||||
});
|
||||
downloadedFiles = [];
|
||||
});
|
||||
|
||||
it('should successfully download a YouTube video', async () => {
|
||||
// Create a new YtDlp instance
|
||||
const ytdlp = new YtDlp();
|
||||
|
||||
// Check if yt-dlp is installed
|
||||
const isInstalled = await ytdlp.isInstalled();
|
||||
expect(isInstalled).toBe(true);
|
||||
|
||||
// Download the video with specific options to keep the test fast
|
||||
// Use a lower quality format to speed up the test
|
||||
const downloadOptions = {
|
||||
outputDir: TEST_DIR,
|
||||
format: 'worst[ext=mp4]', // Use lowest quality for faster test
|
||||
outputTemplate: 'youtube-test-%(id)s.%(ext)s'
|
||||
};
|
||||
|
||||
// Execute the download
|
||||
const filePath = await ytdlp.downloadVideo(YOUTUBE_URL, downloadOptions);
|
||||
console.log(`Downloaded file: ${filePath}`);
|
||||
|
||||
// Add to cleanup list
|
||||
downloadedFiles.push(filePath);
|
||||
|
||||
// Assert that the file exists
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
|
||||
// Assert that the file has content (not empty)
|
||||
const stats = fs.statSync(filePath);
|
||||
expect(stats.size).toBeGreaterThan(0);
|
||||
}, 60000); // Increase timeout to 60 seconds as downloads may take time
|
||||
|
||||
it('should retrieve video information correctly', async () => {
|
||||
const ytdlp = new YtDlp();
|
||||
|
||||
// Get video info
|
||||
const videoInfo = await ytdlp.getVideoInfo(YOUTUBE_URL);
|
||||
|
||||
// Assert video properties
|
||||
expect(videoInfo).toBeDefined();
|
||||
expect(videoInfo.id).toBeDefined();
|
||||
expect(videoInfo.title).toBeDefined();
|
||||
expect(videoInfo.webpage_url).toBeDefined();
|
||||
|
||||
// Verify the video ID matches the expected ID from the URL
|
||||
const expectedVideoId = '_oVI0GW-Xd4';
|
||||
expect(videoInfo.id).toBe(expectedVideoId);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,285 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import { YtDlp } from './ytdlp.js';
|
||||
import { logger } from './logger.js';
|
||||
import { FormatOptionsSchema, VideoInfoOptionsSchema, DownloadOptionsSchema, TikTokMetadataOptionsSchema } from './types.js';
|
||||
import { z } from 'zod';
|
||||
import { exec } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const ytdlp = new YtDlp();
|
||||
|
||||
// Function to generate user-friendly help message
|
||||
function printHelp() {
|
||||
console.log("\n======= yt-dlp TypeScript Wrapper =======");
|
||||
console.log("A TypeScript wrapper for the yt-dlp video downloader\n");
|
||||
|
||||
console.log("USAGE:");
|
||||
console.log(" ytdlp-ts <command> [options] <url>\n");
|
||||
|
||||
console.log("COMMANDS:");
|
||||
console.log(" download [url] Download a video from the specified URL");
|
||||
console.log(" info [url] Get information about a video");
|
||||
console.log(" formats [url] List available formats for a video");
|
||||
console.log(" tiktok:meta [url] Scrape metadata from a TikTok video and save as JSON\n");
|
||||
|
||||
console.log("DOWNLOAD OPTIONS:");
|
||||
console.log(" --format, -f Specify video format code");
|
||||
console.log(" --output, -o Specify output filename template");
|
||||
console.log(" --quiet, -q Activate quiet mode");
|
||||
console.log(" --verbose, -v Print various debugging information");
|
||||
console.log(" --mp3 Download only the audio in MP3 format\n");
|
||||
|
||||
console.log("INFO OPTIONS:");
|
||||
console.log(" --dump-json Output JSON information");
|
||||
console.log(" --flat-playlist Flat playlist output\n");
|
||||
|
||||
console.log("FORMATS OPTIONS:");
|
||||
console.log(" --all Show all available formats\n");
|
||||
|
||||
console.log("EXAMPLES:");
|
||||
console.log(" ytdlp-ts download https://www.tiktok.com/@woman.power.quote/video/7476910372121970");
|
||||
console.log(" ytdlp-ts download https://www.youtube.com/watch?v=_oVI0GW-Xd4 -f \"bestvideo[height<=1080]+bestaudio/best[height<=1080]\"");
|
||||
console.log(" ytdlp-ts download https://www.youtube.com/watch?v=_oVI0GW-Xd4 --mp3");
|
||||
console.log(" ytdlp-ts info https://www.tiktok.com/@woman.power.quote/video/7476910372121970 --dump-json");
|
||||
console.log(" ytdlp-ts formats https://www.youtube.com/watch?v=_oVI0GW-Xd4 --all\n");
|
||||
|
||||
console.log("For more information, visit https://github.com/yt-dlp/yt-dlp");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if ffmpeg is installed on the system
|
||||
* @returns {Promise<boolean>} True if ffmpeg is installed, false otherwise
|
||||
*/
|
||||
async function isFFmpegInstalled(): Promise<boolean> {
|
||||
try {
|
||||
// Try to execute ffmpeg -version command
|
||||
await execAsync('ffmpeg -version');
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for help flags directly in process.argv
|
||||
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Create a simple yargs CLI with clear command structure
|
||||
yargs(hideBin(process.argv))
|
||||
.scriptName('yt-dlp-wrapper')
|
||||
.usage('$0 <cmd> [args]')
|
||||
.command(
|
||||
'download [url]',
|
||||
'Download a video',
|
||||
(yargs) => {
|
||||
return yargs
|
||||
.positional('url', {
|
||||
type: 'string',
|
||||
describe: 'URL of the video to download',
|
||||
demandOption: true,
|
||||
})
|
||||
.option('format', {
|
||||
type: 'string',
|
||||
describe: 'Video format code',
|
||||
alias: 'f',
|
||||
})
|
||||
.option('output', {
|
||||
type: 'string',
|
||||
describe: 'Output filename template',
|
||||
alias: 'o',
|
||||
})
|
||||
.option('quiet', {
|
||||
type: 'boolean',
|
||||
describe: 'Activate quiet mode',
|
||||
alias: 'q',
|
||||
default: false,
|
||||
})
|
||||
.option('verbose', {
|
||||
type: 'boolean',
|
||||
describe: 'Print various debugging information',
|
||||
alias: 'v',
|
||||
default: false,
|
||||
})
|
||||
.option('mp3', {
|
||||
type: 'boolean',
|
||||
describe: 'Download only the audio in MP3 format',
|
||||
default: false,
|
||||
});
|
||||
},
|
||||
async (argv) => {
|
||||
try {
|
||||
logger.info(`Starting download process for: ${argv.url}`);
|
||||
logger.debug(`Download options: mp3=${argv.mp3}, format=${argv.format}, output=${argv.output}`);
|
||||
|
||||
// Check if ffmpeg is installed when MP3 option is specified
|
||||
if (argv.mp3) {
|
||||
logger.info('MP3 option detected. Checking for ffmpeg installation...');
|
||||
const ffmpegInstalled = await isFFmpegInstalled();
|
||||
|
||||
if (!ffmpegInstalled) {
|
||||
logger.error('\x1b[31mError: ffmpeg is not installed or not found in PATH\x1b[0m');
|
||||
logger.error('\nTo download videos as MP3, ffmpeg is required. Please install ffmpeg:');
|
||||
logger.error('\n • Windows: https://ffmpeg.org/download.html or install via Chocolatey/Scoop');
|
||||
logger.error(' • macOS: brew install ffmpeg');
|
||||
logger.error(' • Linux: apt install ffmpeg / yum install ffmpeg / etc. (depending on your distribution)');
|
||||
logger.error('\nAfter installing, make sure ffmpeg is in your PATH and try again.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
logger.info('ffmpeg is installed. Proceeding with MP3 download...');
|
||||
}
|
||||
|
||||
// Parse and validate options using Zod
|
||||
const options = DownloadOptionsSchema.parse({
|
||||
format: argv.format,
|
||||
output: argv.output,
|
||||
quiet: argv.quiet,
|
||||
verbose: argv.verbose,
|
||||
audioOnly: argv.mp3 ? true : undefined,
|
||||
audioFormat: argv.mp3 ? 'mp3' : undefined,
|
||||
});
|
||||
|
||||
logger.debug(`Parsed download options: ${JSON.stringify(options)}`);
|
||||
logger.info(`Starting download with options: ${JSON.stringify({
|
||||
url: argv.url,
|
||||
mp3: argv.mp3,
|
||||
format: argv.format,
|
||||
output: argv.output
|
||||
})}`);
|
||||
|
||||
const downloadedFile = await ytdlp.downloadVideo(argv.url as string, options);
|
||||
|
||||
logger.info(`Download completed successfully: ${downloadedFile}`);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.error('Invalid options:', error.errors);
|
||||
} else {
|
||||
logger.error('Failed to download video:', error);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
)
|
||||
.command(
|
||||
'info [url]',
|
||||
'Get video information',
|
||||
(yargs) => {
|
||||
return yargs
|
||||
.positional('url', {
|
||||
type: 'string',
|
||||
describe: 'URL of the video',
|
||||
demandOption: true,
|
||||
})
|
||||
.option('dump-json', {
|
||||
type: 'boolean',
|
||||
describe: 'Output JSON information',
|
||||
default: false,
|
||||
})
|
||||
.option('flat-playlist', {
|
||||
type: 'boolean',
|
||||
describe: 'Flat playlist output',
|
||||
default: false,
|
||||
});
|
||||
},
|
||||
async (argv) => {
|
||||
try {
|
||||
logger.info(`Starting info retrieval for: ${argv.url}`);
|
||||
|
||||
// Parse and validate options using Zod
|
||||
const options = VideoInfoOptionsSchema.parse({
|
||||
dumpJson: argv.dumpJson,
|
||||
flatPlaylist: argv.flatPlaylist,
|
||||
});
|
||||
|
||||
const info = await ytdlp.getVideoInfo(argv.url as string, options);
|
||||
|
||||
logger.info(`Info retrieval completed successfully`);
|
||||
console.log(JSON.stringify(info, null, 2));
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.error('Invalid options:', error.errors);
|
||||
} else {
|
||||
logger.error('Failed to get video info:', error);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
)
|
||||
.command(
|
||||
'formats [url]',
|
||||
'List available formats of a video',
|
||||
(yargs) => {
|
||||
return yargs
|
||||
.positional('url', {
|
||||
type: 'string',
|
||||
describe: 'URL of the video',
|
||||
demandOption: true,
|
||||
})
|
||||
.option('all', {
|
||||
type: 'boolean',
|
||||
describe: 'Show all available formats',
|
||||
default: false,
|
||||
});
|
||||
},
|
||||
async (argv) => {
|
||||
try {
|
||||
logger.info(`Getting available formats for video: ${argv.url}`);
|
||||
|
||||
// Parse and validate options using Zod
|
||||
const options = FormatOptionsSchema.parse({
|
||||
all: argv.all,
|
||||
});
|
||||
|
||||
const formats = await ytdlp.listFormats(argv.url as string, options);
|
||||
|
||||
logger.info(`Format listing completed successfully`);
|
||||
console.log(formats);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.error('Invalid options:', error.errors);
|
||||
} else {
|
||||
logger.error('Failed to list formats:', error);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
)
|
||||
.example(
|
||||
'$0 download https://www.tiktok.com/@woman.power.quote/video/7476910372121971970',
|
||||
'Download a TikTok video with default settings'
|
||||
)
|
||||
.example(
|
||||
'$0 download https://www.youtube.com/watch?v=_oVI0GW-Xd4 -f "bestvideo[height<=1080]+bestaudio/best[height<=1080]"',
|
||||
'Download a YouTube video in 1080p or lower quality'
|
||||
)
|
||||
.example(
|
||||
'$0 download https://www.youtube.com/watch?v=_oVI0GW-Xd4 --mp3',
|
||||
'Extract and download only the audio in MP3 format'
|
||||
)
|
||||
.example(
|
||||
'$0 info https://www.tiktok.com/@woman.power.quote/video/7476910372121971970 --dump-json',
|
||||
'Retrieve and display detailed video metadata in JSON format'
|
||||
)
|
||||
.example(
|
||||
'$0 formats https://www.youtube.com/watch?v=_oVI0GW-Xd4 --all',
|
||||
'List all available video and audio formats for a YouTube video'
|
||||
)
|
||||
.example(
|
||||
'$0 tiktok:meta https://www.tiktok.com/@username/video/1234567890 -o metadata.json',
|
||||
'Scrape metadata from a TikTok video and save it to metadata.json'
|
||||
)
|
||||
.demandCommand(1, 'You need to specify a command')
|
||||
.strict()
|
||||
.help()
|
||||
.alias('h', 'help')
|
||||
.version()
|
||||
.alias('V', 'version')
|
||||
.wrap(800) // Fixed width value instead of yargs.terminalWidth() which isn't compatible with ESM
|
||||
.showHelpOnFail(true)
|
||||
.parse();
|
||||
@ -1,24 +0,0 @@
|
||||
// Export YtDlp class
|
||||
export { YtDlp } from './ytdlp.js';
|
||||
|
||||
// Export all types and schemas
|
||||
export {
|
||||
// Core types
|
||||
YtDlpOptions,
|
||||
DownloadOptions,
|
||||
FormatOptions,
|
||||
VideoInfoOptions,
|
||||
VideoFormat,
|
||||
VideoInfo,
|
||||
|
||||
// Zod schemas
|
||||
YtDlpOptionsSchema,
|
||||
DownloadOptionsSchema,
|
||||
FormatOptionsSchema,
|
||||
VideoInfoOptionsSchema,
|
||||
VideoFormatSchema,
|
||||
VideoInfoSchema,
|
||||
} from './types.js';
|
||||
|
||||
// Export logger
|
||||
export { logger } from './logger.js';
|
||||
@ -1,55 +0,0 @@
|
||||
import { Logger, ILogObj } from 'tslog';
|
||||
|
||||
// Configure log levels
|
||||
export enum LogLevel {
|
||||
SILLY = 'silly',
|
||||
TRACE = 'trace',
|
||||
DEBUG = 'debug',
|
||||
INFO = 'info',
|
||||
WARN = 'warn',
|
||||
ERROR = 'error',
|
||||
FATAL = 'fatal'
|
||||
}
|
||||
|
||||
// Mapping from string LogLevel to numeric values expected by tslog
|
||||
const logLevelToTsLogLevel: Record<LogLevel, number> = {
|
||||
[LogLevel.SILLY]: 0,
|
||||
[LogLevel.TRACE]: 1,
|
||||
[LogLevel.DEBUG]: 2,
|
||||
[LogLevel.INFO]: 3,
|
||||
[LogLevel.WARN]: 4,
|
||||
[LogLevel.ERROR]: 5,
|
||||
[LogLevel.FATAL]: 6
|
||||
};
|
||||
|
||||
// Convert a LogLevel string to its corresponding numeric value
|
||||
const getNumericLogLevel = (level: LogLevel): number => {
|
||||
return logLevelToTsLogLevel[level];
|
||||
};
|
||||
// Custom transport for logs if needed
|
||||
const logToTransport = (logObject: ILogObj) => {
|
||||
// Here you can implement custom transport like file or external service
|
||||
// For example, log to file or send to a log management service
|
||||
// console.log("Custom transport:", JSON.stringify(logObject));
|
||||
};
|
||||
|
||||
// Create the logger instance
|
||||
export const logger = new Logger({
|
||||
name: "yt-dlp-wrapper"
|
||||
});
|
||||
|
||||
// Add transport if needed
|
||||
// logger.attachTransport(
|
||||
// {
|
||||
// silly: logToTransport,
|
||||
// debug: logToTransport,
|
||||
// trace: logToTransport,
|
||||
// info: logToTransport,
|
||||
// warn: logToTransport,
|
||||
// error: logToTransport,
|
||||
// fatal: logToTransport,
|
||||
// },
|
||||
// LogLevel.INFO
|
||||
// );
|
||||
|
||||
export default logger;
|
||||
@ -1,2 +0,0 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import fs from 'fs/promises';
|
||||
@ -1,315 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Basic YouTube DLP options schema
|
||||
export const YtDlpOptionsSchema = z.object({
|
||||
// Path to the yt-dlp executable
|
||||
executablePath: z.string().optional(),
|
||||
|
||||
// Output options
|
||||
output: z.string().optional(),
|
||||
format: z.string().optional(),
|
||||
formatSort: z.string().optional(),
|
||||
mergeOutputFormat: z.enum(['mp4', 'flv', 'webm', 'mkv', 'avi']).optional(),
|
||||
|
||||
// Download options
|
||||
limit: z.number().int().positive().optional(),
|
||||
maxFilesize: z.string().optional(),
|
||||
minFilesize: z.string().optional(),
|
||||
|
||||
// Filesystem options
|
||||
noOverwrites: z.boolean().optional(),
|
||||
continue: z.boolean().optional(),
|
||||
noPart: z.boolean().optional(),
|
||||
|
||||
// Thumbnail options
|
||||
writeThumbnail: z.boolean().optional(),
|
||||
writeAllThumbnails: z.boolean().optional(),
|
||||
|
||||
// Subtitles options
|
||||
writeSubtitles: z.boolean().optional(),
|
||||
writeAutoSubtitles: z.boolean().optional(),
|
||||
subLang: z.string().optional(),
|
||||
|
||||
// Authentication options
|
||||
username: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
|
||||
// Video selection options
|
||||
playlistStart: z.number().int().positive().optional(),
|
||||
playlistEnd: z.number().int().positive().optional(),
|
||||
playlistItems: z.string().optional(),
|
||||
|
||||
// Post-processing options
|
||||
extractAudio: z.boolean().optional(),
|
||||
audioFormat: z.enum(['best', 'aac', 'flac', 'mp3', 'm4a', 'opus', 'vorbis', 'wav']).optional(),
|
||||
audioQuality: z.string().optional(),
|
||||
remuxVideo: z.enum(['mp4', 'mkv', 'flv', 'webm', 'mov', 'avi']).optional(),
|
||||
recodeVideo: z.enum(['mp4', 'flv', 'webm', 'mkv', 'avi']).optional(),
|
||||
|
||||
// Verbosity and simulation options
|
||||
quiet: z.boolean().optional(),
|
||||
verbose: z.boolean().optional(),
|
||||
noWarnings: z.boolean().optional(),
|
||||
simulate: z.boolean().optional(),
|
||||
|
||||
// Workarounds
|
||||
noCheckCertificates: z.boolean().optional(),
|
||||
preferInsecure: z.boolean().optional(),
|
||||
userAgent: z.string().optional(),
|
||||
|
||||
// Extra arguments as string array
|
||||
extraArgs: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
// Type derived from the schema
|
||||
export type YtDlpOptions = z.infer<typeof YtDlpOptionsSchema>;
|
||||
|
||||
// Video information schema
|
||||
export const VideoInfoSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
formats: z.array(
|
||||
z.object({
|
||||
format_id: z.string(),
|
||||
format: z.string(),
|
||||
ext: z.string(),
|
||||
resolution: z.string().optional(),
|
||||
fps: z.number().optional(),
|
||||
filesize: z.number().optional(),
|
||||
tbr: z.number().optional(),
|
||||
protocol: z.string(),
|
||||
vcodec: z.string(),
|
||||
acodec: z.string(),
|
||||
})
|
||||
),
|
||||
thumbnails: z.array(
|
||||
z.object({
|
||||
url: z.string(),
|
||||
height: z.number().optional(),
|
||||
width: z.number().optional(),
|
||||
})
|
||||
).optional(),
|
||||
description: z.string().optional(),
|
||||
upload_date: z.string().optional(),
|
||||
uploader: z.string().optional(),
|
||||
uploader_id: z.string().optional(),
|
||||
uploader_url: z.string().optional(),
|
||||
channel_id: z.string().optional(),
|
||||
channel_url: z.string().optional(),
|
||||
duration: z.number().optional(),
|
||||
view_count: z.number().optional(),
|
||||
like_count: z.number().optional(),
|
||||
dislike_count: z.number().optional(),
|
||||
average_rating: z.number().optional(),
|
||||
age_limit: z.number().optional(),
|
||||
webpage_url: z.string(),
|
||||
categories: z.array(z.string()).optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
is_live: z.boolean().optional(),
|
||||
was_live: z.boolean().optional(),
|
||||
playable_in_embed: z.boolean().optional(),
|
||||
availability: z.string().optional(),
|
||||
});
|
||||
|
||||
export type VideoInfo = z.infer<typeof VideoInfoSchema>;
|
||||
|
||||
// Download result schema
|
||||
export const DownloadResultSchema = z.object({
|
||||
videoInfo: VideoInfoSchema,
|
||||
filePath: z.string(),
|
||||
downloadedBytes: z.number().optional(),
|
||||
elapsedTime: z.number().optional(),
|
||||
averageSpeed: z.number().optional(), // in bytes/s
|
||||
success: z.boolean(),
|
||||
error: z.string().optional(),
|
||||
});
|
||||
|
||||
export type DownloadResult = z.infer<typeof DownloadResultSchema>;
|
||||
|
||||
// Command execution result schema
|
||||
export const CommandResultSchema = z.object({
|
||||
command: z.string(),
|
||||
stdout: z.string(),
|
||||
stderr: z.string(),
|
||||
success: z.boolean(),
|
||||
exitCode: z.number(),
|
||||
});
|
||||
|
||||
export type CommandResult = z.infer<typeof CommandResultSchema>;
|
||||
|
||||
// Progress update schema for download progress events
|
||||
export const ProgressUpdateSchema = z.object({
|
||||
videoId: z.string(),
|
||||
percent: z.number().min(0).max(100),
|
||||
totalSize: z.number().optional(),
|
||||
downloadedBytes: z.number(),
|
||||
speed: z.number(), // in bytes/s
|
||||
eta: z.number().optional(), // in seconds
|
||||
status: z.enum(['downloading', 'finished', 'error']),
|
||||
message: z.string().optional(),
|
||||
});
|
||||
|
||||
export type ProgressUpdate = z.infer<typeof ProgressUpdateSchema>;
|
||||
|
||||
// Error types
|
||||
export enum YtDlpErrorType {
|
||||
PROCESS_ERROR = 'PROCESS_ERROR',
|
||||
VALIDATION_ERROR = 'VALIDATION_ERROR',
|
||||
DOWNLOAD_ERROR = 'DOWNLOAD_ERROR',
|
||||
UNSUPPORTED_URL = 'UNSUPPORTED_URL',
|
||||
NETWORK_ERROR = 'NETWORK_ERROR',
|
||||
}
|
||||
|
||||
// Custom error schema
|
||||
export const YtDlpErrorSchema = z.object({
|
||||
type: z.nativeEnum(YtDlpErrorType),
|
||||
message: z.string(),
|
||||
details: z.record(z.any()).optional(),
|
||||
command: z.string().optional(),
|
||||
});
|
||||
export type YtDlpError = z.infer<typeof YtDlpErrorSchema>;
|
||||
|
||||
// Download options schema
|
||||
export const DownloadOptionsSchema = z.object({
|
||||
outputDir: z.string().optional(),
|
||||
format: z.string().optional(),
|
||||
outputTemplate: z.string().optional(),
|
||||
audioOnly: z.boolean().optional(),
|
||||
audioFormat: z.string().optional(),
|
||||
subtitles: z.union([z.boolean(), z.array(z.string())]).optional(),
|
||||
maxFileSize: z.number().optional(),
|
||||
rateLimit: z.string().optional(),
|
||||
additionalArgs: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export type DownloadOptions = z.infer<typeof DownloadOptionsSchema>;
|
||||
|
||||
// Format options schema for listing video formats
|
||||
export const FormatOptionsSchema = z.object({
|
||||
all: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
export type FormatOptions = z.infer<typeof FormatOptionsSchema>;
|
||||
|
||||
// Video info options schema
|
||||
export const VideoInfoOptionsSchema = z.object({
|
||||
dumpJson: z.boolean().optional().default(false),
|
||||
flatPlaylist: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
export type VideoInfoOptions = z.infer<typeof VideoInfoOptionsSchema>;
|
||||
|
||||
// Video format schema representing a single format option returned by yt-dlp
|
||||
// Video format schema representing a single format option returned by yt-dlp
|
||||
export const VideoFormatSchema = z.object({
|
||||
format_id: z.string(),
|
||||
format: z.string(),
|
||||
ext: z.string(),
|
||||
resolution: z.string().optional(),
|
||||
fps: z.number().optional(),
|
||||
filesize: z.number().optional(),
|
||||
tbr: z.number().optional(),
|
||||
protocol: z.string(),
|
||||
vcodec: z.string(),
|
||||
acodec: z.string(),
|
||||
width: z.number().optional(),
|
||||
height: z.number().optional(),
|
||||
url: z.string().optional(),
|
||||
format_note: z.string().optional(),
|
||||
container: z.string().optional(),
|
||||
quality: z.number().optional(),
|
||||
preference: z.number().optional(),
|
||||
});
|
||||
|
||||
export type VideoFormat = z.infer<typeof VideoFormatSchema>;
|
||||
|
||||
// TikTok metadata options schema
|
||||
export const TikTokMetadataOptionsSchema = z.object({
|
||||
url: z.string().url(),
|
||||
outputPath: z.string(),
|
||||
format: z.enum(['json', 'pretty']).optional().default('json'),
|
||||
includeComments: z.boolean().optional().default(false),
|
||||
timeout: z.number().int().positive().optional().default(30000),
|
||||
userAgent: z.string().optional(),
|
||||
proxy: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TikTokMetadataOptions = z.infer<typeof TikTokMetadataOptionsSchema>;
|
||||
|
||||
// TikTok metadata schema
|
||||
export const TikTokMetadataSchema = z.object({
|
||||
id: z.string(),
|
||||
url: z.string().url(),
|
||||
timestamp: z.number(),
|
||||
scrapedAt: z.string().datetime(),
|
||||
author: z.object({
|
||||
id: z.string(),
|
||||
uniqueId: z.string(), // username
|
||||
nickname: z.string(),
|
||||
avatarUrl: z.string().url().optional(),
|
||||
verified: z.boolean().optional(),
|
||||
secUid: z.string().optional(),
|
||||
following: z.number().optional(),
|
||||
followers: z.number().optional(),
|
||||
likes: z.number().optional(),
|
||||
profileUrl: z.string().url().optional(),
|
||||
}),
|
||||
video: z.object({
|
||||
id: z.string(),
|
||||
description: z.string(),
|
||||
createTime: z.number(),
|
||||
width: z.number(),
|
||||
height: z.number(),
|
||||
duration: z.number(),
|
||||
ratio: z.string().optional(),
|
||||
coverUrl: z.string().url().optional(),
|
||||
playUrl: z.string().url().optional(),
|
||||
downloadUrl: z.string().url().optional(),
|
||||
shareCount: z.number().optional(),
|
||||
commentCount: z.number().optional(),
|
||||
likeCount: z.number().optional(),
|
||||
viewCount: z.number().optional(),
|
||||
hashTags: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
title: z.string().optional(),
|
||||
})
|
||||
).optional(),
|
||||
mentions: z.array(z.string()).optional(),
|
||||
musicInfo: z.object({
|
||||
id: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
authorName: z.string().optional(),
|
||||
coverUrl: z.string().url().optional(),
|
||||
playUrl: z.string().url().optional(),
|
||||
duration: z.number().optional(),
|
||||
}).optional(),
|
||||
}),
|
||||
comments: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
text: z.string(),
|
||||
createTime: z.number(),
|
||||
likeCount: z.number(),
|
||||
authorId: z.string(),
|
||||
authorName: z.string(),
|
||||
authorAvatar: z.string().url().optional(),
|
||||
replies: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
text: z.string(),
|
||||
createTime: z.number(),
|
||||
likeCount: z.number(),
|
||||
authorId: z.string(),
|
||||
authorName: z.string(),
|
||||
authorAvatar: z.string().url().optional(),
|
||||
})
|
||||
).optional(),
|
||||
})
|
||||
).optional(),
|
||||
});
|
||||
|
||||
export type TikTokMetadata = z.infer<typeof TikTokMetadataSchema>;
|
||||
|
||||
|
||||
@ -1,635 +0,0 @@
|
||||
import { exec, spawn } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { VideoInfo, DownloadOptions, YtDlpOptions, FormatOptions, VideoInfoOptions, VideoFormat, VideoFormatSchema } from './types.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
/**
|
||||
* A wrapper class for the yt-dlp command line tool
|
||||
*/
|
||||
export class YtDlp {
|
||||
private executable: string = 'yt-dlp';
|
||||
|
||||
/**
|
||||
* Create a new YtDlp instance
|
||||
* @param options Configuration options for yt-dlp
|
||||
*/
|
||||
constructor(private options: YtDlpOptions = {}) {
|
||||
if (options.executablePath) {
|
||||
this.executable = options.executablePath;
|
||||
}
|
||||
|
||||
logger.debug('YtDlp initialized with options:', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if yt-dlp is installed and accessible
|
||||
* @returns Promise resolving to true if yt-dlp is installed, false otherwise
|
||||
*/
|
||||
async isInstalled(): Promise<boolean> {
|
||||
try {
|
||||
const { stdout } = await execAsync(`${this.executable} --version`);
|
||||
logger.debug(`yt-dlp version: ${stdout.trim()}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.warn('yt-dlp is not installed or not found in PATH');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a video from a given URL
|
||||
* @param url The URL of the video to download
|
||||
* @param options Download options
|
||||
* @returns Promise resolving to the path of the downloaded file
|
||||
*/
|
||||
async downloadVideo(url: string, options: DownloadOptions = {}): Promise<string> {
|
||||
if (!url) {
|
||||
logger.error('Download failed: No URL provided');
|
||||
throw new Error('URL is required');
|
||||
}
|
||||
|
||||
logger.info(`Downloading video from: ${url}`);
|
||||
logger.debug(`Download options: ${JSON.stringify({
|
||||
...options,
|
||||
// Redact any sensitive information
|
||||
userAgent: this.options.userAgent ? '[REDACTED]' : undefined
|
||||
}, null, 2)}`);
|
||||
|
||||
// Prepare output directory
|
||||
const outputDir = options.outputDir || '.';
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
logger.debug(`Creating output directory: ${outputDir}`);
|
||||
try {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
logger.debug(`Output directory created successfully`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to create output directory: ${(error as Error).message}`);
|
||||
throw new Error(`Failed to create output directory ${outputDir}: ${(error as Error).message}`);
|
||||
}
|
||||
} else {
|
||||
logger.debug(`Output directory already exists: ${outputDir}`);
|
||||
}
|
||||
|
||||
// Build command arguments
|
||||
const args: string[] = [];
|
||||
|
||||
// Add user agent if specified in global options
|
||||
if (this.options.userAgent) {
|
||||
args.push('--user-agent', this.options.userAgent);
|
||||
}
|
||||
|
||||
// Format selection
|
||||
if (options.format) {
|
||||
args.push('-f', options.format);
|
||||
}
|
||||
|
||||
// Output template
|
||||
const outputTemplate = options.outputTemplate || '%(title)s.%(ext)s';
|
||||
args.push('-o', path.join(outputDir, outputTemplate));
|
||||
|
||||
// Add other options
|
||||
if (options.audioOnly) {
|
||||
logger.debug(`Audio-only mode enabled, extracting audio with format: ${options.audioFormat || 'default'}`);
|
||||
logger.info(`Extracting audio in ${options.audioFormat || 'best available'} format`);
|
||||
|
||||
// Add extract audio flag
|
||||
args.push('-x');
|
||||
|
||||
// Handle audio format
|
||||
if (options.audioFormat) {
|
||||
logger.debug(`Setting audio format to: ${options.audioFormat}`);
|
||||
args.push('--audio-format', options.audioFormat);
|
||||
|
||||
// For MP3 format, ensure we're using the right audio quality
|
||||
if (options.audioFormat.toLowerCase() === 'mp3') {
|
||||
logger.debug('MP3 format requested, setting audio quality');
|
||||
args.push('--audio-quality', '0'); // 0 is best quality
|
||||
|
||||
// Ensure ffmpeg installed message for MP3 conversion
|
||||
logger.debug('MP3 conversion requires ffmpeg to be installed');
|
||||
logger.info('Note: MP3 conversion requires ffmpeg to be installed on your system');
|
||||
}
|
||||
}
|
||||
logger.debug(`Audio extraction command arguments: ${args.join(' ')}`);
|
||||
}
|
||||
|
||||
if (options.subtitles) {
|
||||
args.push('--write-subs');
|
||||
if (Array.isArray(options.subtitles)) {
|
||||
args.push('--sub-lang', options.subtitles.join(','));
|
||||
}
|
||||
}
|
||||
|
||||
if (options.maxFileSize) {
|
||||
args.push('--max-filesize', options.maxFileSize.toString());
|
||||
}
|
||||
|
||||
if (options.rateLimit) {
|
||||
args.push('--limit-rate', options.rateLimit);
|
||||
}
|
||||
|
||||
// Add custom arguments if provided
|
||||
if (options.additionalArgs) {
|
||||
args.push(...options.additionalArgs);
|
||||
}
|
||||
|
||||
// Add the URL
|
||||
args.push(url);
|
||||
// Create a copy of args with sensitive information redacted for logging
|
||||
const logSafeArgs = [...args].map(arg => {
|
||||
if (arg === this.options.userAgent && this.options.userAgent) {
|
||||
return '[REDACTED]';
|
||||
}
|
||||
return arg;
|
||||
});
|
||||
|
||||
// Log the command for debugging
|
||||
logger.debug(`Executing download command: ${this.executable} ${logSafeArgs.join(' ')}`);
|
||||
logger.debug(`Download configuration: audioOnly=${!!options.audioOnly}, format=${options.format || 'default'}, audioFormat=${options.audioFormat || 'n/a'}`);
|
||||
|
||||
// Add additional debugging for audio extraction
|
||||
if (options.audioOnly) {
|
||||
logger.debug(`Audio extraction details: format=${options.audioFormat || 'auto'}, mp3=${options.audioFormat?.toLowerCase() === 'mp3'}`);
|
||||
logger.info(`Audio extraction mode: ${options.audioFormat || 'best quality'}`);
|
||||
}
|
||||
|
||||
// Print to console for user feedback
|
||||
if (options.audioOnly) {
|
||||
logger.info(`Downloading audio in ${options.audioFormat || 'best'} format from: ${url}`);
|
||||
} else {
|
||||
logger.info(`Downloading video from: ${url}`);
|
||||
}
|
||||
logger.debug('Executing download command');
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.debug('Spawning yt-dlp process');
|
||||
|
||||
try {
|
||||
const ytdlpProcess = spawn(this.executable, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let downloadedFile: string | null = null;
|
||||
let progressData = {
|
||||
percent: 0,
|
||||
totalSize: '0',
|
||||
downloadedSize: '0',
|
||||
speed: '0',
|
||||
eta: '0'
|
||||
};
|
||||
|
||||
ytdlpProcess.stdout.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
stdout += output;
|
||||
logger.debug(`yt-dlp output: ${output.trim()}`);
|
||||
|
||||
// Try to extract the output filename
|
||||
const destinationMatch = output.match(/Destination: (.+)/);
|
||||
if (destinationMatch && destinationMatch[1]) {
|
||||
downloadedFile = destinationMatch[1].trim();
|
||||
logger.debug(`Detected output filename: ${downloadedFile}`);
|
||||
}
|
||||
|
||||
// Alternative method to extract the output filename
|
||||
const alreadyDownloadedMatch = output.match(/\[download\] (.+) has already been downloaded/);
|
||||
if (alreadyDownloadedMatch && alreadyDownloadedMatch[1]) {
|
||||
downloadedFile = alreadyDownloadedMatch[1].trim();
|
||||
logger.debug(`Video was already downloaded: ${downloadedFile}`);
|
||||
}
|
||||
|
||||
// Track download progress
|
||||
const progressMatch = output.match(/\[download\]\s+(\d+\.?\d*)%\s+of\s+~?(\S+)\s+at\s+(\S+)\s+ETA\s+(\S+)/);
|
||||
if (progressMatch) {
|
||||
progressData = {
|
||||
percent: parseFloat(progressMatch[1]),
|
||||
totalSize: progressMatch[2],
|
||||
downloadedSize: (parseFloat(progressMatch[1]) * parseFloat(progressMatch[2]) / 100).toFixed(2) + 'MB',
|
||||
speed: progressMatch[3],
|
||||
eta: progressMatch[4]
|
||||
};
|
||||
|
||||
// Log progress more frequently for better user feedback
|
||||
if (Math.floor(progressData.percent) % 5 === 0) {
|
||||
logger.info(`Download progress: ${progressData.percent.toFixed(1)}% (${progressData.downloadedSize}/${progressData.totalSize}) at ${progressData.speed} (ETA: ${progressData.eta})`);
|
||||
}
|
||||
}
|
||||
|
||||
// Capture the file after extraction (important for audio conversions)
|
||||
const extractingMatch = output.match(/\[ExtractAudio\] Destination: (.+)/);
|
||||
if (extractingMatch && extractingMatch[1]) {
|
||||
downloadedFile = extractingMatch[1].trim();
|
||||
logger.debug(`Audio extraction destination: ${downloadedFile}`);
|
||||
|
||||
// Log audio conversion information
|
||||
if (options.audioOnly) {
|
||||
logger.info(`Converting to ${options.audioFormat || 'best format'}: ${downloadedFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Also capture ffmpeg conversion progress for audio
|
||||
const ffmpegMatch = output.match(/\[ffmpeg\] Destination: (.+)/);
|
||||
if (ffmpegMatch && ffmpegMatch[1]) {
|
||||
downloadedFile = ffmpegMatch[1].trim();
|
||||
logger.debug(`ffmpeg conversion destination: ${downloadedFile}`);
|
||||
|
||||
if (options.audioOnly && options.audioFormat) {
|
||||
logger.info(`Converting to ${options.audioFormat}: ${downloadedFile}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ytdlpProcess.stderr.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
stderr += output;
|
||||
logger.error(`yt-dlp error: ${output.trim()}`);
|
||||
|
||||
// Try to detect common error types for better error reporting
|
||||
if (output.includes('No such file or directory')) {
|
||||
logger.error('yt-dlp executable not found. Please make sure it is installed and in your PATH.');
|
||||
} else if (output.includes('HTTP Error 403: Forbidden')) {
|
||||
logger.error('Access forbidden - the server denied access. This might be due to rate limiting or geo-restrictions.');
|
||||
} else if (output.includes('HTTP Error 404: Not Found')) {
|
||||
logger.error('The requested video was not found. It might have been deleted or made private.');
|
||||
} else if (output.includes('Unsupported URL')) {
|
||||
logger.error('The URL is not supported by yt-dlp. Check if the website is supported or if you need a newer version of yt-dlp.');
|
||||
}
|
||||
});
|
||||
|
||||
ytdlpProcess.on('close', (code) => {
|
||||
logger.debug(`yt-dlp process exited with code ${code}`);
|
||||
|
||||
if (code === 0) {
|
||||
if (downloadedFile) {
|
||||
// Verify the file actually exists
|
||||
try {
|
||||
const stats = fs.statSync(downloadedFile);
|
||||
logger.info(`Successfully downloaded: ${downloadedFile} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
|
||||
resolve(downloadedFile);
|
||||
} catch (error) {
|
||||
logger.warn(`File reported as downloaded but not found on disk: ${downloadedFile}`);
|
||||
logger.debug(`File access error: ${(error as Error).message}`);
|
||||
|
||||
// Try to find an alternative filename
|
||||
const possibleFiles = this.searchPossibleFilenames(stdout, outputDir);
|
||||
if (possibleFiles.length > 0) {
|
||||
logger.info(`Found alternative downloaded file: ${possibleFiles[0]}`);
|
||||
resolve(possibleFiles[0]);
|
||||
} else {
|
||||
logger.info('Download reported as successful, but could not locate the output file');
|
||||
resolve('Download completed');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Try to find the downloaded file from stdout if it wasn't captured
|
||||
logger.debug('No downloadedFile captured, searching stdout for filename');
|
||||
|
||||
const possibleFiles = this.searchPossibleFilenames(stdout, outputDir);
|
||||
if (possibleFiles.length > 0) {
|
||||
logger.info(`Successfully downloaded: ${possibleFiles[0]}`);
|
||||
resolve(possibleFiles[0]);
|
||||
} else {
|
||||
logger.info('Download successful, but could not determine the output file name');
|
||||
logger.debug('Full stdout for debugging:');
|
||||
logger.debug(stdout);
|
||||
resolve('Download completed');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.error(`Download failed with exit code ${code}`);
|
||||
|
||||
// Provide more context based on the error code
|
||||
let errorContext = '';
|
||||
if (code === 1) {
|
||||
errorContext = 'General error - check URL and network connection';
|
||||
} else if (code === 2) {
|
||||
errorContext = 'Network error - check your internet connection';
|
||||
} else if (code === 3) {
|
||||
errorContext = 'File system error - check permissions and disk space';
|
||||
}
|
||||
|
||||
reject(new Error(`yt-dlp exited with code ${code} (${errorContext}): ${stderr}`));
|
||||
}
|
||||
});
|
||||
|
||||
ytdlpProcess.on('error', (err) => {
|
||||
logger.error(`Failed to start yt-dlp process: ${err.message}`);
|
||||
logger.debug(`Error details: ${JSON.stringify(err)}`);
|
||||
|
||||
// Check for common spawn errors and provide helpful messages
|
||||
if (err.message.includes('ENOENT')) {
|
||||
reject(new Error(`yt-dlp executable not found. Make sure it's installed and available in your PATH. Error: ${err.message}`));
|
||||
} else if (err.message.includes('EACCES')) {
|
||||
reject(new Error(`Permission denied when executing yt-dlp. Check file permissions. Error: ${err.message}`));
|
||||
} else {
|
||||
reject(new Error(`Failed to start yt-dlp: ${err.message}`));
|
||||
}
|
||||
});
|
||||
// Handle unexpected termination
|
||||
process.on('SIGINT', () => {
|
||||
logger.warn('Process interrupted, terminating download');
|
||||
ytdlpProcess.kill('SIGINT');
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`Exception during process spawn: ${(error as Error).message}`);
|
||||
reject(new Error(`Failed to start download process: ${(error as Error).message}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for possible filenames in yt-dlp output
|
||||
* @param stdout The stdout output from yt-dlp
|
||||
* @param outputDir The output directory
|
||||
* @returns Array of possible filenames found
|
||||
*/
|
||||
private searchPossibleFilenames(stdout: string, outputDir: string): string[] {
|
||||
const possibleFiles: string[] = [];
|
||||
|
||||
// Various regex patterns to find filenames in the yt-dlp output
|
||||
const patterns = [
|
||||
/\[download\] (.+?) has already been downloaded/,
|
||||
/\[download\] Destination: (.+)/,
|
||||
/\[ffmpeg\] Destination: (.+)/,
|
||||
/\[ExtractAudio\] Destination: (.+)/,
|
||||
/\[Merger\] Merging formats into "(.+)"/,
|
||||
/\[Merger\] Merged into (.+)/
|
||||
];
|
||||
|
||||
// Try each pattern
|
||||
for (const pattern of patterns) {
|
||||
const matches = stdout.matchAll(new RegExp(pattern, 'g'));
|
||||
for (const match of matches) {
|
||||
if (match[1]) {
|
||||
const filePath = match[1].trim();
|
||||
// Check if it's a relative or absolute path
|
||||
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(outputDir, filePath);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
logger.debug(`Found output file: ${fullPath}`);
|
||||
possibleFiles.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return possibleFiles;
|
||||
}
|
||||
/**
|
||||
* Get information about a video without downloading it
|
||||
* @param url The URL of the video to get information for
|
||||
* @returns Promise resolving to video information
|
||||
*/
|
||||
/**
|
||||
* Escapes a string for shell use based on the current platform
|
||||
* @param str The string to escape
|
||||
* @returns The escaped string
|
||||
*/
|
||||
private escapeShellArg(str: string): string {
|
||||
if (os.platform() === 'win32') {
|
||||
// Windows: Double quotes need to be escaped with backslash
|
||||
// and the whole string wrapped in double quotes
|
||||
return `"${str.replace(/"/g, '\\"')}"`;
|
||||
} else {
|
||||
// Unix-like: Single quotes provide the strongest escaping
|
||||
// Double any existing single quotes and wrap in single quotes
|
||||
return `'${str.replace(/'/g, "'\\''")}'`;
|
||||
}
|
||||
}
|
||||
|
||||
async getVideoInfo(url: string, options: VideoInfoOptions = { dumpJson: false, flatPlaylist: false }): Promise<VideoInfo> {
|
||||
if (!url) {
|
||||
throw new Error('URL is required');
|
||||
}
|
||||
|
||||
logger.info(`Getting video info for: ${url}`);
|
||||
|
||||
try {
|
||||
// Build command with options
|
||||
const args: string[] = ['--dump-json'];
|
||||
|
||||
// Add user agent if specified in global options
|
||||
if (this.options.userAgent) {
|
||||
args.push('--user-agent', this.options.userAgent);
|
||||
}
|
||||
|
||||
// Add VideoInfoOptions flags
|
||||
if (options.flatPlaylist) {
|
||||
args.push('--flat-playlist');
|
||||
}
|
||||
|
||||
args.push(url);
|
||||
|
||||
// Properly escape arguments for the exec call
|
||||
const escapedArgs = args.map(arg => {
|
||||
// Only escape arguments that need escaping (contains spaces or special characters)
|
||||
return /[\s"'$&()<>`|;]/.test(arg) ? this.escapeShellArg(arg) : arg;
|
||||
});
|
||||
|
||||
const { stdout } = await execAsync(`${this.executable} ${escapedArgs.join(' ')}`);
|
||||
|
||||
const videoInfo = JSON.parse(stdout);
|
||||
logger.debug('Video info retrieved successfully');
|
||||
|
||||
return videoInfo;
|
||||
} catch (error) {
|
||||
logger.error('Failed to get video info:', error);
|
||||
throw new Error(`Failed to get video info: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List available formats for a video
|
||||
* @param url The URL of the video to get formats for
|
||||
* @returns Promise resolving to an array of VideoFormat objects
|
||||
*/
|
||||
async listFormats(url: string, options: FormatOptions = { all: false }): Promise<VideoFormat[]> {
|
||||
if (!url) {
|
||||
throw new Error('URL is required');
|
||||
}
|
||||
|
||||
logger.info(`Listing formats for: ${url}`);
|
||||
|
||||
try {
|
||||
// Build command with options
|
||||
const formatFlag = options.all ? '--list-formats-all' : '-F';
|
||||
|
||||
// Properly escape URL if needed
|
||||
const escapedUrl = /[\s"'$&()<>`|;]/.test(url) ? this.escapeShellArg(url) : url;
|
||||
const { stdout } = await execAsync(`${this.executable} ${formatFlag} ${escapedUrl}`);
|
||||
logger.debug('Format list retrieved successfully');
|
||||
|
||||
// Parse the output to extract format information
|
||||
return this.parseFormatOutput(stdout);
|
||||
} catch (error) {
|
||||
logger.error('Failed to list formats:', error);
|
||||
throw new Error(`Failed to list formats: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the format list output from yt-dlp into an array of VideoFormat objects
|
||||
* @param output The raw output from yt-dlp format listing
|
||||
* @returns Array of VideoFormat objects
|
||||
*/
|
||||
private parseFormatOutput(output: string): VideoFormat[] {
|
||||
const formats: VideoFormat[] = [];
|
||||
const lines = output.split('\n');
|
||||
|
||||
// Find the line with table headers to determine where the format list starts
|
||||
let formatListStartIndex = 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].includes('format code') || lines[i].includes('ID')) {
|
||||
formatListStartIndex = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Regular expressions to match various format components
|
||||
const formatIdRegex = /^(\S+)/;
|
||||
const extensionRegex = /(\w+)\s+/;
|
||||
const resolutionRegex = /(\d+x\d+|\d+p)/;
|
||||
const fpsRegex = /(\d+)fps/;
|
||||
const filesizeRegex = /(\d+(\.\d+)?)(K|M|G|T)iB/;
|
||||
const bitrateRegex = /(\d+(\.\d+)?)(k|m)bps/;
|
||||
const codecRegex = /(mp4|webm|m4a|mp3|opus|vorbis)\s+([\w.]+)/i;
|
||||
const formatNoteRegex = /(audio only|video only|tiny|small|medium|large|best)/i;
|
||||
|
||||
// Process each line that contains format information
|
||||
for (let i = formatListStartIndex; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line || line.includes('----')) continue; // Skip empty lines or separators
|
||||
|
||||
// Extract format ID - typically the first part of the line
|
||||
const formatIdMatch = line.match(formatIdRegex);
|
||||
if (!formatIdMatch) continue;
|
||||
|
||||
const formatId = formatIdMatch[1];
|
||||
|
||||
// Create a base format object
|
||||
const format: Partial<VideoFormat> = {
|
||||
format_id: formatId,
|
||||
format: line, // Use the full line as the format description
|
||||
ext: 'unknown',
|
||||
protocol: 'https',
|
||||
vcodec: 'unknown',
|
||||
acodec: 'unknown'
|
||||
};
|
||||
|
||||
// Try to extract format components
|
||||
// Extract extension
|
||||
const extMatch = line.substring(formatId.length).match(extensionRegex);
|
||||
if (extMatch) {
|
||||
format.ext = extMatch[1];
|
||||
}
|
||||
|
||||
// Extract resolution
|
||||
const resMatch = line.match(resolutionRegex);
|
||||
if (resMatch) {
|
||||
format.resolution = resMatch[1];
|
||||
|
||||
// If resolution is in the form of "1280x720", extract width and height
|
||||
const dimensions = format.resolution.split('x');
|
||||
if (dimensions.length === 2) {
|
||||
format.width = parseInt(dimensions[0], 10);
|
||||
format.height = parseInt(dimensions[1], 10);
|
||||
} else if (format.resolution.endsWith('p')) {
|
||||
// If resolution is like "720p", extract height
|
||||
format.height = parseInt(format.resolution.replace('p', ''), 10);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract FPS
|
||||
const fpsMatch = line.match(fpsRegex);
|
||||
if (fpsMatch) {
|
||||
format.fps = parseInt(fpsMatch[1], 10);
|
||||
}
|
||||
|
||||
// Extract filesize
|
||||
const sizeMatch = line.match(filesizeRegex);
|
||||
if (sizeMatch) {
|
||||
let size = parseFloat(sizeMatch[1]);
|
||||
const unit = sizeMatch[3];
|
||||
|
||||
// Convert to bytes
|
||||
if (unit === 'K') size *= 1024;
|
||||
else if (unit === 'M') size *= 1024 * 1024;
|
||||
else if (unit === 'G') size *= 1024 * 1024 * 1024;
|
||||
else if (unit === 'T') size *= 1024 * 1024 * 1024 * 1024;
|
||||
|
||||
format.filesize = Math.round(size);
|
||||
}
|
||||
|
||||
// Extract bitrate
|
||||
const bitrateMatch = line.match(bitrateRegex);
|
||||
if (bitrateMatch) {
|
||||
let bitrate = parseFloat(bitrateMatch[1]);
|
||||
const unit = bitrateMatch[3];
|
||||
|
||||
// Convert to Kbps
|
||||
if (unit === 'm') bitrate *= 1000;
|
||||
|
||||
format.tbr = bitrate;
|
||||
}
|
||||
|
||||
// Extract format note
|
||||
const noteMatch = line.match(formatNoteRegex);
|
||||
if (noteMatch) {
|
||||
format.format_note = noteMatch[1];
|
||||
}
|
||||
|
||||
// Determine audio/video codec
|
||||
if (line.includes('audio only')) {
|
||||
format.vcodec = 'none';
|
||||
// Try to get audio codec
|
||||
const codecMatch = line.match(codecRegex);
|
||||
if (codecMatch) {
|
||||
format.acodec = codecMatch[2] || format.acodec;
|
||||
}
|
||||
} else if (line.includes('video only')) {
|
||||
format.acodec = 'none';
|
||||
// Try to get video codec
|
||||
const codecMatch = line.match(codecRegex);
|
||||
if (codecMatch) {
|
||||
format.vcodec = codecMatch[2] || format.vcodec;
|
||||
}
|
||||
} else {
|
||||
// Both audio and video
|
||||
const codecMatch = line.match(codecRegex);
|
||||
if (codecMatch) {
|
||||
format.container = codecMatch[1];
|
||||
if (codecMatch[2]) {
|
||||
if (line.includes('video')) {
|
||||
format.vcodec = codecMatch[2];
|
||||
} else if (line.includes('audio')) {
|
||||
format.acodec = codecMatch[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the format to our result array
|
||||
formats.push(format as VideoFormat);
|
||||
}
|
||||
|
||||
return formats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the path to the yt-dlp executable
|
||||
* @param path Path to the yt-dlp executable
|
||||
*/
|
||||
setExecutablePath(path: string): void {
|
||||
if (!path) {
|
||||
throw new Error('Executable path cannot be empty');
|
||||
}
|
||||
this.executable = path;
|
||||
logger.debug(`yt-dlp executable path set to: ${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default YtDlp;
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,121 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
"lib": ["es2020", "dom"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "libReplacement": true, /* Enable lib replacement. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "NodeNext", /* Specify what module code is generated. */
|
||||
"rootDir": "./src", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "NodeNext", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
"resolveJsonModule": true, /* Enable importing .json files. */
|
||||
"moduleDetection": "force", /* Control what method is used to detect module-format JS files. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
"declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
||||
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
|
||||
"allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"files": [
|
||||
"src/index.ts"
|
||||
],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"ts-node": {
|
||||
"esm": true
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
// Enable verbose test output
|
||||
reporters: ['verbose'],
|
||||
|
||||
// Enable ESM support
|
||||
environment: 'node',
|
||||
|
||||
// Ensure includes are properly configured for TypeScript files
|
||||
include: ['src/**/*.{test,spec}.ts'],
|
||||
|
||||
// Enable code coverage
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'html'],
|
||||
exclude: ['node_modules/', 'dist/'],
|
||||
},
|
||||
|
||||
// Add global timeout for long-running tests like video downloads
|
||||
testTimeout: 30000,
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// Get __dirname equivalent in ESM
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export default {
|
||||
mode: 'production',
|
||||
entry: {
|
||||
main: './src/index.ts',
|
||||
'cli-bundle': './src/cli.ts',
|
||||
},
|
||||
target: 'node',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js', '.mjs', '.json'],
|
||||
extensionAlias: {
|
||||
'.js': ['.js', '.ts'],
|
||||
'.mjs': ['.mjs', '.mts'],
|
||||
'.cjs': ['.cjs', '.cts'],
|
||||
},
|
||||
// This ensures imports without file extensions still work properly
|
||||
fullySpecified: false,
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
clean: true,
|
||||
},
|
||||
experiments: {
|
||||
outputModule: true,
|
||||
},
|
||||
optimization: {
|
||||
minimize: false
|
||||
},
|
||||
};
|
||||
|
||||
2639
reference/tiktok/docs/player.md
Normal file
2639
reference/tiktok/docs/player.md
Normal file
File diff suppressed because it is too large
Load Diff
497
reference/tiktok/docs/tiktok-deobfuscation-analysis.md
Normal file
497
reference/tiktok/docs/tiktok-deobfuscation-analysis.md
Normal file
@ -0,0 +1,497 @@
|
||||
# TikTok Web Application - Deobfuscation Analysis
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides a comprehensive analysis of deobfuscated TikTok JavaScript bundles, revealing the internal architecture, features, and technical implementation of TikTok's web application.
|
||||
|
||||
## Files Analyzed
|
||||
|
||||
### 1. `4004.ab578596.js` → `4004.ab578596_deobfuscated.js`
|
||||
**Core Functionality Bundle** - Contains essential web application features
|
||||
|
||||
### 2. `11054.689f275a.js` → `11054.689f275a_deobfuscated.js`
|
||||
**Type Definitions Bundle** - Contains comprehensive type system and enums
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
TikTok's web application uses a sophisticated modular architecture with:
|
||||
|
||||
- **Webpack-based bundling** with loadable chunks for code splitting
|
||||
- **React-based UI** with hooks and state management
|
||||
- **RxJS observables** for reactive programming
|
||||
- **Dependency injection** system for services
|
||||
- **Comprehensive type system** with 1000+ enum definitions
|
||||
|
||||
---
|
||||
|
||||
## Core Features Analysis
|
||||
|
||||
### 🎥 Video Technology Stack
|
||||
|
||||
#### H265/HEVC Codec Support
|
||||
```javascript
|
||||
// Automatic codec detection and optimization
|
||||
function detectH265Support() {
|
||||
if (deviceUtils.fU()) return false; // Skip on certain devices
|
||||
|
||||
if (typeof MediaSource === "undefined") return false;
|
||||
|
||||
// Check MediaSource support
|
||||
if (!MediaSource.isTypeSupported(h265CodecString)) return false;
|
||||
|
||||
// Check video element support
|
||||
var testVideo = document.createElement("video");
|
||||
return testVideo.canPlayType(h265CodecString) === "probably";
|
||||
}
|
||||
```
|
||||
|
||||
**Key Insights:**
|
||||
- Automatic H264/H265 codec selection based on browser capabilities
|
||||
- Caching system with 14-day expiration for codec support detection
|
||||
- MediaCapabilities API integration for advanced codec testing
|
||||
- Fallback mechanisms for unsupported devices
|
||||
|
||||
#### Video Quality Management
|
||||
- Quality levels: 3, 4, 31 are considered valid for H265
|
||||
- Dynamic codec switching based on device performance
|
||||
- Preloading optimization for better user experience
|
||||
|
||||
### 🔍 Search & Discovery System
|
||||
|
||||
#### A/B Testing Framework
|
||||
```javascript
|
||||
// Multiple search UI experiments running simultaneously
|
||||
function useSearchBarStyle() {
|
||||
var searchBarStyle = abTestUtils.qt(abTestVersion, "search_bar_style_opt") || "v1";
|
||||
var isV2 = searchBarStyle === "v2";
|
||||
var isV3 = searchBarStyle === "v3";
|
||||
|
||||
return {
|
||||
isSearchBarStyleV1: searchBarStyle === "v1",
|
||||
isSearchBarStyleV2: isV2,
|
||||
isSearchBarStyleV3: isV3,
|
||||
withNewStyle: isV2 || isV3
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Active A/B Tests:**
|
||||
- Search bar style variations (v1, v2, v3)
|
||||
- Related search panel display logic
|
||||
- Personalized vs non-personalized search
|
||||
- Live search integration toggle
|
||||
- Search suggestion behavior
|
||||
|
||||
#### Search Result Types
|
||||
```javascript
|
||||
var SearchDataType = {
|
||||
Video: 1, // Regular video content
|
||||
Users: 4, // User profiles
|
||||
UserLive: 20, // Users currently live streaming
|
||||
Lives: 61 // Live stream rooms
|
||||
};
|
||||
```
|
||||
|
||||
### 🤖 Machine Learning Integration
|
||||
|
||||
#### On-Device ML Predictions
|
||||
- **Video Preloading**: ML models predict which videos users will watch next
|
||||
- **Comment Preloading**: Intelligent comment loading based on user behavior
|
||||
- **Content Recommendations**: Real-time recommendation adjustments
|
||||
- **Performance Optimization**: Device capability-based feature enabling
|
||||
|
||||
#### ML Model Management
|
||||
```javascript
|
||||
// ML prediction pipeline for video preloading
|
||||
function createMLEngine(config) {
|
||||
return Promise.all([
|
||||
require.e("35111"),
|
||||
require.e("44582"),
|
||||
require.e("8668")
|
||||
]).then(function(modules) {
|
||||
var Sibyl = modules.Sibyl;
|
||||
var engine = new Sibyl({ biz: "TIKTOK_WEB_FYP" });
|
||||
return engine.createStrategyEngine(config);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 📱 For You Page (FYP) Management
|
||||
|
||||
#### Content Delivery Optimization
|
||||
- **Batch Processing**: Efficient content loading in batches
|
||||
- **Cache Management**: Smart caching with automatic cleanup
|
||||
- **Item Tracking**: Detailed analytics on content consumption
|
||||
- **Performance Monitoring**: Real-time performance metrics
|
||||
|
||||
---
|
||||
|
||||
## Type System Analysis
|
||||
|
||||
### 🏛️ Compliance Framework
|
||||
|
||||
#### Texas Data Classification System
|
||||
The most comprehensive aspect - **1000+ data categories** for compliance:
|
||||
|
||||
```javascript
|
||||
var TexasCatalog = {
|
||||
Texas_UserData_PublicData: 1,
|
||||
Texas_UserData_ProtectedData: 2,
|
||||
Texas_UserData_BuyersData_AccountBasicInformation: 14,
|
||||
Texas_UserData_BuyersData_TransactionOrderInformation: 17,
|
||||
// ... 1000+ more categories
|
||||
};
|
||||
```
|
||||
|
||||
**Compliance Scope:**
|
||||
- User data protection (GDPR, CCPA, Texas laws)
|
||||
- Financial transaction data
|
||||
- Content moderation data
|
||||
- Infrastructure and engineering data
|
||||
- Third-party business data
|
||||
|
||||
#### Age Rating Systems
|
||||
```javascript
|
||||
var ESRBAgeRatingMaskEnum = {
|
||||
ESRB_AGE_RATING_MASK_ENUM_E: 1, // Everyone
|
||||
ESRB_AGE_RATING_MASK_ENUM_E10: 2, // Everyone 10+
|
||||
ESRB_AGE_RATING_MASK_ENUM_T: 3, // Teen
|
||||
ESRB_AGE_RATING_MASK_ENUM_M: 4, // Mature
|
||||
ESRB_AGE_RATING_MASK_ENUM_AO: 5 // Adults Only
|
||||
};
|
||||
```
|
||||
|
||||
### 🎮 Live Streaming Features
|
||||
|
||||
#### Linkmic (Co-hosting) System
|
||||
```javascript
|
||||
var LinkmicStatus = {
|
||||
DISABLE: 0,
|
||||
ENABLE: 1,
|
||||
JUST_FOLLOWING: 2, // Only followers can join
|
||||
MULTI_LINKING: 3, // Multiple guests allowed
|
||||
MULTI_LINKING_ONLY_FOLLOWING: 4
|
||||
};
|
||||
|
||||
var LinkmicUserStatus = {
|
||||
USERSTATUS_NONE: 0,
|
||||
USERSTATUS_LINKED: 1, // Currently in linkmic
|
||||
USERSTATUS_APPLYING: 2, // Requesting to join
|
||||
USERSTATUS_INVITING: 3 // Being invited
|
||||
};
|
||||
```
|
||||
|
||||
#### Battle/Competition System
|
||||
```javascript
|
||||
var BattleType = {
|
||||
NormalBattle: 1,
|
||||
TeamBattle: 2,
|
||||
IndividualBattle: 3,
|
||||
BattleType1vN: 4, // 1 vs Many battles
|
||||
TakeTheStage: 51, // Performance competitions
|
||||
GroupShow: 52, // Group performances
|
||||
Beans: 53, // Bean catching games
|
||||
GroupRankList: 54 // Ranking competitions
|
||||
};
|
||||
```
|
||||
|
||||
### 💰 Monetization Systems
|
||||
|
||||
#### Gift System
|
||||
```javascript
|
||||
var GiftBadgeType = {
|
||||
GIFT_BADGE_TYPE_CAMPAIGN_GIFT_BADGE: 1,
|
||||
GIFT_BADGE_TYPE_TRENDING_GIFT_BADGE: 2,
|
||||
GIFT_BADGE_TYPE_FANS_CLUB_GIFT_BADGE: 9,
|
||||
GIFT_BADGE_TYPE_PARTNERSHIP_GIFT_BADGE: 10,
|
||||
GIFT_BADGE_TYPE_VAULT: 15,
|
||||
GIFT_BADGE_TYPE_VIEWER_PICKS: 17
|
||||
};
|
||||
```
|
||||
|
||||
#### Subscription Tiers
|
||||
- Multiple subscription levels with different benefits
|
||||
- Creator monetization through subscriptions
|
||||
- VIP privileges and exclusive content access
|
||||
- Fan club systems with tiered benefits
|
||||
|
||||
---
|
||||
|
||||
## Technical Implementation Details
|
||||
|
||||
### 🔧 Module System
|
||||
|
||||
#### Dependency Injection
|
||||
```javascript
|
||||
// Service registration and dependency management
|
||||
var serviceContainer = {
|
||||
search: SearchService,
|
||||
user: UserService,
|
||||
live: LiveService,
|
||||
personalization: PersonalizationService
|
||||
};
|
||||
```
|
||||
|
||||
#### State Management
|
||||
- **Jotai atoms** for React state management
|
||||
- **RxJS streams** for reactive data flow
|
||||
- **Service dispatchers** for action handling
|
||||
- **Memoized selectors** for performance optimization
|
||||
|
||||
### 🌐 Internationalization
|
||||
|
||||
#### Multi-language Support
|
||||
- Dynamic language switching
|
||||
- Region-specific feature toggles
|
||||
- Localized content filtering
|
||||
- Cultural compliance adaptations
|
||||
|
||||
### 📊 Analytics & Tracking
|
||||
|
||||
#### Event Tracking System
|
||||
```javascript
|
||||
// Comprehensive user interaction tracking
|
||||
function handleSearchImpression(params) {
|
||||
analytics.sendEvent("search_impression", {
|
||||
search_type: params.searchType,
|
||||
enter_from: params.enterFrom,
|
||||
rank: params.rank,
|
||||
search_keyword: params.keyword,
|
||||
search_result_id: params.resultId,
|
||||
impr_id: params.impressionId
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Tracked Events:**
|
||||
- Search interactions and results
|
||||
- Video viewing patterns
|
||||
- Live stream engagement
|
||||
- Gift sending and receiving
|
||||
- User relationship changes
|
||||
- Content creation activities
|
||||
|
||||
---
|
||||
|
||||
## Security & Privacy
|
||||
|
||||
### 🔒 Data Protection
|
||||
|
||||
#### Privacy Controls
|
||||
- Granular data access controls
|
||||
- User consent management
|
||||
- Data retention policies
|
||||
- Cross-border data transfer restrictions
|
||||
|
||||
#### Content Safety
|
||||
```javascript
|
||||
var AuditStatus = {
|
||||
AuditStatusPass: 1,
|
||||
AuditStatusFailed: 2,
|
||||
AuditStatusReviewing: 3,
|
||||
AuditStatusForbidden: 4
|
||||
};
|
||||
```
|
||||
|
||||
### 🛡️ Moderation Systems
|
||||
|
||||
#### Automated Moderation
|
||||
- Machine learning-based content screening
|
||||
- Real-time safety checks
|
||||
- Community guidelines enforcement
|
||||
- Appeal and review processes
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### ⚡ Loading Strategies
|
||||
|
||||
#### Code Splitting
|
||||
- Dynamic imports for feature modules
|
||||
- Lazy loading of non-critical components
|
||||
- Chunk-based resource loading
|
||||
- Progressive enhancement patterns
|
||||
|
||||
#### Caching Mechanisms
|
||||
```javascript
|
||||
// Smart caching with expiration
|
||||
function getCachedH265Support() {
|
||||
var cacheTime = Number(storageUtils._S(h265TimeCacheKey, "0"));
|
||||
var currentTime = Date.now();
|
||||
|
||||
// Cache expires after ~14 days
|
||||
var cacheExpired = currentTime - cacheTime > 12096e5;
|
||||
|
||||
if (cacheExpired || cachedSupport === "") {
|
||||
// Refresh cache
|
||||
cachedH265Support = detectH265Support();
|
||||
// ... update cache
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 🎯 Preloading Intelligence
|
||||
- ML-driven content preloading
|
||||
- Predictive comment loading
|
||||
- Smart resource prefetching
|
||||
- Bandwidth-aware optimizations
|
||||
|
||||
---
|
||||
|
||||
## Business Logic Insights
|
||||
|
||||
### 💼 Creator Economy
|
||||
|
||||
#### Monetization Streams
|
||||
1. **Live Gifts** - Virtual gift purchases during streams
|
||||
2. **Subscriptions** - Monthly creator subscriptions
|
||||
3. **TikTok Shop** - E-commerce integration
|
||||
4. **Brand Partnerships** - Sponsored content
|
||||
5. **Creator Fund** - Revenue sharing program
|
||||
|
||||
#### Fan Engagement
|
||||
- Fan club systems with levels
|
||||
- Exclusive subscriber content
|
||||
- Interactive live features (polls, games)
|
||||
- Creator-fan direct messaging
|
||||
|
||||
### 🎮 Gaming Integration
|
||||
|
||||
#### Game Types
|
||||
```javascript
|
||||
var GameKind = {
|
||||
Effect: 1, // AR/VR effects
|
||||
Wmini: 2, // Mini-games
|
||||
Wgamex: 3, // Extended games
|
||||
Cloud: 4 // Cloud gaming
|
||||
};
|
||||
```
|
||||
|
||||
#### Interactive Features
|
||||
- Live gaming sessions
|
||||
- Audience participation games
|
||||
- Esports tournament integration
|
||||
- Gaming content discovery
|
||||
|
||||
---
|
||||
|
||||
## Global Compliance Strategy
|
||||
|
||||
### 📋 Regulatory Compliance
|
||||
|
||||
#### Data Localization
|
||||
- Region-specific data handling
|
||||
- Local law compliance (Texas, EU, etc.)
|
||||
- Cross-border transfer controls
|
||||
- Jurisdiction-specific features
|
||||
|
||||
#### Content Regulations
|
||||
- Age-appropriate content filtering
|
||||
- Regional content restrictions
|
||||
- Cultural sensitivity controls
|
||||
- Government compliance features
|
||||
|
||||
---
|
||||
|
||||
## Development Insights
|
||||
|
||||
### 🔍 Code Quality Observations
|
||||
|
||||
#### Strengths
|
||||
- ✅ Comprehensive type system
|
||||
- ✅ Extensive error handling
|
||||
- ✅ Performance optimizations
|
||||
- ✅ Modular architecture
|
||||
- ✅ Accessibility considerations
|
||||
|
||||
#### Areas of Complexity
|
||||
- ⚠️ Heavy obfuscation makes debugging difficult
|
||||
- ⚠️ Extensive A/B testing creates code complexity
|
||||
- ⚠️ Large bundle sizes impact initial load time
|
||||
- ⚠️ Deep dependency chains
|
||||
|
||||
### 🛠️ Technology Stack
|
||||
|
||||
#### Frontend Technologies
|
||||
- **React** - UI framework with hooks
|
||||
- **RxJS** - Reactive programming
|
||||
- **Webpack** - Module bundling
|
||||
- **TypeScript** - Type safety (compiled to JS)
|
||||
- **Jotai** - State management
|
||||
|
||||
#### Performance Technologies
|
||||
- **Web Workers** - Background processing
|
||||
- **WebAssembly** - High-performance computing
|
||||
- **Service Workers** - Caching and offline support
|
||||
- **HTTP/2** - Efficient resource loading
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### 🔧 For Developers
|
||||
|
||||
1. **Understanding the Codebase**
|
||||
- Focus on the service layer architecture
|
||||
- Study the state management patterns
|
||||
- Understand the A/B testing framework
|
||||
|
||||
2. **Performance Optimization**
|
||||
- Leverage the existing caching mechanisms
|
||||
- Understand the preloading strategies
|
||||
- Monitor the ML prediction accuracy
|
||||
|
||||
3. **Feature Development**
|
||||
- Follow the established module patterns
|
||||
- Use the existing type definitions
|
||||
- Integrate with the analytics system
|
||||
|
||||
### 🔒 For Security Analysis
|
||||
|
||||
1. **Data Flow Analysis**
|
||||
- Track data through the compliance system
|
||||
- Understand privacy control mechanisms
|
||||
- Monitor cross-border data transfers
|
||||
|
||||
2. **Content Safety**
|
||||
- Study the moderation pipeline
|
||||
- Understand the safety classification system
|
||||
- Monitor real-time safety checks
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The deobfuscated TikTok web application reveals a remarkably sophisticated platform with:
|
||||
|
||||
- **Advanced video technology** optimized for web browsers
|
||||
- **Comprehensive compliance framework** meeting global regulations
|
||||
- **Rich creator economy** with multiple monetization streams
|
||||
- **Cutting-edge ML integration** for personalization and performance
|
||||
- **Extensive live streaming features** supporting complex social interactions
|
||||
|
||||
This analysis demonstrates TikTok's commitment to technical excellence, user privacy, and global compliance while delivering a feature-rich social media experience.
|
||||
|
||||
---
|
||||
|
||||
## Technical Specifications
|
||||
|
||||
### Bundle Information
|
||||
- **Original Size**: ~1 line minified per file
|
||||
- **Deobfuscated Size**: 544+ lines (4004) + 200+ lines (11054)
|
||||
- **Module Count**: 6+ core modules in analyzed bundles
|
||||
- **Type Definitions**: 1000+ enum values
|
||||
- **Compliance Categories**: 1000+ data classification types
|
||||
|
||||
### Browser Compatibility
|
||||
- Modern browsers with ES6+ support
|
||||
- WebAssembly support for ML features
|
||||
- MediaSource Extensions for video streaming
|
||||
- Service Worker support for caching
|
||||
|
||||
---
|
||||
|
||||
*Generated from deobfuscation analysis of TikTok web application JavaScript bundles*
|
||||
1
reference/tiktok/files/11054.689f275a.js
Normal file
1
reference/tiktok/files/11054.689f275a.js
Normal file
File diff suppressed because one or more lines are too long
708
reference/tiktok/files/11054.689f275a_deobfuscated.js
Normal file
708
reference/tiktok/files/11054.689f275a_deobfuscated.js
Normal file
@ -0,0 +1,708 @@
|
||||
/**
|
||||
* TikTok Web Application - Type Definitions and Enums Bundle
|
||||
* Original file: 11054.689f275a.js
|
||||
*
|
||||
* This bundle contains comprehensive type definitions, enums, and constants for:
|
||||
* - Live streaming features (linkmic, battles, gifts, etc.)
|
||||
* - User management and permissions
|
||||
* - Content moderation and compliance
|
||||
* - Search functionality
|
||||
* - Video and audio processing
|
||||
* - E-commerce integration
|
||||
* - Gaming features
|
||||
* - Analytics and tracking
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// Initialize loadable chunks array
|
||||
(self.__LOADABLE_LOADED_CHUNKS__ = self.__LOADABLE_LOADED_CHUNKS__ || []).push([["11054"], {
|
||||
|
||||
/**
|
||||
* Module 82793: Main Type Definitions Export
|
||||
* Central module that exports all type definitions and enums
|
||||
*/
|
||||
82793: function(exports, module, require) {
|
||||
require.d(exports, {
|
||||
d: function() { return LiveModuleClass; }
|
||||
});
|
||||
|
||||
// Import required dependencies
|
||||
var moduleBase = require(48748);
|
||||
var classUtils = require(95170);
|
||||
var inheritanceUtils = require(7120);
|
||||
var objectUtils = require(5377);
|
||||
var assignUtils = require(45996);
|
||||
var typeUtils = require(79262);
|
||||
var userUtils = require(91781);
|
||||
var deviceUtils = require(36075);
|
||||
|
||||
/**
|
||||
* EXEMPTION TYPES
|
||||
* Data exemption categories for compliance
|
||||
*/
|
||||
var ExemptionType = {
|
||||
Dsl: 1,
|
||||
Encrypted_Data: 2,
|
||||
DeprecatedEmptyField: 3,
|
||||
NonUsData: 4,
|
||||
Bytes: 5
|
||||
};
|
||||
|
||||
/**
|
||||
* TEXAS CATALOG
|
||||
* Data classification system for Texas compliance
|
||||
*/
|
||||
var TexasCatalog = {
|
||||
Texas_Unknown: 0,
|
||||
Texas_UserData_PublicData: 1,
|
||||
Texas_UserData_ProtectedData: 2,
|
||||
Texas_UserData_ExceptedDataInteroperabilityData_IDFields: 3,
|
||||
Texas_UserData_ExceptedDataInteroperabilityData_UserStatus: 4,
|
||||
Texas_UserData_ExceptedDataInteroperabilityData_VideoStatus: 5,
|
||||
Texas_UserData_ExceptedDataInteroperabilityData_BlockOrUnblockList: 6,
|
||||
Texas_UserData_ExceptedDataInteroperabilityData_VideoCommentStatus: 7,
|
||||
Texas_UserData_ExceptedDataInteroperabilityData_LiveRoomStatus: 8,
|
||||
Texas_UserData_ExceptedDataInteroperabilityData_UserOrContentSafetyStatus: 9,
|
||||
Texas_UserData_ExceptedDataInteroperabilityData_PermissionSettings: 10,
|
||||
Texas_UserData_ExceptedDataInteroperabilityData_SocialInteractionActivity: 11,
|
||||
Texas_UserData_ExceptedDataInteroperabilityData_ContentCharacteristics: 12,
|
||||
Texas_UserData_ExceptedDataInteroperabilityData_EventTime: 13,
|
||||
Texas_UserData_BuyersData_AccountBasicInformation: 14,
|
||||
Texas_UserData_BuyersData_AccountContactInformation: 15,
|
||||
Texas_UserData_BuyersData_AccountPaymentMethod: 16,
|
||||
Texas_UserData_BuyersData_TransactionOrderInformation: 17,
|
||||
Texas_UserData_BuyersData_TransactionCustomerService: 18,
|
||||
Texas_UserData_BuyersData_LogisticsOrderInfo: 19
|
||||
// ... (extensive list continues with 1000+ entries)
|
||||
};
|
||||
|
||||
/**
|
||||
* TIKTOK CATALOG
|
||||
* Main TikTok data classification system
|
||||
*/
|
||||
var TikTokCatalog = {
|
||||
TikTok_Unknown: 0,
|
||||
TikTok_TikTok_UserCore_BaseInfo: 1,
|
||||
TikTok_TikTok_UserCore_Settings: 2,
|
||||
TikTok_TikTok_UserCore_FeatureAndTag: 3,
|
||||
TikTok_TikTok_UserCore_AccountPrivilege: 4,
|
||||
TikTok_TikTok_UserCore_DataEarth: 5,
|
||||
TikTok_TikTok_UserCore_UMP: 6,
|
||||
TikTok_TikTok_UserCore_Recommendation: 7,
|
||||
TikTok_TikTok_Exploring_Hashtag: 8,
|
||||
TikTok_TikTok_Exploring_Playlist: 9,
|
||||
TikTok_TikTok_Exploring_ChallengeCount: 10,
|
||||
TikTok_TikTok_Exploring_MusicCount: 11,
|
||||
TikTok_TikTok_Exploring_MVCount: 12,
|
||||
TikTok_TikTok_Exploring_StickerCount: 13,
|
||||
TikTok_TikTok_Exploring_AnchorCount: 14,
|
||||
TikTok_TikTok_Exploring_PlaylistCount: 15,
|
||||
TikTok_TikTok_Exploring_VoteCount: 16,
|
||||
TikTok_TikTok_Exploring_POI: 17,
|
||||
TikTok_TikTok_Exploring_CreationList: 18,
|
||||
TikTok_TikTok_Exploring_HashtagList: 19,
|
||||
TikTok_TikTok_Exploring_HashtagBasedItemList: 20
|
||||
// ... (extensive list continues with 1000+ entries covering all TikTok features)
|
||||
};
|
||||
|
||||
/**
|
||||
* AUDIENCE TYPES
|
||||
* User audience classification
|
||||
*/
|
||||
var Audience = {
|
||||
AUDIENCE_UNSPECIFIED: 0,
|
||||
USER: 1,
|
||||
DEV: 2
|
||||
};
|
||||
|
||||
/**
|
||||
* AUTHENTICATION LEVELS
|
||||
* User authentication status levels
|
||||
*/
|
||||
var AuthLevel = {
|
||||
AUTH_LEVEL_UNSPECIFIED: 0,
|
||||
UN_AUTH: 1,
|
||||
AUTHENTICATED: 2,
|
||||
AUTHORIZED: 3
|
||||
};
|
||||
|
||||
/**
|
||||
* HASHTAG NAMESPACES
|
||||
* Different hashtag categorization systems
|
||||
*/
|
||||
var HashtagNamespace = {
|
||||
Global: 0,
|
||||
Gaming: 1
|
||||
};
|
||||
|
||||
/**
|
||||
* AGE RATING SYSTEMS
|
||||
* ESRB and PEGI age rating enums
|
||||
*/
|
||||
var ESRBAgeRatingMaskEnum = {
|
||||
ESRB_AGE_RATING_MASK_ENUM_UNKNOWN: 0,
|
||||
ESRB_AGE_RATING_MASK_ENUM_E: 1,
|
||||
ESRB_AGE_RATING_MASK_ENUM_E10: 2,
|
||||
ESRB_AGE_RATING_MASK_ENUM_T: 3,
|
||||
ESRB_AGE_RATING_MASK_ENUM_M: 4,
|
||||
ESRB_AGE_RATING_MASK_ENUM_AO: 5
|
||||
};
|
||||
|
||||
var PEGIAgeRatingMaskEnum = {
|
||||
PEGI_AGE_RATING_MASK_ENUM_UNKNOWN: 0,
|
||||
PEGI_AGE_RATING_MASK_ENUM_3: 1,
|
||||
PEGI_AGE_RATING_MASK_ENUM_7: 2,
|
||||
PEGI_AGE_RATING_MASK_ENUM_12: 3,
|
||||
PEGI_AGE_RATING_MASK_ENUM_16: 4,
|
||||
PEGI_AGE_RATING_MASK_ENUM_18: 5
|
||||
};
|
||||
|
||||
/**
|
||||
* LINKMIC (LIVE INTERACTION) ENUMS
|
||||
* Live streaming interaction features
|
||||
*/
|
||||
var LinkmicVendor = {
|
||||
UNKNOWN: 0,
|
||||
AGORO: 1,
|
||||
ZEGO: 2,
|
||||
BYTE: 4,
|
||||
TWILIO: 8
|
||||
};
|
||||
|
||||
var LinkmicStatus = {
|
||||
DISABLE: 0,
|
||||
ENABLE: 1,
|
||||
JUST_FOLLOWING: 2,
|
||||
MULTI_LINKING: 3,
|
||||
MULTI_LINKING_ONLY_FOLLOWING: 4
|
||||
};
|
||||
|
||||
var LinkmicUserStatus = {
|
||||
USERSTATUS_NONE: 0,
|
||||
USERSTATUS_LINKED: 1,
|
||||
USERSTATUS_APPLYING: 2,
|
||||
USERSTATUS_INVITING: 3
|
||||
};
|
||||
|
||||
var LinkmicPlayType = {
|
||||
PLAYTYPE_INVITE: 0,
|
||||
PLAYTYPE_APPLY: 1,
|
||||
PLAYTYPE_RESERVE: 2,
|
||||
PLAYTYPE_OFFLIVE: 3,
|
||||
PLAYTYPE_OFFLINE: 4
|
||||
};
|
||||
|
||||
/**
|
||||
* MUTE STATUS
|
||||
* Audio mute states
|
||||
*/
|
||||
var MuteStatus = {
|
||||
MUTE: 0,
|
||||
UNMUTE: 1
|
||||
};
|
||||
|
||||
/**
|
||||
* COHOST PERMISSION TYPES
|
||||
* Different levels of co-hosting permissions
|
||||
*/
|
||||
var CoHostPermissoinType = {
|
||||
NO_PERM: 0,
|
||||
COHOST_PERM: 1,
|
||||
MULTIHOST_PERM: 2
|
||||
};
|
||||
|
||||
/**
|
||||
* BATTLE SYSTEM ENUMS
|
||||
* Live streaming battle/competition features
|
||||
*/
|
||||
var BattleStatus = {
|
||||
BattleNotStarted: 0,
|
||||
BattleStarted: 1,
|
||||
BattleFinished: 2,
|
||||
BattlePunishStarted: 3,
|
||||
BattlePunishFinished: 4
|
||||
};
|
||||
|
||||
var BattleType = {
|
||||
UnknownBattleType: 0,
|
||||
NormalBattle: 1,
|
||||
TeamBattle: 2,
|
||||
IndividualBattle: 3,
|
||||
BattleType1vN: 4,
|
||||
TakeTheStage: 51,
|
||||
GroupShow: 52,
|
||||
Beans: 53,
|
||||
GroupRankList: 54
|
||||
};
|
||||
|
||||
var BattleScene = {
|
||||
BATTLE_SCENE_NORMAL: 0,
|
||||
BATTLE_SCENE_TEAM_PAIR: 1
|
||||
};
|
||||
|
||||
/**
|
||||
* GIFT SYSTEM ENUMS
|
||||
* Virtual gift and monetization features
|
||||
*/
|
||||
var GiftTypeServer = {
|
||||
UnknownGiftType: 0,
|
||||
SmallGiftType: 1,
|
||||
BigGiftType: 2,
|
||||
LuckyMoneyGiftType: 3,
|
||||
FaceRecognitionGiftType: 4
|
||||
};
|
||||
|
||||
var GiftBadgeType = {
|
||||
GIFT_BADGE_TYPE_DEFAULT_GIFT_BADGE: 0,
|
||||
GIFT_BADGE_TYPE_CAMPAIGN_GIFT_BADGE: 1,
|
||||
GIFT_BADGE_TYPE_TRENDING_GIFT_BADGE: 2,
|
||||
GIFT_BADGE_TYPE_NEW_GIFT_BADGE: 3,
|
||||
GIFT_BADGE_TYPE_RANDOM_GIFT_BADGE: 4,
|
||||
GIFT_BADGE_TYPE_COLOR_GIFT_BADGE: 5,
|
||||
GIFT_BADGE_TYPE_AUDIO_GIFT_BADGE: 6,
|
||||
GIFT_BADGE_TYPE_UNIVERSE_GIFT_BADGE: 7,
|
||||
GIFT_BADGE_TYPE_GLUP_GIFT_BADGE: 8,
|
||||
GIFT_BADGE_TYPE_FANS_CLUB_GIFT_BADGE: 9,
|
||||
GIFT_BADGE_TYPE_PARTNERSHIP_GIFT_BADGE: 10,
|
||||
GIFT_BADGE_TYPE_CHRISTMAS_GIFT_BADGE: 11,
|
||||
GIFT_BADGE_TYPE_CUSTOM_GIFT_BADGE: 12,
|
||||
GIFT_BADGE_TYPE_GALLERY_GIFTER: 13,
|
||||
GIFT_BADGE_TYPE_PK: 14,
|
||||
GIFT_BADGE_TYPE_VAULT: 15,
|
||||
GIFT_BADGE_TYPE_LIVE_GOAL: 16,
|
||||
GIFT_BADGE_TYPE_VIEWER_PICKS: 17
|
||||
};
|
||||
|
||||
/**
|
||||
* SUBSCRIPTION SYSTEM ENUMS
|
||||
* Creator subscription and monetization features
|
||||
*/
|
||||
var SubBenefitEnableStatus = {
|
||||
SubBenefitEnableStatusUnknown: 0,
|
||||
SubBenefitEnableStatusEnable: 1,
|
||||
SubBenefitEnableStatusPending: 2,
|
||||
SubBenefitEnableStatusDisable: 3,
|
||||
SubBenefitEnableStatusLackPermission: 10
|
||||
};
|
||||
|
||||
var SubscriptionFontStyle = {
|
||||
SubscriptionFontStyle_Normal: 0,
|
||||
SubscriptionFontStyle_Bold: 1
|
||||
};
|
||||
|
||||
/**
|
||||
* GAME INTEGRATION ENUMS
|
||||
* Gaming features and integration
|
||||
*/
|
||||
var GameKind = {
|
||||
GameKindUnknown: 0,
|
||||
Effect: 1,
|
||||
Wmini: 2,
|
||||
Wgamex: 3,
|
||||
Cloud: 4
|
||||
};
|
||||
|
||||
var GameTaskStatus = {
|
||||
GAME_TASK_STATUS_UNKNOWN: 0,
|
||||
GAME_TASK_STATUS_LOCKED: 1,
|
||||
GAME_TASK_STATUS_READY: 2,
|
||||
GAME_TASK_STATUS_RUNNING: 3,
|
||||
GAME_TASK_STATUS_DONE: 4,
|
||||
GAME_TASK_STATUS_FAILED: 5
|
||||
};
|
||||
|
||||
/**
|
||||
* CONTENT MODERATION ENUMS
|
||||
* Content safety and moderation systems
|
||||
*/
|
||||
var AuditStatus = {
|
||||
AuditStatusUnknown: 0,
|
||||
AuditStatusPass: 1,
|
||||
AuditStatusFailed: 2,
|
||||
AuditStatusReviewing: 3,
|
||||
AuditStatusForbidden: 4
|
||||
};
|
||||
|
||||
var VideoReviewLabelType = {
|
||||
VIDEO_REVIEW_LABEL_TYPE_UNKNOWN: 0,
|
||||
VIDEO_REVIEW_LABEL_TYPE_APPROVED: 1,
|
||||
VIDEO_REVIEW_LABEL_TYPE_UNDER_REVIEW: 2,
|
||||
VIDEO_REVIEW_LABEL_TYPE_REJECTED: 3,
|
||||
VIDEO_REVIEW_LABEL_TYPE_CONFIRM_AUDIO: 4,
|
||||
VIDEO_REVIEW_LABEL_TYPE_STRIPPING_FAILED: 5
|
||||
};
|
||||
|
||||
/**
|
||||
* LIVE STREAMING ENUMS
|
||||
* Live room and streaming features
|
||||
*/
|
||||
var LiveRoomMode = {
|
||||
LiveRoomModeNormal: 0,
|
||||
LiveRoomModeOBS: 1,
|
||||
LiveRoomModeMedia: 2,
|
||||
LiveRoomModeAudio: 3,
|
||||
LiveRoomModeScreen: 4,
|
||||
LiveRoomModeLiveStudio: 6,
|
||||
LiveRoomModeLiveVoice: 7
|
||||
};
|
||||
|
||||
var RoomType = {
|
||||
RoomType_Unknown: 0,
|
||||
RoomType_HorizontalScreen_AlienationRoom: 1,
|
||||
RoomType_VerticalScreen_AlienationRoom: 2,
|
||||
RoomType_HorizontalScreen_NormalRoom: 3,
|
||||
RoomType_VerticalScreen_NormalRoom: 4
|
||||
};
|
||||
|
||||
/**
|
||||
* NOTIFICATION SYSTEM ENUMS
|
||||
* In-app notification and messaging
|
||||
*/
|
||||
var NotifyScene = {
|
||||
NotifyScene_Unknown: 0,
|
||||
NotifyScene_GiftGallery_ClickToFull: 1,
|
||||
NotifyScene_GiftGallery_NewGift: 2,
|
||||
NotifyScene_FollowButton_ExpandGuide: 3,
|
||||
NotifyScene_FansClub_ClaimGuide: 4,
|
||||
NotifyScene_FansClub_LevelUpGuide: 5,
|
||||
NotifyScene_CloseGuide_Close: 6,
|
||||
NotifyScene_LiveGoal_RemindSet: 7,
|
||||
NotifyScene_LiveGoal_StartInLive: 8,
|
||||
NotifyScene_LiveGoal_StartBeforeLive: 9,
|
||||
NotifyScene_LiveGoal_Finish: 10
|
||||
// ... (100+ more notification scenarios)
|
||||
};
|
||||
|
||||
var NotifyComponent = {
|
||||
NotifyComponent_Unknown: 0,
|
||||
NotifyComponent_MidTouch: 1,
|
||||
NotifyComponent_RankList: 2,
|
||||
NotifyComponent_Banner: 3,
|
||||
NotifyComponent_RealtimeLiveCenter: 4,
|
||||
NotifyComponent_AdvanceMessage: 5
|
||||
};
|
||||
|
||||
/**
|
||||
* E-COMMERCE ENUMS
|
||||
* TikTok Shop and commerce features
|
||||
*/
|
||||
var BusinessType = {
|
||||
BusinessType_Unknown: 0,
|
||||
BusinessType_CF: 1,
|
||||
BusinessType_TCM: 2,
|
||||
BusinessType_SHOUTOUTS: 3,
|
||||
BusinessType_TIKTOK_SHOP: 4,
|
||||
BusinessType_MAGIC: 5,
|
||||
BusinessType_LIVE_ACCEL: 6,
|
||||
BusinessType_TCM_ID: 7,
|
||||
BusinessType_TCM_TH: 8,
|
||||
BusinessType_CREATOR_PLUS: 9,
|
||||
BusinessType_TCM_US: 10,
|
||||
BusinessType_EVENT_TICKET: 11,
|
||||
BusinessType_TIKTOK_SHOP_ID: 12
|
||||
// ... (more business types)
|
||||
};
|
||||
|
||||
/**
|
||||
* SEARCH SYSTEM ENUMS
|
||||
* Search functionality and result types
|
||||
*/
|
||||
var SearchResultType = {
|
||||
NORMAL: "0",
|
||||
NEW: "1",
|
||||
HOT: "2",
|
||||
RECOM: "3",
|
||||
EXCLUSIVE: "4",
|
||||
LOCAL: "5",
|
||||
BURST: "6",
|
||||
BOIL: "7",
|
||||
WENDACLUE: "8",
|
||||
LONGVIDEO: "9",
|
||||
LIVE: "10",
|
||||
UPDATE: "15",
|
||||
BUBBLEWORDS: "16",
|
||||
SAV: "17",
|
||||
MUSIC: "18"
|
||||
};
|
||||
|
||||
var SearchDataType = {
|
||||
Video: 1,
|
||||
Users: 4,
|
||||
UserLive: 20,
|
||||
Lives: 61
|
||||
};
|
||||
|
||||
/**
|
||||
* LIVE MODULE CLASS
|
||||
* Main class for managing live streaming functionality
|
||||
*/
|
||||
var LiveModuleClass = function(baseClass) {
|
||||
function LiveModule(userModule) {
|
||||
var instance;
|
||||
classUtils._(this, LiveModule);
|
||||
instance = moduleBase._(this, LiveModule);
|
||||
instance.userModule = userModule;
|
||||
instance.defaultState = {};
|
||||
return instance;
|
||||
}
|
||||
|
||||
inheritanceUtils._(LiveModule, baseClass);
|
||||
|
||||
var prototype = LiveModule.prototype;
|
||||
|
||||
/**
|
||||
* Set a live room item in the state
|
||||
*/
|
||||
prototype.setItem = function(state, roomData) {
|
||||
if (roomData.id_str) {
|
||||
state[roomData.id_str] = roomData;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update an existing live room item
|
||||
*/
|
||||
prototype.updateItem = function(state, updateData) {
|
||||
if (updateData.id_str && state[updateData.id_str]) {
|
||||
state[updateData.id_str] = assignUtils.A(state[updateData.id_str], updateData);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set multiple live room items at once
|
||||
*/
|
||||
prototype.multiSetItem = function(state, roomDataArray) {
|
||||
var self = this;
|
||||
roomDataArray.forEach(function(roomData) {
|
||||
return self.setItem(state, roomData);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add live room items and associated user data
|
||||
*/
|
||||
prototype.addItems = function(roomDataArray) {
|
||||
var userDataArray = roomDataArray
|
||||
.filter(function(room) { return !!room.owner; })
|
||||
.map(function(room) {
|
||||
var owner = room.owner;
|
||||
return userUtils.bg(assignUtils._(objectUtils._(objectUtils._({}, owner), {
|
||||
unique_id: owner ? owner.display_id : undefined,
|
||||
custom_verify: owner ? owner.authentication_info : undefined,
|
||||
room_id: room.id_str
|
||||
})));
|
||||
});
|
||||
|
||||
return [
|
||||
this.userModule.getActions().multiSetUser(userDataArray),
|
||||
this.getActions().multiSetItem(roomDataArray)
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Mark a video as deleted
|
||||
*/
|
||||
prototype.setDeleteVideo = function(state, videoId) {
|
||||
if (state[videoId]) {
|
||||
state[videoId] = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
return LiveModule;
|
||||
}(moduleBase.E);
|
||||
|
||||
// Apply decorators and metadata
|
||||
function applyDecorators(decorators, target, propertyKey, descriptor) {
|
||||
var result;
|
||||
var argumentCount = arguments.length;
|
||||
var currentDescriptor = argumentCount < 3 ? target :
|
||||
descriptor === null ? descriptor = Object.getOwnPropertyDescriptor(target, propertyKey) :
|
||||
descriptor;
|
||||
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
||||
currentDescriptor = Reflect.decorate(decorators, target, propertyKey, descriptor);
|
||||
} else {
|
||||
for (var i = decorators.length - 1; i >= 0; i--) {
|
||||
if (result = decorators[i]) {
|
||||
currentDescriptor = (argumentCount < 3 ? result(currentDescriptor) :
|
||||
argumentCount > 3 ? result(target, propertyKey, currentDescriptor) :
|
||||
result(target, propertyKey)) || currentDescriptor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (argumentCount > 3 && currentDescriptor) {
|
||||
Object.defineProperty(target, propertyKey, currentDescriptor);
|
||||
}
|
||||
|
||||
return currentDescriptor;
|
||||
}
|
||||
|
||||
// Apply method decorators
|
||||
applyDecorators([
|
||||
// Action decorator for setItem method
|
||||
{ h5: function() { return function() {}; } }
|
||||
], LiveModuleClass.prototype, "setItem", null);
|
||||
|
||||
applyDecorators([
|
||||
// Action decorator for updateItem method
|
||||
{ h5: function() { return function() {}; } }
|
||||
], LiveModuleClass.prototype, "updateItem", null);
|
||||
|
||||
applyDecorators([
|
||||
// Action decorator for multiSetItem method
|
||||
{ h5: function() { return function() {}; } }
|
||||
], LiveModuleClass.prototype, "multiSetItem", null);
|
||||
|
||||
applyDecorators([
|
||||
// Action decorator for setDeleteVideo method
|
||||
{ h5: function() { return function() {}; } }
|
||||
], LiveModuleClass.prototype, "setDeleteVideo", null);
|
||||
|
||||
// Apply class decorator
|
||||
LiveModuleClass = applyDecorators([
|
||||
// Module decorator
|
||||
{ nV: function(name) { return function() {}; } }
|
||||
], LiveModuleClass);
|
||||
|
||||
/**
|
||||
* EXPORT STRUCTURE
|
||||
* Organize all type definitions into logical groups
|
||||
*/
|
||||
var typeDefinitions = {};
|
||||
|
||||
// Annotations namespace
|
||||
var annotations = {};
|
||||
annotations.ExemptionType = ExemptionType;
|
||||
annotations.TexasCatalog = TexasCatalog;
|
||||
annotations.TikTokCatalog = TikTokCatalog;
|
||||
|
||||
// Common namespace
|
||||
var common = {};
|
||||
common.default = LiveModuleClass;
|
||||
|
||||
// Compliance namespace
|
||||
var compliance = {};
|
||||
compliance.Audience = Audience;
|
||||
compliance.AuthLevel = AuthLevel;
|
||||
|
||||
// Image namespace
|
||||
var image = {};
|
||||
image.annotations = annotations;
|
||||
image.common = common;
|
||||
image.compliance = compliance;
|
||||
|
||||
// Hashtag namespace
|
||||
var hashtag = {};
|
||||
hashtag.ESRBAgeRatingMaskEnum = ESRBAgeRatingMaskEnum;
|
||||
hashtag.HashtagNamespace = HashtagNamespace;
|
||||
hashtag.PEGIAgeRatingMaskEnum = PEGIAgeRatingMaskEnum;
|
||||
hashtag.annotations = annotations;
|
||||
hashtag.common = common;
|
||||
hashtag.image = image;
|
||||
|
||||
// Linkmic namespace
|
||||
var linkmic = {};
|
||||
linkmic.LinkmicVendor = LinkmicVendor;
|
||||
linkmic.LinkmicStatus = LinkmicStatus;
|
||||
linkmic.LinkmicUserStatus = LinkmicUserStatus;
|
||||
linkmic.LinkmicPlayType = LinkmicPlayType;
|
||||
linkmic.MuteStatus = MuteStatus;
|
||||
linkmic.CoHostPermissoinType = CoHostPermissoinType;
|
||||
linkmic.annotations = annotations;
|
||||
linkmic.common = common;
|
||||
linkmic.hashtag = hashtag;
|
||||
linkmic.image = image;
|
||||
|
||||
// Battle namespace
|
||||
var battle = {};
|
||||
battle.BattleStatus = BattleStatus;
|
||||
battle.BattleType = BattleType;
|
||||
battle.BattleScene = BattleScene;
|
||||
battle.annotations = annotations;
|
||||
battle.image = image;
|
||||
battle.linkmic = linkmic;
|
||||
|
||||
// Gift namespace
|
||||
var gift = {};
|
||||
gift.GiftTypeServer = GiftTypeServer;
|
||||
gift.GiftBadgeType = GiftBadgeType;
|
||||
gift.annotations = annotations;
|
||||
gift.common = common;
|
||||
gift.compliance = compliance;
|
||||
gift.image = image;
|
||||
|
||||
// User namespace
|
||||
var user = {};
|
||||
user.annotations = annotations;
|
||||
user.common = common;
|
||||
user.compliance = compliance;
|
||||
user.image = image;
|
||||
user.linkmic = linkmic;
|
||||
user.battle = battle;
|
||||
user.gift = gift;
|
||||
|
||||
// Main live namespace
|
||||
var live = {};
|
||||
live.annotations = annotations;
|
||||
live.common = common;
|
||||
live.compliance = compliance;
|
||||
live.image = image;
|
||||
live.user = user;
|
||||
live.gift = gift;
|
||||
live.battle = battle;
|
||||
live.linkmic = linkmic;
|
||||
live.hashtag = hashtag;
|
||||
|
||||
// Export the complete type system
|
||||
return LiveModuleClass;
|
||||
}
|
||||
|
||||
}]);
|
||||
|
||||
/**
|
||||
* SUMMARY OF TYPE SYSTEM
|
||||
*
|
||||
* This deobfuscated file reveals TikTok's comprehensive type system including:
|
||||
*
|
||||
* 1. COMPLIANCE FRAMEWORK:
|
||||
* - Texas data classification (1000+ categories)
|
||||
* - GDPR/privacy compliance types
|
||||
* - Age rating systems (ESRB, PEGI)
|
||||
* - Data exemption categories
|
||||
*
|
||||
* 2. LIVE STREAMING FEATURES:
|
||||
* - Multi-user linkmic (co-hosting)
|
||||
* - Battle/competition systems
|
||||
* - Gift and monetization systems
|
||||
* - Room management and permissions
|
||||
*
|
||||
* 3. CONTENT SYSTEMS:
|
||||
* - Video/audio processing states
|
||||
* - Content moderation workflows
|
||||
* - Search result categorization
|
||||
* - Hashtag and music systems
|
||||
*
|
||||
* 4. USER MANAGEMENT:
|
||||
* - Authentication levels
|
||||
* - Permission systems
|
||||
* - Subscription tiers
|
||||
* - Creator monetization
|
||||
*
|
||||
* 5. GAMING INTEGRATION:
|
||||
* - Game types and states
|
||||
* - Task management
|
||||
* - Reward systems
|
||||
* - Esports features
|
||||
*
|
||||
* 6. E-COMMERCE:
|
||||
* - TikTok Shop integration
|
||||
* - Payment processing
|
||||
* - Product management
|
||||
* - Order fulfillment
|
||||
*
|
||||
* This type system demonstrates TikTok's sophisticated architecture
|
||||
* supporting complex social media, commerce, and entertainment features
|
||||
* while maintaining strict compliance with global data protection laws.
|
||||
*/
|
||||
2
reference/tiktok/files/3813.1e571ef0.js
Normal file
2
reference/tiktok/files/3813.1e571ef0.js
Normal file
File diff suppressed because one or more lines are too long
266
reference/tiktok/files/3813.1e571ef0_deobfuscated.js
Normal file
266
reference/tiktok/files/3813.1e571ef0_deobfuscated.js
Normal file
@ -0,0 +1,266 @@
|
||||
/**
|
||||
* TikTok Utility Bundle - Deobfuscated JavaScript
|
||||
* Original file: 3813.1e571ef0.js
|
||||
*
|
||||
* This bundle contains core utility modules for:
|
||||
* - Error handling and invariant checking
|
||||
* - Dynamic script loading
|
||||
* - PropTypes validation (React development)
|
||||
* - Core utility functions for the TikTok web application
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// Initialize loadable chunks array
|
||||
(self.__LOADABLE_LOADED_CHUNKS__ = self.__LOADABLE_LOADED_CHUNKS__ || []).push([["3813"], {
|
||||
|
||||
/**
|
||||
* Module 6085: Invariant Error Handler
|
||||
* Core error handling utility for development and production
|
||||
*/
|
||||
6085: function(exports) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Invariant function for error checking
|
||||
* Used throughout TikTok's codebase for assertions and error handling
|
||||
*
|
||||
* @param {boolean} condition - Condition to check
|
||||
* @param {string} message - Error message template with %s placeholders
|
||||
* @param {...any} args - Arguments to replace %s placeholders
|
||||
* @throws {Error} Throws InvariantViolation error if condition is false
|
||||
*/
|
||||
exports.exports = function invariant(condition, message, arg1, arg2, arg3, arg4, arg5, arg6) {
|
||||
if (!condition) {
|
||||
var error;
|
||||
|
||||
if (message === undefined) {
|
||||
// Production mode - generic error message
|
||||
error = new Error(
|
||||
"Minified exception occurred; use the non-minified dev environment " +
|
||||
"for the full error message and additional helpful warnings."
|
||||
);
|
||||
} else {
|
||||
// Development mode - detailed error message
|
||||
var args = [arg1, arg2, arg3, arg4, arg5, arg6];
|
||||
var argIndex = 0;
|
||||
|
||||
error = new Error(
|
||||
message.replace(/%s/g, function() {
|
||||
return args[argIndex++];
|
||||
})
|
||||
);
|
||||
error.name = "Invariant Violation";
|
||||
}
|
||||
|
||||
// Set framesToPop for better stack traces
|
||||
error.framesToPop = 1;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Module 57971: Dynamic Script Loader
|
||||
* Utility for dynamically loading JavaScript files
|
||||
*/
|
||||
57971: function(exports) {
|
||||
/**
|
||||
* Set up load/error event handlers for script element
|
||||
* @param {HTMLScriptElement} scriptElement - Script element to set up
|
||||
* @param {Function} callback - Callback function (error, element)
|
||||
*/
|
||||
function setupScriptHandlers(scriptElement, callback) {
|
||||
scriptElement.onload = function() {
|
||||
// Clean up event handlers
|
||||
this.onerror = this.onload = null;
|
||||
callback(null, scriptElement);
|
||||
};
|
||||
|
||||
scriptElement.onerror = function() {
|
||||
// Clean up event handlers
|
||||
this.onerror = this.onload = null;
|
||||
callback(new Error("Failed to load " + this.src), scriptElement);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically load a JavaScript file
|
||||
* @param {string} src - Script source URL
|
||||
* @param {Object|Function} options - Loading options or callback
|
||||
* @param {Function} callback - Callback function
|
||||
*/
|
||||
exports.exports = function loadScript(src, options, callback) {
|
||||
var head = document.head || document.getElementsByTagName("head")[0];
|
||||
var scriptElement = document.createElement("script");
|
||||
|
||||
// Handle function as second parameter
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
// Default callback
|
||||
callback = callback || function() {};
|
||||
options = options || {};
|
||||
|
||||
// Set script attributes
|
||||
scriptElement.type = options.type || "text/javascript";
|
||||
scriptElement.charset = options.charset || "utf8";
|
||||
scriptElement.async = !("async" in options) || !!options.async;
|
||||
scriptElement.src = src;
|
||||
|
||||
// Set custom attributes if provided
|
||||
if (options.attrs) {
|
||||
setAttributes(scriptElement, options.attrs);
|
||||
}
|
||||
|
||||
// Set script text content if provided
|
||||
if (options.text) {
|
||||
scriptElement.text = "" + options.text;
|
||||
}
|
||||
|
||||
// Set up appropriate event handlers based on browser support
|
||||
var handlerFunction = ("onload" in scriptElement) ?
|
||||
setupScriptHandlers :
|
||||
setupLegacyScriptHandlers;
|
||||
|
||||
handlerFunction(scriptElement, callback);
|
||||
|
||||
// Fallback for scripts without onload support
|
||||
if (!scriptElement.onload) {
|
||||
setupScriptHandlers(scriptElement, callback);
|
||||
}
|
||||
|
||||
// Add script to document head
|
||||
head.appendChild(scriptElement);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set multiple attributes on an element
|
||||
* @param {HTMLElement} element - Target element
|
||||
* @param {Object} attributes - Attributes to set
|
||||
*/
|
||||
function setAttributes(element, attributes) {
|
||||
for (var attributeName in attributes) {
|
||||
element.setAttribute(attributeName, attributes[attributeName]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy script handler for older browsers
|
||||
* @param {HTMLScriptElement} scriptElement - Script element
|
||||
* @param {Function} callback - Callback function
|
||||
*/
|
||||
function setupLegacyScriptHandlers(scriptElement, callback) {
|
||||
scriptElement.onreadystatechange = function() {
|
||||
if (this.readyState === "complete" || this.readyState === "loaded") {
|
||||
this.onreadystatechange = null;
|
||||
callback(null, scriptElement);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Module 77298: PropTypes Validation System
|
||||
* React PropTypes validation utilities for development
|
||||
*/
|
||||
77298: function(exports, module, require) {
|
||||
"use strict";
|
||||
|
||||
var ReactPropTypesSecret = require(31649);
|
||||
|
||||
/**
|
||||
* Empty function for production mode
|
||||
*/
|
||||
function emptyFunction() {}
|
||||
|
||||
/**
|
||||
* Empty function with resetWarningCache method
|
||||
*/
|
||||
function emptyFunctionWithReset() {}
|
||||
emptyFunctionWithReset.resetWarningCache = emptyFunction;
|
||||
|
||||
/**
|
||||
* PropTypes factory function
|
||||
* Creates PropTypes validators for React components
|
||||
*/
|
||||
exports.exports = function createPropTypes() {
|
||||
/**
|
||||
* PropType validator function
|
||||
* @param {any} props - Component props
|
||||
* @param {string} propName - Property name being validated
|
||||
* @param {string} componentName - Component name
|
||||
* @param {string} location - Location of the prop (e.g., 'prop', 'context')
|
||||
* @param {string} propFullName - Full property name
|
||||
* @param {string} secret - React PropTypes secret for validation
|
||||
*/
|
||||
function propTypeValidator(props, propName, componentName, location, propFullName, secret) {
|
||||
if (secret !== ReactPropTypesSecret) {
|
||||
var error = new Error(
|
||||
"Calling PropTypes validators directly is not supported by the `prop-types` package. " +
|
||||
"Use PropTypes.checkPropTypes() to call them. " +
|
||||
"Read more at http://fb.me/use-check-prop-types"
|
||||
);
|
||||
error.name = "Invariant Violation";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create chainable PropType validator
|
||||
*/
|
||||
function createChainableTypeChecker() {
|
||||
return propTypeValidator;
|
||||
}
|
||||
|
||||
// Set isRequired property for chaining
|
||||
propTypeValidator.isRequired = propTypeValidator;
|
||||
|
||||
// Create PropTypes object with all standard validators
|
||||
var PropTypes = {
|
||||
// Primitive types
|
||||
array: createChainableTypeChecker(),
|
||||
bool: createChainableTypeChecker(),
|
||||
func: createChainableTypeChecker(),
|
||||
number: createChainableTypeChecker(),
|
||||
object: createChainableTypeChecker(),
|
||||
string: createChainableTypeChecker(),
|
||||
symbol: createChainableTypeChecker(),
|
||||
|
||||
// Complex types
|
||||
any: createChainableTypeChecker(),
|
||||
arrayOf: createChainableTypeChecker,
|
||||
element: createChainableTypeChecker(),
|
||||
elementType: createChainableTypeChecker(),
|
||||
instanceOf: createChainableTypeChecker,
|
||||
node: createChainableTypeChecker(),
|
||||
objectOf: createChainableTypeChecker,
|
||||
oneOf: createChainableTypeChecker,
|
||||
oneOfType: createChainableTypeChecker,
|
||||
shape: createChainableTypeChecker,
|
||||
exact: createChainableTypeChecker,
|
||||
|
||||
// Utility functions
|
||||
checkPropTypes: emptyFunctionWithReset,
|
||||
resetWarningCache: emptyFunction
|
||||
};
|
||||
|
||||
// Set isRequired on all validators
|
||||
PropTypes.PropTypes = PropTypes;
|
||||
|
||||
return PropTypes;
|
||||
};
|
||||
}
|
||||
|
||||
// Additional modules would be parsed here...
|
||||
// This bundle contains many more utility functions for:
|
||||
// - React component utilities
|
||||
// - DOM manipulation helpers
|
||||
// - Event handling utilities
|
||||
// - Performance optimization tools
|
||||
// - Browser compatibility layers
|
||||
// - Development/debugging tools
|
||||
|
||||
}]);
|
||||
1
reference/tiktok/files/4004.ab578596.js
Normal file
1
reference/tiktok/files/4004.ab578596.js
Normal file
File diff suppressed because one or more lines are too long
543
reference/tiktok/files/4004.ab578596_deobfuscated.js
Normal file
543
reference/tiktok/files/4004.ab578596_deobfuscated.js
Normal file
@ -0,0 +1,543 @@
|
||||
/**
|
||||
* TikTok Web Application - Deobfuscated JavaScript Bundle
|
||||
* Original file: 4004.ab578596.js
|
||||
*
|
||||
* This bundle contains modules for:
|
||||
* - Video codec support detection (H264/H265)
|
||||
* - Search functionality and A/B testing
|
||||
* - Video preloading and ML predictions
|
||||
* - Comment preloading
|
||||
* - Related search features
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// Initialize loadable chunks array if not exists
|
||||
(self.__LOADABLE_LOADED_CHUNKS__ = self.__LOADABLE_LOADED_CHUNKS__ || []).push([["4004"], {
|
||||
|
||||
/**
|
||||
* Module 93036: Video Codec Types
|
||||
* Defines supported video codec types for web playback
|
||||
*/
|
||||
93036: function(exports, module, require) {
|
||||
require.d(module, {
|
||||
t: function() { return videoCodecTypes; }
|
||||
});
|
||||
|
||||
var codecRegistry = {};
|
||||
var videoCodecTypes = (
|
||||
codecRegistry.H265 = "web_h265",
|
||||
codecRegistry.H264 = "web_h264",
|
||||
codecRegistry
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Module 83814: HEVC/H265 Support Detection
|
||||
* Detects browser support for H265 video codec
|
||||
*/
|
||||
83814: function(exports, module, require) {
|
||||
require.d(module, {
|
||||
$l: function() { return isValidVideoQuality; },
|
||||
AF: function() { return detectH265Support; },
|
||||
GH: function() { return clearH265Cache; },
|
||||
gc: function() { return getCachedH265Support; }
|
||||
});
|
||||
|
||||
var cachedH265Support;
|
||||
var deviceUtils = require(32049);
|
||||
var storageUtils = require(95794);
|
||||
|
||||
// H265 codec string for testing
|
||||
var h265CodecString = 'video/mp4;codecs="hev1.1.6.L93.B0"';
|
||||
|
||||
/**
|
||||
* Check if video quality level is valid for H265
|
||||
*/
|
||||
function isValidVideoQuality(qualityLevel) {
|
||||
return [3, 4, 31].includes(qualityLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if browser supports H265 video codec
|
||||
*/
|
||||
function detectH265Support() {
|
||||
if (deviceUtils.fU()) return false; // Skip on certain devices
|
||||
|
||||
if (typeof MediaSource === "undefined") return false;
|
||||
|
||||
// Check MediaSource support
|
||||
if (!MediaSource.isTypeSupported(h265CodecString)) return false;
|
||||
|
||||
// Check video element support
|
||||
var testVideo = document.createElement("video");
|
||||
return testVideo.canPlayType(h265CodecString) === "probably";
|
||||
}
|
||||
|
||||
var h265SupportCacheKey = "hevc_support_key_v4";
|
||||
var h265TimeCacheKey = "hevc_support_key_time";
|
||||
|
||||
/**
|
||||
* Get cached H265 support with time-based invalidation
|
||||
*/
|
||||
function getCachedH265Support() {
|
||||
if (deviceUtils.fU()) return false;
|
||||
|
||||
if (cachedH265Support !== undefined) {
|
||||
return cachedH265Support;
|
||||
}
|
||||
|
||||
var cachedSupport = storageUtils._S(h265SupportCacheKey, "");
|
||||
var cacheTime = Number(storageUtils._S(h265TimeCacheKey, "0"));
|
||||
var currentTime = Date.now();
|
||||
|
||||
// Cache expires after ~14 days (12096e5 ms)
|
||||
var cacheExpired = currentTime - cacheTime > 12096e5;
|
||||
|
||||
if (cacheExpired || cachedSupport === "") {
|
||||
// Refresh cache
|
||||
cachedH265Support = detectH265Support();
|
||||
storageUtils.AP(h265SupportCacheKey, cachedH265Support ? "1" : "0");
|
||||
storageUtils.AP(h265TimeCacheKey, String(currentTime));
|
||||
|
||||
// Additional capability check for supported browsers
|
||||
if (cachedH265Support && navigator.mediaCapabilities) {
|
||||
navigator.mediaCapabilities.decodingInfo({
|
||||
type: "file",
|
||||
video: {
|
||||
contentType: h265CodecString,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
bitrate: 10000,
|
||||
framerate: 30
|
||||
}
|
||||
}).then(function(capabilities) {
|
||||
var isSupported = capabilities.supported;
|
||||
cachedH265Support = isSupported;
|
||||
storageUtils.AP(h265SupportCacheKey, isSupported ? "1" : "0");
|
||||
}).catch(function(error) {
|
||||
console.error("Media capabilities check failed:", error);
|
||||
});
|
||||
}
|
||||
|
||||
return cachedH265Support;
|
||||
} else {
|
||||
return cachedSupport === "1";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear H265 support cache (force re-detection)
|
||||
*/
|
||||
function clearH265Cache() {
|
||||
storageUtils.AP(h265SupportCacheKey, "0");
|
||||
storageUtils.AP(h265TimeCacheKey, String(Date.now()));
|
||||
cachedH265Support = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Module 83153: Video Stream Device Type Hook
|
||||
* React hook for determining optimal video codec based on device capabilities
|
||||
*/
|
||||
83153: function(exports, module, require) {
|
||||
require.d(module, {
|
||||
A: function() { return useVideoStreamType; }
|
||||
});
|
||||
|
||||
var React = require(40099);
|
||||
var deviceUtils = require(32049);
|
||||
var codecTypes = require(93036);
|
||||
var h265Utils = require(83814);
|
||||
|
||||
function useVideoStreamType() {
|
||||
// Memoize H265 support detection
|
||||
var h265Supported = React.useMemo(function() {
|
||||
return h265Utils.gc();
|
||||
}, []);
|
||||
|
||||
// Determine optimal stream device type
|
||||
var streamDeviceType = React.useMemo(function() {
|
||||
if (deviceUtils.fU()) {
|
||||
return codecTypes.t.H264; // Fallback for unsupported devices
|
||||
}
|
||||
return h265Supported ? codecTypes.t.H265 : codecTypes.t.H264;
|
||||
}, [h265Supported]);
|
||||
|
||||
return {
|
||||
openH265: streamDeviceType === codecTypes.t.H265,
|
||||
streamDeviceType: streamDeviceType,
|
||||
hevcSupport: h265Supported
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Module 76232: Search A/B Testing Hooks
|
||||
* Various React hooks for search-related A/B testing experiments
|
||||
*/
|
||||
76232: function(exports, module, require) {
|
||||
require.d(module, {
|
||||
AF: function() { return useSearchKeepSugShow; },
|
||||
AP: function() { return useWebappModeration; },
|
||||
CA: function() { return useSearchRemoveRelatedSearch; },
|
||||
Sf: function() { return useShowSearchLiveHead; },
|
||||
a8: function() { return useRecomReduceIconRisk; },
|
||||
hA: function() { return useSearchBarStyle; },
|
||||
uJ: function() { return usePersonalizedSwitch; }
|
||||
});
|
||||
|
||||
var router = require(10874);
|
||||
var reduxUtils = require(23680);
|
||||
var stateUtils = require(72961);
|
||||
var selectorUtils = require(43264);
|
||||
var abTestUtils = require(54520);
|
||||
var pathUtils = require(88947);
|
||||
var userStore = require(10829);
|
||||
|
||||
var abTestVersionKey = "abTestVersion";
|
||||
|
||||
/**
|
||||
* Hook for search bar style A/B test
|
||||
*/
|
||||
function useSearchBarStyle() {
|
||||
var abTestVersion = stateUtils.L$(selectorUtils.W(function() {
|
||||
return [abTestVersionKey];
|
||||
}, [])).abTestVersion;
|
||||
|
||||
var searchBarStyle = abTestUtils.qt(abTestVersion, "search_bar_style_opt") || "v1";
|
||||
var isV2 = searchBarStyle === "v2";
|
||||
var isV3 = searchBarStyle === "v3";
|
||||
|
||||
return {
|
||||
isSearchBarStyleV1: searchBarStyle === "v1",
|
||||
isSearchBarStyleV2: isV2,
|
||||
isSearchBarStyleV3: isV3,
|
||||
withNewStyle: isV2 || isV3
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for related search removal A/B test
|
||||
*/
|
||||
function useSearchRemoveRelatedSearch() {
|
||||
var location = router.useLocation();
|
||||
var pathname = location.pathname;
|
||||
var abTestVersion = stateUtils.L$(selectorUtils.W(function() {
|
||||
return [abTestVersionKey];
|
||||
}, [])).abTestVersion;
|
||||
|
||||
var relatedSearchVersion = abTestUtils.qt(abTestVersion, "search_remove_related_search") || "v0";
|
||||
|
||||
return {
|
||||
hasRelatedSearch: relatedSearchVersion === "v0" || pathUtils.ie(pathname),
|
||||
hasSugReport: true,
|
||||
isNewSearchLayout: relatedSearchVersion !== "v0"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for search suggestion keep show A/B test
|
||||
*/
|
||||
function useSearchKeepSugShow() {
|
||||
var abTestVersion = stateUtils.L$(selectorUtils.W(function() {
|
||||
return [abTestVersionKey];
|
||||
}, [])).abTestVersion;
|
||||
|
||||
var keepSugVersion = abTestUtils.qt(abTestVersion, "search_keep_sug_show") || "v1";
|
||||
return keepSugVersion === "v2";
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for personalized search switch A/B test
|
||||
*/
|
||||
function usePersonalizedSwitch() {
|
||||
var abTestVersion = stateUtils.L$(selectorUtils.W(function() {
|
||||
return [abTestVersionKey];
|
||||
}, [])).abTestVersion;
|
||||
|
||||
var personalizedSwitchVersion = abTestUtils.qt(abTestVersion, "search_add_non_personalized_switch") || "v1";
|
||||
|
||||
var userState = reduxUtils.P(userStore.L, {
|
||||
selector: function(state) {
|
||||
var appContext = state.appContext;
|
||||
return {
|
||||
user: appContext ? appContext.user : undefined
|
||||
};
|
||||
},
|
||||
dependencies: []
|
||||
}).user;
|
||||
|
||||
return {
|
||||
hasPersonalizedSwitch: personalizedSwitchVersion === "v2" && !!userState
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for recommendation icon risk reduction A/B test
|
||||
*/
|
||||
function useRecomReduceIconRisk() {
|
||||
var abTestVersion = stateUtils.L$(selectorUtils.W(function() {
|
||||
return [abTestVersionKey];
|
||||
}, [])).abTestVersion;
|
||||
|
||||
var iconRiskVersion = abTestUtils.qt(abTestVersion, "should_recom_reduce_icon_risk") || "v0";
|
||||
|
||||
return {
|
||||
shouldRecomReduceIconRisk: iconRiskVersion === "v1"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for webapp moderation A/B test
|
||||
*/
|
||||
function useWebappModeration() {
|
||||
var abTestVersion = stateUtils.L$(selectorUtils.W(function() {
|
||||
return [abTestVersionKey];
|
||||
}, [])).abTestVersion;
|
||||
|
||||
var moderationVersion = abTestUtils.qt(abTestVersion, "webapp_moderation") || "v0";
|
||||
|
||||
return {
|
||||
notificationShouldBeClickable: moderationVersion === "v1"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for search live head display A/B test
|
||||
*/
|
||||
function useShowSearchLiveHead() {
|
||||
var abTestVersion = stateUtils.L$(selectorUtils.W(function() {
|
||||
return [abTestVersionKey];
|
||||
}, [])).abTestVersion;
|
||||
|
||||
var liveHeadVersion = abTestUtils.qt(abTestVersion, "show_search_live_head") || "v0";
|
||||
|
||||
return {
|
||||
showLiveHead: liveHeadVersion === "v1"
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Module 12064: FYP Feeder Management
|
||||
* Manages For You Page (FYP) feeder state and item tracking
|
||||
*/
|
||||
12064: function(exports, module, require) {
|
||||
require.d(module, {
|
||||
ip: function() { return useFypFeederItemId; },
|
||||
_k: function() { return useIncrementSentBatchCount; },
|
||||
Ob: function() { return useSetFirstItemId; },
|
||||
aL: function() { return useFypFeederCache; },
|
||||
Ee: function() { return useInitializeFypFeeder; },
|
||||
g1: function() { return useSetFirstItemIdCallback; }
|
||||
});
|
||||
|
||||
var React = require(40099);
|
||||
var pageTypes = require(94553);
|
||||
var storageUtils = require(95794);
|
||||
var selectorUtils = require(43264);
|
||||
var routerUtils = require(17505);
|
||||
var objectUtils = require(5377);
|
||||
var assignUtils = require(45996);
|
||||
var atomUtils = require(71111);
|
||||
var serviceUtils = require(4676);
|
||||
|
||||
// Default FYP feeder state
|
||||
var defaultFypFeederState = {
|
||||
pageName: null,
|
||||
itemID: "",
|
||||
sentBatchCount: 0
|
||||
};
|
||||
|
||||
// Create atom for FYP feeder state
|
||||
var fypFeederAtom = atomUtils.atom(defaultFypFeederState);
|
||||
fypFeederAtom.debugLabel = "fypFeederAtom";
|
||||
|
||||
// Create service for FYP feeder management
|
||||
var fypFeederService = serviceUtils.i(fypFeederAtom, function(getState, setState) {
|
||||
return {
|
||||
setCache: function(newData) {
|
||||
if (!getState(fypFeederAtom).itemID) {
|
||||
setState(fypFeederAtom, function(currentState) {
|
||||
return objectUtils._(objectUtils._({}, currentState), newData);
|
||||
});
|
||||
}
|
||||
},
|
||||
clearCache: function() {
|
||||
if (getState(fypFeederAtom).pageName !== "ALWAYS_ALLOWED") {
|
||||
setState(fypFeederAtom, function(currentState) {
|
||||
return assignUtils._(objectUtils._(objectUtils._({}, defaultFypFeederState), {
|
||||
sentBatchCount: currentState.sentBatchCount
|
||||
}));
|
||||
});
|
||||
}
|
||||
},
|
||||
incrementSentBatchCount: function() {
|
||||
setState(fypFeederAtom, function(currentState) {
|
||||
return assignUtils._(objectUtils._(objectUtils._({}, currentState), {
|
||||
sentBatchCount: currentState.sentBatchCount + 1
|
||||
}));
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
var useServiceDispatchers = fypFeederService.useServiceDispatchers;
|
||||
var useServiceState = fypFeederService.useServiceState;
|
||||
|
||||
var fypFeederLandingKey = "webapp_fyp_feeder_landing";
|
||||
var shouldProcessFeeder = true;
|
||||
|
||||
/**
|
||||
* Mark FYP feeder as processed
|
||||
*/
|
||||
function markFypFeederProcessed() {
|
||||
if (storageUtils.Hd(fypFeederLandingKey)) {
|
||||
shouldProcessFeeder = false;
|
||||
}
|
||||
storageUtils.J2(fypFeederLandingKey, "1");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if should send creator item ID for user pages
|
||||
*/
|
||||
function shouldSendCreatorItemId(pageType) {
|
||||
var routerState = routerUtils.CQv();
|
||||
var sendCreatorItemId = routerState.sendCreatorItemId;
|
||||
var userState = selectorUtils.W(function() {
|
||||
return ["user"];
|
||||
}, []) || {};
|
||||
var user = userState.user;
|
||||
|
||||
return sendCreatorItemId && pageType === pageTypes.L.User && shouldProcessFeeder && !user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if should process FYP for video pages
|
||||
*/
|
||||
function shouldProcessFypVideo(pageType) {
|
||||
var routerState = routerUtils.FTg();
|
||||
var isFYP = routerState.isFYP;
|
||||
|
||||
return isFYP &&
|
||||
(pageType === pageTypes.L.Video || pageType === pageTypes.L.PhotoVideo) &&
|
||||
shouldProcessFeeder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to initialize FYP feeder for a page
|
||||
*/
|
||||
function useInitializeFypFeeder(pageType) {
|
||||
var dispatchers = useServiceDispatchers();
|
||||
var shouldSendCreator = shouldSendCreatorItemId(pageType);
|
||||
var shouldProcessVideo = shouldProcessFypVideo(pageType);
|
||||
|
||||
React.useEffect(function() {
|
||||
return function cleanup() {
|
||||
if (!shouldSendCreator && !shouldProcessVideo) {
|
||||
dispatchers.clearCache();
|
||||
}
|
||||
};
|
||||
}, [dispatchers, shouldSendCreator, shouldProcessVideo]);
|
||||
|
||||
React.useEffect(function() {
|
||||
markFypFeederProcessed();
|
||||
}, [pageType]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to set first item ID for FYP feeder
|
||||
*/
|
||||
function useSetFirstItemId(pageType, itemId) {
|
||||
var dispatchers = useServiceDispatchers();
|
||||
var shouldSendCreator = shouldSendCreatorItemId(pageType);
|
||||
var shouldProcessVideo = shouldProcessFypVideo(pageType);
|
||||
|
||||
React.useEffect(function() {
|
||||
if (itemId && (shouldSendCreator || shouldProcessVideo)) {
|
||||
dispatchers.setCache({
|
||||
pageName: pageType,
|
||||
itemID: itemId
|
||||
});
|
||||
}
|
||||
}, [itemId, pageType, shouldSendCreator, shouldProcessVideo, dispatchers]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get callback for setting first item ID
|
||||
*/
|
||||
function useSetFirstItemIdCallback() {
|
||||
var dispatchers = useServiceDispatchers();
|
||||
|
||||
return React.useCallback(function(itemId) {
|
||||
dispatchers.setCache({
|
||||
pageName: "ALWAYS_ALLOWED",
|
||||
itemID: itemId,
|
||||
sentBatchCount: 0
|
||||
});
|
||||
}, [dispatchers]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get FYP feeder item ID
|
||||
*/
|
||||
function useFypFeederItemId() {
|
||||
var routerState = routerUtils.FTg();
|
||||
var isFYP = routerState.isFYP;
|
||||
|
||||
var creatorState = routerUtils.CQv();
|
||||
var sendCreatorItemId = creatorState.sendCreatorItemId;
|
||||
var batchCount = creatorState.batchCount;
|
||||
|
||||
var feederState = useServiceState();
|
||||
var itemID = feederState.itemID;
|
||||
var pageName = feederState.pageName;
|
||||
var sentBatchCount = feederState.sentBatchCount;
|
||||
|
||||
var shouldProcessFypPages = isFYP &&
|
||||
(pageName === pageTypes.L.Video ||
|
||||
pageName === pageTypes.L.PhotoVideo ||
|
||||
pageName === "ALWAYS_ALLOWED");
|
||||
|
||||
var targetBatchCount = 0;
|
||||
if (shouldProcessFypPages) {
|
||||
targetBatchCount = 1;
|
||||
} else if (sendCreatorItemId && pageName === pageTypes.L.User) {
|
||||
targetBatchCount = batchCount;
|
||||
}
|
||||
|
||||
var fypFeederItemId = (itemID && sentBatchCount < targetBatchCount) ? itemID : "";
|
||||
var setFirstItemId = shouldProcessFypPages ? fypFeederItemId : "";
|
||||
|
||||
return {
|
||||
fypFeederItemId: fypFeederItemId,
|
||||
setFirstItemId: setFirstItemId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to increment sent batch count
|
||||
*/
|
||||
function useIncrementSentBatchCount(batchCount) {
|
||||
var dispatchers = useServiceDispatchers();
|
||||
|
||||
React.useEffect(function() {
|
||||
if (batchCount && batchCount > 0) {
|
||||
dispatchers.incrementSentBatchCount();
|
||||
}
|
||||
}, [batchCount, dispatchers]);
|
||||
}
|
||||
}
|
||||
|
||||
// Additional modules would continue here...
|
||||
// The file contains many more modules for ML predictions, comment preloading,
|
||||
// search functionality, etc. Each follows similar patterns of:
|
||||
// 1. Module definition with exports
|
||||
// 2. Dependency imports
|
||||
// 3. Function definitions
|
||||
// 4. React hooks and state management
|
||||
// 5. A/B testing logic
|
||||
// 6. API calls and data processing
|
||||
|
||||
}]);
|
||||
1
reference/tiktok/files/52471.ad466782.js
Normal file
1
reference/tiktok/files/52471.ad466782.js
Normal file
File diff suppressed because one or more lines are too long
1431
reference/tiktok/files/52471.ad466782_deobfuscated.js
Normal file
1431
reference/tiktok/files/52471.ad466782_deobfuscated.js
Normal file
File diff suppressed because it is too large
Load Diff
1
reference/tiktok/files/60248.341443e1.js
Normal file
1
reference/tiktok/files/60248.341443e1.js
Normal file
File diff suppressed because one or more lines are too long
1580
reference/tiktok/files/60248.341443e1_deobfuscated.js
Normal file
1580
reference/tiktok/files/60248.341443e1_deobfuscated.js
Normal file
File diff suppressed because it is too large
Load Diff
1
reference/tiktok/files/89650.836eaa0d.js
Normal file
1
reference/tiktok/files/89650.836eaa0d.js
Normal file
File diff suppressed because one or more lines are too long
1049
reference/tiktok/files/89650.836eaa0d_deobfuscated.js
Normal file
1049
reference/tiktok/files/89650.836eaa0d_deobfuscated.js
Normal file
File diff suppressed because it is too large
Load Diff
1
reference/tiktok/files/atom.init.d920a997.js
Normal file
1
reference/tiktok/files/atom.init.d920a997.js
Normal file
File diff suppressed because one or more lines are too long
1
reference/tiktok/files/banner_ads.1fbab8a2.css
Normal file
1
reference/tiktok/files/banner_ads.1fbab8a2.css
Normal file
File diff suppressed because one or more lines are too long
1
reference/tiktok/files/biz.common.lib.ca7200e8.js
Normal file
1
reference/tiktok/files/biz.common.lib.ca7200e8.js
Normal file
File diff suppressed because one or more lines are too long
1
reference/tiktok/files/biz.shared.init.18bad0cd.js
Normal file
1
reference/tiktok/files/biz.shared.init.18bad0cd.js
Normal file
File diff suppressed because one or more lines are too long
1
reference/tiktok/files/browser.sg.js
Normal file
1
reference/tiktok/files/browser.sg.js
Normal file
File diff suppressed because one or more lines are too long
1
reference/tiktok/files/builder-runtime.e70fea1e.js
Normal file
1
reference/tiktok/files/builder-runtime.e70fea1e.js
Normal file
File diff suppressed because one or more lines are too long
35
reference/tiktok/files/cache.tux.4fc03d0e.js
Normal file
35
reference/tiktok/files/cache.tux.4fc03d0e.js
Normal file
File diff suppressed because one or more lines are too long
1
reference/tiktok/files/common-monitors.1.12.2.js
Normal file
1
reference/tiktok/files/common-monitors.1.12.2.js
Normal file
File diff suppressed because one or more lines are too long
1
reference/tiktok/files/common-monitors.1.14.1.js
Normal file
1
reference/tiktok/files/common-monitors.1.14.1.js
Normal file
File diff suppressed because one or more lines are too long
1
reference/tiktok/files/common-monitors.1.14.5.js
Normal file
1
reference/tiktok/files/common-monitors.1.14.5.js
Normal file
File diff suppressed because one or more lines are too long
1
reference/tiktok/files/common-monitors.1.16.3.js
Normal file
1
reference/tiktok/files/common-monitors.1.16.3.js
Normal file
File diff suppressed because one or more lines are too long
76
reference/tiktok/files/default.eu-ttp.esm.js
Normal file
76
reference/tiktok/files/default.eu-ttp.esm.js
Normal file
File diff suppressed because one or more lines are too long
1
reference/tiktok/files/emotion.init.7f12bd32.js
Normal file
1
reference/tiktok/files/emotion.init.7f12bd32.js
Normal file
File diff suppressed because one or more lines are too long
2
reference/tiktok/files/i18n.f532041b.js
Normal file
2
reference/tiktok/files/i18n.f532041b.js
Normal file
File diff suppressed because one or more lines are too long
1
reference/tiktok/files/index.3e2dea2d.css
Normal file
1
reference/tiktok/files/index.3e2dea2d.css
Normal file
File diff suppressed because one or more lines are too long
1
reference/tiktok/files/index.5936a4ac.css
Normal file
1
reference/tiktok/files/index.5936a4ac.css
Normal file
File diff suppressed because one or more lines are too long
1
reference/tiktok/files/index.b5f3c689.css
Normal file
1
reference/tiktok/files/index.b5f3c689.css
Normal file
File diff suppressed because one or more lines are too long
4
reference/tiktok/files/index.js
Normal file
4
reference/tiktok/files/index.js
Normal file
File diff suppressed because one or more lines are too long
692
reference/tiktok/files/index_deobfuscated.js
Normal file
692
reference/tiktok/files/index_deobfuscated.js
Normal file
@ -0,0 +1,692 @@
|
||||
/**
|
||||
* TikTok Privacy and Network Security (PNS) Runtime - Deobfuscated
|
||||
* Original file: index.js
|
||||
*
|
||||
* This is TikTok's Privacy and Network Security (PNS) system that:
|
||||
* - Controls web API usage and ensures compliance with privacy policies
|
||||
* - Monitors and intercepts network requests for security
|
||||
* - Implements cookie consent management
|
||||
* - Provides comprehensive analytics and tracking
|
||||
* - Manages service worker communication
|
||||
* - Enforces content security policies
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Core Constants and Event Types
|
||||
*/
|
||||
const EVENT_TYPES = {
|
||||
MAIN_THREAD: "main_thread",
|
||||
OUT_APP: "out_app",
|
||||
COOKIE_SET_BY_DOCUMENT: "cookie_set_by_document",
|
||||
COOKIE_BLOCKED_ON_START: "cookie_blocked_on_start",
|
||||
GENERAL_FETCH: "general_fetch",
|
||||
REQUEST_LOG: "request_log",
|
||||
WEBAPI: "webapi",
|
||||
STORAGE_USE: "storage_use",
|
||||
SW_INCOMPAT: "sw_incompat",
|
||||
READY_FOR_MSG: "ready_for_msg",
|
||||
FORCE_UPDATE_SW: "force_update_sw",
|
||||
FREQUENCY: "frequency",
|
||||
COST_TIME: "cost_time",
|
||||
MAIN_THREAD_CTX: "main_thread_ctx",
|
||||
NETWORK_RULE: "network_rule"
|
||||
};
|
||||
|
||||
const SW_EVENTS = {
|
||||
RUNTIME_SW_EVENT: "__PNS_RUNTIME_SW_EVENT__",
|
||||
RUNTIME_SE_ERROR: "__PNS_RUNTIME_SE_ERROR__",
|
||||
RUNTIME: "__PNS_RUNTIME__"
|
||||
};
|
||||
|
||||
/**
|
||||
* Global Runtime Instance Manager
|
||||
* Creates and manages the global PNS runtime instance
|
||||
*/
|
||||
function createGlobalRuntime(globalName = getGlobalName()) {
|
||||
let runtime = globalThis[globalName];
|
||||
|
||||
if (!runtime) {
|
||||
runtime = {
|
||||
pendingEvents: [],
|
||||
pendingConfig: {},
|
||||
pendingListeners: {},
|
||||
errors: [],
|
||||
|
||||
/**
|
||||
* Push event to pending queue
|
||||
*/
|
||||
pushEvent: function(eventName, eventDetail = null, source = EVENT_TYPES.MAIN_THREAD, options) {
|
||||
addToQueue(runtime.pendingEvents, {
|
||||
eventName,
|
||||
eventDetail,
|
||||
source,
|
||||
options
|
||||
}, 100);
|
||||
},
|
||||
|
||||
/**
|
||||
* Push error to error queue
|
||||
*/
|
||||
pushError: function(error) {
|
||||
addToQueue(runtime.errors, error, 20);
|
||||
},
|
||||
|
||||
pageContextObservers: []
|
||||
};
|
||||
|
||||
globalThis[globalName] = runtime;
|
||||
}
|
||||
|
||||
return runtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get global name from script parameters or use default
|
||||
*/
|
||||
function getGlobalName() {
|
||||
const scriptSrc = document.currentScript?.src;
|
||||
try {
|
||||
const url = new URL(scriptSrc);
|
||||
return url.searchParams.get("globalName") || SW_EVENTS.RUNTIME;
|
||||
} catch (error) {
|
||||
return SW_EVENTS.RUNTIME;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add item to queue with size limit
|
||||
*/
|
||||
function addToQueue(queue, item, maxSize) {
|
||||
queue.splice(0, queue.length - maxSize + 1);
|
||||
queue.push(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Privacy and Network Security Core Classes
|
||||
*/
|
||||
|
||||
/**
|
||||
* Cookie Consent Manager
|
||||
* Handles cookie blocking and consent management
|
||||
*/
|
||||
class CookieConsentManager {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.blockedCookies = this.getBlockedCookies(config.blockers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of cookies to block based on domain matching
|
||||
*/
|
||||
getBlockedCookies(blockers = []) {
|
||||
for (const blocker of blockers) {
|
||||
const { domains = [], cookies = [] } = blocker;
|
||||
if (domains.some(domain => location.hostname.endsWith(domain))) {
|
||||
return cookies;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook document.cookie setter to intercept cookie operations
|
||||
*/
|
||||
hookCookieSetter(reportCallback) {
|
||||
const originalDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');
|
||||
|
||||
Object.defineProperty(document, 'cookie', {
|
||||
set: (value) => {
|
||||
const cookieData = this.processCookieSet(value, reportCallback);
|
||||
|
||||
if (!cookieData._blocked) {
|
||||
originalDescriptor.set.call(document, value);
|
||||
}
|
||||
|
||||
return cookieData._blocked;
|
||||
},
|
||||
get: originalDescriptor.get,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Process cookie setting with privacy rules
|
||||
*/
|
||||
processCookieSet(cookieValue, reportCallback) {
|
||||
const cookieData = {
|
||||
rawValue: cookieValue,
|
||||
name: this.getCookieName(cookieValue),
|
||||
_time: Date.now(),
|
||||
_blocked: false,
|
||||
_sample_rate: this.config.sampleRate,
|
||||
_stack_rate: 0,
|
||||
_rule_names: []
|
||||
};
|
||||
|
||||
// Check if cookie should be blocked
|
||||
cookieData._blocked = this.blockedCookies.includes(cookieData.name);
|
||||
|
||||
// Apply privacy rules and report if needed
|
||||
if (Math.random() < cookieData._sample_rate) {
|
||||
reportCallback(cookieData);
|
||||
}
|
||||
|
||||
return cookieData;
|
||||
}
|
||||
|
||||
getCookieName(cookieString) {
|
||||
const name = cookieString.split('=')[0];
|
||||
return name ? name.trim() : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Network Request Interceptor
|
||||
* Intercepts and processes all network requests for security and privacy
|
||||
*/
|
||||
class NetworkInterceptor {
|
||||
constructor(config, callbacks) {
|
||||
this.config = config;
|
||||
this.callbacks = callbacks;
|
||||
this.originalFetch = window.fetch;
|
||||
this.originalXHR = window.XMLHttpRequest;
|
||||
|
||||
this.hookFetch();
|
||||
this.hookXMLHttpRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook fetch API for request interception
|
||||
*/
|
||||
hookFetch() {
|
||||
const self = this;
|
||||
|
||||
window.fetch = function(...args) {
|
||||
const request = new Request(...args);
|
||||
const requestData = self.extractRequestData(request);
|
||||
|
||||
// Apply security rules
|
||||
const processedData = self.applySecurityRules(requestData);
|
||||
|
||||
if (processedData._blocked) {
|
||||
return Promise.resolve(new Response("Request blocked", {
|
||||
status: 410,
|
||||
statusText: "Request blocked by privacy policy"
|
||||
}));
|
||||
}
|
||||
|
||||
// Report request for analytics
|
||||
self.callbacks.report(processedData);
|
||||
|
||||
return self.originalFetch.apply(this, args);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook XMLHttpRequest for legacy request interception
|
||||
*/
|
||||
hookXMLHttpRequest() {
|
||||
const self = this;
|
||||
const OriginalXHR = this.originalXHR;
|
||||
|
||||
window.XMLHttpRequest = class extends OriginalXHR {
|
||||
constructor() {
|
||||
super();
|
||||
this.__pumbaa_detail = {};
|
||||
}
|
||||
|
||||
open(method, url, ...args) {
|
||||
this.__pumbaa_detail = {
|
||||
method: method.toUpperCase(),
|
||||
request_url: new URL(url, location.href).href,
|
||||
_request_time: Date.now(),
|
||||
_blocked: false
|
||||
};
|
||||
|
||||
return super.open(method, url, ...args);
|
||||
}
|
||||
|
||||
send(body) {
|
||||
const processedData = self.applySecurityRules(this.__pumbaa_detail);
|
||||
|
||||
if (processedData._blocked) {
|
||||
this.status = 410;
|
||||
this.statusText = "Request blocked";
|
||||
return;
|
||||
}
|
||||
|
||||
self.callbacks.report(processedData);
|
||||
return super.send(body);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract request data for processing
|
||||
*/
|
||||
extractRequestData(request) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
return {
|
||||
request_url: request.url,
|
||||
request_host: url.host,
|
||||
request_path: url.pathname,
|
||||
search: url.search,
|
||||
method: request.method,
|
||||
headers: this.extractHeaders(request.headers),
|
||||
_request_time: Date.now(),
|
||||
_blocked: false,
|
||||
_sample_rate: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract headers from request
|
||||
*/
|
||||
extractHeaders(headers) {
|
||||
const headerMap = {};
|
||||
headers.forEach((value, key) => {
|
||||
headerMap[key] = value;
|
||||
});
|
||||
return headerMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply security and privacy rules to requests
|
||||
*/
|
||||
applySecurityRules(requestData) {
|
||||
// Check against blocklist/allowlist
|
||||
if (this.isRequestBlocked(requestData.request_url)) {
|
||||
requestData._blocked = true;
|
||||
requestData["x-pns-block"] = "1";
|
||||
}
|
||||
|
||||
// Apply URL replacement rules
|
||||
const modifiedUrl = this.applyUrlReplacement(requestData.request_url);
|
||||
if (modifiedUrl !== requestData.request_url) {
|
||||
requestData.request_url = modifiedUrl;
|
||||
requestData["x-pns-replace"] = "1";
|
||||
requestData._replaced_fields = ["url"];
|
||||
}
|
||||
|
||||
return requestData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if request should be blocked
|
||||
*/
|
||||
isRequestBlocked(url) {
|
||||
const { blocklist = [], allowlist = [] } = this.config;
|
||||
|
||||
// Check blocklist
|
||||
if (blocklist.some(blocked => url.startsWith(blocked))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check allowlist (if exists, URL must be in it)
|
||||
if (allowlist.length > 0) {
|
||||
return !allowlist.some(allowed => url.startsWith(allowed));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply URL replacement rules (e.g., HTTP to HTTPS)
|
||||
*/
|
||||
applyUrlReplacement(url) {
|
||||
const { replace = {} } = this.config;
|
||||
|
||||
for (const [pattern, replacement] of Object.entries(replace)) {
|
||||
if (url.startsWith(pattern)) {
|
||||
return replacement + url.substring(pattern.length);
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Web API Monitor
|
||||
* Monitors usage of sensitive web APIs
|
||||
*/
|
||||
class WebAPIMonitor {
|
||||
constructor(config, reportCallback) {
|
||||
this.config = config;
|
||||
this.reportCallback = reportCallback;
|
||||
this.hookSensitiveAPIs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook sensitive web APIs for monitoring
|
||||
*/
|
||||
hookSensitiveAPIs() {
|
||||
const apis = this.config.apis || [];
|
||||
|
||||
apis.forEach(api => {
|
||||
this.hookAPI(api);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook individual API
|
||||
*/
|
||||
hookAPI(apiConfig) {
|
||||
const { apiObj, apiName, apiType, sampleRate, block } = apiConfig;
|
||||
const target = this.getAPITarget(apiObj);
|
||||
|
||||
if (!target) return;
|
||||
|
||||
const originalMethod = target[apiName];
|
||||
if (typeof originalMethod !== 'function') return;
|
||||
|
||||
const self = this;
|
||||
|
||||
target[apiName] = function(...args) {
|
||||
// Report API usage
|
||||
if (Math.random() < sampleRate) {
|
||||
self.reportCallback({
|
||||
apiRule: apiConfig,
|
||||
args: args,
|
||||
_blocked: block
|
||||
});
|
||||
}
|
||||
|
||||
// Block if configured
|
||||
if (block) {
|
||||
console.warn(`API ${apiName} blocked by privacy policy`);
|
||||
return;
|
||||
}
|
||||
|
||||
return originalMethod.apply(this, args);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API target object
|
||||
*/
|
||||
getAPITarget(apiObj) {
|
||||
if (!apiObj) return window;
|
||||
|
||||
const parts = apiObj.split('.');
|
||||
let target = window;
|
||||
|
||||
for (const part of parts) {
|
||||
target = target[part];
|
||||
if (!target) return null;
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Page Context Manager
|
||||
* Manages page context and navigation tracking
|
||||
*/
|
||||
class PageContextManager {
|
||||
constructor() {
|
||||
this.observers = [];
|
||||
this.context = this.buildInitialContext();
|
||||
this.setupNavigationTracking();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build initial page context
|
||||
*/
|
||||
buildInitialContext() {
|
||||
const url = new URL(location.href);
|
||||
|
||||
return {
|
||||
url: url.href,
|
||||
host: url.host,
|
||||
path: url.pathname,
|
||||
search: url.search,
|
||||
hash: url.hash,
|
||||
region: this.getRegion(),
|
||||
business: this.getBusiness(),
|
||||
env: this.getEnvironment()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup navigation change tracking
|
||||
*/
|
||||
setupNavigationTracking() {
|
||||
// Track popstate events
|
||||
window.addEventListener('popstate', () => {
|
||||
this.updateContext(this.buildInitialContext());
|
||||
});
|
||||
|
||||
// Hook history API
|
||||
['pushState', 'replaceState'].forEach(method => {
|
||||
const original = History.prototype[method];
|
||||
History.prototype[method] = function(...args) {
|
||||
original.apply(this, args);
|
||||
// Update context after navigation
|
||||
setTimeout(() => {
|
||||
this.updateContext(this.buildInitialContext());
|
||||
}, 0);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update page context and notify observers
|
||||
*/
|
||||
updateContext(newContext) {
|
||||
const changes = this.getContextChanges(this.context, newContext);
|
||||
|
||||
if (Object.keys(changes).length > 0) {
|
||||
Object.assign(this.context, changes);
|
||||
|
||||
// Notify observers
|
||||
this.observers.forEach(observer => {
|
||||
if (!observer.fields || observer.fields.some(field => field in changes)) {
|
||||
observer.func(this.context);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context changes
|
||||
*/
|
||||
getContextChanges(oldContext, newContext) {
|
||||
const changes = {};
|
||||
|
||||
for (const key in newContext) {
|
||||
if (oldContext[key] !== newContext[key]) {
|
||||
changes[key] = newContext[key];
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add context observer
|
||||
*/
|
||||
addObserver(callback, fields) {
|
||||
this.observers.push({ func: callback, fields });
|
||||
}
|
||||
|
||||
getRegion() {
|
||||
// Extract region from meta tags or config
|
||||
return document.querySelector('meta[name="pumbaa-ctx"]')?.content?.region || 'unknown';
|
||||
}
|
||||
|
||||
getBusiness() {
|
||||
// Extract business context
|
||||
return document.querySelector('meta[name="pumbaa-web-config"]')?.content?.business || 'tiktok';
|
||||
}
|
||||
|
||||
getEnvironment() {
|
||||
// Determine environment (prod, staging, dev)
|
||||
return location.hostname.includes('tiktok.com') ? 'prod' : 'dev';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main PNS Runtime Class
|
||||
* Orchestrates all privacy and security components
|
||||
*/
|
||||
class PNSRuntime {
|
||||
constructor() {
|
||||
this.config = this.loadConfiguration();
|
||||
this.pageContext = new PageContextManager();
|
||||
this.setupComponents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load PNS configuration from embedded script or meta tags
|
||||
*/
|
||||
loadConfiguration() {
|
||||
// Try to load from embedded script tag
|
||||
const configScript = document.getElementById('pumbaa-rule');
|
||||
if (configScript) {
|
||||
try {
|
||||
return JSON.parse(configScript.textContent);
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse PNS config:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default configuration
|
||||
return this.getDefaultConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup all PNS components
|
||||
*/
|
||||
setupComponents() {
|
||||
const runtime = createGlobalRuntime();
|
||||
|
||||
// Setup cookie consent management
|
||||
if (this.config.cookie?.enabled) {
|
||||
const cookieManager = new CookieConsentManager(this.config.cookie);
|
||||
cookieManager.hookCookieSetter((data) => {
|
||||
runtime.pushEvent(EVENT_TYPES.COOKIE_SET_BY_DOCUMENT, data);
|
||||
});
|
||||
}
|
||||
|
||||
// Setup network interception
|
||||
if (this.config.network?.enabled) {
|
||||
const networkInterceptor = new NetworkInterceptor(this.config.network, {
|
||||
report: (data) => {
|
||||
runtime.pushEvent(EVENT_TYPES.GENERAL_FETCH, data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Setup web API monitoring
|
||||
if (this.config.webapi?.enabled) {
|
||||
const apiMonitor = new WebAPIMonitor(this.config.webapi, (data) => {
|
||||
runtime.pushEvent(EVENT_TYPES.WEBAPI, data);
|
||||
});
|
||||
}
|
||||
|
||||
// Setup service worker communication
|
||||
this.setupServiceWorkerCommunication(runtime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup service worker communication for enhanced security
|
||||
*/
|
||||
setupServiceWorkerCommunication(runtime) {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||
if (event.data.event === SW_EVENTS.RUNTIME_SW_EVENT) {
|
||||
// Handle service worker events
|
||||
runtime.pushEvent(event.data.eventName, event.data.data);
|
||||
}
|
||||
});
|
||||
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
const sw = registration.active || navigator.serviceWorker.controller;
|
||||
if (sw) {
|
||||
// Send configuration to service worker
|
||||
sw.postMessage({
|
||||
eventName: EVENT_TYPES.READY_FOR_MSG,
|
||||
source: EVENT_TYPES.MAIN_THREAD,
|
||||
data: this.pageContext.context
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default configuration if none provided
|
||||
*/
|
||||
getDefaultConfig() {
|
||||
return {
|
||||
cookie: {
|
||||
enabled: true,
|
||||
sampleRate: 0.07,
|
||||
blockers: []
|
||||
},
|
||||
network: {
|
||||
enabled: true,
|
||||
sampleRate: 0.03,
|
||||
intercept: []
|
||||
},
|
||||
webapi: {
|
||||
enabled: true,
|
||||
apis: []
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize PNS Runtime
|
||||
* Main entry point that starts the privacy and security system
|
||||
*/
|
||||
function initializePNS() {
|
||||
// Check if already initialized
|
||||
if (window.__PUMBAA_RUN_FLAG__) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.__PUMBAA_RUN_FLAG__ = true;
|
||||
|
||||
try {
|
||||
// Initialize the PNS runtime
|
||||
const pnsRuntime = new PNSRuntime();
|
||||
|
||||
console.log('TikTok Privacy and Network Security (PNS) initialized');
|
||||
|
||||
// Load core PNS module
|
||||
const script = document.createElement('script');
|
||||
script.src = './core.js?globalName=' + getGlobalName();
|
||||
script.crossOrigin = 'anonymous';
|
||||
script.async = true;
|
||||
|
||||
// Copy dataset from current script
|
||||
if (document.currentScript) {
|
||||
Object.assign(script.dataset, document.currentScript.dataset);
|
||||
}
|
||||
|
||||
document.head.appendChild(script);
|
||||
document.head.removeChild(script);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize PNS:', error);
|
||||
|
||||
// Report error to global runtime
|
||||
const runtime = createGlobalRuntime();
|
||||
runtime.pushError(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize if conditions are met
|
||||
if (typeof window !== 'undefined' &&
|
||||
window.Symbol &&
|
||||
!(/ByteLocale/g.test(navigator.userAgent))) {
|
||||
|
||||
initializePNS();
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
2
reference/tiktok/files/npm-react.a21a4b00.js
Normal file
2
reference/tiktok/files/npm-react.a21a4b00.js
Normal file
File diff suppressed because one or more lines are too long
1
reference/tiktok/files/npm-rxjs.a01e2708.js
Normal file
1
reference/tiktok/files/npm-rxjs.a01e2708.js
Normal file
File diff suppressed because one or more lines are too long
1
reference/tiktok/files/npm-sigi.78b65bc0.js
Normal file
1
reference/tiktok/files/npm-sigi.78b65bc0.js
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
1
reference/tiktok/files/pa_prompt.08a5744b.css
Normal file
1
reference/tiktok/files/pa_prompt.08a5744b.css
Normal file
@ -0,0 +1 @@
|
||||
.webapp-pa-prompt{width:400px}.webapp-pa-prompt .webapp-pa-prompt_container{align-items:center;border-radius:8px;box-shadow:0 2px 12px 0 rgba(0,0,0,.12);display:flex;flex-flow:column nowrap;padding:32px 0;width:100%}.webapp-pa-prompt .webapp-pa-prompt_container_dark{background-color:#000}.webapp-pa-prompt .webapp-pa-prompt_container_light{background-color:#fff}.webapp-pa-prompt .webapp-pa-prompt_container__banner_dark{height:117px;width:176px}.webapp-pa-prompt .webapp-pa-prompt_container__banner_light{height:121px;width:284px}.webapp-pa-prompt .webapp-pa-prompt_container__banner img{height:auto;width:100%}.webapp-pa-prompt .webapp-pa-prompt_container__title{font-size:24px;font-style:normal;font-weight:700;line-height:36px;margin-top:30px;text-align:center;width:336px}.webapp-pa-prompt .webapp-pa-prompt_container__description{font-size:16px;font-style:normal;font-weight:400;line-height:20.8px;margin-top:16px;text-align:center;width:336px}.webapp-pa-prompt .webapp-pa-prompt_container__description_dark{color:#f6f6f6}.webapp-pa-prompt .webapp-pa-prompt_container__description_light{color:#000}.webapp-pa-prompt .webapp-pa-prompt_container__description_bold{display:inline;font-weight:500}.webapp-pa-prompt .webapp-pa-prompt_container__description_bold_clickable{cursor:pointer}.webapp-pa-prompt .webapp-pa-prompt_container__pa{border-radius:8px;box-shadow:0 2px 12px 0 rgba(0,0,0,.12);display:flex;flex-flow:column nowrap;margin-top:16px;padding:16px 20px 0 20px;width:343px}.webapp-pa-prompt .webapp-pa-prompt_container__pa_dark{background-color:#1e1e1e}.webapp-pa-prompt .webapp-pa-prompt_container__pa_title{font-size:15px;font-style:normal;font-weight:700;line-height:20px}.webapp-pa-prompt .webapp-pa-prompt_container__pa_desc{font-size:13px;font-style:normal;font-weight:400;line-height:17px;margin-bottom:16px;margin-top:8px}.webapp-pa-prompt .webapp-pa-prompt_container__pa_desc_dark{color:hsla(0,0%,100%,.88)}.webapp-pa-prompt .webapp-pa-prompt_container__pa_desc_light{color:rgba(0,0,0,.65)}.webapp-pa-prompt .webapp-pa-prompt_container__pa_button{color:#fe2c55;cursor:pointer;font-size:15px;font-style:normal;font-weight:600;line-height:18px;padding:16px 0;text-align:center}.webapp-pa-prompt .webapp-pa-prompt_container__ga{border-radius:8px;box-shadow:0 2px 12px 0 rgba(0,0,0,.12);display:flex;flex-flow:column nowrap;margin-top:20px;padding:16px 20px 0 20px;width:343px}.webapp-pa-prompt .webapp-pa-prompt_container__ga_dark{background-color:#1e1e1e}.webapp-pa-prompt .webapp-pa-prompt_container__ga_title{font-size:15px;font-style:normal;font-weight:700;line-height:20px}.webapp-pa-prompt .webapp-pa-prompt_container__ga_desc{font-size:13px;font-style:normal;font-weight:400;line-height:17px;margin-bottom:16px;margin-top:8px}.webapp-pa-prompt .webapp-pa-prompt_container__ga_desc_dark{color:hsla(0,0%,100%,.88)}.webapp-pa-prompt .webapp-pa-prompt_container__ga_desc_light{color:rgba(0,0,0,.65)}.webapp-pa-prompt .webapp-pa-prompt_container__ga_button{color:#fe2c55;cursor:pointer;font-size:15px;font-style:normal;font-weight:600;line-height:18px;padding:16px 0;text-align:center}.webapp-pa-prompt .webapp-pa-prompt_container__remarks{font-size:12px;font-style:normal;font-weight:400;line-height:18px;margin-top:16px;text-align:center}.webapp-pa-prompt .webapp-pa-prompt_container__remarks_dark{color:hsla(0,0%,100%,.88)}.webapp-pa-prompt .webapp-pa-prompt_container__remarks_light{color:rgba(22,24,35,.5)}
|
||||
9109
reference/tiktok/files/player.init.254f7048.js
Normal file
9109
reference/tiktok/files/player.init.254f7048.js
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user