tphim 2.2.0 → 2.2.2

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 CHANGED
@@ -1,4 +1,4 @@
1
- # 💎 TPHIM - Ultimate Video Pipeline
1
+ # 💎 TPHIM - Ultimate Video Pipeline v2.2.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
 
@@ -8,9 +8,17 @@
8
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.
9
9
  - **Cloud Ready:** Upload trực tiếp lên Tebi.io hoặc bất kỳ S3-compatible storage nào.
10
10
  - **Interactive CLI:** Giao diện terminal đẹp mắt với neon theme.
11
+ - **🆕 Dual Preview Sprites:** Tự động tạo cả High-Res và Low-Res preview sprites cho tối ưu mobile/desktop.
11
12
 
12
13
  ---
13
14
 
15
+ ## 📋 Changelog v2.2.1
16
+ - ✨ **NEW:** Dual Preview Sprites (High-Res + Low-Res) với FFmpeg Complex Filter
17
+ - ⚡ **OPTIMIZATION:** Tiết kiệm CPU với single-pass processing
18
+ - 📱 **MOBILE:** Low-Res sprites (80x45 tiles) tối ưu cho mobile và preview nhanh
19
+ - 🔧 **TECH:** Sử dụng `child_process.spawn` thay cho `execSync` để better performance
20
+ - 🚀 **VPS:** Auto-detect Ubuntu VPS và tối ưu FFmpeg parameters (preset: superfast, tune: zerolatency)
21
+ i
14
22
  ## 🛠️ Hướng dẫn cài đặt (VPS Ubuntu / Google Cloud / Linux)
15
23
 
16
24
  Để chạy được bộ công cụ này trên Linux/VPS, bạn cần cài đặt các "vũ khí" sau:
@@ -208,5 +216,5 @@ ntxa -h # Tương tự
208
216
 
209
217
  ---
210
218
 
211
- *Phát triển bởi TXA - Ultimate Video Pipeline 2030 v2.2.0* 🍿🎬
212
- *Last Update: February 2026*
219
+ *Phát triển bởi TXA - Ultimate Video Pipeline 2030 v2.2.2* 🍿🎬
220
+ *Last Update: March 2026*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tphim",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
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",
@@ -67,4 +67,4 @@
67
67
  "engines": {
68
68
  "node": ">=18.0.0"
69
69
  }
70
- }
70
+ }
package/pipeline.mjs CHANGED
@@ -1,8 +1,14 @@
1
1
  #!/usr/bin/env node
2
- // pipeline.mjs
2
+ // pipeline.mjs v2.2.2
3
3
  // Usage: node pipeline.mjs "URL_hoac_duong_dan_mp4" "slug" "Tên Phim" [vi|en|both|skip]
4
-
5
- import { execSync } from 'child_process';
4
+ //
5
+ // v2.2.2 FEATURES:
6
+ // - Track and Display Total Execution Time (xxhxxmxxs)
7
+ // - Dual Preview Sprites (High-Res + Low-Res) with FFmpeg Complex Filter
8
+ // - Optimized CPU usage with single-pass processing
9
+ // - Low-Res sprites optimized for mobile/preview (80x45 tiles, lighter file)
10
+
11
+ import { execSync, spawn } from 'child_process';
6
12
  import { existsSync, mkdirSync, writeFileSync, rmSync, unlinkSync, readFileSync } from 'fs';
7
13
  import os from 'os';
8
14
  import path, { dirname } from 'path';
@@ -14,8 +20,44 @@ import { generateReadme } from './scripts/gen-readme.mjs';
14
20
 
15
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
16
22
  const IS_WIN = os.platform() === 'win32';
23
+ const IS_LINUX = os.platform() === 'linux';
17
24
  const SHELL_OPTS = IS_WIN ? { shell: 'cmd.exe' } : {};
18
25
 
26
+ // Function to detect if running on Ubuntu VPS and optimize accordingly
27
+ function detectVPSOptimization() {
28
+ if (!IS_LINUX) return { isVPS: false, cpuCores: 4, preset: 'ultrafast', tune: 'fastdecode' };
29
+
30
+ try {
31
+ // Check for Ubuntu/Debian
32
+ const releaseInfo = readFileSync('/etc/os-release', 'utf8');
33
+ const isUbuntu = releaseInfo.includes('ubuntu') || releaseInfo.includes('Ubuntu');
34
+
35
+ // Check if running in cloud environment (VPS indicators)
36
+ const isVPS = existsSync('/proc/vz') || // Virtuozzo/OpenVZ
37
+ existsSync('/proc/xen') || // Xen
38
+ existsSync('/sys/class/dmi/id/product_name') &&
39
+ readFileSync('/sys/class/dmi/id/product_name', 'utf8').toLowerCase().includes('cloud');
40
+
41
+ // Get CPU cores for optimization
42
+ const cpuCores = os.cpus().length;
43
+
44
+ // Optimize FFmpeg parameters based on environment
45
+ let preset = 'ultrafast';
46
+ let tune = 'fastdecode';
47
+
48
+ if (isVPS && isUbuntu) {
49
+ console.log(' 🚀 VPS Ubuntu detected - Applying performance optimizations...');
50
+ preset = 'superfast'; // Balance between speed and quality
51
+ tune = 'fastdecode,zerolatency'; // Optimize for streaming
52
+ }
53
+
54
+ return { isVPS, isUbuntu, cpuCores, preset, tune };
55
+ } catch (error) {
56
+ console.log(' ⚠ Could not detect VPS environment, using default settings');
57
+ return { isVPS: false, cpuCores: 4, preset: 'ultrafast', tune: 'fastdecode' };
58
+ }
59
+ }
60
+
19
61
  // Resolve binaries
20
62
  const FFMPEG_BIN = ffmpeg || 'ffmpeg';
21
63
  const YTDLP_BIN = 'yt-dlp'; // default to PATH if not found in project
@@ -85,6 +127,82 @@ function getRandomProxy(proxies, exclude = []) {
85
127
 
86
128
  const PROXY_LIST = loadProxies();
87
129
 
130
+ // Function to create dual preview sprites using complex filter
131
+ function generateDualPreviewSprites(mp4Path, outputDir) {
132
+ return new Promise((resolve, reject) => {
133
+ const highResPath = path.join(outputDir, 'preview.jpg');
134
+ const lowResPath = path.join(outputDir, 'preview_low.jpg');
135
+
136
+ console.log(' 📸 [2.5/5] Generating Dual Preview Sprites (High-Res + Low-Res)...');
137
+
138
+ // Complex filter để tạo cả 2 phiên bản từ 1 luồng
139
+ const complexFilter = [
140
+ '[0:v]split=2[v0][v1]', // Split video stream thành 2 nhánh
141
+ '[v0]fps=1/10,scale=160:90,tile=10x10,format=yuv420p[v_high]', // High-res version
142
+ '[v1]fps=1/10,scale=80:45,tile=10x10,format=yuv420p[v_low]' // Low-res version (50% size)
143
+ ].join(';');
144
+
145
+ const args = [
146
+ '-i', mp4Path,
147
+ '-filter_complex', complexFilter,
148
+ '-map', '[v_high]',
149
+ '-q:v', '3', // Higher quality for high-res
150
+ '-y',
151
+ highResPath,
152
+ '-map', '[v_low]',
153
+ '-q:v', '8', // Lower quality for low-res (lighter file)
154
+ '-y',
155
+ lowResPath
156
+ ];
157
+
158
+ const ffmpeg = spawn(FFMPEG_BIN, args, {
159
+ stdio: ['ignore', 'pipe', 'pipe'],
160
+ shell: IS_WIN
161
+ });
162
+
163
+ let stdout = '';
164
+ let stderr = '';
165
+
166
+ ffmpeg.stdout.on('data', (data) => {
167
+ stdout += data.toString();
168
+ });
169
+
170
+ ffmpeg.stderr.on('data', (data) => {
171
+ stderr += data.toString();
172
+ });
173
+
174
+ ffmpeg.on('close', (code) => {
175
+ if (code === 0) {
176
+ console.log(' ✅ Dual Preview Sprites ready (High-Res + Low-Res).');
177
+ resolve({ highResPath, lowResPath });
178
+ } else {
179
+ console.error(' ⚠ Dual sprite generation failed:', stderr);
180
+ reject(new Error(`FFmpeg exited with code ${code}`));
181
+ }
182
+ });
183
+
184
+ ffmpeg.on('error', (error) => {
185
+ console.error(' ⚠ FFmpeg spawn error:', error);
186
+ reject(error);
187
+ });
188
+ });
189
+ }
190
+
191
+ function formatDuration(ms) {
192
+ const seconds = Math.floor((ms / 1000) % 60);
193
+ const minutes = Math.floor((ms / (1000 * 60)) % 60);
194
+ const hours = Math.floor((ms / (1000 * 60 * 60)) % 24);
195
+ const days = Math.floor(ms / (1000 * 60 * 60 * 24));
196
+
197
+ const parts = [];
198
+ if (days > 0) parts.push(`${days}d`);
199
+ if (hours > 0 || days > 0) parts.push(`${hours}h`);
200
+ if (minutes > 0 || hours > 0 || days > 0) parts.push(`${minutes}m`);
201
+ parts.push(`${seconds}s`);
202
+
203
+ return parts.join('');
204
+ }
205
+
88
206
  export async function run() {
89
207
  // Nếu chạy trực tiếp từ CLI
90
208
  let [, , input, slug, title = slug, langArg = 'vi'] = process.argv;
@@ -116,6 +234,7 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
116
234
  mkdirSync('downloads', { recursive: true });
117
235
  mkdirSync(path.join('hls', slug), { recursive: true });
118
236
 
237
+ let startTime = Date.now();
119
238
  // ─── BƯỚC 1: Download ─────────────────────────────────────────
120
239
  const mp4Path = existsSync(input) && input.toLowerCase().endsWith('.mp4')
121
240
  ? input
@@ -159,6 +278,7 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
159
278
  }
160
279
  };
161
280
 
281
+ startTime = Date.now();
162
282
  let downloaded = false;
163
283
 
164
284
  // Attempt 1: Direct download (no proxy)
@@ -220,6 +340,13 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
220
340
  const spritePath = path.join('hls', slug, 'preview.jpg');
221
341
  const nokia3gpPath = path.join('hls', slug, `${slug}.3gp`);
222
342
 
343
+ // Detect VPS environment and optimize
344
+ const vpsOpt = detectVPSOptimization();
345
+ console.log(` 💻 System: ${IS_LINUX ? 'Linux' : 'Windows'} | CPU Cores: ${vpsOpt.cpuCores}`);
346
+ if (vpsOpt.isVPS && vpsOpt.isUbuntu) {
347
+ console.log(` 🚀 Ubuntu VPS Mode: Enabled (preset: ${vpsOpt.preset}, tune: ${vpsOpt.tune})`);
348
+ }
349
+
223
350
  // Danh sách toàn bộ chất lượng hỗ trợ
224
351
  const ALL_QUALITIES = [
225
352
  { label: '1080p', w: 1920, h: 1080, bv: '4500k', ba: '192k' },
@@ -250,7 +377,7 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
250
377
 
251
378
  selectedQualities.forEach((q, i) => {
252
379
  maps += `-map 0:v -map 0:a `;
253
- outputs += `-c:v:${i} libx264 -preset ultrafast -tune fastdecode -b:v:${i} ${q.bv} -s:v:${i} ${q.w}x${q.h} -c:a:${i} aac -b:a:${i} ${q.ba} -movflags +faststart `;
380
+ outputs += `-c:v:${i} libx264 -preset ${vpsOpt.preset} -tune ${vpsOpt.tune} -b:v:${i} ${q.bv} -s:v:${i} ${q.w}x${q.h} -c:a:${i} aac -b:a:${i} ${q.ba} -movflags +faststart `;
254
381
  varStreamMapArr.push(`v:${i},a:${i}`);
255
382
  });
256
383
 
@@ -258,9 +385,12 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
258
385
  const timestamp = Date.now();
259
386
  const encodedPrefix = Buffer.from(`txa_${timestamp}`).toString('base64').replace(/[^a-zA-Z0-9]/g, '').substring(0, 8);
260
387
 
388
+ // Optimize thread count based on CPU cores
389
+ const threadCount = vpsOpt.isVPS ? Math.min(vpsOpt.cpuCores, 8) : 0; // 0 = auto, but limit on VPS
390
+
261
391
  const ffmpegCmd = [
262
- `"${FFMPEG_BIN}"`, '-i', `"${mp4Path}"`,
263
- '-threads 0', '-preset ultrafast', '-tune fastdecode',
392
+ `"${FFMPEG_BIN}"`, '-i', `"${mp4Path}"`,
393
+ `-threads ${threadCount}`, `-preset ${vpsOpt.preset}`, `-tune ${vpsOpt.tune}`,
264
394
  maps,
265
395
  outputs,
266
396
  '-var_stream_map', `"${varStreamMapArr.join(' ')}"`,
@@ -276,7 +406,7 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
276
406
  // --- BƯỚC 2.1: XUẤT 3GP CHO MÁY CỔ (NOKIA S40/S60) ---
277
407
  console.log(' [2.1/5] Exporting 3GP legacy format...');
278
408
  console.log(' 📱 [2.1/5] Exporting 3GP legacy format...');
279
- const nokiaCmd = `"${FFMPEG_BIN}" -i "${mp4Path}" -s 320x240 -vcodec mpeg4 -acodec aac -ar 16000 -ac 1 -b:v 250k -b:a 32k -f 3gp "${nokia3gpPath}"`;
409
+ 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}"`;
280
410
  try {
281
411
  execSync(nokiaCmd, { stdio: 'ignore', ...SHELL_OPTS });
282
412
  console.log(' ✅ Legacy 3GP ready.');
@@ -288,15 +418,26 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
288
418
  }
289
419
 
290
420
  // --- STEP 2.5: PREVIEW SPRITE --- (Independent check)
291
- if (!existsSync(spritePath)) {
292
- console.log(' 📸 [2.5/5] Generating Neon Preview SpriteSheet...');
293
- const spriteCmd = `"${FFMPEG_BIN}" -i "${mp4Path}" -vf "fps=1/10,scale=160:90,tile=10x10" -q:v 3 "${spritePath}"`;
421
+ const spriteLowPath = path.join('hls', slug, 'preview_low.jpg');
422
+
423
+ if (!existsSync(spritePath) || !existsSync(spriteLowPath)) {
294
424
  try {
295
- execSync(spriteCmd, { stdio: 'ignore', ...SHELL_OPTS });
296
- console.log(' ✅ Preview SpriteSheet ready.');
297
- } catch (e) { console.log(' Preview generation failed.'); }
425
+ const outDir = path.join('hls', slug);
426
+ const { highResPath, lowResPath } = await generateDualPreviewSprites(mp4Path, outDir);
427
+ console.log(' Dual Preview Sprites generated successfully.');
428
+ } catch (e) {
429
+ console.log(' ⚠ Dual sprite generation failed, falling back to single sprite...');
430
+ // Fallback to original method
431
+ const spriteCmd = `"${FFMPEG_BIN}" -i "${mp4Path}" -vf "fps=1/10,scale=160:90,tile=10x10" -q:v 3 "${spritePath}"`;
432
+ try {
433
+ execSync(spriteCmd, { stdio: 'ignore', ...SHELL_OPTS });
434
+ console.log(' ✅ Fallback Preview SpriteSheet ready.');
435
+ } catch (fallbackError) {
436
+ console.log(' ⚠ All sprite generation methods failed.');
437
+ }
438
+ }
298
439
  } else {
299
- console.log(` ⏭ Preview SpriteSheet đã có sẵn`);
440
+ console.log(` ⏭ Dual Preview Sprites already exist`);
300
441
  }
301
442
 
302
443
  // ─── BƯỚC 3: Tạo README ───────────────────────────────────────
@@ -333,14 +474,14 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
333
474
 
334
475
  // ─── BƯỚC 5: Upload Tebi.io ───────────────────────────────────
335
476
  console.log('\n🚀 [5/5] Deploying to Quantum Cloud (Tebi.io)...');
336
-
477
+
337
478
  // Optimize upload with parallel processing
338
479
  const uploadCmd = `node ${path.join(__dirname, 'scripts', 'upload-tebi.mjs')} "${slug}"`;
339
-
480
+
340
481
  // Run upload with higher process priority and optimized settings
341
482
  try {
342
- execSync(uploadCmd, {
343
- stdio: 'inherit',
483
+ execSync(uploadCmd, {
484
+ stdio: 'inherit',
344
485
  cwd: process.cwd(),
345
486
  maxBuffer: 1024 * 1024, // Increase buffer size
346
487
  env: {
@@ -357,18 +498,20 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
357
498
  // --- FINAL STEP: GENERATE MASTER README.md ---
358
499
  const finalReadmePath = path.join('hls', slug, 'README.md');
359
500
  const bucketName = process.env.TEBI_BUCKET || "txa-vod";
360
- const cloudUrl = `https://s3.tebi.io/${bucketName}/${slug}/${masterName}`;
361
- const nokiaUrl = `https://s3.tebi.io/${bucketName}/${slug}/${slug}.3gp`;
501
+ const cloudUrl = `https://s3.tebi.io/${bucketName}/hls/${slug}/${masterName}`;
502
+ const nokiaUrl = `https://s3.tebi.io/${bucketName}/hls/${slug}/${slug}.3gp`;
362
503
 
363
504
  const report = `
364
505
  # 🎬 TXA PIPELINE REPORT: ${title}
365
506
  Generated on: ${new Date().toLocaleString()}
366
507
 
367
508
  ## 📦 Asset Checklist
509
+ - [x] **Total Execution Time:** ${formatDuration(Date.now() - startTime)}
368
510
  - [x] **Master Playlist:** \`${masterName}\` (AUTO Quality)
369
511
  - [x] **Video Qualities:** ${selectedQualities.map(q => q.label).join(', ')} (Randomized)
370
512
  - [${existsSync(nokia3gpPath) ? 'x' : ' '}] **Legacy Format:** \`${slug}.3gp\` (Nokia S40/S60)
371
- - [${existsSync(spritePath) ? 'x' : ' '}] **Preview SpriteSheet:** \`preview.jpg\` (10x10 Grid)
513
+ - [${existsSync(spritePath) ? 'x' : ' '}] **High-Res Preview SpriteSheet:** \`preview.jpg\` (160x90 tiles, 10x10 grid)
514
+ - [${existsSync(spriteLowPath) ? 'x' : ' '}] **Low-Res Preview SpriteSheet:** \`preview_low.jpg\` (80x45 tiles, 10x10 grid)
372
515
  - [${existsSync(path.join('hls', slug, 'vi.vtt')) ? 'x' : ' '}] **Vietnamese Subtitles:** AI Generated
373
516
  - [${existsSync(path.join('hls', slug, 'en.vtt')) ? 'x' : ' '}] **English Subtitles:** AI Generated
374
517
 
@@ -378,6 +521,10 @@ Generated on: ${new Date().toLocaleString()}
378
521
  ## 📱 Legacy Mobile Link (Nokia/Java)
379
522
  > **[DOWNLOAD 3GP](${nokiaUrl})**
380
523
 
524
+ ## 🖼️ Preview Sprites Information
525
+ - **High-Res Version:** \`preview.jpg\` - Full quality for desktop/fast connections
526
+ - **Low-Res Version:** \`preview_low.jpg\` - Optimized for mobile/preview (50% size, lighter file)
527
+
381
528
  ---
382
529
  *Generated by Antigravity AI Engine v2030*
383
530
  `;
@@ -387,6 +534,9 @@ Generated on: ${new Date().toLocaleString()}
387
534
  console.log(`📄 Summary saved to: ${finalReadmePath}`);
388
535
  console.log(`🔗 Cloud URL: ${cloudUrl}`);
389
536
  if (existsSync(nokia3gpPath)) console.log(`📱 Nokia 3GP: ${nokiaUrl}`);
537
+ if (existsSync(spritePath)) console.log(`🖼️ High-Res Preview: https://s3.tebi.io/${bucketName}/hls/${slug}/preview.jpg`);
538
+ if (existsSync(spriteLowPath)) console.log(`📱 Low-Res Preview: https://s3.tebi.io/${bucketName}/hls/${slug}/preview_low.jpg`);
539
+ console.log(`⏱ TOTAL TIME: ${formatDuration(Date.now() - startTime)}`);
390
540
 
391
541
  // --- AUTO CLEANUP ---
392
542
  console.log('\n🧹 [CLEANUP] Purging local temporary files...');
package/pro-terminal.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * NROTXA ULTIMATE CLI 2030 EDITION v2.2.0
3
+ * NROTXA ULTIMATE CLI 2030 EDITION v2.2.2
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.2.0 | STATUS: READY ⚡\n'));
114
+ console.log(chalk.gray(' ⚡ TPHIM ULTIMATE VIDEO CORE v2.2.2 | 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'));
@@ -170,7 +170,7 @@ async function main() {
170
170
  )
171
171
  );
172
172
 
173
- console.log(chalk.gray(' ⚡ NEON-INFUSED VIDEO CORE v2.2.0 | STATUS: READY ⚡\n'));
173
+ console.log(chalk.gray(' ⚡ NEON-INFUSED VIDEO CORE v2.2.2 | STATUS: READY ⚡\n'));
174
174
 
175
175
  p.intro(`${neonPurple('▣ SYSTEM INITIALIZED - BATCH MODE ENABLED')}`);
176
176
 
@@ -64,6 +64,8 @@ export function generateReadme(slug, mp4Path, info = {}) {
64
64
  const duration = getVideoDuration(mp4Path);
65
65
  const totalSize = getFolderSizeMB(hlsDir);
66
66
  const tsCount = countFiles(hlsDir, '.ts');
67
+ const hasHighResSprite = fs.existsSync(path.join(hlsDir, 'preview.jpg'));
68
+ const hasLowResSprite = fs.existsSync(path.join(hlsDir, 'preview_low.jpg'));
67
69
  const hasSub = fs.existsSync(path.join(hlsDir, 'vi.vtt'));
68
70
  const hasSubEn = fs.existsSync(path.join(hlsDir, 'en.vtt'));
69
71
  const streamInfo = getStreamInfo(mp4Path);
@@ -101,14 +103,21 @@ export function generateReadme(slug, mp4Path, info = {}) {
101
103
  Master playlist : hls/${slug}/master.m3u8
102
104
  → Đây là link chính truyền vào TXAPlayer
103
105
 
104
- 📝 SUBTITLE
106
+ �️ PREVIEW SPRITES
107
+ High-Res Sprite : ${hasHighResSprite ? 'preview.jpg ✅ (160x90 tiles)' : '❌ Chưa có'}
108
+ Low-Res Sprite : ${hasLowResSprite ? 'preview_low.jpg ✅ (80x45 tiles)' : '❌ Chưa có'}
109
+ Usage : Frontend có thể chọn dựa trên tốc độ mạng
110
+
111
+ � SUBTITLE
105
112
  Tiếng Việt : ${hasSub ? 'hls/' + slug + '/vi.vtt ✅' : '❌ Chưa có'}
106
113
  Tiếng Anh : ${hasSubEn ? 'hls/' + slug + '/en.vtt ✅' : '❌ Chưa có'}
107
114
  Tạo bởi : faster-whisper model small (offline, CPU)
108
115
 
109
116
  ☁️ TEBI.IO (sau khi upload)
110
- HLS URL : ${publicUrl}/hls/${slug}/master.m3u8
111
- VTT (VI) : ${publicUrl}/hls/${slug}/vi.vtt
117
+ HLS URL : ${publicUrl}/hls/${slug}/master.m3u8
118
+ VTT (VI) : ${publicUrl}/hls/${slug}/vi.vtt
119
+ High-Res Sprite: ${hasHighResSprite ? publicUrl + '/hls/' + slug + '/preview.jpg' : '❌ Chưa có'}
120
+ Low-Res Sprite : ${hasLowResSprite ? publicUrl + '/hls/' + slug + '/preview_low.jpg' : '❌ Chưa có'}
112
121
 
113
122
  ================================================================================
114
123
  ⚠️ Có thể xóa folder này sau khi upload lên Tebi.io thành công
@@ -129,7 +129,7 @@ function getAllFiles(dir) {
129
129
  return result;
130
130
  }
131
131
  // Tự chạy nếu gọi trực tiếp
132
- if (import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/')) || process.argv[1].endsWith('upload-tebi.mjs')) {
132
+ if (import.meta.url.endsWith(process.argv[1]?.replace(/\\/g, '/')) || process.argv[1]?.endsWith('upload-tebi.mjs')) {
133
133
  const slug = process.argv[2];
134
134
  if (slug) {
135
135
  uploadHLSFolder(slug).catch(console.error);