tphim 1.0.1 → 1.0.3
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 +47 -23
- package/index.js +11 -2
- package/package.json +1 -1
- package/pro-terminal.mjs +86 -12
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
# 💎
|
|
1
|
+
# 💎 TPHIM - Ultimate Video Pipeline
|
|
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
|
|
|
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
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.
|
|
8
|
-
- **Neon Hybrid Player:** Trình phát V5 hỗ trợ tận răng cho cả Desktop và Mobile với cử chỉ vuốt cực xịn.
|
|
9
8
|
- **Cloud Ready:** Upload trực tiếp lên Tebi.io hoặc bất kỳ S3-compatible storage nào.
|
|
9
|
+
- **Interactive CLI:** Giao diện terminal đẹp mắt với neon theme.
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -48,7 +48,7 @@ Bạn hoàn toàn có thể chạy Pipeline này ngay trên điện thoại:
|
|
|
48
48
|
pkg update && pkg upgrade
|
|
49
49
|
pkg install nodejs ffmpeg python python-pip wget
|
|
50
50
|
```
|
|
51
|
-
2. **Cài đặt yt-dlp:**
|
|
51
|
+
2. **Cài đặt yt-dlp:**(không nhất thiết phải cài)
|
|
52
52
|
```bash
|
|
53
53
|
pip install yt-dlp
|
|
54
54
|
```
|
|
@@ -61,14 +61,14 @@ Bạn hoàn toàn có thể chạy Pipeline này ngay trên điện thoại:
|
|
|
61
61
|
|
|
62
62
|
Cài đặt vào dự án của bạn:
|
|
63
63
|
```bash
|
|
64
|
-
npm install
|
|
64
|
+
npm install tphim
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
Trong code Node.js:
|
|
68
68
|
```javascript
|
|
69
|
-
import {
|
|
69
|
+
import { executePipeline } from 'tphim';
|
|
70
70
|
|
|
71
|
-
await
|
|
71
|
+
await executePipeline({
|
|
72
72
|
input: "https://link-phim.com/phim.m3u8",
|
|
73
73
|
slug: "phim-hay-2030",
|
|
74
74
|
title: "Phim Hay 2030",
|
|
@@ -78,32 +78,56 @@ await runPipeline({
|
|
|
78
78
|
|
|
79
79
|
---
|
|
80
80
|
|
|
81
|
-
##
|
|
81
|
+
## ⌨️ Sử dụng CLI (Terminal)
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
### Cài đặt globally:
|
|
84
|
+
```bash
|
|
85
|
+
npm install -g tphim
|
|
86
|
+
```
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
### Các lệnh CLI:
|
|
89
|
+
```bash
|
|
90
|
+
# Xem hướng dẫn
|
|
91
|
+
ntxa help
|
|
89
92
|
|
|
90
|
-
|
|
93
|
+
# Chạy interactive mode
|
|
94
|
+
ntxa
|
|
95
|
+
|
|
96
|
+
# Chạy với tên phim mặc định
|
|
97
|
+
ntxa "Tên Phim Của Bạn"
|
|
91
98
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
# Xử lý batch nhiều link
|
|
100
|
+
ntxa
|
|
101
|
+
# (sẽ hỏi nhập nhiều link cách nhau bằng dấu phẩy)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Cấu hình bắt buộc:
|
|
105
|
+
Tạo file `.env` với các biến sau:
|
|
106
|
+
```env
|
|
107
|
+
TEBI_ENDPOINT=https://s3.tebi.io
|
|
108
|
+
TEBI_ACCESS_KEY_ID=your_access_key
|
|
109
|
+
TEBI_SECRET_ACCESS_KEY=your_secret_key
|
|
110
|
+
TEBI_BUCKET=your_bucket_name
|
|
111
|
+
TEBI_PUBLIC_URL=https://your-bucket.tebi.io
|
|
95
112
|
```
|
|
96
113
|
|
|
97
114
|
---
|
|
98
115
|
|
|
99
|
-
##
|
|
116
|
+
## 🔧 Tính năng CLI
|
|
100
117
|
|
|
101
|
-
|
|
118
|
+
**Interactive Mode:**
|
|
119
|
+
- Nhập multiple links (cách nhau bằng dấu phẩy)
|
|
120
|
+
- Tự động fetch metadata từ video
|
|
121
|
+
- Chọn ngôn ngữ phụ đề (VI/EN/Both)
|
|
122
|
+
- Batch processing với progress bar
|
|
102
123
|
|
|
124
|
+
**Help System:**
|
|
103
125
|
```bash
|
|
104
|
-
ntxa
|
|
105
|
-
#
|
|
106
|
-
ntxa
|
|
126
|
+
ntxa help # Hiển thị toàn bộ hướng dẫn
|
|
127
|
+
ntxa --help # Tương tự
|
|
128
|
+
ntxa -h # Tương tự
|
|
107
129
|
```
|
|
108
130
|
|
|
109
|
-
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
*Phát triển bởi TXA - Ultimate Video Pipeline 2030* 🍿🎬
|
package/index.js
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
// index.js
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* TPHIM - Ultimate Video Pipeline
|
|
4
4
|
* The ultimate video processing pipeline and player library
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { executePipeline } from './pipeline.mjs';
|
|
8
|
-
import { readFileSync } from 'fs';
|
|
8
|
+
import { readFileSync, copyFileSync, existsSync } from 'fs';
|
|
9
9
|
import { join, dirname } from 'path';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
|
|
12
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
|
|
14
|
+
// Auto-copy .env.example to .env if .env doesn't exist
|
|
15
|
+
const envPath = join(__dirname, '.env');
|
|
16
|
+
const envExamplePath = join(__dirname, '.env.example');
|
|
17
|
+
|
|
18
|
+
if (!existsSync(envPath) && existsSync(envExamplePath)) {
|
|
19
|
+
copyFileSync(envExamplePath, envPath);
|
|
20
|
+
console.log('📄 Auto-copied .env.example to .env');
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
/**
|
|
15
24
|
* Node-side API: Run the full VOD pipeline.
|
|
16
25
|
* @param {Object} options - pipeline options { input, slug, title, langArg }
|
package/package.json
CHANGED
package/pro-terminal.mjs
CHANGED
|
@@ -10,8 +10,22 @@ import figlet from 'figlet';
|
|
|
10
10
|
import ora from 'ora';
|
|
11
11
|
import { spawn } from 'child_process';
|
|
12
12
|
import { executePipeline } from './pipeline.mjs';
|
|
13
|
+
import { copyFileSync, existsSync } from 'fs';
|
|
14
|
+
import { join, dirname } from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
13
16
|
import 'dotenv/config';
|
|
14
17
|
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
// Auto-copy .env.example to .env if .env doesn't exist
|
|
21
|
+
const envPath = join(__dirname, '.env');
|
|
22
|
+
const envExamplePath = join(__dirname, '.env.example');
|
|
23
|
+
|
|
24
|
+
if (!existsSync(envPath) && existsSync(envExamplePath)) {
|
|
25
|
+
copyFileSync(envExamplePath, envPath);
|
|
26
|
+
console.log(chalk.cyan('📄 Auto-copied .env.example to .env'));
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
// Color palette
|
|
16
30
|
const neonCyan = chalk.hex('#00f2ff');
|
|
17
31
|
const neonPurple = chalk.hex('#bc13fe');
|
|
@@ -27,6 +41,37 @@ function removeVietnameseTones(str) {
|
|
|
27
41
|
.trim();
|
|
28
42
|
}
|
|
29
43
|
|
|
44
|
+
function validateUrl(url) {
|
|
45
|
+
try {
|
|
46
|
+
new URL(url);
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function extractTitleFromUrl(url) {
|
|
54
|
+
const urlParts = url.split('/').filter(p => p && p !== 'http:' && p !== 'https:');
|
|
55
|
+
const lastPart = urlParts[urlParts.length - 1];
|
|
56
|
+
const secondLastPart = urlParts[urlParts.length - 2];
|
|
57
|
+
|
|
58
|
+
// Clean up parts
|
|
59
|
+
const cleanLast = lastPart?.replace(/\.(mp4|m3u8|mkv|avi|mov|flv|webm|html|htm)$/i, '').replace(/[_\-]/g, ' ').trim();
|
|
60
|
+
const cleanSecond = secondLastPart?.replace(/[_\-]/g, ' ').trim();
|
|
61
|
+
|
|
62
|
+
// Prefer second to last part (usually folder/slug structure)
|
|
63
|
+
if (cleanSecond && cleanSecond.length > 2 && !['index', 'master', 'video', 'movie', 'play'].includes(cleanSecond.toLowerCase())) {
|
|
64
|
+
return cleanSecond;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Fallback to last part
|
|
68
|
+
if (cleanLast && cleanLast.length > 2 && !['index', 'master', 'video', 'movie', 'play'].includes(cleanLast.toLowerCase())) {
|
|
69
|
+
return cleanLast;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return `movie-${Date.now()}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
30
75
|
function checkEnv() {
|
|
31
76
|
const required = [
|
|
32
77
|
'TEBI_ENDPOINT',
|
|
@@ -127,6 +172,15 @@ async function main() {
|
|
|
127
172
|
hint: chalk.yellow('Separate multiple links with COMMA (,)'),
|
|
128
173
|
validate: (value) => {
|
|
129
174
|
if (!value) return 'System requires a data source.';
|
|
175
|
+
|
|
176
|
+
const links = value.split(',').map(l => l.trim()).filter(l => l);
|
|
177
|
+
const invalidLinks = links.filter(link => !validateUrl(link));
|
|
178
|
+
|
|
179
|
+
if (invalidLinks.length > 0) {
|
|
180
|
+
return `Invalid URL(s): ${invalidLinks.join(', ')}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return undefined;
|
|
130
184
|
},
|
|
131
185
|
}),
|
|
132
186
|
title: () =>
|
|
@@ -178,20 +232,40 @@ async function main() {
|
|
|
178
232
|
try {
|
|
179
233
|
s.message('🛰️ Fetching metadata...');
|
|
180
234
|
const { execSync } = await import('child_process');
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
235
|
+
|
|
236
|
+
// Try multiple methods to get title
|
|
237
|
+
let fetchedTitle = '';
|
|
238
|
+
|
|
239
|
+
// Method 1: Try yt-dlp title
|
|
240
|
+
try {
|
|
241
|
+
fetchedTitle = execSync(
|
|
242
|
+
`yt-dlp --print title --skip-download "${inputUrl}"`,
|
|
243
|
+
{ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 10000 }
|
|
244
|
+
).trim();
|
|
245
|
+
} catch (e1) {
|
|
246
|
+
// Method 2: Try yt-dlp with different flags
|
|
247
|
+
try {
|
|
248
|
+
fetchedTitle = execSync(
|
|
249
|
+
`yt-dlp --get-title --no-download "${inputUrl}"`,
|
|
250
|
+
{ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 10000 }
|
|
251
|
+
).trim();
|
|
252
|
+
} catch (e2) {
|
|
253
|
+
// Method 3: Try generic approach
|
|
254
|
+
try {
|
|
255
|
+
const info = execSync(
|
|
256
|
+
`yt-dlp --dump-json --no-download "${inputUrl}"`,
|
|
257
|
+
{ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 15000 }
|
|
258
|
+
);
|
|
259
|
+
const parsed = JSON.parse(info);
|
|
260
|
+
fetchedTitle = parsed.title || parsed.fulltitle || parsed.description?.split('\n')[0] || '';
|
|
261
|
+
} catch (e3) {
|
|
262
|
+
// Method 4: Extract from URL as last resort
|
|
263
|
+
fetchedTitle = extractTitleFromUrl(inputUrl);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
192
266
|
}
|
|
193
267
|
|
|
194
|
-
currentTitle = fetchedTitle;
|
|
268
|
+
currentTitle = fetchedTitle || extractTitleFromUrl(inputUrl);
|
|
195
269
|
} catch (e) {
|
|
196
270
|
currentTitle = `movie-${Date.now()}`;
|
|
197
271
|
}
|