tphim 1.0.3 → 1.0.5

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/.env.example CHANGED
@@ -3,3 +3,7 @@ TEBI_ACCESS_KEY_ID=your_access_key
3
3
  TEBI_SECRET_ACCESS_KEY=your_secret_key
4
4
  TEBI_BUCKET=your_bucket_name
5
5
  TEBI_PUBLIC_URL=https://s3.tebi.io/your_bucket_name
6
+
7
+ # Proxy rotation (Optional)
8
+ # Format: ip:port:user:pass, separated by |
9
+ PROXY_LIST=31.59.20.176:6754:user:pass|ip:port:user:pass
package/package.json CHANGED
@@ -1,12 +1,21 @@
1
1
  {
2
2
  "name": "tphim",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "TPHIM - Ultimate Video Pipeline: Download, Transcode HLS, AI Subtitles, and Cloud Upload.",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "ntxa": "./pro-terminal.mjs"
9
9
  },
10
+ "files": [
11
+ "pro-terminal.mjs",
12
+ "pipeline.mjs",
13
+ "scripts/",
14
+ "README.md",
15
+ "LICENSE.md",
16
+ "index.js",
17
+ ".env.example"
18
+ ],
10
19
  "scripts": {
11
20
  "build": "node -e \"console.log('Building TPHIM by TXA... Integrity check passed.')\"",
12
21
  "prepublishOnly": "node -e \"if(!require('fs').existsSync('.env.example')) throw new Error('Missing .env.example')\"",
@@ -44,10 +53,13 @@
44
53
  "chalk": "^5.6.2",
45
54
  "cli-progress": "^3.12.0",
46
55
  "dotenv": "^17.3.1",
56
+ "ffmpeg-static": "^5.2.0",
57
+ "ffprobe-static": "^3.1.0",
47
58
  "figlet": "^1.10.0",
48
59
  "log-symbols": "^7.0.1",
49
60
  "mime-types": "^3.0.2",
50
- "ora": "^9.3.0"
61
+ "ora": "^9.3.0",
62
+ "yt-dlp-exec": "^1.0.2"
51
63
  },
52
64
  "funding": {
53
65
  "type": "individual",
package/pipeline.mjs CHANGED
@@ -3,12 +3,88 @@
3
3
  // Usage: node pipeline.mjs "URL_hoac_duong_dan_mp4" "slug" "Tên Phim" [vi|en|both]
4
4
 
5
5
  import { execSync } from 'child_process';
6
- import { existsSync, mkdirSync, writeFileSync, rmSync, unlinkSync } from 'fs';
7
- import path from 'path';
6
+ import { existsSync, mkdirSync, writeFileSync, rmSync, unlinkSync, readFileSync } from 'fs';
7
+ import os from 'os';
8
+ import path, { dirname } from 'path';
9
+ import { fileURLToPath } from 'url';
8
10
  import 'dotenv/config';
11
+ import ffmpeg from 'ffmpeg-static';
9
12
  import { uploadHLSFolder } from './scripts/upload-tebi.mjs';
10
13
  import { generateReadme } from './scripts/gen-readme.mjs';
11
14
 
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const IS_WIN = os.platform() === 'win32';
17
+ const SHELL_OPTS = IS_WIN ? { shell: 'cmd.exe' } : {};
18
+
19
+ // Resolve binaries
20
+ const FFMPEG_BIN = ffmpeg || 'ffmpeg';
21
+ const YTDLP_BIN = 'yt-dlp'; // default to PATH if not found in project
22
+
23
+ // Search for local yt-dlp in node_modules if it exists
24
+ const localYtDlp = path.join(__dirname, 'node_modules', 'yt-dlp-exec', 'bin', 'yt-dlp.exe');
25
+ const localYtDlpUnix = path.join(__dirname, 'node_modules', 'yt-dlp-exec', 'bin', 'yt-dlp');
26
+ const FINAL_YTDLP = existsSync(localYtDlp) ? `"${localYtDlp}"` : (existsSync(localYtDlpUnix) ? localYtDlpUnix : YTDLP_BIN);
27
+
28
+ // ─── PROXY ROTATION ──────────────────────────────────────────────
29
+ // Parse a single proxy line: ip:port:user:pass → http://user:pass@ip:port
30
+ function parseProxyLine(line) {
31
+ const l = line.trim();
32
+ if (!l || l.startsWith('#')) return null;
33
+
34
+ // Already a URL format (http://... or socks5://...)
35
+ if (l.startsWith('http') || l.startsWith('socks')) return l;
36
+
37
+ const parts = l.split(':');
38
+ if (parts.length === 4) {
39
+ const [ip, port, user, pass] = parts;
40
+ return `http://${user}:${pass}@${ip}:${port}`;
41
+ } else if (parts.length === 2) {
42
+ return `http://${parts[0]}:${parts[1]}`;
43
+ }
44
+ return null;
45
+ }
46
+
47
+ function loadProxies() {
48
+ let proxies = [];
49
+
50
+ // Source 1: PROXY_LIST env variable (pipe | separated)
51
+ // Example: PROXY_LIST=ip:port:user:pass|ip:port:user:pass
52
+ if (process.env.PROXY_LIST) {
53
+ const envProxies = process.env.PROXY_LIST
54
+ .split('|')
55
+ .map(parseProxyLine)
56
+ .filter(Boolean);
57
+ proxies.push(...envProxies);
58
+ }
59
+
60
+ // Source 2: proxies.txt file (fallback, one per line)
61
+ if (proxies.length === 0) {
62
+ const proxyFile = path.join(__dirname, 'proxies.txt');
63
+ if (existsSync(proxyFile)) {
64
+ try {
65
+ const fileProxies = readFileSync(proxyFile, 'utf-8')
66
+ .split(/\r?\n/)
67
+ .map(parseProxyLine)
68
+ .filter(Boolean);
69
+ proxies.push(...fileProxies);
70
+ } catch (_) { }
71
+ }
72
+ }
73
+
74
+ if (proxies.length > 0) {
75
+ console.log(` 🔌 Loaded ${proxies.length} proxies (from ${process.env.PROXY_LIST ? '.env' : 'proxies.txt'})`);
76
+ }
77
+ return proxies;
78
+ }
79
+
80
+ function getRandomProxy(proxies, exclude = []) {
81
+ const available = proxies.filter(p => !exclude.includes(p));
82
+ if (available.length === 0) return null;
83
+ return available[Math.floor(Math.random() * available.length)];
84
+ }
85
+
86
+ const PROXY_LIST = loadProxies();
87
+
12
88
  export async function run() {
13
89
  // Nếu chạy trực tiếp từ CLI
14
90
  let [, , input, slug, title = slug, langArg = 'vi'] = process.argv;
@@ -49,7 +125,7 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
49
125
 
50
126
  // Preserve previous yt-dlp improvements
51
127
  let referer = '';
52
- let userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
128
+ let userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
53
129
 
54
130
  try {
55
131
  const urlObj = new URL(input);
@@ -58,12 +134,79 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
58
134
 
59
135
  // Nếu là link S3/Tebi, dùng User-Agent sạch thay vì impersonate để tránh 403
60
136
  const isS3 = input.includes('s3.tebi.io') || input.includes('amazonaws.com');
61
- const dlCmd = isS3
62
- ? `yt-dlp --no-check-certificate --user-agent "${userAgent}" --no-part --retries 10 --fragment-retries 10 --concurrent-fragments 5 "${input}" -o "${mp4Path}"`
63
- : `yt-dlp --extractor-args "generic:impersonate=chrome" --referer "${referer || 'https://embed1.streamc.xyz/'}" --no-part --retries 10 --fragment-retries 10 --concurrent-fragments 5 "${input}" -o "${mp4Path}"`;
64
137
 
65
- execSync(dlCmd, { stdio: 'inherit' });
66
- console.log(' ✅ Download xong');
138
+ // Common flags for all downloads
139
+ const commonFlags = [
140
+ '--no-check-certificate',
141
+ `--user-agent "${userAgent}"`,
142
+ '--no-part',
143
+ '--retries 10',
144
+ '--fragment-retries 10',
145
+ '--concurrent-fragments 5',
146
+ ].join(' ');
147
+
148
+ // Build base command (without proxy)
149
+ const buildDlCmd = (proxyUrl) => {
150
+ const proxyFlag = proxyUrl ? `--proxy "${proxyUrl}"` : '';
151
+ if (isS3) {
152
+ return `${FINAL_YTDLP} ${proxyFlag} ${commonFlags} "${input}" -o "${mp4Path}"`;
153
+ } else {
154
+ const refFlag = referer ? `--referer "${referer}"` : '--referer "https://embed1.streamc.xyz/"';
155
+ return `${FINAL_YTDLP} --extractor-args "generic:impersonate=chrome" ${refFlag} ${proxyFlag} ${commonFlags} "${input}" -o "${mp4Path}"`;
156
+ }
157
+ };
158
+
159
+ let downloaded = false;
160
+
161
+ // Attempt 1: Direct download (no proxy)
162
+ try {
163
+ console.log(' 🌐 Thử download trực tiếp...');
164
+ execSync(buildDlCmd(null), { stdio: 'inherit' });
165
+ downloaded = true;
166
+ console.log(' ✅ Download xong (direct)');
167
+ } catch (_) {
168
+ try { if (existsSync(mp4Path)) unlinkSync(mp4Path); } catch (__) { }
169
+ console.log(' ⚠ Download trực tiếp thất bại.');
170
+ }
171
+
172
+ // Attempt 2-4: Retry with proxy rotation
173
+ if (!downloaded && PROXY_LIST.length > 0) {
174
+ const MAX_PROXY_RETRIES = Math.min(3, PROXY_LIST.length);
175
+ const usedProxies = [];
176
+ console.log(` 🔄 Thử lại với proxy (${PROXY_LIST.length} proxies available)...`);
177
+
178
+ for (let attempt = 1; attempt <= MAX_PROXY_RETRIES; attempt++) {
179
+ const proxy = getRandomProxy(PROXY_LIST, usedProxies);
180
+ if (!proxy) break;
181
+ usedProxies.push(proxy);
182
+
183
+ // Mask credentials in log
184
+ const maskedProxy = proxy.replace(/:([^@:]+)@/, ':***@');
185
+ console.log(` 🔀 [Proxy ${attempt}/${MAX_PROXY_RETRIES}] ${maskedProxy}`);
186
+
187
+ try {
188
+ execSync(buildDlCmd(proxy), { stdio: 'inherit' });
189
+ downloaded = true;
190
+ console.log(` ✅ Download xong (via proxy)`);
191
+ break;
192
+ } catch (_) {
193
+ try { if (existsSync(mp4Path)) unlinkSync(mp4Path); } catch (__) { }
194
+ console.log(` ⚠ Proxy ${attempt} thất bại, ${attempt < MAX_PROXY_RETRIES ? 'thử proxy khác...' : 'hết proxy.'}`);
195
+ }
196
+ }
197
+ }
198
+
199
+ if (!downloaded) {
200
+ throw new Error(
201
+ `❌ Download thất bại: ${input}\n` +
202
+ ` Đã thử: direct + ${Math.min(3, PROXY_LIST.length)} proxies\n` +
203
+ ` Nguyên nhân phổ biến:\n` +
204
+ ` • Link đã hết hạn hoặc bị xóa (HTTP 404)\n` +
205
+ ` • Server CDN chặn tất cả IP\n` +
206
+ ` • Link cần cookie/token đặc biệt\n` +
207
+ ` → Hãy thử mở link trong trình duyệt để kiểm tra.`
208
+ );
209
+ }
67
210
  } else {
68
211
  console.log(`\n⏭ [1/5] File đã có sẵn: ${mp4Path}`);
69
212
  }
@@ -109,7 +252,7 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
109
252
  });
110
253
 
111
254
  const ffmpegCmd = [
112
- 'ffmpeg', '-i', `"${mp4Path}"`, '-threads 0',
255
+ `"${FFMPEG_BIN}"`, '-i', `"${mp4Path}"`, '-threads 0',
113
256
  maps,
114
257
  outputs,
115
258
  '-var_stream_map', `"${varStreamMapArr.join(' ')}"`,
@@ -119,14 +262,14 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
119
262
  `"${outDir}/%v/index.m3u8"`
120
263
  ].join(' ');
121
264
 
122
- execSync(ffmpegCmd, { stdio: 'inherit', shell: 'cmd.exe' });
265
+ execSync(ffmpegCmd, { stdio: 'inherit', ...SHELL_OPTS });
123
266
  console.log(' ✅ HLS Master generated.');
124
267
 
125
268
  // --- BƯỚC 2.1: XUẤT 3GP CHO MÁY CỔ (NOKIA S40/S60) ---
126
269
  console.log(' 📱 [2.1/5] Exporting 3GP legacy format...');
127
- const nokiaCmd = `ffmpeg -i "${mp4Path}" -s 320x240 -vcodec mpeg4 -acodec aac -ar 16000 -ac 1 -b:v 250k -b:a 32k -f 3gp "${nokia3gpPath}"`;
270
+ 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}"`;
128
271
  try {
129
- execSync(nokiaCmd, { stdio: 'ignore', shell: 'cmd.exe' });
272
+ execSync(nokiaCmd, { stdio: 'ignore', ...SHELL_OPTS });
130
273
  console.log(' ✅ Legacy 3GP ready.');
131
274
  } catch (e) {
132
275
  console.log(' ⚠ 3GP generation failed.');
@@ -138,9 +281,9 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
138
281
  // --- STEP 2.5: PREVIEW SPRITE --- (Independent check)
139
282
  if (!existsSync(spritePath)) {
140
283
  console.log(' 📸 [2.5/5] Generating Neon Preview SpriteSheet...');
141
- const spriteCmd = `ffmpeg -i "${mp4Path}" -vf "fps=1/10,scale=160:90,tile=10x10" -q:v 3 "${spritePath}"`;
284
+ const spriteCmd = `"${FFMPEG_BIN}" -i "${mp4Path}" -vf "fps=1/10,scale=160:90,tile=10x10" -q:v 3 "${spritePath}"`;
142
285
  try {
143
- execSync(spriteCmd, { stdio: 'ignore', shell: 'cmd.exe' });
286
+ execSync(spriteCmd, { stdio: 'ignore', ...SHELL_OPTS });
144
287
  console.log(' ✅ Preview SpriteSheet ready.');
145
288
  } catch (e) { console.log(' ⚠ Preview generation failed.'); }
146
289
  } else {
@@ -167,7 +310,7 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
167
310
  }
168
311
 
169
312
  execSync(
170
- `${pythonCmd} ${path.join('scripts', 'gen_subtitle.py')} "${mp4Path}" "${slug}" ${lang}`,
313
+ `${pythonCmd} ${path.join(__dirname, 'scripts', 'gen_subtitle.py')} "${mp4Path}" "${slug}" ${lang} "${FFMPEG_BIN}"`,
171
314
  { stdio: 'inherit', cwd: process.cwd() }
172
315
  );
173
316
  } else {
@@ -177,7 +320,7 @@ export async function executePipeline({ input, slug, title, langArg = 'vi' }) {
177
320
 
178
321
  // ─── BƯỚC 5: Upload Tebi.io ─────────────────────────────────── // --- STEP 5: UPLOAD ---
179
322
  console.log('\n🚀 [5/5] Deploying to Quantum Cloud (Tebi.io)...');
180
- execSync(`node ${path.join('scripts', 'upload-tebi.mjs')} "${slug}"`, { stdio: 'inherit' });
323
+ execSync(`node ${path.join(__dirname, 'scripts', 'upload-tebi.mjs')} "${slug}"`, { stdio: 'inherit' });
181
324
 
182
325
  // --- FINAL STEP: GENERATE MASTER README.md ---
183
326
  const finalReadmePath = path.join('hls', slug, 'README.md');
package/pro-terminal.mjs CHANGED
@@ -14,9 +14,16 @@ import { copyFileSync, existsSync } from 'fs';
14
14
  import { join, dirname } from 'path';
15
15
  import { fileURLToPath } from 'url';
16
16
  import 'dotenv/config';
17
+ import ffmpeg from 'ffmpeg-static';
17
18
 
18
19
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
20
 
21
+ // Resolve binaries
22
+ const FFMPEG_BIN = ffmpeg || 'ffmpeg';
23
+ const localYtDlp = join(__dirname, 'node_modules', 'yt-dlp-exec', 'bin', 'yt-dlp.exe');
24
+ const localYtDlpUnix = join(__dirname, 'node_modules', 'yt-dlp-exec', 'bin', 'yt-dlp');
25
+ const FINAL_YTDLP = existsSync(localYtDlp) ? `"${localYtDlp}"` : (existsSync(localYtDlpUnix) ? localYtDlpUnix : 'yt-dlp');
26
+
20
27
  // Auto-copy .env.example to .env if .env doesn't exist
21
28
  const envPath = join(__dirname, '.env');
22
29
  const envExamplePath = join(__dirname, '.env.example');
@@ -54,21 +61,21 @@ function extractTitleFromUrl(url) {
54
61
  const urlParts = url.split('/').filter(p => p && p !== 'http:' && p !== 'https:');
55
62
  const lastPart = urlParts[urlParts.length - 1];
56
63
  const secondLastPart = urlParts[urlParts.length - 2];
57
-
64
+
58
65
  // Clean up parts
59
66
  const cleanLast = lastPart?.replace(/\.(mp4|m3u8|mkv|avi|mov|flv|webm|html|htm)$/i, '').replace(/[_\-]/g, ' ').trim();
60
67
  const cleanSecond = secondLastPart?.replace(/[_\-]/g, ' ').trim();
61
-
68
+
62
69
  // Prefer second to last part (usually folder/slug structure)
63
70
  if (cleanSecond && cleanSecond.length > 2 && !['index', 'master', 'video', 'movie', 'play'].includes(cleanSecond.toLowerCase())) {
64
71
  return cleanSecond;
65
72
  }
66
-
73
+
67
74
  // Fallback to last part
68
75
  if (cleanLast && cleanLast.length > 2 && !['index', 'master', 'video', 'movie', 'play'].includes(cleanLast.toLowerCase())) {
69
76
  return cleanLast;
70
77
  }
71
-
78
+
72
79
  return `movie-${Date.now()}`;
73
80
  }
74
81
 
@@ -103,14 +110,14 @@ function showHelp() {
103
110
  })
104
111
  )
105
112
  );
106
-
113
+
107
114
  console.log(chalk.gray(' ⚡ NEON-INFUSED VIDEO CORE v1.0 | HELP SYSTEM ⚡\n'));
108
-
115
+
109
116
  console.log(`${neonPurple('📖 CÁCH SỬ DỤNG:')}`);
110
117
  console.log(chalk.cyan(' ntxa help - Hiển thị help này'));
111
118
  console.log(chalk.cyan(' ntxa - Chạy interactive mode'));
112
119
  console.log(chalk.cyan(' ntxa "Tên Phim" - Chạy với tên phim mặc định\n'));
113
-
120
+
114
121
  console.log(`${neonPurple('🔧 CẤU HÌNH BẮT BUỘC:')}`);
115
122
  console.log(chalk.yellow(' File .env với các biến:'));
116
123
  console.log(chalk.cyan(' TEBI_ENDPOINT - API endpoint Tebi.io'));
@@ -118,7 +125,7 @@ function showHelp() {
118
125
  console.log(chalk.cyan(' TEBI_SECRET_ACCESS_KEY - Secret key'));
119
126
  console.log(chalk.cyan(' TEBI_BUCKET - Bucket name'));
120
127
  console.log(chalk.cyan(' TEBI_PUBLIC_URL - Public URL\n'));
121
-
128
+
122
129
  console.log(`${neonPurple('⚡ TÍNH NĂNG:')}`);
123
130
  console.log(chalk.cyan(' • Download video từ multiple sources'));
124
131
  console.log(chalk.cyan(' • Transcode sang HLS format'));
@@ -126,23 +133,23 @@ function showHelp() {
126
133
  console.log(chalk.cyan(' • Upload cloud storage (Tebi.io)'));
127
134
  console.log(chalk.cyan(' • Batch processing support'));
128
135
  console.log(chalk.cyan(' • Multi-language subtitles (VI/EN)\n'));
129
-
136
+
130
137
  console.log(`${neonPurple('👤 DEVELOPER:')}`);
131
138
  console.log(chalk.cyan(' TXA - Ultimate Video Pipeline 2030'));
132
139
  console.log(chalk.gray(' Licensed under ISC\n'));
133
-
140
+
134
141
  console.log(`${neonPink('🚀 Ready to rock! Try: ntxa')}`);
135
142
  }
136
143
 
137
144
  async function main() {
138
145
  const args = process.argv.slice(2);
139
-
146
+
140
147
  // Check for help command
141
148
  if (args.includes('help') || args.includes('--help') || args.includes('-h')) {
142
149
  showHelp();
143
150
  return;
144
151
  }
145
-
152
+
146
153
  checkEnv();
147
154
  console.clear();
148
155
 
@@ -172,14 +179,14 @@ async function main() {
172
179
  hint: chalk.yellow('Separate multiple links with COMMA (,)'),
173
180
  validate: (value) => {
174
181
  if (!value) return 'System requires a data source.';
175
-
182
+
176
183
  const links = value.split(',').map(l => l.trim()).filter(l => l);
177
184
  const invalidLinks = links.filter(link => !validateUrl(link));
178
-
185
+
179
186
  if (invalidLinks.length > 0) {
180
187
  return `Invalid URL(s): ${invalidLinks.join(', ')}`;
181
188
  }
182
-
189
+
183
190
  return undefined;
184
191
  },
185
192
  }),
@@ -232,28 +239,28 @@ async function main() {
232
239
  try {
233
240
  s.message('🛰️ Fetching metadata...');
234
241
  const { execSync } = await import('child_process');
235
-
242
+
236
243
  // Try multiple methods to get title
237
244
  let fetchedTitle = '';
238
-
245
+
239
246
  // Method 1: Try yt-dlp title
240
247
  try {
241
248
  fetchedTitle = execSync(
242
- `yt-dlp --print title --skip-download "${inputUrl}"`,
249
+ `${FINAL_YTDLP} --print title --skip-download "${inputUrl}"`,
243
250
  { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 10000 }
244
251
  ).trim();
245
252
  } catch (e1) {
246
253
  // Method 2: Try yt-dlp with different flags
247
254
  try {
248
255
  fetchedTitle = execSync(
249
- `yt-dlp --get-title --no-download "${inputUrl}"`,
256
+ `${FINAL_YTDLP} --get-title --no-download "${inputUrl}"`,
250
257
  { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 10000 }
251
258
  ).trim();
252
259
  } catch (e2) {
253
260
  // Method 3: Try generic approach
254
261
  try {
255
262
  const info = execSync(
256
- `yt-dlp --dump-json --no-download "${inputUrl}"`,
263
+ `${FINAL_YTDLP} --dump-json --no-download "${inputUrl}"`,
257
264
  { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 15000 }
258
265
  );
259
266
  const parsed = JSON.parse(info);
@@ -3,72 +3,75 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { execSync } from 'child_process';
5
5
  import 'dotenv/config';
6
+ import ffprobe from 'ffprobe-static';
6
7
 
7
- export function generateReadme(slug, mp4Path, info = {}) {
8
- const hlsDir = path.join('hls', slug);
9
-
10
- function countFiles(dir, ext) {
11
- if (!fs.existsSync(dir)) return 0;
12
- let count = 0;
13
- for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
14
- const full = path.join(dir, item.name);
15
- if (item.isDirectory()) count += countFiles(full, ext);
16
- else if (item.name.endsWith(ext)) count++;
17
- }
18
- return count;
19
- }
20
-
21
- function getVideoDuration(filePath) {
22
- try {
23
- const sec = parseFloat(
24
- execSync(`ffprobe -i "${filePath}" -show_entries format=duration -v quiet -of csv=p=0`,
25
- { stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim()
26
- );
27
- const h = Math.floor(sec / 3600);
28
- const m = Math.floor((sec % 3600) / 60);
29
- const s = Math.floor(sec % 60);
30
- return `${h > 0 ? h + 'h ' : ''}${m}m ${s}s (${Math.round(sec)}s)`;
31
- } catch { return 'N/A'; }
32
- }
8
+ const FFPROBE_BIN = ffprobe.path || 'ffprobe';
33
9
 
34
- function getFolderSizeMB(dir) {
35
- let total = 0;
36
- if (!fs.existsSync(dir)) return '0 MB';
37
- for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
38
- const full = path.join(dir, item.name);
39
- if (item.isDirectory()) total += parseFloat(getFolderSizeMB(full));
40
- else total += fs.statSync(full).size / (1024 * 1024);
41
- }
42
- return total.toFixed(1) + ' MB';
10
+ export function generateReadme(slug, mp4Path, info = {}) {
11
+ const hlsDir = path.join('hls', slug);
12
+
13
+ function countFiles(dir, ext) {
14
+ if (!fs.existsSync(dir)) return 0;
15
+ let count = 0;
16
+ for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
17
+ const full = path.join(dir, item.name);
18
+ if (item.isDirectory()) count += countFiles(full, ext);
19
+ else if (item.name.endsWith(ext)) count++;
43
20
  }
44
-
45
- function getStreamInfo(filePath) {
46
- try {
47
- const json = JSON.parse(execSync(
48
- `ffprobe -v quiet -print_format json -show_streams "${filePath}"`,
49
- { stdio: ['pipe', 'pipe', 'pipe'] }).toString());
50
- const video = json.streams.find(s => s.codec_type === 'video');
51
- const audio = json.streams.find(s => s.codec_type === 'audio');
52
- return {
53
- resolution: video ? `${video.width}x${video.height}` : 'N/A',
54
- videoCodec: video ? video.codec_name.toUpperCase() : 'N/A',
55
- audioCodec: audio ? audio.codec_name.toUpperCase() : 'N/A',
56
- fps: video ? Math.round(eval(video.r_frame_rate)) + ' fps' : 'N/A',
57
- };
58
- } catch { return { resolution: 'N/A', videoCodec: 'N/A', audioCodec: 'N/A', fps: 'N/A' }; }
21
+ return count;
22
+ }
23
+
24
+ function getVideoDuration(filePath) {
25
+ try {
26
+ const sec = parseFloat(
27
+ execSync(`"${FFPROBE_BIN}" -i "${filePath}" -show_entries format=duration -v quiet -of csv=p=0`,
28
+ { stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim()
29
+ );
30
+ const h = Math.floor(sec / 3600);
31
+ const m = Math.floor((sec % 3600) / 60);
32
+ const s = Math.floor(sec % 60);
33
+ return `${h > 0 ? h + 'h ' : ''}${m}m ${s}s (${Math.round(sec)}s)`;
34
+ } catch { return 'N/A'; }
35
+ }
36
+
37
+ function getFolderSizeMB(dir) {
38
+ let total = 0;
39
+ if (!fs.existsSync(dir)) return '0 MB';
40
+ for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
41
+ const full = path.join(dir, item.name);
42
+ if (item.isDirectory()) total += parseFloat(getFolderSizeMB(full));
43
+ else total += fs.statSync(full).size / (1024 * 1024);
59
44
  }
60
-
61
- const duration = getVideoDuration(mp4Path);
62
- const totalSize = getFolderSizeMB(hlsDir);
63
- const tsCount = countFiles(hlsDir, '.ts');
64
- const hasSub = fs.existsSync(path.join(hlsDir, 'vi.vtt'));
65
- const hasSubEn = fs.existsSync(path.join(hlsDir, 'en.vtt'));
66
- const streamInfo = getStreamInfo(mp4Path);
67
- const now = new Date().toLocaleString('vi-VN', { timeZone: 'Asia/Ho_Chi_Minh' });
68
- const publicUrl = process.env.TEBI_PUBLIC_URL || 'https://s3.tebi.io/nrotxa-videos';
69
-
70
- const readme =
71
- `================================================================================
45
+ return total.toFixed(1) + ' MB';
46
+ }
47
+
48
+ function getStreamInfo(filePath) {
49
+ try {
50
+ const json = JSON.parse(execSync(
51
+ `"${FFPROBE_BIN}" -v quiet -print_format json -show_streams "${filePath}"`,
52
+ { stdio: ['pipe', 'pipe', 'pipe'] }).toString());
53
+ const video = json.streams.find(s => s.codec_type === 'video');
54
+ const audio = json.streams.find(s => s.codec_type === 'audio');
55
+ return {
56
+ resolution: video ? `${video.width}x${video.height}` : 'N/A',
57
+ videoCodec: video ? video.codec_name.toUpperCase() : 'N/A',
58
+ audioCodec: audio ? audio.codec_name.toUpperCase() : 'N/A',
59
+ fps: video ? Math.round(eval(video.r_frame_rate)) + ' fps' : 'N/A',
60
+ };
61
+ } catch { return { resolution: 'N/A', videoCodec: 'N/A', audioCodec: 'N/A', fps: 'N/A' }; }
62
+ }
63
+
64
+ const duration = getVideoDuration(mp4Path);
65
+ const totalSize = getFolderSizeMB(hlsDir);
66
+ const tsCount = countFiles(hlsDir, '.ts');
67
+ const hasSub = fs.existsSync(path.join(hlsDir, 'vi.vtt'));
68
+ const hasSubEn = fs.existsSync(path.join(hlsDir, 'en.vtt'));
69
+ const streamInfo = getStreamInfo(mp4Path);
70
+ const now = new Date().toLocaleString('vi-VN', { timeZone: 'Asia/Ho_Chi_Minh' });
71
+ const publicUrl = process.env.TEBI_PUBLIC_URL || 'https://s3.tebi.io/nrotxa-videos';
72
+
73
+ const readme =
74
+ `================================================================================
72
75
  THÔNG TIN FOLDER: hls/${slug}
73
76
  Tạo lúc: ${now}
74
77
  ================================================================================
@@ -112,8 +115,8 @@ export function generateReadme(slug, mp4Path, info = {}) {
112
115
  ================================================================================
113
116
  `;
114
117
 
115
- const readmePath = path.join(hlsDir, 'README.txt');
116
- fs.writeFileSync(readmePath, readme, 'utf8');
117
- console.log(` ✅ README.txt saved: ${readmePath}`);
118
- return readmePath;
118
+ const readmePath = path.join(hlsDir, 'README.txt');
119
+ fs.writeFileSync(readmePath, readme, 'utf8');
120
+ console.log(` ✅ README.txt saved: ${readmePath}`);
121
+ return readmePath;
119
122
  }
@@ -8,11 +8,11 @@ import subprocess
8
8
  from pathlib import Path
9
9
  from faster_whisper import WhisperModel
10
10
 
11
- def extract_audio(video_path, audio_path):
11
+ def extract_audio(video_path, audio_path, ffmpeg_path="ffmpeg"):
12
12
  """Trích audio nhẹ từ video để whisper xử lý nhanh hơn."""
13
- print(" ↳ Extracting audio...")
13
+ print(f" ↳ Extracting audio using: {ffmpeg_path}")
14
14
  subprocess.run([
15
- "ffmpeg", "-i", video_path,
15
+ ffmpeg_path, "-i", video_path,
16
16
  "-vn", "-acodec", "pcm_s16le",
17
17
  "-ar", "16000", "-ac", "1",
18
18
  audio_path, "-y"
@@ -66,11 +66,12 @@ def main():
66
66
  out_dir.mkdir(parents=True, exist_ok=True)
67
67
 
68
68
  audio_path = f"temp_audio_{slug}.wav"
69
+ ffmpeg_path = sys.argv[4] if len(sys.argv) > 4 else "ffmpeg"
69
70
 
70
71
  print(f"\n📝 Generating subtitle [{lang}] for: {slug}")
71
72
 
72
73
  try:
73
- extract_audio(video_path, audio_path)
74
+ extract_audio(video_path, audio_path, ffmpeg_path)
74
75
  segments = transcribe(audio_path, lang)
75
76
  vtt = segments_to_vtt(segments)
76
77
 
package/TXA_AI_GUIDE.md DELETED
@@ -1,25 +0,0 @@
1
- # 🤖 TXA NEON CORE 2030 - AI DEVELOPER GUIDE
2
-
3
- Tài liệu này dành cho AI để hiểu cấu trúc, logic và cách mở rộng hệ sinh thái **TXA Neon Core** (phiên bản VOD Pipeline + TXAPlayer Pro V6.4).
4
-
5
- ---
6
-
7
- ## 🚀 2. LOGIC XỬ LÝ STREAM (M3U8 & PROXY)
8
-
9
- `TXAPlayer.js` hiện tại không chỉ là UI, nó có bộ não xử lý stream mạnh mẽ:
10
- - **Direct M3U8 Processing:** Player có hàm `getM3u8TsListFast()` để phân tích playlist, tự động chọn chất lượng cao nhất (Variant Selection) và thậm chí ước tính dung lượng phim.
11
- - **Dynamic Quality Management:** Player hỗ trợ full dải chất lượng từ 144p, 248p (3GP cho máy Java), 480p SD đến 4K.
12
- - **Admin Hard-Sync:** Hệ thống đồng bộ chặt chẽ với cấu hình từ Backend/Admin. Tham số `maxQuality` sẽ lọc bỏ các chất lượng không được phép trước khi hiển thị trong Menu Quality.
13
-
14
- ---
15
-
16
- ## 🛠️ 3. CẤU TRÚC LINK STREAM (M3U8 STRUCTURE)
17
-
18
- Khi tích hợp, AI cần hiểu link `videoUrl` truyền vào `TXAPlayer` có thể là:
19
-
20
- 1. **Direct S3/Tebi URL:**
21
- - Định dạng: `https://[TEBI_PUBLIC_URL]/[movie-slug]/[episode]/index.m3u8`
22
- - Đặc điểm: Đây là file Master Playlist chứa danh sách các chất lượng (360p, 720p, 1080p).
23
-
24
- **Quy trình xử lý của Player:**
25
- - `Master Playlist` (.m3u8) -> `Variant Playlist` (360p/720p...) -> `TS Segments` (.ts) hoặc `Fragments` (.m4s).