tphim 2.3.2 → 2.5.1
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 +3 -3
- package/package.json +3 -2
- package/pipeline.mjs +102 -138
- package/pro-terminal.mjs +22 -9
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 💎 TPHIM - Ultimate Video Pipeline v2.
|
|
1
|
+
# 💎 TPHIM - Ultimate Video Pipeline v2.5.1
|
|
2
2
|
|
|
3
3
|
**TPHIM** là bộ công cụ tối thượng để xây dựng hệ thống VOD (Video on Demand) chuyên nghiệp. Hỗ trợ tự động hóa từ khâu tải video, transcode HLS đa chất lượng, tạo phụ đề AI và upload lên Cloud (S3/Tebi).
|
|
4
4
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
## 📋 Changelog v2.
|
|
15
|
+
## 📋 Changelog v2.5.1
|
|
16
16
|
- ✨ **NEW:** Dual Preview Sprites (High-Res + Low-Res) với FFmpeg Complex Filter
|
|
17
17
|
- ⚡ **OPTIMIZATION:** Tiết kiệm CPU với single-pass processing
|
|
18
18
|
- 📱 **MOBILE:** Low-Res sprites (80x45 tiles) tối ưu cho mobile và preview nhanh
|
|
@@ -216,5 +216,5 @@ ntxa -h # Tương tự
|
|
|
216
216
|
|
|
217
217
|
---
|
|
218
218
|
|
|
219
|
-
*Phát triển bởi TXA - Ultimate Video Pipeline 2030 v2.
|
|
219
|
+
*Phát triển bởi TXA - Ultimate Video Pipeline 2030 v2.5.1* 🍿🎬
|
|
220
220
|
*Last Update: March 2026*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tphim",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
4
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",
|
|
@@ -59,7 +59,8 @@
|
|
|
59
59
|
"figlet": "^1.10.0",
|
|
60
60
|
"log-symbols": "^7.0.1",
|
|
61
61
|
"mime-types": "^3.0.2",
|
|
62
|
-
"ora": "^9.3.0"
|
|
62
|
+
"ora": "^9.3.0",
|
|
63
|
+
"tphim": "^2.5.0"
|
|
63
64
|
},
|
|
64
65
|
"funding": {
|
|
65
66
|
"type": "individual",
|
package/pipeline.mjs
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// pipeline.mjs v2.
|
|
2
|
+
// pipeline.mjs v2.5.1
|
|
3
3
|
// Usage: node pipeline.mjs "URL_hoac_duong_dan_mp4" "slug" "Tên Phim" [vi|en|both|skip]
|
|
4
4
|
//
|
|
5
|
-
// v2.
|
|
6
|
-
// -
|
|
7
|
-
// -
|
|
8
|
-
// -
|
|
9
|
-
// -
|
|
5
|
+
// v2.5.1 FEATURES:
|
|
6
|
+
// - Batch HLS Transcoding (2 qualities at a time)
|
|
7
|
+
// - Incremental Uploads (Upload immediately after each batch)
|
|
8
|
+
// - Fast-Track Storyboards (Capture & upload after first batch)
|
|
9
|
+
// - Native HLS Upload Module integration
|
|
10
10
|
|
|
11
11
|
import { execSync, spawn } from 'child_process';
|
|
12
|
-
import { existsSync, mkdirSync, writeFileSync, rmSync, unlinkSync, readFileSync } from 'fs';
|
|
12
|
+
import { existsSync, mkdirSync, writeFileSync, rmSync, unlinkSync, readFileSync, appendFileSync } from 'fs';
|
|
13
13
|
import os from 'os';
|
|
14
14
|
import path, { dirname } from 'path';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
@@ -257,15 +257,26 @@ function formatDuration(ms) {
|
|
|
257
257
|
return parts.join('');
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
function updateMasterPlaylist(masterPath, processedQualities) {
|
|
261
|
+
let content = "#EXTM3U\n#EXT-X-VERSION:3\n";
|
|
262
|
+
// Sort high to low for better player compatibility
|
|
263
|
+
[...processedQualities].sort((a, b) => b.h - a.h).forEach(q => {
|
|
264
|
+
const bw = parseInt(q.bv) * 1000 + (parseInt(q.ba) || 128) * 1000;
|
|
265
|
+
content += `#EXT-X-STREAM-INF:BANDWIDTH=${bw},RESOLUTION=${q.w}x${q.h}\n`;
|
|
266
|
+
content += `${q.label}/index.m3u8\n`;
|
|
267
|
+
});
|
|
268
|
+
writeFileSync(masterPath, content);
|
|
269
|
+
}
|
|
270
|
+
|
|
260
271
|
export async function run() {
|
|
261
272
|
// Nếu chạy trực tiếp từ CLI
|
|
262
|
-
let [, , input, slug, title = slug, langArg = 'vi'] = process.argv;
|
|
273
|
+
let [, , input, slug, title = slug, langArg = 'vi', skip3gp = 'false'] = process.argv;
|
|
263
274
|
|
|
264
275
|
// Hỗ trợ truyền args vào function trong tương lai nếu cần
|
|
265
|
-
return await executePipeline({ input, slug, title, langArg });
|
|
276
|
+
return await executePipeline({ input, slug, title, langArg, skip3gp: skip3gp === 'true' });
|
|
266
277
|
}
|
|
267
278
|
|
|
268
|
-
export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
|
|
279
|
+
export async function executePipeline({ input, slug, title, langArg = 'vi', skip3gp = false }) {
|
|
269
280
|
// --- ENV SHIELD ---
|
|
270
281
|
const required = ['TEBI_ENDPOINT', 'TEBI_ACCESS_KEY_ID', 'TEBI_SECRET_ACCESS_KEY', 'TEBI_BUCKET'];
|
|
271
282
|
const missing = required.filter(k => !process.env[k]);
|
|
@@ -274,11 +285,7 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
|
|
|
274
285
|
}
|
|
275
286
|
|
|
276
287
|
if (!input || !slug) {
|
|
277
|
-
console.log('
|
|
278
|
-
console.log(' node pipeline.mjs "URL_video" "slug" "Tên Phim" vi');
|
|
279
|
-
console.log(' node pipeline.mjs "D:\\film.mp4" "slug" "Tên Phim" vi');
|
|
280
|
-
console.log(' node pipeline.mjs "URL_video" "slug" "Tên Phim" both');
|
|
281
|
-
console.log(' node pipeline.mjs "URL_video" "slug" "Tên Phim" skip');
|
|
288
|
+
console.log(' node pipeline.mjs "URL_video" "slug" "Tên Phim" vi true (skip 3GP)');
|
|
282
289
|
return;
|
|
283
290
|
}
|
|
284
291
|
console.log('\n╔══════════════════════════════════════════════════════════════════╗');
|
|
@@ -408,10 +415,6 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
|
|
|
408
415
|
throw new Error(
|
|
409
416
|
`❌ Download thất bại: ${input}\n` +
|
|
410
417
|
` Đã thử: direct + ${Math.min(5, PROXY_LIST.length)} proxies\n` +
|
|
411
|
-
` Nguyên nhân phổ biến:\n` +
|
|
412
|
-
` • Link đã hết hạn hoặc bị xóa (HTTP 404)\n` +
|
|
413
|
-
` • Server CDN chặn tất cả IP\n` +
|
|
414
|
-
` • Link cần cookie/token đặc biệt\n` +
|
|
415
418
|
` → Hãy thử mở link trong trình duyệt để kiểm tra.`
|
|
416
419
|
);
|
|
417
420
|
}
|
|
@@ -419,58 +422,24 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
|
|
|
419
422
|
console.log(`\n⏭ [1/5] File đã có sẵn: ${mp4Path}`);
|
|
420
423
|
}
|
|
421
424
|
|
|
422
|
-
// ---
|
|
423
|
-
const spriteLowPath = path.join('hls', slug, 'preview_low.jpg');
|
|
424
|
-
const storyboardCheckPath = path.join('hls', slug, 'L1_M0.jpg');
|
|
425
|
-
const outDir = path.join('hls', slug);
|
|
426
|
-
|
|
427
|
-
// Detect VPS environment and optimize
|
|
425
|
+
// --- BATCH PROCESSING LOGIC ---
|
|
428
426
|
const vpsOpt = detectVPSOptimization();
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
console.log('\n📸 [2/5] Capturing Storyboards & Sprites (Turbo Mode)...');
|
|
435
|
-
|
|
436
|
-
// 1. New Storyboard format (L1, L2, L3)
|
|
437
|
-
if (!existsSync(storyboardCheckPath)) {
|
|
438
|
-
try {
|
|
439
|
-
await generateStoryboards(mp4Path, outDir);
|
|
440
|
-
} catch (e) {
|
|
441
|
-
console.log(' ⚠ Storyboard generation failed:', e.message);
|
|
442
|
-
}
|
|
443
|
-
} else {
|
|
444
|
-
console.log(` ⏭ Storyboard levels already exist`);
|
|
445
|
-
}
|
|
427
|
+
const masterName = `txa-${slug}.m3u8`;
|
|
428
|
+
const masterPath = path.join('hls', slug, masterName);
|
|
429
|
+
const logPath = path.join('hls', slug, 'pipeline.log');
|
|
430
|
+
const outDir = path.join('hls', slug);
|
|
446
431
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
} catch (e) {
|
|
452
|
-
console.log(' ⚠ Dual sprite generation failed, falling back to single sprite...');
|
|
453
|
-
const spriteCmd = `"${FFMPEG_BIN}" -i "${mp4Path}" -vf "fps=1/10,scale=160:90,tile=10x10" -q:v 3 "${spritePath}"`;
|
|
454
|
-
try {
|
|
455
|
-
execSync(spriteCmd, { stdio: 'inherit', ...SHELL_OPTS });
|
|
456
|
-
console.log(' ✅ Fallback Preview SpriteSheet ready.');
|
|
457
|
-
} catch (fallbackError) {
|
|
458
|
-
console.log(' ⚠ All sprite generation methods failed.');
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
} else {
|
|
462
|
-
console.log(` ⏭ Dual Preview Sprites already exist`);
|
|
463
|
-
}
|
|
432
|
+
const log = (msg) => {
|
|
433
|
+
console.log(msg);
|
|
434
|
+
appendFileSync(logPath, msg + '\n');
|
|
435
|
+
};
|
|
464
436
|
|
|
465
|
-
|
|
466
|
-
const masterPath = path.join('hls', slug, masterName);
|
|
467
|
-
const nokia3gpPath = path.join('hls', slug, `${slug}.3gp`);
|
|
437
|
+
writeFileSync(logPath, `🎬 Log for ${title} - ${new Date().toLocaleString()}\n\n`);
|
|
468
438
|
|
|
469
|
-
// Danh sách toàn bộ chất lượng hỗ trợ
|
|
470
439
|
const ALL_QUALITIES = [
|
|
471
440
|
{ label: '1080p', w: 1920, h: 1080, bv: '4500k', ba: '192k' },
|
|
472
441
|
{ label: '720p', w: 1280, h: 720, bv: '1500k', ba: '128k' },
|
|
473
|
-
{ label: '480p', w: 854, h: 480, bv: '1000k', ba: '128k' },
|
|
442
|
+
{ label: '480p', w: 854, h: 480, bv: '1000k', ba: '128k' },
|
|
474
443
|
{ label: '360p', w: 640, h: 360, bv: '600k', ba: '96k' },
|
|
475
444
|
{ label: '240p', w: 426, h: 240, bv: '400k', ba: '64k' },
|
|
476
445
|
{ label: '144p', w: 256, h: 144, bv: '200k', ba: '48k' }
|
|
@@ -482,33 +451,35 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
|
|
|
482
451
|
.slice(0, count)
|
|
483
452
|
.sort((a, b) => b.h - a.h);
|
|
484
453
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
454
|
+
const batches = [];
|
|
455
|
+
for (let i = 0; i < selectedQualities.length; i += 2) {
|
|
456
|
+
batches.push(selectedQualities.slice(i, i + 2));
|
|
457
|
+
}
|
|
488
458
|
|
|
489
|
-
|
|
459
|
+
let processedQualities = [];
|
|
460
|
+
const timestamp = Date.now();
|
|
461
|
+
const encodedPrefix = Buffer.from(`txa_${timestamp}`).toString('base64').replace(/[^a-zA-Z0-9]/g, '').substring(0, 8);
|
|
490
462
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
463
|
+
for (let bIndex = 0; bIndex < batches.length; bIndex++) {
|
|
464
|
+
const batch = batches[bIndex];
|
|
465
|
+
log(`\n📦 [Batch ${bIndex + 1}/${batches.length}] Processing: ${batch.map(q => q.label).join(', ')}`);
|
|
466
|
+
|
|
467
|
+
let filterParts = [`[0:v]split=${batch.length}`];
|
|
468
|
+
let splitLabels = batch.map((_, i) => `[v_raw${i}]`);
|
|
495
469
|
filterParts[0] += splitLabels.join('');
|
|
496
470
|
|
|
497
471
|
let outputs = '';
|
|
498
472
|
let maps = '';
|
|
499
473
|
let varStreamMapArr = [];
|
|
500
474
|
|
|
501
|
-
|
|
475
|
+
batch.forEach((q, i) => {
|
|
502
476
|
filterParts.push(`[v_raw${i}]scale=${q.w}:${q.h},format=yuv420p[v${i}]`);
|
|
503
477
|
maps += `-map "[v${i}]" -map 0:a `;
|
|
504
|
-
outputs += `-c:v:${i} libx264 -preset
|
|
478
|
+
outputs += `-c:v:${i} libx264 -preset ${vpsOpt.preset} -tune ${vpsOpt.tune} -b:v:${i} ${q.bv} -c:a:${i} aac -b:a:${i} ${q.ba} -movflags +faststart `;
|
|
505
479
|
varStreamMapArr.push(`v:${i},a:${i}`);
|
|
506
480
|
});
|
|
507
481
|
|
|
508
|
-
const
|
|
509
|
-
const encodedPrefix = Buffer.from(`txa_${timestamp}`).toString('base64').replace(/[^a-zA-Z0-9]/g, '').substring(0, 8);
|
|
510
|
-
const threadCount = vpsOpt.isVPS ? Math.min(vpsOpt.cpuCores * 2, 16) : 0; // Oversubscribe for maximum saturation
|
|
511
|
-
|
|
482
|
+
const threadCount = vpsOpt.isVPS ? Math.min(vpsOpt.cpuCores * 2, 16) : 0;
|
|
512
483
|
const ffmpegCmd = [
|
|
513
484
|
`"${FFMPEG_BIN}"`, '-i', `"${mp4Path}"`,
|
|
514
485
|
`-threads ${threadCount}`,
|
|
@@ -516,90 +487,78 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
|
|
|
516
487
|
maps,
|
|
517
488
|
outputs,
|
|
518
489
|
'-var_stream_map', `"${varStreamMapArr.join(' ')}"`,
|
|
519
|
-
'-
|
|
520
|
-
'-f hls -hls_time 6 -hls_list_size 0 -hls_segment_type mpegts', // 6s segments are faster to write than 4s
|
|
490
|
+
'-f hls -hls_time 6 -hls_list_size 0 -hls_segment_type mpegts',
|
|
521
491
|
'-hls_segment_filename', `"${outDir}/%v/${encodedPrefix}_%04d.ts"`,
|
|
522
492
|
`"${outDir}/%v/index.m3u8"`
|
|
523
493
|
].join(' ');
|
|
524
494
|
|
|
495
|
+
log(` 🛠 Running FFmpeg Transcoding...`);
|
|
525
496
|
execSync(ffmpegCmd, { stdio: 'inherit', ...SHELL_OPTS });
|
|
526
|
-
console.log(' HLS Master generated.');
|
|
527
497
|
|
|
528
|
-
//
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
498
|
+
// Rename folders from index (0, 1) to labels
|
|
499
|
+
batch.forEach((q, i) => {
|
|
500
|
+
const oldFolder = path.join(outDir, i.toString());
|
|
501
|
+
const newFolder = path.join(outDir, q.label);
|
|
502
|
+
if (existsSync(oldFolder)) {
|
|
503
|
+
if (existsSync(newFolder)) rmSync(newFolder, { recursive: true, force: true });
|
|
504
|
+
execSync(IS_WIN ? `move "${oldFolder}" "${newFolder}"` : `mv "${oldFolder}" "${newFolder}"`, SHELL_OPTS);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
processedQualities.push(...batch);
|
|
509
|
+
updateMasterPlaylist(masterPath, processedQualities);
|
|
510
|
+
|
|
511
|
+
// Upload Batch
|
|
512
|
+
log(` 🚀 Uploading Batch ${bIndex + 1} to Tebi...`);
|
|
513
|
+
await uploadHLSFolder(slug);
|
|
514
|
+
|
|
515
|
+
// After Batch 1, capture and upload storyboards
|
|
516
|
+
if (bIndex === 0) {
|
|
517
|
+
log(`\n📸 [Fast-Track] First Batch Uploaded. Generating Storyboards...`);
|
|
518
|
+
await generateStoryboards(mp4Path, outDir);
|
|
519
|
+
await generateDualPreviewSprites(mp4Path, outDir);
|
|
520
|
+
log(` 🚀 Uploading Preview Assets...`);
|
|
521
|
+
await uploadHLSFolder(slug);
|
|
537
522
|
}
|
|
538
|
-
} else {
|
|
539
|
-
console.log(`\n⏭ [2/5] HLS đã có sẵn`);
|
|
540
523
|
}
|
|
541
524
|
|
|
525
|
+
// --- STEP 3: LEGACY & DOCS ---
|
|
526
|
+
const nokia3gpPath = path.join('hls', slug, `${slug}.3gp`);
|
|
527
|
+
if (!skip3gp && !existsSync(nokia3gpPath)) {
|
|
528
|
+
log('\n📱 Exporting legacy 3GP format...');
|
|
529
|
+
const nokiaCmd = `"${FFMPEG_BIN}" -i "${mp4Path}" -s 320x240 -vcodec mpeg4 -acodec aac -ar 16000 -ac 1 -b:v 250k -b:a 32k -preset ${vpsOpt.preset} -f 3gp "${nokia3gpPath}"`;
|
|
530
|
+
try { execSync(nokiaCmd, { stdio: 'ignore', ...SHELL_OPTS }); } catch (e) { log(' ⚠ 3GP failed.'); }
|
|
531
|
+
} else if (skip3gp) {
|
|
532
|
+
log('\n⏭ Skipping legacy 3GP export (skip3gp=true)');
|
|
533
|
+
}
|
|
542
534
|
|
|
543
|
-
|
|
544
|
-
console.log('\n📄 [3/5] Generating README.txt...');
|
|
535
|
+
log('\n📄 Generating README and Subtitles...');
|
|
545
536
|
generateReadme(slug, mp4Path, { title });
|
|
546
537
|
|
|
547
|
-
// ─── BƯỚC 4: Tạo Subtitle (Python faster-whisper) ─────────────
|
|
548
538
|
if (langArg !== 'skip') {
|
|
549
|
-
console.log('\n📝 [4/5] Generating subtitles (faster-whisper offline)...');
|
|
550
539
|
const langs = langArg === 'both' ? ['vi', 'en'] : [langArg];
|
|
551
|
-
|
|
552
540
|
for (const lang of langs) {
|
|
553
541
|
const vttPath = path.join('hls', slug, `${lang}.vtt`);
|
|
554
542
|
if (!existsSync(vttPath)) {
|
|
555
|
-
// Detection logic for python command (python vs python3)
|
|
556
543
|
let pythonCmd = 'python';
|
|
544
|
+
try { execSync('python --version', { stdio: 'ignore' }); } catch (e) { pythonCmd = 'python3'; }
|
|
557
545
|
try {
|
|
558
|
-
execSync(
|
|
559
|
-
} catch (e) {
|
|
560
|
-
pythonCmd = 'python3';
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
execSync(
|
|
564
|
-
`${pythonCmd} ${path.join(__dirname, 'scripts', 'gen_subtitle.py')} "${mp4Path}" "${slug}" ${lang} "${FFMPEG_BIN}"`,
|
|
565
|
-
{ stdio: 'inherit', cwd: process.cwd() }
|
|
566
|
-
);
|
|
567
|
-
} else {
|
|
568
|
-
console.log(` ⏭ Subtitle [${lang}] đã có sẵn`);
|
|
546
|
+
execSync(`${pythonCmd} ${path.join(__dirname, 'scripts', 'gen_subtitle.py')} "${mp4Path}" "${slug}" ${lang} "${FFMPEG_BIN}"`, { stdio: 'inherit', cwd: process.cwd() });
|
|
547
|
+
} catch (e) { log(` ⚠ Subtitle [${lang}] failed.`); }
|
|
569
548
|
}
|
|
570
549
|
}
|
|
571
|
-
} else {
|
|
572
|
-
console.log('\n⏭ [4/5] Skipping subtitle generation (selected "skip" option)');
|
|
573
550
|
}
|
|
574
551
|
|
|
575
|
-
//
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
// Optimize upload with parallel processing
|
|
579
|
-
const uploadCmd = `node ${path.join(__dirname, 'scripts', 'upload-tebi.mjs')} "${slug}"`;
|
|
552
|
+
// Final Sync Upload
|
|
553
|
+
log('\n🚀 Final sync upload...');
|
|
554
|
+
await uploadHLSFolder(slug);
|
|
580
555
|
|
|
581
|
-
// Run upload with higher process priority and optimized settings
|
|
582
|
-
try {
|
|
583
|
-
execSync(uploadCmd, {
|
|
584
|
-
stdio: 'inherit',
|
|
585
|
-
cwd: process.cwd(),
|
|
586
|
-
maxBuffer: 1024 * 1024, // Increase buffer size
|
|
587
|
-
env: {
|
|
588
|
-
...process.env,
|
|
589
|
-
NODE_OPTIONS: '--max-old-space-size=4096', // Optimize Node.js memory
|
|
590
|
-
UV_THREADPOOL_SIZE: '16' // Increase libuv thread pool
|
|
591
|
-
}
|
|
592
|
-
});
|
|
593
|
-
} catch (uploadError) {
|
|
594
|
-
console.error(' ⚠ Upload failed:', uploadError.message);
|
|
595
|
-
throw uploadError;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// --- FINAL STEP: GENERATE MASTER README.md ---
|
|
599
|
-
const finalReadmePath = path.join('hls', slug, 'README.md');
|
|
600
556
|
const bucketName = process.env.TEBI_BUCKET || "txa-vod";
|
|
601
557
|
const cloudUrl = `https://s3.tebi.io/${bucketName}/hls/${slug}/${masterName}`;
|
|
558
|
+
const finalReadmePath = path.join('hls', slug, 'README.md');
|
|
602
559
|
const nokiaUrl = `https://s3.tebi.io/${bucketName}/hls/${slug}/${slug}.3gp`;
|
|
560
|
+
const spritePath = path.join('hls', slug, 'preview.jpg');
|
|
561
|
+
const spriteLowPath = path.join('hls', slug, 'preview_low.jpg');
|
|
603
562
|
|
|
604
563
|
const report = `
|
|
605
564
|
# 🎬 TXA PIPELINE REPORT: ${title}
|
|
@@ -609,7 +568,7 @@ Generated on: ${new Date().toLocaleString()}
|
|
|
609
568
|
- [x] **Total Execution Time:** ${formatDuration(Date.now() - startTime)}
|
|
610
569
|
- [x] **Master Playlist:** \`${masterName}\` (AUTO Quality)
|
|
611
570
|
- [x] **Video Qualities:** ${selectedQualities.map(q => q.label).join(', ')} (Randomized)
|
|
612
|
-
- [${existsSync(nokia3gpPath) ? 'x' : ' '}] **Legacy Format:** \`${slug}.3gp\` (Nokia S40/S60)
|
|
571
|
+
- [${skip3gp ? 'skip' : (existsSync(nokia3gpPath) ? 'x' : ' ')}] **Legacy Format:** \`${slug}.3gp\` (Nokia S40/S60)
|
|
613
572
|
- [${existsSync(spritePath) ? 'x' : ' '}] **High-Res Preview SpriteSheet:** \`preview.jpg\` (160x90 tiles, 10x10 grid)
|
|
614
573
|
- [${existsSync(spriteLowPath) ? 'x' : ' '}] **Low-Res Preview SpriteSheet:** \`preview_low.jpg\` (80x45 tiles, 10x10 grid)
|
|
615
574
|
- [${existsSync(path.join('hls', slug, 'vi.vtt')) ? 'x' : ' '}] **Vietnamese Subtitles:** AI Generated
|
|
@@ -619,7 +578,7 @@ Generated on: ${new Date().toLocaleString()}
|
|
|
619
578
|
> **[WATCH NOW (HLS)](${cloudUrl})**
|
|
620
579
|
|
|
621
580
|
## 📱 Legacy Mobile Link (Nokia/Java)
|
|
622
|
-
>
|
|
581
|
+
> ${skip3gp ? '*Disabled by user*' : `**[DOWNLOAD 3GP](${nokiaUrl})**`}
|
|
623
582
|
|
|
624
583
|
## 🖼️ Preview Sprites Information
|
|
625
584
|
- **High-Res Version:** \`preview.jpg\` - Full quality for desktop/fast connections
|
|
@@ -654,7 +613,12 @@ Generated on: ${new Date().toLocaleString()}
|
|
|
654
613
|
}
|
|
655
614
|
|
|
656
615
|
// Tự khởi chạy nếu được gọi trực tiếp qua CLI
|
|
657
|
-
|
|
616
|
+
const isCLI = process.argv[1] && (
|
|
617
|
+
import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/')) ||
|
|
618
|
+
process.argv[1].endsWith('pipeline.mjs')
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
if (isCLI) {
|
|
658
622
|
run().catch(err => {
|
|
659
623
|
console.error('\n❌ LỖI:', err.message);
|
|
660
624
|
process.exit(1);
|
package/pro-terminal.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* NROTXA ULTIMATE CLI 2030 EDITION v2.
|
|
3
|
+
* NROTXA ULTIMATE CLI 2030 EDITION v2.5.1
|
|
4
4
|
* High-performance, Neon-infused Video Pipeline with Resume Upload
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -111,7 +111,7 @@ function showHelp() {
|
|
|
111
111
|
)
|
|
112
112
|
);
|
|
113
113
|
|
|
114
|
-
console.log(chalk.gray(' ⚡ TPHIM ULTIMATE VIDEO CORE v2.
|
|
114
|
+
console.log(chalk.gray(' ⚡ TPHIM ULTIMATE VIDEO CORE v2.5.1 | 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'));
|
|
@@ -129,11 +129,9 @@ function showHelp() {
|
|
|
129
129
|
|
|
130
130
|
console.log(`${neonPurple('⚡ TÍNH NĂNG:')}`);
|
|
131
131
|
console.log(chalk.cyan(' • Download video từ multiple sources'));
|
|
132
|
-
console.log(chalk.cyan(' •
|
|
133
|
-
console.log(chalk.cyan(' •
|
|
134
|
-
console.log(chalk.cyan(' • Skip
|
|
135
|
-
console.log(chalk.cyan(' • Resume upload (crash recovery)'));
|
|
136
|
-
console.log(chalk.cyan(' • Upload cloud storage (Tebi.io)'));
|
|
132
|
+
console.log(chalk.cyan(' • Batch HLS Transcoding (Incremental Uploads)'));
|
|
133
|
+
console.log(chalk.cyan(' • Fast-Track Storyboards (After first batch)'));
|
|
134
|
+
console.log(chalk.cyan(' • Skip 3GP export option'));
|
|
137
135
|
console.log(chalk.cyan(' • Batch processing support'));
|
|
138
136
|
console.log(chalk.cyan(' • Proxy rotation tự động'));
|
|
139
137
|
console.log(chalk.cyan(' • Encoded segment names (base64)'));
|
|
@@ -154,6 +152,12 @@ async function main() {
|
|
|
154
152
|
return;
|
|
155
153
|
}
|
|
156
154
|
|
|
155
|
+
// Check for version command
|
|
156
|
+
if (args.some(arg => ['-v', '--version', '-V', '--Version'].includes(arg))) {
|
|
157
|
+
console.log(chalk.cyan('TPHIM Ultimate Video Pipeline ') + chalk.bold('v2.5.1'));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
157
161
|
checkEnv();
|
|
158
162
|
console.clear();
|
|
159
163
|
|
|
@@ -170,7 +174,7 @@ async function main() {
|
|
|
170
174
|
)
|
|
171
175
|
);
|
|
172
176
|
|
|
173
|
-
console.log(chalk.gray(' ⚡ NEON-INFUSED VIDEO CORE v2.
|
|
177
|
+
console.log(chalk.gray(' ⚡ NEON-INFUSED VIDEO CORE v2.5.1 | STATUS: READY ⚡\n'));
|
|
174
178
|
|
|
175
179
|
p.intro(`${neonPurple('▣ SYSTEM INITIALIZED - BATCH MODE ENABLED')}`);
|
|
176
180
|
|
|
@@ -211,6 +215,14 @@ async function main() {
|
|
|
211
215
|
{ value: 'both', label: 'Dual Core (VI+EN)', hint: 'Premium' },
|
|
212
216
|
],
|
|
213
217
|
}),
|
|
218
|
+
skip3gp: () =>
|
|
219
|
+
p.select({
|
|
220
|
+
message: '📱 Legacy 3GP Export:',
|
|
221
|
+
options: [
|
|
222
|
+
{ value: false, label: '✅ Yes', hint: 'Generate 3GP for older devices' },
|
|
223
|
+
{ value: true, label: '❌ No', hint: 'Skip 3GP (Saves time)' },
|
|
224
|
+
],
|
|
225
|
+
}),
|
|
214
226
|
confirm: () =>
|
|
215
227
|
p.confirm({
|
|
216
228
|
message: '🚀 Initiate batch pipeline sequence now?',
|
|
@@ -291,7 +303,8 @@ async function main() {
|
|
|
291
303
|
input: inputUrl,
|
|
292
304
|
slug: finalSlug,
|
|
293
305
|
title: currentTitle,
|
|
294
|
-
langArg: group.lang
|
|
306
|
+
langArg: group.lang,
|
|
307
|
+
skip3gp: group.skip3gp
|
|
295
308
|
});
|
|
296
309
|
|
|
297
310
|
s.stop(`${neonPink(`✅ [${i + 1}/${links.length}] COMPLETE:`)} ${currentTitle}`);
|