aigc-vault-x-video-service 1.0.0
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/LICENSE +21 -0
- package/README.md +99 -0
- package/bin/aigc-vault-x-video-service.mjs +345 -0
- package/package.json +30 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AIGC Vault
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# AIGC Vault X Video Service
|
|
2
|
+
|
|
3
|
+
Local helper service for AIGC Vault. It resolves X/Twitter status URLs with `yt-dlp`, downloads MP4 files to the user's machine, and exposes local file URLs for the browser extension.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
brew install node yt-dlp ffmpeg
|
|
9
|
+
npm install -g aigc-vault-x-video-service
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Start
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
aigc-vault-x-video-service
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The default service URL is:
|
|
19
|
+
|
|
20
|
+
```txt
|
|
21
|
+
http://127.0.0.1:8899
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The default download directory is:
|
|
25
|
+
|
|
26
|
+
```txt
|
|
27
|
+
~/Movies/AIGC-Vault-X-Videos
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Auto-start on macOS
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
aigc-vault-x-video-service --install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Remove auto-start:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
aigc-vault-x-video-service --uninstall
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Options
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
aigc-vault-x-video-service --host 127.0.0.1 --port 8899
|
|
46
|
+
aigc-vault-x-video-service --download-dir ~/Movies/AIGC-Vault-X-Videos
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
If you want another computer on the same network to access this service, start it with a LAN host such as:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
aigc-vault-x-video-service --host 0.0.0.0
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Then set the AIGC Vault extension service URL to that computer's LAN address, for example:
|
|
56
|
+
|
|
57
|
+
```txt
|
|
58
|
+
http://192.168.1.23:8899
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## API
|
|
62
|
+
|
|
63
|
+
### GET `/health`
|
|
64
|
+
|
|
65
|
+
Returns service status.
|
|
66
|
+
|
|
67
|
+
### POST `/resolve-x-video`
|
|
68
|
+
|
|
69
|
+
Body:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"url": "https://x.com/user/status/123",
|
|
74
|
+
"download": true
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Response:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"success": true,
|
|
83
|
+
"videoUrl": "http://127.0.0.1:8899/files/123.mp4",
|
|
84
|
+
"filePath": "/Users/name/Movies/AIGC-Vault-X-Videos/123.mp4",
|
|
85
|
+
"fileName": "123.mp4"
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Only `x.com` / `twitter.com` status URLs are accepted.
|
|
90
|
+
|
|
91
|
+
## Notes
|
|
92
|
+
|
|
93
|
+
This service uses:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
yt-dlp --cookies-from-browser chrome -S "res,ext:mp4:m4a" --merge-output-format mp4
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Chrome may ask for macOS Keychain permission when `yt-dlp` reads browser cookies. Enter the Mac login password and allow access.
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
5
|
+
import { createReadStream, mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
|
|
6
|
+
import { mkdir, readdir, stat } from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
|
|
10
|
+
const SERVICE_LABEL = 'com.aigcvault.x-video-service';
|
|
11
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
12
|
+
const DEFAULT_PORT = 8899;
|
|
13
|
+
const DEFAULT_DOWNLOAD_DIR = path.join(os.homedir(), 'Movies', 'AIGC-Vault-X-Videos');
|
|
14
|
+
|
|
15
|
+
function parseArgs(argv) {
|
|
16
|
+
const args = {
|
|
17
|
+
command: 'serve',
|
|
18
|
+
host: process.env.X_VIDEO_SERVICE_HOST || DEFAULT_HOST,
|
|
19
|
+
port: Number(process.env.X_VIDEO_SERVICE_PORT || DEFAULT_PORT),
|
|
20
|
+
downloadDir: process.env.X_VIDEO_DOWNLOAD_DIR || DEFAULT_DOWNLOAD_DIR,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < argv.length; i++) {
|
|
24
|
+
const arg = argv[i];
|
|
25
|
+
if (arg === 'install' || arg === '--install') args.command = 'install';
|
|
26
|
+
else if (arg === 'uninstall' || arg === '--uninstall') args.command = 'uninstall';
|
|
27
|
+
else if (arg === 'help' || arg === '--help' || arg === '-h') args.command = 'help';
|
|
28
|
+
else if (arg === '--host') args.host = argv[++i] || args.host;
|
|
29
|
+
else if (arg.startsWith('--host=')) args.host = arg.slice('--host='.length);
|
|
30
|
+
else if (arg === '--port') args.port = Number(argv[++i] || args.port);
|
|
31
|
+
else if (arg.startsWith('--port=')) args.port = Number(arg.slice('--port='.length));
|
|
32
|
+
else if (arg === '--download-dir') args.downloadDir = argv[++i] || args.downloadDir;
|
|
33
|
+
else if (arg.startsWith('--download-dir=')) args.downloadDir = arg.slice('--download-dir='.length);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
args.downloadDir = path.resolve(args.downloadDir);
|
|
37
|
+
if (!Number.isFinite(args.port) || args.port <= 0) args.port = DEFAULT_PORT;
|
|
38
|
+
return args;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function printHelp() {
|
|
42
|
+
console.log(`AIGC Vault X video service
|
|
43
|
+
|
|
44
|
+
Usage:
|
|
45
|
+
aigc-vault-x-video-service
|
|
46
|
+
aigc-vault-x-video-service --host 127.0.0.1 --port 8899
|
|
47
|
+
aigc-vault-x-video-service --download-dir ~/Movies/AIGC-Vault-X-Videos
|
|
48
|
+
aigc-vault-x-video-service --install
|
|
49
|
+
aigc-vault-x-video-service --uninstall
|
|
50
|
+
|
|
51
|
+
Prerequisites:
|
|
52
|
+
brew install yt-dlp ffmpeg
|
|
53
|
+
|
|
54
|
+
Endpoints:
|
|
55
|
+
GET /health
|
|
56
|
+
POST /resolve-x-video
|
|
57
|
+
GET /files/<filename>.mp4
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function corsHeaders() {
|
|
62
|
+
return {
|
|
63
|
+
'Access-Control-Allow-Origin': '*',
|
|
64
|
+
'Access-Control-Allow-Headers': 'content-type',
|
|
65
|
+
'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sendJson(res, status, data) {
|
|
70
|
+
res.writeHead(status, {
|
|
71
|
+
...corsHeaders(),
|
|
72
|
+
'content-type': 'application/json; charset=utf-8',
|
|
73
|
+
});
|
|
74
|
+
res.end(JSON.stringify(data));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isAllowedStatusUrl(value = '') {
|
|
78
|
+
try {
|
|
79
|
+
const parsed = new URL(String(value));
|
|
80
|
+
const host = parsed.hostname.toLowerCase();
|
|
81
|
+
return ['x.com', 'www.x.com', 'twitter.com', 'www.twitter.com'].includes(host)
|
|
82
|
+
&& /^\/[^/]+\/status\/\d+/.test(parsed.pathname);
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readJsonBody(req) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
let raw = '';
|
|
91
|
+
req.on('data', chunk => {
|
|
92
|
+
raw += chunk;
|
|
93
|
+
if (raw.length > 16 * 1024) {
|
|
94
|
+
reject(new Error('Request body is too large'));
|
|
95
|
+
req.destroy();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
req.on('end', () => {
|
|
99
|
+
if (!raw.trim()) return resolve({});
|
|
100
|
+
try {
|
|
101
|
+
resolve(JSON.parse(raw));
|
|
102
|
+
} catch {
|
|
103
|
+
reject(new Error('Request body is not valid JSON'));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
req.on('error', reject);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function runYtDlp(url, downloadDir) {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const outputTemplate = path.join(downloadDir, '%(id)s.%(ext)s');
|
|
113
|
+
const args = [
|
|
114
|
+
'--cookies-from-browser',
|
|
115
|
+
'chrome',
|
|
116
|
+
'-S',
|
|
117
|
+
'res,ext:mp4:m4a',
|
|
118
|
+
'--merge-output-format',
|
|
119
|
+
'mp4',
|
|
120
|
+
'-o',
|
|
121
|
+
outputTemplate,
|
|
122
|
+
url,
|
|
123
|
+
];
|
|
124
|
+
const child = spawn('yt-dlp', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
125
|
+
let stdout = '';
|
|
126
|
+
let stderr = '';
|
|
127
|
+
|
|
128
|
+
child.stdout.on('data', chunk => { stdout += chunk.toString(); });
|
|
129
|
+
child.stderr.on('data', chunk => { stderr += chunk.toString(); });
|
|
130
|
+
child.on('error', error => {
|
|
131
|
+
reject(new Error(`Failed to start yt-dlp: ${error.message}`));
|
|
132
|
+
});
|
|
133
|
+
child.on('close', code => {
|
|
134
|
+
if (code === 0) return resolve({ stdout, stderr });
|
|
135
|
+
reject(new Error((stderr || stdout || `yt-dlp exited with code ${code}`).trim()));
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function findNewestMp4(downloadDir, beforeMs) {
|
|
141
|
+
const entries = await readdir(downloadDir, { withFileTypes: true });
|
|
142
|
+
const files = [];
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
if (!entry.isFile() || path.extname(entry.name).toLowerCase() !== '.mp4') continue;
|
|
145
|
+
const filePath = path.join(downloadDir, entry.name);
|
|
146
|
+
const info = await stat(filePath);
|
|
147
|
+
if (info.mtimeMs + 2000 >= beforeMs) {
|
|
148
|
+
files.push({ name: entry.name, path: filePath, mtimeMs: info.mtimeMs });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
files.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
152
|
+
return files[0] || null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function handleResolve(req, res, config) {
|
|
156
|
+
try {
|
|
157
|
+
const body = await readJsonBody(req);
|
|
158
|
+
const url = String(body.url || '').trim();
|
|
159
|
+
if (!isAllowedStatusUrl(url)) {
|
|
160
|
+
return sendJson(res, 400, { success: false, error: 'Only x.com / twitter.com status URLs are allowed' });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await mkdir(config.downloadDir, { recursive: true });
|
|
164
|
+
const startedAt = Date.now();
|
|
165
|
+
await runYtDlp(url, config.downloadDir);
|
|
166
|
+
const file = await findNewestMp4(config.downloadDir, startedAt);
|
|
167
|
+
if (!file) {
|
|
168
|
+
return sendJson(res, 500, { success: false, error: 'Download finished, but no MP4 file was found' });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
sendJson(res, 200, {
|
|
172
|
+
success: true,
|
|
173
|
+
videoUrl: `http://${config.host}:${config.port}/files/${encodeURIComponent(file.name)}`,
|
|
174
|
+
filePath: file.path,
|
|
175
|
+
fileName: file.name,
|
|
176
|
+
});
|
|
177
|
+
} catch (error) {
|
|
178
|
+
sendJson(res, 500, { success: false, error: error?.message || String(error) });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function handleFile(req, res, pathname, config) {
|
|
183
|
+
const encodedName = pathname.slice('/files/'.length);
|
|
184
|
+
const decodedName = decodeURIComponent(encodedName);
|
|
185
|
+
const fileName = path.basename(decodedName);
|
|
186
|
+
if (!fileName || fileName !== decodedName || path.extname(fileName).toLowerCase() !== '.mp4') {
|
|
187
|
+
return sendJson(res, 400, { success: false, error: 'Invalid filename' });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const filePath = path.join(config.downloadDir, fileName);
|
|
191
|
+
try {
|
|
192
|
+
const info = await stat(filePath);
|
|
193
|
+
if (!info.isFile()) throw new Error('Not a file');
|
|
194
|
+
res.writeHead(200, {
|
|
195
|
+
...corsHeaders(),
|
|
196
|
+
'content-type': 'video/mp4',
|
|
197
|
+
'content-length': info.size,
|
|
198
|
+
'accept-ranges': 'bytes',
|
|
199
|
+
'cache-control': 'public, max-age=3600',
|
|
200
|
+
});
|
|
201
|
+
createReadStream(filePath).pipe(res);
|
|
202
|
+
} catch {
|
|
203
|
+
sendJson(res, 404, { success: false, error: 'File not found' });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function startServer(config) {
|
|
208
|
+
const server = http.createServer(async (req, res) => {
|
|
209
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
210
|
+
res.setHeader('Access-Control-Allow-Headers', 'content-type');
|
|
211
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
|
|
212
|
+
|
|
213
|
+
if (req.method === 'OPTIONS') {
|
|
214
|
+
res.writeHead(204, corsHeaders());
|
|
215
|
+
return res.end();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const parsed = new URL(req.url || '/', `http://${req.headers.host || `${config.host}:${config.port}`}`);
|
|
219
|
+
if (req.method === 'GET' && parsed.pathname === '/health') {
|
|
220
|
+
return sendJson(res, 200, {
|
|
221
|
+
success: true,
|
|
222
|
+
status: 'ok',
|
|
223
|
+
service: 'aigc-vault-x-video-service',
|
|
224
|
+
downloadDir: config.downloadDir,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (req.method === 'POST' && parsed.pathname === '/resolve-x-video') {
|
|
228
|
+
return handleResolve(req, res, config);
|
|
229
|
+
}
|
|
230
|
+
if (req.method === 'GET' && parsed.pathname.startsWith('/files/')) {
|
|
231
|
+
return handleFile(req, res, parsed.pathname, config);
|
|
232
|
+
}
|
|
233
|
+
sendJson(res, 404, { success: false, error: 'Not Found' });
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
server.listen(config.port, config.host, () => {
|
|
237
|
+
console.log(`AIGC Vault X video service listening at http://${config.host}:${config.port}`);
|
|
238
|
+
console.log(`Download directory: ${config.downloadDir}`);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function findExecutable(name) {
|
|
243
|
+
const candidates = [
|
|
244
|
+
`/opt/homebrew/bin/${name}`,
|
|
245
|
+
`/usr/local/bin/${name}`,
|
|
246
|
+
`/usr/bin/${name}`,
|
|
247
|
+
];
|
|
248
|
+
return candidates.find(existsSync) || name;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function installLaunchAgent(config) {
|
|
252
|
+
if (process.platform !== 'darwin') {
|
|
253
|
+
console.error('Auto-start install currently supports macOS only.');
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const launchAgentsDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
258
|
+
const logDir = path.join(os.homedir(), 'Library', 'Logs');
|
|
259
|
+
const plistPath = path.join(launchAgentsDir, `${SERVICE_LABEL}.plist`);
|
|
260
|
+
const logPath = path.join(logDir, 'aigc-vault-x-video-service.log');
|
|
261
|
+
const errPath = path.join(logDir, 'aigc-vault-x-video-service.err.log');
|
|
262
|
+
const nodePath = findExecutable('node');
|
|
263
|
+
const currentScript = new URL(import.meta.url).pathname;
|
|
264
|
+
const command = [
|
|
265
|
+
JSON.stringify(nodePath),
|
|
266
|
+
JSON.stringify(currentScript),
|
|
267
|
+
'--host',
|
|
268
|
+
JSON.stringify(config.host),
|
|
269
|
+
'--port',
|
|
270
|
+
String(config.port),
|
|
271
|
+
'--download-dir',
|
|
272
|
+
JSON.stringify(config.downloadDir),
|
|
273
|
+
].join(' ');
|
|
274
|
+
|
|
275
|
+
const escapedCommand = command.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
276
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
277
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
278
|
+
<plist version="1.0">
|
|
279
|
+
<dict>
|
|
280
|
+
<key>Label</key>
|
|
281
|
+
<string>${SERVICE_LABEL}</string>
|
|
282
|
+
<key>ProgramArguments</key>
|
|
283
|
+
<array>
|
|
284
|
+
<string>/bin/zsh</string>
|
|
285
|
+
<string>-lc</string>
|
|
286
|
+
<string>${escapedCommand}</string>
|
|
287
|
+
</array>
|
|
288
|
+
<key>RunAtLoad</key>
|
|
289
|
+
<true/>
|
|
290
|
+
<key>KeepAlive</key>
|
|
291
|
+
<true/>
|
|
292
|
+
<key>StandardOutPath</key>
|
|
293
|
+
<string>${logPath}</string>
|
|
294
|
+
<key>StandardErrorPath</key>
|
|
295
|
+
<string>${errPath}</string>
|
|
296
|
+
<key>EnvironmentVariables</key>
|
|
297
|
+
<dict>
|
|
298
|
+
<key>PATH</key>
|
|
299
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
300
|
+
</dict>
|
|
301
|
+
</dict>
|
|
302
|
+
</plist>
|
|
303
|
+
`;
|
|
304
|
+
|
|
305
|
+
mkdirSync(launchAgentsDir, { recursive: true });
|
|
306
|
+
mkdirSync(logDir, { recursive: true });
|
|
307
|
+
writeFileSync(plistPath, plist);
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
execFileSync('launchctl', ['bootout', `gui/${process.getuid()}`, plistPath], { stdio: 'ignore' });
|
|
311
|
+
} catch {}
|
|
312
|
+
|
|
313
|
+
execFileSync('launchctl', ['bootstrap', `gui/${process.getuid()}`, plistPath], { stdio: 'inherit' });
|
|
314
|
+
execFileSync('launchctl', ['enable', `gui/${process.getuid()}/${SERVICE_LABEL}`], { stdio: 'inherit' });
|
|
315
|
+
execFileSync('launchctl', ['kickstart', '-k', `gui/${process.getuid()}/${SERVICE_LABEL}`], { stdio: 'inherit' });
|
|
316
|
+
|
|
317
|
+
console.log('AIGC Vault X video service auto-start is installed.');
|
|
318
|
+
console.log(`Service URL: http://${config.host}:${config.port}`);
|
|
319
|
+
console.log(`Config: ${plistPath}`);
|
|
320
|
+
console.log(`Log: ${logPath}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function uninstallLaunchAgent() {
|
|
324
|
+
if (process.platform !== 'darwin') {
|
|
325
|
+
console.error('Auto-start uninstall currently supports macOS only.');
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${SERVICE_LABEL}.plist`);
|
|
330
|
+
try {
|
|
331
|
+
execFileSync('launchctl', ['bootout', `gui/${process.getuid()}`, plistPath], { stdio: 'inherit' });
|
|
332
|
+
} catch {}
|
|
333
|
+
try {
|
|
334
|
+
execFileSync('launchctl', ['disable', `gui/${process.getuid()}/${SERVICE_LABEL}`], { stdio: 'ignore' });
|
|
335
|
+
} catch {}
|
|
336
|
+
rmSync(plistPath, { force: true });
|
|
337
|
+
console.log('AIGC Vault X video service auto-start is removed.');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const config = parseArgs(process.argv.slice(2));
|
|
341
|
+
|
|
342
|
+
if (config.command === 'help') printHelp();
|
|
343
|
+
else if (config.command === 'install') installLaunchAgent(config);
|
|
344
|
+
else if (config.command === 'uninstall') uninstallLaunchAgent();
|
|
345
|
+
else startServer(config);
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aigc-vault-x-video-service",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Local X/Twitter video resolver service for AIGC Vault.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"aigc-vault-x-video-service": "bin/aigc-vault-x-video-service.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node bin/aigc-vault-x-video-service.mjs",
|
|
16
|
+
"check": "node --check bin/aigc-vault-x-video-service.mjs"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"aigc-vault",
|
|
20
|
+
"x",
|
|
21
|
+
"twitter",
|
|
22
|
+
"yt-dlp",
|
|
23
|
+
"video"
|
|
24
|
+
],
|
|
25
|
+
"author": "",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
}
|
|
30
|
+
}
|