tphim 2.3.0 → 2.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tphim",
3
- "version": "2.3.0",
3
+ "version": "2.3.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",
@@ -49,6 +49,7 @@
49
49
  "homepage": "https://film.nrotxa.online",
50
50
  "dependencies": {
51
51
  "@aws-sdk/client-s3": "^3.999.0",
52
+ "@aws-sdk/lib-storage": "^3.1008.0",
52
53
  "@clack/prompts": "^1.0.1",
53
54
  "chalk": "^5.6.2",
54
55
  "cli-progress": "^3.12.0",
@@ -67,4 +68,4 @@
67
68
  "engines": {
68
69
  "node": ">=18.0.0"
69
70
  }
70
- }
71
+ }
@@ -0,0 +1,150 @@
1
+
2
+ import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
3
+ import { Upload } from "@aws-sdk/lib-storage";
4
+ import { spawn } from 'child_process';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import 'dotenv/config';
8
+ import ffmpeg from 'ffmpeg-static';
9
+ import { lookup } from 'mime-types';
10
+
11
+ const FFMPEG_BIN = ffmpeg || 'ffmpeg';
12
+ const s3 = new S3Client({
13
+ region: 'us-east-1',
14
+ endpoint: process.env.TEBI_ENDPOINT,
15
+ credentials: {
16
+ accessKeyId: process.env.TEBI_ACCESS_KEY_ID,
17
+ secretAccessKey: process.env.TEBI_SECRET_ACCESS_KEY,
18
+ },
19
+ forcePathStyle: true,
20
+ });
21
+
22
+ const slug = "ninh-an-nhu-mong-direct";
23
+ const bucket = process.env.TEBI_BUCKET;
24
+ const desktop = "C:\\Users\\TXA3000\\Desktop";
25
+
26
+ const filesToUpload = [
27
+ { local: path.join(desktop, "360p - NINH AN NHƯ MỘNG - TM.mp4"), remote: `movies/${slug}/360p.mp4` },
28
+ { local: path.join(desktop, "144p - Ninh An Như Mộng - TM.mp4"), remote: `movies/${slug}/144p.mp4` },
29
+ { local: path.join(desktop, "720p Thuyết Minh_Ninh An Như Mộng.mp4"), remote: `movies/${slug}/720p.mp4` }
30
+ ];
31
+
32
+ async function uploadFile(localPath, remotePath) {
33
+ if (!fs.existsSync(localPath)) {
34
+ console.log(`\n⚠️ Skip: ${path.basename(localPath)} (Not found)`);
35
+ return;
36
+ }
37
+ console.log(`\n📡 Uploading: ${path.basename(localPath)} -> ${remotePath}`);
38
+ const stream = fs.createReadStream(localPath);
39
+ try {
40
+ const uploader = new Upload({
41
+ client: s3,
42
+ params: {
43
+ Bucket: bucket,
44
+ Key: remotePath,
45
+ Body: stream,
46
+ ContentType: lookup(localPath) || 'video/mp4',
47
+ ACL: 'public-read',
48
+ },
49
+ queueSize: 4,
50
+ partSize: 5 * 1024 * 1024,
51
+ });
52
+
53
+ uploader.on("httpUploadProgress", (progress) => {
54
+ const pct = Math.round((progress.loaded / (progress.total || 1)) * 100);
55
+ process.stdout.write(`\r ⚡ Progress: ${pct}% (${(progress.loaded / 1024 / 1024).toFixed(1)} MB)`);
56
+ });
57
+
58
+ await uploader.done();
59
+ console.log(`\n✅ Done: ${path.basename(localPath)}`);
60
+ } catch (e) {
61
+ console.error(`\n❌ Upload failed for ${path.basename(localPath)}:`, e.message);
62
+ }
63
+ }
64
+
65
+ async function captureStoryboards(inputPath, outputDir) {
66
+ if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
67
+ console.log(`\n🚀 Capturing Storyboards from ${path.basename(inputPath)} (Turbo Mode)...`);
68
+
69
+ const args = [
70
+ '-skip_frame', 'nokey', '-threads', '0', '-i', inputPath,
71
+ '-filter_complex', [
72
+ '[0:v]split=3[v1][v2][v3]',
73
+ '[v1]fps=1/10,scale=160:90,tile=10x10[out1]',
74
+ '[v2]fps=1/30,scale=240:135,tile=5x5[out2]',
75
+ '[v3]fps=1/60,scale=320:180,tile=3x3[out3]'
76
+ ].join(';'),
77
+ '-map', '[out1]', '-q:v', '4', '-start_number', '0', '-vsync', '0', path.join(outputDir, 'L1_M%d.jpg'),
78
+ '-map', '[out2]', '-q:v', '4', '-start_number', '0', '-vsync', '0', path.join(outputDir, 'L2_M%d.jpg'),
79
+ '-map', '[out3]', '-q:v', '4', '-start_number', '0', '-vsync', '0', path.join(outputDir, 'L3_M%d.jpg'),
80
+ '-y'
81
+ ];
82
+
83
+ return new Promise((resolve, reject) => {
84
+ const proc = spawn(FFMPEG_BIN, args, { shell: false });
85
+ proc.on('close', code => code === 0 ? resolve() : reject(new Error('FFmpeg failed')));
86
+ });
87
+ }
88
+
89
+ async function uploadFolder(localDir, remotePrefix) {
90
+ console.log(`\n📡 Batch uploading storyboards...`);
91
+ const files = fs.readdirSync(localDir);
92
+ for (const file of files) {
93
+ const fullPath = path.join(localDir, file);
94
+ if (fs.statSync(fullPath).isFile()) {
95
+ await uploadFile(fullPath, `${remotePrefix}/${file}`);
96
+ }
97
+ }
98
+ }
99
+
100
+ async function main() {
101
+ const reportPath = path.join(desktop, "NINH_AN_NHU_MONG_LINKS.txt");
102
+ try {
103
+ // 1. Upload MP4s
104
+ for (const f of filesToUpload) {
105
+ await uploadFile(f.local, f.remote);
106
+ }
107
+
108
+ // 2. Capture Storyboard (from 720p)
109
+ const hqFile = filesToUpload[2].local;
110
+ const storyboardDir = path.join(process.cwd(), 'temp_storyboard');
111
+ if (fs.existsSync(hqFile)) {
112
+ await captureStoryboards(hqFile, storyboardDir);
113
+
114
+ // 3. Upload Storyboards
115
+ await uploadFolder(storyboardDir, `movies/${slug}/storyboard`);
116
+ }
117
+
118
+ // 4. Generate Report
119
+ const report = `
120
+ ========================================
121
+ REPORT: NINH AN NHƯ MỘNG DIRECT LINKS
122
+ ========================================
123
+
124
+ 🎬 MP4 DIRECT LINKS:
125
+ - 720p: https://s3.tebi.io/${bucket}/movies/${slug}/720p.mp4
126
+ - 360p: https://s3.tebi.io/${bucket}/movies/${slug}/360p.mp4
127
+ - 144p: https://s3.tebi.io/${bucket}/movies/${slug}/144p.mp4
128
+
129
+ 📸 STORYBOARD LINKS (L1, L2, L3):
130
+ - Base Path: https://s3.tebi.io/${bucket}/movies/${slug}/storyboard/
131
+ - Example L1: https://s3.tebi.io/${bucket}/movies/${slug}/storyboard/L1_M0.jpg
132
+ - Example L2: https://s3.tebi.io/${bucket}/movies/${slug}/storyboard/L2_M0.jpg
133
+ - Example L3: https://s3.tebi.io/${bucket}/movies/${slug}/storyboard/L3_M0.jpg
134
+
135
+ 🚀 Player usage:
136
+ Sử dụng link MP4 ở trên. Player sẽ tự động tìm
137
+ thông tin storyboard nếu được cập nhật theo TXA_AI_GUIDE.
138
+ ========================================
139
+ `;
140
+ fs.writeFileSync(reportPath, report);
141
+ console.log(`\n✨ ALL DONE! Report saved to: ${reportPath}`);
142
+
143
+ // Cleanup
144
+ if (fs.existsSync(storyboardDir)) fs.rmSync(storyboardDir, { recursive: true, force: true });
145
+ } catch (err) {
146
+ console.error("❌ Error:", err);
147
+ }
148
+ }
149
+
150
+ main();
@@ -16,12 +16,14 @@ const s3 = new S3Client({
16
16
  forcePathStyle: true, // Tebi cần pathStyle, không dùng subdomain
17
17
  });
18
18
 
19
+ import { Upload } from "@aws-sdk/lib-storage";
20
+ import { createReadStream, statSync } from 'fs';
21
+
19
22
  export async function uploadHLSFolder(slug) {
20
23
  const localDir = join('hls', slug);
21
24
  const files = getAllFiles(localDir);
22
25
  const progressFile = join('hls', slug, '.upload-progress.json');
23
26
 
24
- // Load existing progress
25
27
  let uploadedFiles = [];
26
28
  if (existsSync(progressFile)) {
27
29
  try {
@@ -33,58 +35,66 @@ export async function uploadHLSFolder(slug) {
33
35
  }
34
36
  }
35
37
 
36
- // Filter files that haven't been uploaded yet
37
38
  const filesToUpload = files.filter(file => !uploadedFiles.includes(relative('.', file).replace(/\\/g, '/')));
38
39
 
39
- console.log(`\n☁️ Uploading ${filesToUpload.length} files to Tebi.io... (${files.length - filesToUpload.length} already uploaded)`);
40
+ console.log(`\n☁️ Uploading ${filesToUpload.length} files to Tebi.io (Turbo Mode)...`);
40
41
  let uploaded = uploadedFiles.length;
42
+ const total = files.length;
43
+
44
+ const cacheMap = {
45
+ '.ts': 'public, max-age=31536000',
46
+ '.jpg': 'public, max-age=31536000',
47
+ '.m3u8': 'public, max-age=300',
48
+ '.vtt': 'public, max-age=86400',
49
+ '.txt': 'no-cache',
50
+ };
41
51
 
42
- for (const filePath of filesToUpload) {
52
+ // Hàm upload từng file đơn lẻ sử dụng lib-storage
53
+ const uploadSingleFile = async (filePath) => {
43
54
  const s3Key = relative('.', filePath).replace(/\\/g, '/');
44
55
  const ext = extname(filePath).toLowerCase();
45
-
46
- // Check if file already exists on cloud
56
+
47
57
  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)`);
58
+ const stream = createReadStream(filePath);
59
+ const size = statSync(filePath).size;
60
+
61
+ const uploader = new Upload({
62
+ client: s3,
63
+ params: {
64
+ Bucket: process.env.TEBI_BUCKET,
65
+ Key: s3Key,
66
+ Body: stream,
67
+ ACL: 'public-read',
68
+ ContentType: lookup(filePath) || 'application/octet-stream',
69
+ CacheControl: cacheMap[ext] || 'public, max-age=3600',
70
+ },
71
+ queueSize: 1, // Sẽ điều khiển concurrency ở cấp độ folder
72
+ partSize: 5 * 1024 * 1024,
73
+ });
74
+
75
+ await uploader.done();
76
+
53
77
  uploaded++;
54
78
  uploadedFiles.push(s3Key);
55
- continue;
79
+
80
+ // Lưu progress
81
+ writeFileSync(progressFile, JSON.stringify({
82
+ uploadedFiles,
83
+ totalFiles: total,
84
+ lastUpdate: new Date().toISOString()
85
+ }));
86
+
87
+ process.stdout.write(`\r ↳ Progress: ${uploaded}/${total} files...`);
56
88
  } catch (e) {
57
- // File doesn't exist, proceed with upload
89
+ console.error(`\n❌ Lỗi upload ${s3Key}:`, e.message);
58
90
  }
91
+ };
59
92
 
60
- const cacheMap = {
61
- '.ts': 'public, max-age=31536000',
62
- '.jpg': 'public, max-age=31536000',
63
- '.m3u8': 'public, max-age=300',
64
- '.vtt': 'public, max-age=86400',
65
- '.txt': 'no-cache',
66
- };
67
-
68
- await s3.send(new PutObjectCommand({
69
- Bucket: process.env.TEBI_BUCKET,
70
- Key: s3Key,
71
- Body: readFileSync(filePath),
72
- ACL: 'public-read', // Mở khóa file công khai ngay khi upload
73
- ContentType: lookup(filePath) || 'application/octet-stream',
74
- CacheControl: cacheMap[ext] || 'public, max-age=3600',
75
- }));
76
-
77
- uploaded++;
78
- uploadedFiles.push(s3Key);
79
-
80
- // Save progress after each successful upload
81
- writeFileSync(progressFile, JSON.stringify({
82
- uploadedFiles,
83
- totalFiles: files.length,
84
- lastUpdate: new Date().toISOString()
85
- }));
86
-
87
- process.stdout.write(`\r ↳ ${uploaded}/${files.length} files...`);
93
+ // Chạy song song với concurrency limit (ví dụ: 10 file cùng lúc)
94
+ const CONCURRENCY = 10;
95
+ for (let i = 0; i < filesToUpload.length; i += CONCURRENCY) {
96
+ const chunk = filesToUpload.slice(i, i + CONCURRENCY);
97
+ await Promise.all(chunk.map(file => uploadSingleFile(file)));
88
98
  }
89
99
 
90
100
  // Clean up progress file when complete