tphim 2.0.3 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -4
- package/package.json +2 -2
- package/pipeline.mjs +32 -25
- package/pro-terminal.mjs +7 -4
- package/scripts/upload-tebi.mjs +56 -5
package/README.md
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
## 🚀 Tính năng vượt trội
|
|
6
6
|
- **Batch Pipeline:** Xử lý hàng loạt phim chỉ với 1 dòng lệnh.
|
|
7
|
-
- **AI Subtitles:** Tự động tạo phụ đề Tiếng Việt/Tiếng Anh bằng công cụ AI (Whisper) chạy offline.
|
|
7
|
+
- **AI Subtitles:** Tự động tạo phụ đề Tiếng Việt/Tiếng Anh bằng công cụ AI (Whisper) chạy offline. Hỗ trợ bỏ qua phụ đề để tăng tốc độ.
|
|
8
|
+
- **Resume Upload:** Tự động lưu tiến trình upload, tiếp tục từ điểm dừng nếu bị gián đoạn.
|
|
8
9
|
- **Cloud Ready:** Upload trực tiếp lên Tebi.io hoặc bất kỳ S3-compatible storage nào.
|
|
9
10
|
- **Interactive CLI:** Giao diện terminal đẹp mắt với neon theme.
|
|
10
11
|
|
|
@@ -124,7 +125,7 @@ await executePipeline({
|
|
|
124
125
|
input: "https://link-phim.com/phim.m3u8",
|
|
125
126
|
slug: "phim-hay-2030",
|
|
126
127
|
title: "Phim Hay 2030",
|
|
127
|
-
langArg: "both"
|
|
128
|
+
langArg: "vi" // hoặc "en", "both", "skip"
|
|
128
129
|
});
|
|
129
130
|
```
|
|
130
131
|
|
|
@@ -170,8 +171,9 @@ TEBI_PUBLIC_URL=https://your-bucket.tebi.io
|
|
|
170
171
|
**Interactive Mode:**
|
|
171
172
|
- Nhập multiple links (cách nhau bằng dấu phẩy)
|
|
172
173
|
- Tự động fetch metadata từ video
|
|
173
|
-
- Chọn ngôn ngữ phụ đề (VI/EN/Both)
|
|
174
|
+
- Chọn ngôn ngữ phụ đề (VI/EN/Both/Skip)
|
|
174
175
|
- Batch processing với progress bar
|
|
176
|
+
- **Resume Upload**: Tự động tiếp tục upload nếu bị gián đoạn
|
|
175
177
|
|
|
176
178
|
**Help System:**
|
|
177
179
|
```bash
|
|
@@ -180,6 +182,31 @@ ntxa --help # Tương tự
|
|
|
180
182
|
ntxa -h # Tương tự
|
|
181
183
|
```
|
|
182
184
|
|
|
185
|
+
## 🔄 Resume Upload Feature
|
|
186
|
+
|
|
187
|
+
**Tự động lưu và khôi phục tiến trình upload:**
|
|
188
|
+
|
|
189
|
+
- **Progress Tracking**: Tạo file `.upload-progress.json` trong thư mục HLS
|
|
190
|
+
- **Smart Resume**: Chỉ upload những file chưa có trên cloud
|
|
191
|
+
- **Crash Recovery**: Nếu mất kết nối, chạy lại là tiếp tục từ điểm dừng
|
|
192
|
+
- **Auto Cleanup**: Xóa progress file khi upload hoàn tất
|
|
193
|
+
|
|
194
|
+
**Cách hoạt động:**
|
|
195
|
+
```bash
|
|
196
|
+
# Lần đầu chạy
|
|
197
|
+
☁️ Uploading 1227 files to Tebi.io... (0 already uploaded)
|
|
198
|
+
↳ 107/1227 files...
|
|
199
|
+
|
|
200
|
+
# Nếu bị gián đoạn và chạy lại
|
|
201
|
+
🔄 Found progress file. Resuming from 106 uploaded files...
|
|
202
|
+
☁️ Uploading 1121 files to Tebi.io... (106 already uploaded)
|
|
203
|
+
↳ 113/1227 files...
|
|
204
|
+
|
|
205
|
+
# Bỏ qua file đã tồn tại
|
|
206
|
+
⏭ Skipping hls/test-phim-2024/1080p/index.m3u8 (already exists on cloud)
|
|
207
|
+
```
|
|
208
|
+
|
|
183
209
|
---
|
|
184
210
|
|
|
185
|
-
*Phát triển bởi TXA - Ultimate Video Pipeline 2030* 🍿🎬
|
|
211
|
+
*Phát triển bởi TXA - Ultimate Video Pipeline 2030 v2.2.0* 🍿🎬
|
|
212
|
+
*Last Update: February 2026*
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tphim",
|
|
3
|
-
"version": "2.0
|
|
4
|
-
"description": "TPHIM - Ultimate Video Pipeline: Download, Transcode HLS, AI Subtitles, and Cloud Upload.",
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "TPHIM - Ultimate Video Pipeline: Download, Transcode HLS, AI Subtitles (with skip option), Resume Upload, and Cloud Upload.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
package/pipeline.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// pipeline.mjs
|
|
3
|
-
// Usage: node pipeline.mjs "URL_hoac_duong_dan_mp4" "slug" "Tên Phim" [vi|en|both]
|
|
3
|
+
// Usage: node pipeline.mjs "URL_hoac_duong_dan_mp4" "slug" "Tên Phim" [vi|en|both|skip]
|
|
4
4
|
|
|
5
5
|
import { execSync } from 'child_process';
|
|
6
6
|
import { existsSync, mkdirSync, writeFileSync, rmSync, unlinkSync, readFileSync } from 'fs';
|
|
@@ -106,6 +106,7 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
|
|
|
106
106
|
console.log(' node pipeline.mjs "URL_video" "slug" "Tên Phim" vi');
|
|
107
107
|
console.log(' node pipeline.mjs "D:\\film.mp4" "slug" "Tên Phim" vi');
|
|
108
108
|
console.log(' node pipeline.mjs "URL_video" "slug" "Tên Phim" both');
|
|
109
|
+
console.log(' node pipeline.mjs "URL_video" "slug" "Tên Phim" skip');
|
|
109
110
|
return;
|
|
110
111
|
}
|
|
111
112
|
console.log('\n╔══════════════════════════════════════════════════════════════════╗');
|
|
@@ -152,7 +153,9 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
|
|
|
152
153
|
return `${FINAL_YTDLP} ${proxyFlag} ${commonFlags} "${input}" -o "${mp4Path}"`;
|
|
153
154
|
} else {
|
|
154
155
|
const refFlag = referer ? `--referer "${referer}"` : '--referer "https://embed1.streamc.xyz/"';
|
|
155
|
-
|
|
156
|
+
// Remove impersonation on Ubuntu if not available
|
|
157
|
+
const impersonateFlag = IS_WIN ? '--extractor-args "generic:impersonate=chrome"' : '';
|
|
158
|
+
return `${FINAL_YTDLP} ${impersonateFlag} ${refFlag} ${proxyFlag} ${commonFlags} "${input}" -o "${mp4Path}"`;
|
|
156
159
|
}
|
|
157
160
|
};
|
|
158
161
|
|
|
@@ -171,7 +174,7 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
|
|
|
171
174
|
|
|
172
175
|
// Attempt 2-4: Retry with proxy rotation
|
|
173
176
|
if (!downloaded && PROXY_LIST.length > 0) {
|
|
174
|
-
const MAX_PROXY_RETRIES = Math.min(
|
|
177
|
+
const MAX_PROXY_RETRIES = Math.min(5, PROXY_LIST.length);
|
|
175
178
|
const usedProxies = [];
|
|
176
179
|
console.log(` 🔄 Thử lại với proxy (${PROXY_LIST.length} proxies available)...`);
|
|
177
180
|
|
|
@@ -199,7 +202,7 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
|
|
|
199
202
|
if (!downloaded) {
|
|
200
203
|
throw new Error(
|
|
201
204
|
`❌ Download thất bại: ${input}\n` +
|
|
202
|
-
` Đã thử: direct + ${Math.min(
|
|
205
|
+
` Đã thử: direct + ${Math.min(5, PROXY_LIST.length)} proxies\n` +
|
|
203
206
|
` Nguyên nhân phổ biến:\n` +
|
|
204
207
|
` • Link đã hết hạn hoặc bị xóa (HTTP 404)\n` +
|
|
205
208
|
` • Server CDN chặn tất cả IP\n` +
|
|
@@ -301,30 +304,34 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
|
|
|
301
304
|
generateReadme(slug, mp4Path, { title });
|
|
302
305
|
|
|
303
306
|
// ─── BƯỚC 4: Tạo Subtitle (Python faster-whisper) ─────────────
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
307
|
+
if (langArg !== 'skip') {
|
|
308
|
+
console.log('\n📝 [4/5] Generating subtitles (faster-whisper offline)...');
|
|
309
|
+
const langs = langArg === 'both' ? ['vi', 'en'] : [langArg];
|
|
310
|
+
|
|
311
|
+
for (const lang of langs) {
|
|
312
|
+
const vttPath = path.join('hls', slug, `${lang}.vtt`);
|
|
313
|
+
if (!existsSync(vttPath)) {
|
|
314
|
+
// Detection logic for python command (python vs python3)
|
|
315
|
+
let pythonCmd = 'python';
|
|
316
|
+
try {
|
|
317
|
+
execSync('python --version', { stdio: 'ignore' });
|
|
318
|
+
} catch (e) {
|
|
319
|
+
pythonCmd = 'python3';
|
|
320
|
+
}
|
|
317
321
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
322
|
+
execSync(
|
|
323
|
+
`${pythonCmd} ${path.join(__dirname, 'scripts', 'gen_subtitle.py')} "${mp4Path}" "${slug}" ${lang} "${FFMPEG_BIN}"`,
|
|
324
|
+
{ stdio: 'inherit', cwd: process.cwd() }
|
|
325
|
+
);
|
|
326
|
+
} else {
|
|
327
|
+
console.log(` ⏭ Subtitle [${lang}] đã có sẵn`);
|
|
328
|
+
}
|
|
324
329
|
}
|
|
330
|
+
} else {
|
|
331
|
+
console.log('\n⏭ [4/5] Skipping subtitle generation (selected "skip" option)');
|
|
325
332
|
}
|
|
326
333
|
|
|
327
|
-
// ─── BƯỚC 5: Upload Tebi.io ───────────────────────────────────
|
|
334
|
+
// ─── BƯỚC 5: Upload Tebi.io ───────────────────────────────────
|
|
328
335
|
console.log('\n🚀 [5/5] Deploying to Quantum Cloud (Tebi.io)...');
|
|
329
336
|
|
|
330
337
|
// Optimize upload with parallel processing
|
|
@@ -402,4 +409,4 @@ if (import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/')) || process.arg
|
|
|
402
409
|
console.error('\n❌ LỖI:', err.message);
|
|
403
410
|
process.exit(1);
|
|
404
411
|
});
|
|
405
|
-
}
|
|
412
|
+
}
|
package/pro-terminal.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* NROTXA ULTIMATE CLI 2030 EDITION
|
|
4
|
-
* High-performance, Neon-infused Video Pipeline
|
|
3
|
+
* NROTXA ULTIMATE CLI 2030 EDITION v2.2.0
|
|
4
|
+
* High-performance, Neon-infused Video Pipeline with Resume Upload
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as p from '@clack/prompts';
|
|
@@ -111,7 +111,7 @@ function showHelp() {
|
|
|
111
111
|
)
|
|
112
112
|
);
|
|
113
113
|
|
|
114
|
-
console.log(chalk.gray(' ⚡ TPHIM ULTIMATE VIDEO CORE v2.0
|
|
114
|
+
console.log(chalk.gray(' ⚡ TPHIM ULTIMATE VIDEO CORE v2.2.0 | STATUS: READY ⚡\n'));
|
|
115
115
|
|
|
116
116
|
console.log(`${neonPurple('📖 CÁCH SỬ DỤNG:')}`);
|
|
117
117
|
console.log(chalk.cyan(' ntxa help - Hiển thị help này'));
|
|
@@ -131,6 +131,8 @@ function showHelp() {
|
|
|
131
131
|
console.log(chalk.cyan(' • Download video từ multiple sources'));
|
|
132
132
|
console.log(chalk.cyan(' • Transcode HLS 4-6 qualities'));
|
|
133
133
|
console.log(chalk.cyan(' • AI Subtitle generation (Whisper)'));
|
|
134
|
+
console.log(chalk.cyan(' • Skip subtitle option (fast mode)'));
|
|
135
|
+
console.log(chalk.cyan(' • Resume upload (crash recovery)'));
|
|
134
136
|
console.log(chalk.cyan(' • Upload cloud storage (Tebi.io)'));
|
|
135
137
|
console.log(chalk.cyan(' • Batch processing support'));
|
|
136
138
|
console.log(chalk.cyan(' • Proxy rotation tự động'));
|
|
@@ -168,7 +170,7 @@ async function main() {
|
|
|
168
170
|
)
|
|
169
171
|
);
|
|
170
172
|
|
|
171
|
-
console.log(chalk.gray(' ⚡ NEON-INFUSED VIDEO CORE v2.0
|
|
173
|
+
console.log(chalk.gray(' ⚡ NEON-INFUSED VIDEO CORE v2.2.0 | STATUS: READY ⚡\n'));
|
|
172
174
|
|
|
173
175
|
p.intro(`${neonPurple('▣ SYSTEM INITIALIZED - BATCH MODE ENABLED')}`);
|
|
174
176
|
|
|
@@ -203,6 +205,7 @@ async function main() {
|
|
|
203
205
|
p.select({
|
|
204
206
|
message: '🌐 Quantum transcription language:',
|
|
205
207
|
options: [
|
|
208
|
+
{ value: 'skip', label: '⏭ Bỏ qua Subtitle', hint: 'Nhanh nhất' },
|
|
206
209
|
{ value: 'vi', label: 'Tiếng Việt', hint: 'Default' },
|
|
207
210
|
{ value: 'en', label: 'English', hint: 'Secondary' },
|
|
208
211
|
{ value: 'both', label: 'Dual Core (VI+EN)', hint: 'Premium' },
|
package/scripts/upload-tebi.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// scripts/upload-tebi.mjs
|
|
2
2
|
// Tebi.io S3-compatible — dùng @aws-sdk/client-s3 bình thường
|
|
3
|
-
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
4
|
-
import { readdirSync, readFileSync } from 'fs';
|
|
3
|
+
import { S3Client, PutObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
|
|
4
|
+
import { readdirSync, readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs';
|
|
5
5
|
import { join, extname, relative } from 'path';
|
|
6
6
|
import { lookup } from 'mime-types';
|
|
7
7
|
import 'dotenv/config';
|
|
@@ -19,14 +19,44 @@ const s3 = new S3Client({
|
|
|
19
19
|
export async function uploadHLSFolder(slug) {
|
|
20
20
|
const localDir = join('hls', slug);
|
|
21
21
|
const files = getAllFiles(localDir);
|
|
22
|
+
const progressFile = join('hls', slug, '.upload-progress.json');
|
|
23
|
+
|
|
24
|
+
// Load existing progress
|
|
25
|
+
let uploadedFiles = [];
|
|
26
|
+
if (existsSync(progressFile)) {
|
|
27
|
+
try {
|
|
28
|
+
const progressData = JSON.parse(readFileSync(progressFile, 'utf-8'));
|
|
29
|
+
uploadedFiles = progressData.uploadedFiles || [];
|
|
30
|
+
console.log(`\n🔄 Found progress file. Resuming from ${uploadedFiles.length} uploaded files...`);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.log('\n⚠ Progress file corrupted, starting fresh...');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
22
35
|
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
// Filter files that haven't been uploaded yet
|
|
37
|
+
const filesToUpload = files.filter(file => !uploadedFiles.includes(relative('.', file).replace(/\\/g, '/')));
|
|
38
|
+
|
|
39
|
+
console.log(`\n☁️ Uploading ${filesToUpload.length} files to Tebi.io... (${files.length - filesToUpload.length} already uploaded)`);
|
|
40
|
+
let uploaded = uploadedFiles.length;
|
|
25
41
|
|
|
26
|
-
for (const filePath of
|
|
42
|
+
for (const filePath of filesToUpload) {
|
|
27
43
|
const s3Key = relative('.', filePath).replace(/\\/g, '/');
|
|
28
44
|
const ext = extname(filePath).toLowerCase();
|
|
29
45
|
|
|
46
|
+
// Check if file already exists on cloud
|
|
47
|
+
try {
|
|
48
|
+
await s3.send(new HeadObjectCommand({
|
|
49
|
+
Bucket: process.env.TEBI_BUCKET,
|
|
50
|
+
Key: s3Key
|
|
51
|
+
}));
|
|
52
|
+
console.log(`\n ⏭ Skipping ${s3Key} (already exists on cloud)`);
|
|
53
|
+
uploaded++;
|
|
54
|
+
uploadedFiles.push(s3Key);
|
|
55
|
+
continue;
|
|
56
|
+
} catch (e) {
|
|
57
|
+
// File doesn't exist, proceed with upload
|
|
58
|
+
}
|
|
59
|
+
|
|
30
60
|
const cacheMap = {
|
|
31
61
|
'.ts': 'public, max-age=31536000',
|
|
32
62
|
'.m3u8': 'public, max-age=300',
|
|
@@ -44,9 +74,30 @@ export async function uploadHLSFolder(slug) {
|
|
|
44
74
|
}));
|
|
45
75
|
|
|
46
76
|
uploaded++;
|
|
77
|
+
uploadedFiles.push(s3Key);
|
|
78
|
+
|
|
79
|
+
// Save progress after each successful upload
|
|
80
|
+
writeFileSync(progressFile, JSON.stringify({
|
|
81
|
+
uploadedFiles,
|
|
82
|
+
totalFiles: files.length,
|
|
83
|
+
lastUpdate: new Date().toISOString()
|
|
84
|
+
}));
|
|
85
|
+
|
|
47
86
|
process.stdout.write(`\r ↳ ${uploaded}/${files.length} files...`);
|
|
48
87
|
}
|
|
49
88
|
|
|
89
|
+
// Clean up progress file when complete
|
|
90
|
+
if (uploaded === files.length) {
|
|
91
|
+
try {
|
|
92
|
+
if (existsSync(progressFile)) {
|
|
93
|
+
unlinkSync(progressFile);
|
|
94
|
+
console.log('\n 🧹 Cleaned up progress file');
|
|
95
|
+
}
|
|
96
|
+
} catch (e) {
|
|
97
|
+
// Ignore cleanup error
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
50
101
|
const base = process.env.TEBI_PUBLIC_URL;
|
|
51
102
|
const masterName = `txa-${slug}.m3u8`;
|
|
52
103
|
const finalMasterUrl = `${base}/hls/${slug}/${masterName}`;
|