@x12i/youtube-video-uploader-cli 1.1.0 ā 1.3.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/README.md +362 -25
- package/bin/cli.js +735 -2
- package/package.json +7 -1
- package/src/actions/index.js +274 -0
- package/src/actions/journey-overlay.js +567 -0
- package/src/actions/replace-background.js +154 -0
- package/src/actions/shorten.js +271 -0
- package/src/auth.js +1 -1
- package/src/batch/clone-metadata.js +51 -0
- package/src/batch/plan.js +194 -0
- package/src/cli/apply-actions.js +152 -0
- package/src/cli/preview-journey.js +106 -0
- package/src/index.d.ts +596 -1
- package/src/index.js +91 -1
- package/src/media/cache.js +134 -0
- package/src/media/compose.js +227 -0
- package/src/media/download.js +151 -0
- package/src/media/resolve.js +476 -0
- package/src/media/select.js +176 -0
- package/src/providers/index.js +235 -0
- package/src/providers/pexels.js +171 -0
- package/src/providers/pixabay.js +184 -0
- package/src/state.js +11 -0
- package/src/swap/steps.js +161 -0
- package/src/swap/store.js +120 -0
- package/src/swap/swapSong.js +154 -0
- package/src/update.js +392 -0
- package/src/uploader.js +20 -0
package/bin/cli.js
CHANGED
|
@@ -4,12 +4,45 @@ import { Command } from 'commander';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import pc from 'picocolors';
|
|
7
|
-
import {
|
|
7
|
+
import { google } from 'googleapis';
|
|
8
|
+
import {
|
|
9
|
+
batchUpload,
|
|
10
|
+
DAILY_DEFAULT_QUOTA,
|
|
11
|
+
swapSong,
|
|
12
|
+
SwapStore,
|
|
13
|
+
QUOTA_PER_SWAP,
|
|
14
|
+
batchUpdateMetadata,
|
|
15
|
+
searchMedia,
|
|
16
|
+
rankCandidates,
|
|
17
|
+
resolveMediaForMetadata,
|
|
18
|
+
composeVideo,
|
|
19
|
+
batchApplyActions,
|
|
20
|
+
previewJourney,
|
|
21
|
+
} from '../src/index.js';
|
|
22
|
+
import { authenticate } from '../src/auth.js';
|
|
8
23
|
|
|
9
24
|
// Read package.json for version
|
|
10
25
|
const pkgPath = new URL('../package.json', import.meta.url);
|
|
11
26
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
12
27
|
|
|
28
|
+
// Auto-load .env if available
|
|
29
|
+
try {
|
|
30
|
+
process.loadEnvFile?.();
|
|
31
|
+
} catch {}
|
|
32
|
+
try {
|
|
33
|
+
let searchDir = process.cwd();
|
|
34
|
+
for (let i = 0; i < 4; i++) {
|
|
35
|
+
const candidate = path.join(searchDir, '.env');
|
|
36
|
+
if (fs.existsSync(candidate)) {
|
|
37
|
+
process.loadEnvFile?.(candidate);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
const parent = path.dirname(searchDir);
|
|
41
|
+
if (parent === searchDir) break;
|
|
42
|
+
searchDir = parent;
|
|
43
|
+
}
|
|
44
|
+
} catch {}
|
|
45
|
+
|
|
13
46
|
const program = new Command();
|
|
14
47
|
|
|
15
48
|
program
|
|
@@ -24,6 +57,10 @@ program
|
|
|
24
57
|
.option('-l, --limit <number>', 'Maximum number of videos to upload in this run (e.g. 5 or 6)')
|
|
25
58
|
.option('--safe-quota', 'Cap upload batch to stay safely within standard daily 10,000 units quota (default ~5-6 videos)', false)
|
|
26
59
|
.option('--max-quota <units>', 'Maximum allowed API quota units for this run')
|
|
60
|
+
.option('--auto-media', 'Resolve and compose stock media for missing videos before uploading', false)
|
|
61
|
+
.option('--media-provider <provider>', 'Stock media provider for --auto-media: pexels, pixabay, auto (default: auto)')
|
|
62
|
+
.option('--pexels-key <key>', 'Pexels API key')
|
|
63
|
+
.option('--pixabay-key <key>', 'Pixabay API key')
|
|
27
64
|
.option('--client-id <id>', 'Google OAuth Client ID')
|
|
28
65
|
.option('--client-secret <secret>', 'Google OAuth Client Secret')
|
|
29
66
|
.option('--client-secrets-file <file>', 'Path to client_secret.json downloaded from Google Cloud')
|
|
@@ -68,6 +105,10 @@ program
|
|
|
68
105
|
limit: options.limit ? parseInt(options.limit, 10) : undefined,
|
|
69
106
|
safeQuota: options.safeQuota,
|
|
70
107
|
maxQuota: options.maxQuota ? parseInt(options.maxQuota, 10) : undefined,
|
|
108
|
+
autoMedia: options.autoMedia,
|
|
109
|
+
mediaProvider: options.mediaProvider,
|
|
110
|
+
pexelsKey: options.pexelsKey,
|
|
111
|
+
pixabayKey: options.pixabayKey,
|
|
71
112
|
tokenFile,
|
|
72
113
|
clientSecretsFile: options.clientSecretsFile,
|
|
73
114
|
clientId: options.clientId,
|
|
@@ -75,7 +116,17 @@ program
|
|
|
75
116
|
onEvent: (event) => {
|
|
76
117
|
if (isQuiet) return;
|
|
77
118
|
|
|
78
|
-
if (event.type === '
|
|
119
|
+
if (event.type === 'auto_media_start') {
|
|
120
|
+
console.log(pc.cyan(`š¬ Sourcing & resolving stock media (--auto-media)...`));
|
|
121
|
+
} else if (event.type === 'search_start') {
|
|
122
|
+
console.log(` š Searching ${event.provider} for "${event.query}" (${event.songKey})...`);
|
|
123
|
+
} else if (event.type === 'compose_start') {
|
|
124
|
+
console.log(` šļø Composing video for ${pc.bold(event.songKey)} via ffmpeg...`);
|
|
125
|
+
} else if (event.type === 'compose_success') {
|
|
126
|
+
console.log(pc.green(` ā
Composed:`), pc.dim(event.outputPath));
|
|
127
|
+
} else if (event.type === 'auto_media_complete') {
|
|
128
|
+
console.log(pc.green(`⨠Stock media resolution complete.\n`));
|
|
129
|
+
} else if (event.type === 'plan') {
|
|
79
130
|
console.log(
|
|
80
131
|
pc.bold(`Found `) +
|
|
81
132
|
pc.green(pc.bold(`${event.total}`)) +
|
|
@@ -183,4 +234,686 @@ program
|
|
|
183
234
|
}
|
|
184
235
|
});
|
|
185
236
|
|
|
237
|
+
program
|
|
238
|
+
.command('swap')
|
|
239
|
+
.description('Replace a song\'s video in a YouTube playlist: upload the new video, swap the playlist entry in place, unlist (never delete) the old video')
|
|
240
|
+
.argument('<videoFile>', 'Path to the new MP4 video file')
|
|
241
|
+
.requiredOption('--playlist <id>', 'Playlist ID containing the old video')
|
|
242
|
+
.requiredOption('--old-video <videoId>', 'Video ID of the old video to replace')
|
|
243
|
+
.option('-k, --key <songKey>', 'Stable song key for swap state tracking (default: video file basename)')
|
|
244
|
+
.option('--title <title>', 'Override the new video title (default: carried over from the old video)')
|
|
245
|
+
.option('--state-file <file>', 'Swap state filename (default: .swap-history.json in target dir)')
|
|
246
|
+
.option('--token-file <file>', 'Path to store or read OAuth token (default: token.json in cwd)')
|
|
247
|
+
.option('--client-secrets-file <file>', 'Path to client_secret.json downloaded from Google Cloud')
|
|
248
|
+
.option('--client-id <id>', 'Google OAuth Client ID')
|
|
249
|
+
.option('--client-secret <secret>', 'Google OAuth Client Secret')
|
|
250
|
+
.option('--fresh', 'Discard any previous swap state for this key and start over', false)
|
|
251
|
+
.option('-n, --dry-run', 'Print the swap plan without any API calls', false)
|
|
252
|
+
.option('-q, --quiet', 'Minimal output mode', false)
|
|
253
|
+
.action(async (videoFile, options) => {
|
|
254
|
+
const isQuiet = options.quiet;
|
|
255
|
+
const videoPath = path.resolve(videoFile);
|
|
256
|
+
const songKey = options.key || path.basename(videoPath);
|
|
257
|
+
const stateDir = process.cwd();
|
|
258
|
+
|
|
259
|
+
if (!isQuiet) {
|
|
260
|
+
console.log();
|
|
261
|
+
console.log(pc.bold(pc.red(`š Playlist Video Swap`)) + ` ${pc.dim(`v${pkg.version}`)}`);
|
|
262
|
+
console.log(pc.dim(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`));
|
|
263
|
+
console.log(`${pc.bold('š¬ New Video :')} ${pc.white(videoPath)}`);
|
|
264
|
+
console.log(`${pc.bold('šļø Old Video ID :')} ${pc.white(options.oldVideo)}`);
|
|
265
|
+
console.log(`${pc.bold('š Playlist ID :')} ${pc.white(options.playlist)}`);
|
|
266
|
+
console.log(`${pc.bold('š Swap Key :')} ${pc.white(songKey)}`);
|
|
267
|
+
console.log(pc.dim(`Estimated quota : ~${QUOTA_PER_SWAP - 1} shared units + 1 upload`));
|
|
268
|
+
console.log();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!fs.existsSync(videoPath)) {
|
|
272
|
+
console.error(pc.red(`\nā Error: Video file not found: ${videoPath}\n`));
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (options.dryRun) {
|
|
277
|
+
if (!isQuiet) {
|
|
278
|
+
console.log(pc.yellow(`š DRY RUN ā the following steps would run:`));
|
|
279
|
+
console.log(` 1. UPLOAD ā videos.insert (unlisted), metadata carried over from old video`);
|
|
280
|
+
console.log(` 2. LOCATE ā playlistItems.list to find old item id + position`);
|
|
281
|
+
console.log(` 3. INSERT ā playlistItems.insert at the old position`);
|
|
282
|
+
console.log(` 4. REMOVE ā playlistItems.delete for the old item`);
|
|
283
|
+
console.log(` 5. FINALIZE ā old video ā unlisted, new video ā public`);
|
|
284
|
+
console.log();
|
|
285
|
+
console.log(pc.yellow(`⨠Dry run complete. No API calls were made.`));
|
|
286
|
+
console.log();
|
|
287
|
+
}
|
|
288
|
+
process.exit(0);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const tokenFile = options.tokenFile
|
|
293
|
+
? path.resolve(options.tokenFile)
|
|
294
|
+
: path.join(stateDir, 'token.json');
|
|
295
|
+
|
|
296
|
+
const auth = await authenticate({
|
|
297
|
+
tokenFile,
|
|
298
|
+
clientSecretsFile: options.clientSecretsFile,
|
|
299
|
+
clientId: options.clientId,
|
|
300
|
+
clientSecret: options.clientSecret,
|
|
301
|
+
});
|
|
302
|
+
const youtube = google.youtube({ version: 'v3', auth });
|
|
303
|
+
|
|
304
|
+
const store = new SwapStore(stateDir, options.stateFile);
|
|
305
|
+
const existing = store.getRecord(songKey);
|
|
306
|
+
if (existing && !options.fresh && !isQuiet) {
|
|
307
|
+
console.log(pc.cyan(`ā©ļø Resuming swap in progress (last status: ${existing.status}${existing.lastError ? ` ā ${existing.lastError}` : ''})`));
|
|
308
|
+
console.log();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const { record } = await swapSong({
|
|
312
|
+
youtube,
|
|
313
|
+
songKey,
|
|
314
|
+
newVideoPath: videoPath,
|
|
315
|
+
playlistId: options.playlist,
|
|
316
|
+
oldVideoId: options.oldVideo,
|
|
317
|
+
meta: options.title ? { title: options.title } : undefined,
|
|
318
|
+
stateDir,
|
|
319
|
+
stateFile: options.stateFile,
|
|
320
|
+
fresh: options.fresh,
|
|
321
|
+
onEvent: (event) => {
|
|
322
|
+
if (isQuiet) return;
|
|
323
|
+
if (event.type === 'swap_step') {
|
|
324
|
+
console.log(pc.green(` ā
${event.status.toUpperCase()}`));
|
|
325
|
+
} else if (event.type === 'swap_uploaded') {
|
|
326
|
+
console.log(pc.green(` ā¬ļø Uploaded new video: `) + pc.underline(pc.blue(event.url)));
|
|
327
|
+
} else if (event.type === 'swap_error') {
|
|
328
|
+
console.error(pc.red(` ā Failed at swap step: ${event.error.message}`));
|
|
329
|
+
if (event.needsManualReview) {
|
|
330
|
+
console.error(pc.yellow(` ā ļø Needs manual review ā the old video was not found in the playlist.`));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
if (!isQuiet) {
|
|
337
|
+
console.log();
|
|
338
|
+
console.log(pc.green(pc.bold(`š Swap complete!`)));
|
|
339
|
+
console.log(` ⢠Playlist entry replaced: ${pc.bold(options.playlist)} (position preserved)`);
|
|
340
|
+
console.log(` ⢠New video is public : ${pc.underline(pc.blue(`https://youtu.be/${record.newVideoId}`))}`);
|
|
341
|
+
console.log(` ⢠Old video unlisted : ${pc.dim(`https://youtu.be/${options.oldVideo} (direct links still work)`)}`);
|
|
342
|
+
console.log();
|
|
343
|
+
}
|
|
344
|
+
} catch (err) {
|
|
345
|
+
console.error(pc.red(`\nā Error: ${err.message}\n`));
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
program
|
|
351
|
+
.command('update-metadata')
|
|
352
|
+
.alias('update-titles')
|
|
353
|
+
.description('Update titles and snippet metadata for videos and playlists on YouTube and in metadata.json')
|
|
354
|
+
.argument('[directory]', 'Target folder containing metadata.json and upload history (default: current directory)', '.')
|
|
355
|
+
.option('-d, --dir <path>', 'Explicit target directory path')
|
|
356
|
+
.option('-m, --metadata <file>', 'Custom path to metadata.json file')
|
|
357
|
+
.option('--title-postfix <postfix>', 'Postfix to append to each video title (e.g. " (long version)")')
|
|
358
|
+
.option('--playlist-postfix <postfix>', 'Postfix to append to playlist title (e.g. " (long versions)")')
|
|
359
|
+
.option('--title-prefix <prefix>', 'Prefix to prepend to each video title')
|
|
360
|
+
.option('--playlist-title <title>', 'Explicit new title for the playlist')
|
|
361
|
+
.option('--replace-pattern <pattern>', 'Regex/string pattern to replace in titles')
|
|
362
|
+
.option('--replace-with <replacement>', 'Replacement string for --replace-pattern', '')
|
|
363
|
+
.option('--sync-metadata', 'Sync titles directly from metadata.json to YouTube')
|
|
364
|
+
.option('--video <videoId>', 'Target a specific YouTube video ID')
|
|
365
|
+
.option('--title <title>', 'Explicit new title for the targeted video')
|
|
366
|
+
.option('--playlist <playlistId>', 'Target a specific YouTube playlist ID')
|
|
367
|
+
.option('--no-update-file', 'Do not update metadata.json on disk')
|
|
368
|
+
.option('-n, --dry-run', 'Preview changes without making any API calls')
|
|
369
|
+
.option('--token-file <file>', 'Path to store or read OAuth token (default: token.json in target dir)')
|
|
370
|
+
.option('--client-secrets-file <file>', 'Path to client_secret.json downloaded from Google Cloud')
|
|
371
|
+
.option('--client-id <id>', 'Google OAuth Client ID')
|
|
372
|
+
.option('--client-secret <secret>', 'Google OAuth Client Secret')
|
|
373
|
+
.option('-q, --quiet', 'Minimal output mode', false)
|
|
374
|
+
.action(async (directoryArg, options) => {
|
|
375
|
+
const targetDir = path.resolve(options.dir || directoryArg || process.cwd());
|
|
376
|
+
const isQuiet = options.quiet;
|
|
377
|
+
|
|
378
|
+
if (!isQuiet) {
|
|
379
|
+
console.log();
|
|
380
|
+
console.log(pc.bold(pc.cyan(`š YouTube Metadata & Title Updater`)) + ` ${pc.dim(`v${pkg.version}`)}`);
|
|
381
|
+
console.log(pc.dim(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`));
|
|
382
|
+
console.log(`${pc.bold('š Target Folder :')} ${pc.white(targetDir)}`);
|
|
383
|
+
if (options.titlePostfix) {
|
|
384
|
+
console.log(`${pc.bold('š·ļø Title Postfix :')} ${pc.green(JSON.stringify(options.titlePostfix))}`);
|
|
385
|
+
}
|
|
386
|
+
if (options.playlistPostfix) {
|
|
387
|
+
console.log(`${pc.bold('š Playlist Postfix:')} ${pc.green(JSON.stringify(options.playlistPostfix))}`);
|
|
388
|
+
}
|
|
389
|
+
if (options.titlePrefix) {
|
|
390
|
+
console.log(`${pc.bold('š·ļø Title Prefix :')} ${pc.green(JSON.stringify(options.titlePrefix))}`);
|
|
391
|
+
}
|
|
392
|
+
if (options.dryRun) {
|
|
393
|
+
console.log(`${pc.yellow(pc.bold('š Mode :'))} ${pc.yellow('DRY RUN (no updates will be performed)')}`);
|
|
394
|
+
}
|
|
395
|
+
console.log();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const tokenFile = options.tokenFile
|
|
400
|
+
? path.resolve(options.tokenFile)
|
|
401
|
+
: path.join(targetDir, 'token.json');
|
|
402
|
+
|
|
403
|
+
const result = await batchUpdateMetadata(targetDir, {
|
|
404
|
+
titlePostfix: options.titlePostfix,
|
|
405
|
+
titlePrefix: options.titlePrefix,
|
|
406
|
+
playlistPostfix: options.playlistPostfix,
|
|
407
|
+
playlistTitle: options.playlistTitle,
|
|
408
|
+
replacePattern: options.replacePattern,
|
|
409
|
+
replaceWith: options.replaceWith,
|
|
410
|
+
syncMetadata: options.syncMetadata,
|
|
411
|
+
metadataFile: options.metadata,
|
|
412
|
+
updateLocalMetadata: options.updateFile !== false,
|
|
413
|
+
singleVideoId: options.video,
|
|
414
|
+
singleVideoTitle: options.title,
|
|
415
|
+
singlePlaylistId: options.playlist,
|
|
416
|
+
dryRun: options.dryRun,
|
|
417
|
+
tokenFile,
|
|
418
|
+
clientSecretsFile: options.clientSecretsFile,
|
|
419
|
+
clientId: options.clientId,
|
|
420
|
+
clientSecret: options.clientSecret,
|
|
421
|
+
onEvent: (event) => {
|
|
422
|
+
if (isQuiet) return;
|
|
423
|
+
|
|
424
|
+
if (event.type === 'update_plan') {
|
|
425
|
+
if (event.playlist) {
|
|
426
|
+
console.log(pc.bold('š Playlist: ') + pc.dim(event.playlist.playlistId));
|
|
427
|
+
if (event.playlist.changed) {
|
|
428
|
+
console.log(` ${pc.dim('Old:')} ${event.playlist.oldTitle}`);
|
|
429
|
+
console.log(` ${pc.green('New:')} ${pc.bold(event.playlist.newTitle)}`);
|
|
430
|
+
} else {
|
|
431
|
+
console.log(` ${pc.dim('Unchanged:')} ${event.playlist.oldTitle}`);
|
|
432
|
+
}
|
|
433
|
+
console.log();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
console.log(pc.bold(`š¬ Videos (${event.videos.length} total, ${event.changedCount} item(s) to update):`));
|
|
437
|
+
for (const v of event.videos) {
|
|
438
|
+
if (v.changed) {
|
|
439
|
+
console.log(` ⢠${pc.cyan(v.filename)} ${pc.dim(`(${v.videoId})`)}`);
|
|
440
|
+
console.log(` ${pc.dim('Old:')} ${v.oldTitle}`);
|
|
441
|
+
console.log(` ${pc.green('New:')} ${pc.bold(v.newTitle)}`);
|
|
442
|
+
} else {
|
|
443
|
+
console.log(` ⢠${pc.dim(v.filename)} ${pc.dim(`(${v.videoId}) ā unchanged`)}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
console.log();
|
|
447
|
+
console.log(
|
|
448
|
+
pc.bold(`Estimated API Quota: `) +
|
|
449
|
+
pc.green(pc.bold(`${event.estimatedQuota} units`)) +
|
|
450
|
+
pc.dim(` / ${DAILY_DEFAULT_QUOTA} daily default units`)
|
|
451
|
+
);
|
|
452
|
+
console.log();
|
|
453
|
+
} else if (event.type === 'playlist_update_start') {
|
|
454
|
+
process.stdout.write(` ${pc.cyan('š')} Updating playlist title... `);
|
|
455
|
+
} else if (event.type === 'playlist_update_success') {
|
|
456
|
+
console.log(pc.green('UPDATED!'));
|
|
457
|
+
} else if (event.type === 'video_update_start') {
|
|
458
|
+
process.stdout.write(` ${pc.cyan('š¬')} [${event.index + 1}/${event.total}] Updating "${event.item.filename}"... `);
|
|
459
|
+
} else if (event.type === 'video_update_success') {
|
|
460
|
+
console.log(pc.green('UPDATED!'));
|
|
461
|
+
} else if (event.type === 'metadata_file_saved') {
|
|
462
|
+
console.log(` ${pc.dim('š Saved updated titles to metadata.json')}`);
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
if (!isQuiet) {
|
|
468
|
+
if (options.dryRun) {
|
|
469
|
+
console.log(pc.yellow(`⨠Dry run complete. No YouTube updates were performed.`));
|
|
470
|
+
console.log();
|
|
471
|
+
process.exit(0);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
console.log(pc.dim(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`));
|
|
475
|
+
console.log(pc.green(pc.bold(`š All titles and metadata updated successfully!`)));
|
|
476
|
+
console.log();
|
|
477
|
+
}
|
|
478
|
+
} catch (err) {
|
|
479
|
+
console.error(pc.red(`\nā Error: ${err.message}\n`));
|
|
480
|
+
process.exit(1);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
program
|
|
485
|
+
.command('search-media')
|
|
486
|
+
.description('Search and preview ranked stock media from Pexels and Pixabay without downloading')
|
|
487
|
+
.argument('<query>', 'Search query term')
|
|
488
|
+
.option('--type <video|photo>', 'Media type to search (default: video)', 'video')
|
|
489
|
+
.option('--provider <pexels|pixabay|auto>', 'Provider to use (default: auto)', 'auto')
|
|
490
|
+
.option('--orientation <landscape|portrait|square>', 'Orientation filter')
|
|
491
|
+
.option('--min-width <px>', 'Minimum width in pixels')
|
|
492
|
+
.option('--min-height <px>', 'Minimum height in pixels')
|
|
493
|
+
.option('--min-duration <seconds>', 'Minimum duration in seconds (video only)')
|
|
494
|
+
.option('--max-duration <seconds>', 'Maximum duration in seconds (video only)')
|
|
495
|
+
.option('--count <n>', 'Number of results to show (default: 10)', '10')
|
|
496
|
+
.option('--safe-search', 'Enable safe search filtering', true)
|
|
497
|
+
.option('--pexels-key <key>', 'Pexels API key')
|
|
498
|
+
.option('--pixabay-key <key>', 'Pixabay API key')
|
|
499
|
+
.option('--json', 'Print raw structured JSON results instead of a table', false)
|
|
500
|
+
.option('-q, --quiet', 'Minimal output mode', false)
|
|
501
|
+
.action(async (query, options) => {
|
|
502
|
+
const isQuiet = options.quiet;
|
|
503
|
+
const count = parseInt(options.count, 10) || 10;
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
const results = await searchMedia({
|
|
507
|
+
query,
|
|
508
|
+
type: options.type,
|
|
509
|
+
provider: options.provider,
|
|
510
|
+
orientation: options.orientation,
|
|
511
|
+
safeSearch: options.safeSearch !== false,
|
|
512
|
+
pexelsKey: options.pexelsKey,
|
|
513
|
+
pixabayKey: options.pixabayKey,
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const criteria = {
|
|
517
|
+
orientation: options.orientation || 'landscape',
|
|
518
|
+
minWidth: options.minWidth ? parseInt(options.minWidth, 10) : undefined,
|
|
519
|
+
minHeight: options.minHeight ? parseInt(options.minHeight, 10) : undefined,
|
|
520
|
+
minDuration: options.minDuration ? parseFloat(options.minDuration) : undefined,
|
|
521
|
+
maxDuration: options.maxDuration ? parseFloat(options.maxDuration) : undefined,
|
|
522
|
+
avoidReuse: false,
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const ranked = rankCandidates(results, criteria).slice(0, count);
|
|
526
|
+
|
|
527
|
+
if (options.json) {
|
|
528
|
+
console.log(JSON.stringify(ranked.map(r => ({
|
|
529
|
+
...r.candidate,
|
|
530
|
+
score: Math.round(r.score * 1000) / 1000,
|
|
531
|
+
breakdown: r.breakdown,
|
|
532
|
+
})), null, 2));
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (!isQuiet) {
|
|
537
|
+
console.log();
|
|
538
|
+
console.log(pc.bold(pc.cyan(`š Stock Media Search Results`)) + ` ${pc.dim(`("${query}")`)}`);
|
|
539
|
+
console.log(pc.dim(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`));
|
|
540
|
+
if (ranked.length === 0) {
|
|
541
|
+
console.log(pc.yellow(`No matching media assets found.`));
|
|
542
|
+
console.log();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
console.log(
|
|
547
|
+
pc.bold(
|
|
548
|
+
`Rank Provider Asset ID Dimensions Duration Score Creator Page URL`
|
|
549
|
+
)
|
|
550
|
+
);
|
|
551
|
+
console.log(pc.dim(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`));
|
|
552
|
+
|
|
553
|
+
ranked.forEach((item, idx) => {
|
|
554
|
+
const c = item.candidate;
|
|
555
|
+
const rankStr = `#${idx + 1}`.padEnd(6);
|
|
556
|
+
const provStr = c.provider.padEnd(10);
|
|
557
|
+
const idStr = String(c.id).padEnd(10);
|
|
558
|
+
const dimStr = `${c.width}x${c.height}`.padEnd(12);
|
|
559
|
+
const durStr = (c.duration ? `${c.duration.toFixed(1)}s` : 'N/A').padEnd(10);
|
|
560
|
+
const scoreStr = item.score.toFixed(3).padEnd(8);
|
|
561
|
+
const creatorStr = (c.creator?.name || 'Unknown').slice(0, 18).padEnd(20);
|
|
562
|
+
const urlStr = pc.blue(pc.underline(c.pageUrl));
|
|
563
|
+
|
|
564
|
+
console.log(`${pc.green(rankStr)}${provStr}${idStr}${dimStr}${durStr}${pc.yellow(scoreStr)}${creatorStr}${urlStr}`);
|
|
565
|
+
});
|
|
566
|
+
console.log();
|
|
567
|
+
console.log(pc.dim(`Showing top ${ranked.length} of ${results.length} total found items.`));
|
|
568
|
+
console.log();
|
|
569
|
+
}
|
|
570
|
+
} catch (err) {
|
|
571
|
+
console.error(pc.red(`\nā Error: ${err.message}\n`));
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
program
|
|
577
|
+
.command('resolve-media')
|
|
578
|
+
.description('Search, download, and write back stock media to metadata.json')
|
|
579
|
+
.argument('[directory]', 'Target folder containing metadata.json (default: current directory)', '.')
|
|
580
|
+
.option('-d, --dir <path>', 'Explicit target directory path')
|
|
581
|
+
.option('-m, --metadata <file>', 'Custom path to metadata.json file')
|
|
582
|
+
.option('--provider <pexels|pixabay|auto>', 'Provider to use: pexels, pixabay, auto (default: auto)')
|
|
583
|
+
.option('--type <video|photo>', 'Media type to resolve (default: video)')
|
|
584
|
+
.option('--orientation <landscape|portrait|square>', 'Orientation filter')
|
|
585
|
+
.option('--compose', 'Also build the final .mp4 via ffmpeg immediately', false)
|
|
586
|
+
.option('--force', 'Re-resolve entries that already have resolvedMedia', false)
|
|
587
|
+
.option('--allow-repeat-media', 'Disable the avoid-reuse novelty check for this run', false)
|
|
588
|
+
.option('--media-dir <path>', 'Override .media-cache directory location')
|
|
589
|
+
.option('--pexels-key <key>', 'Pexels API key')
|
|
590
|
+
.option('--pixabay-key <key>', 'Pixabay API key')
|
|
591
|
+
.option('-n, --dry-run', 'Show picks without downloading or writing metadata.json', false)
|
|
592
|
+
.option('-q, --quiet', 'Minimal output mode', false)
|
|
593
|
+
.action(async (directoryArg, options) => {
|
|
594
|
+
const targetDir = path.resolve(options.dir || directoryArg || process.cwd());
|
|
595
|
+
const isQuiet = options.quiet;
|
|
596
|
+
|
|
597
|
+
if (!isQuiet) {
|
|
598
|
+
console.log();
|
|
599
|
+
console.log(pc.bold(pc.cyan(`š¬ Stock Media Resolver`)) + ` ${pc.dim(`v${pkg.version}`)}`);
|
|
600
|
+
console.log(pc.dim(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`));
|
|
601
|
+
console.log(`${pc.bold('š Target Folder :')} ${pc.white(targetDir)}`);
|
|
602
|
+
if (options.metadata) {
|
|
603
|
+
console.log(`${pc.bold('š Metadata File :')} ${pc.white(path.resolve(options.metadata))}`);
|
|
604
|
+
}
|
|
605
|
+
if (options.compose) {
|
|
606
|
+
console.log(`${pc.bold('šļø Video Compose :')} ${pc.green('ENABLED (building final .mp4 with ffmpeg)')}`);
|
|
607
|
+
}
|
|
608
|
+
if (options.dryRun) {
|
|
609
|
+
console.log(`${pc.yellow(pc.bold('š Mode :'))} ${pc.yellow('DRY RUN (no downloads or file writes)')}`);
|
|
610
|
+
}
|
|
611
|
+
console.log();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
try {
|
|
615
|
+
const metadataPath = options.metadata ? path.resolve(options.metadata) : targetDir;
|
|
616
|
+
const result = await resolveMediaForMetadata(metadataPath, {
|
|
617
|
+
compose: options.compose,
|
|
618
|
+
force: options.force,
|
|
619
|
+
dryRun: options.dryRun,
|
|
620
|
+
allowRepeatMedia: options.allowRepeatMedia,
|
|
621
|
+
provider: options.provider,
|
|
622
|
+
type: options.type,
|
|
623
|
+
orientation: options.orientation,
|
|
624
|
+
cacheDir: options.mediaDir,
|
|
625
|
+
pexelsKey: options.pexelsKey,
|
|
626
|
+
pixabayKey: options.pixabayKey,
|
|
627
|
+
onEvent: (event) => {
|
|
628
|
+
if (isQuiet) return;
|
|
629
|
+
|
|
630
|
+
if (event.type === 'search_start') {
|
|
631
|
+
process.stdout.write(` š [${event.songKey}] Searching ${pc.cyan(event.provider)} for "${event.query}"... `);
|
|
632
|
+
} else if (event.type === 'search_results') {
|
|
633
|
+
console.log(pc.green(`Found ${event.totalFound} (top pick: #${event.topCandidate.id}, score: ${event.score.toFixed(3)})`));
|
|
634
|
+
} else if (event.type === 'download_start') {
|
|
635
|
+
process.stdout.write(` ā¬ļø Downloading asset ${event.provider}:${event.assetId}... `);
|
|
636
|
+
} else if (event.type === 'download_success') {
|
|
637
|
+
console.log(pc.green(`SAVED`));
|
|
638
|
+
} else if (event.type === 'compose_start') {
|
|
639
|
+
process.stdout.write(` šļø Muxing audio + background into .mp4... `);
|
|
640
|
+
} else if (event.type === 'compose_success') {
|
|
641
|
+
console.log(pc.green(`COMPOSED: `) + pc.dim(event.outputPath));
|
|
642
|
+
} else if (event.type === 'item_resolved') {
|
|
643
|
+
console.log(pc.green(` ā
Resolved `) + pc.bold(event.songKey) + ` -> ${event.resolvedMedia.credit}`);
|
|
644
|
+
console.log();
|
|
645
|
+
} else if (event.type === 'item_skipped') {
|
|
646
|
+
console.log(pc.dim(` āļø Skipped ${event.songKey} (${event.reason})`));
|
|
647
|
+
} else if (event.type === 'item_error') {
|
|
648
|
+
console.error(pc.red(`\n ā Error for ${event.songKey}: ${event.error}`));
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
if (!isQuiet) {
|
|
654
|
+
console.log(pc.dim(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`));
|
|
655
|
+
console.log(pc.bold(`š Summary:`));
|
|
656
|
+
console.log(` ⢠Resolved : ${pc.green(pc.bold(result.resolved.length))}`);
|
|
657
|
+
console.log(` ⢠Skipped : ${pc.dim(result.skipped.length)}`);
|
|
658
|
+
if (result.errors.length > 0) {
|
|
659
|
+
console.log(` ⢠Errors / Review: ${pc.red(pc.bold(result.errors.length))}`);
|
|
660
|
+
}
|
|
661
|
+
console.log();
|
|
662
|
+
|
|
663
|
+
if (options.dryRun) {
|
|
664
|
+
console.log(pc.yellow(`⨠Dry run complete. No media files or metadata were written.`));
|
|
665
|
+
console.log();
|
|
666
|
+
} else if (result.resolved.length > 0) {
|
|
667
|
+
console.log(pc.green(pc.bold(`š Media resolution complete!`)));
|
|
668
|
+
console.log(pc.dim(`Attribution summary saved to ATTRIBUTIONS.md`));
|
|
669
|
+
console.log();
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
} catch (err) {
|
|
673
|
+
console.error(pc.red(`\nā Error: ${err.message}\n`));
|
|
674
|
+
process.exit(1);
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
program
|
|
679
|
+
.command('compose-video')
|
|
680
|
+
.description('Mux a background image or video and audio file into an MP4 video using FFmpeg')
|
|
681
|
+
.requiredOption('--background <file>', 'Path to background media file (video or image)')
|
|
682
|
+
.requiredOption('--audio <file>', 'Path to audio file (wav, mp3, etc.)')
|
|
683
|
+
.requiredOption('--output <file>', 'Output destination path for final .mp4')
|
|
684
|
+
.option('--mode <video-loop|video-trim|ken-burns>', 'Composition mode (default: video-loop)', 'video-loop')
|
|
685
|
+
.option('--width <px>', 'Output video width (default: 1920)', '1920')
|
|
686
|
+
.option('--height <px>', 'Output video height (default: 1080)', '1080')
|
|
687
|
+
.option('--fps <n>', 'Frames per second (default: 30)', '30')
|
|
688
|
+
.option('-n, --dry-run', 'Print the FFmpeg composition plan without executing', false)
|
|
689
|
+
.option('-q, --quiet', 'Minimal output mode', false)
|
|
690
|
+
.action(async (options) => {
|
|
691
|
+
const isQuiet = options.quiet;
|
|
692
|
+
|
|
693
|
+
if (!isQuiet) {
|
|
694
|
+
console.log();
|
|
695
|
+
console.log(pc.bold(pc.cyan(`šļø Video Composer`)));
|
|
696
|
+
console.log(pc.dim(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`));
|
|
697
|
+
console.log(`${pc.bold('š¬ Background :')} ${pc.white(path.resolve(options.background))}`);
|
|
698
|
+
console.log(`${pc.bold('šµ Audio Track:')} ${pc.white(path.resolve(options.audio))}`);
|
|
699
|
+
console.log(`${pc.bold('š¹ Output :')} ${pc.white(path.resolve(options.output))}`);
|
|
700
|
+
console.log(`${pc.bold('āļø Mode :')} ${pc.cyan(options.mode)}`);
|
|
701
|
+
console.log();
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
try {
|
|
705
|
+
const res = await composeVideo({
|
|
706
|
+
backgroundPath: path.resolve(options.background),
|
|
707
|
+
audioPath: path.resolve(options.audio),
|
|
708
|
+
outputPath: path.resolve(options.output),
|
|
709
|
+
mode: options.mode,
|
|
710
|
+
targetWidth: parseInt(options.width, 10) || 1920,
|
|
711
|
+
targetHeight: parseInt(options.height, 10) || 1080,
|
|
712
|
+
fps: parseInt(options.fps, 10) || 30,
|
|
713
|
+
dryRun: options.dryRun,
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
if (!isQuiet) {
|
|
717
|
+
if (options.dryRun) {
|
|
718
|
+
console.log(pc.yellow(`š DRY RUN FFmpeg Plan:`));
|
|
719
|
+
console.log(` ${res.command}`);
|
|
720
|
+
console.log();
|
|
721
|
+
} else {
|
|
722
|
+
console.log(pc.green(pc.bold(`š Video successfully composed!`)));
|
|
723
|
+
console.log(` ⢠Output: ${pc.underline(pc.blue(res.outputPath))}`);
|
|
724
|
+
console.log(` ⢠Duration: ${res.duration?.toFixed(1)}s`);
|
|
725
|
+
console.log();
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
} catch (err) {
|
|
729
|
+
console.error(pc.red(`\nā Error: ${err.message}\n`));
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
program
|
|
735
|
+
.command('apply-actions')
|
|
736
|
+
.description('Run configured video processing actions (shorten, replace-background, journey-overlay) on videos')
|
|
737
|
+
.argument('[directory]', 'Source folder containing metadata.json and video files (default: current directory)', '.')
|
|
738
|
+
.option('-d, --dir <path>', 'Explicit target directory path')
|
|
739
|
+
.option('-m, --metadata <file>', 'Custom path to metadata.json file')
|
|
740
|
+
.option('--actions-file <file>', 'Use a separate actions spec instead of reading actions from metadata.json')
|
|
741
|
+
.option('--output-suffix <suffix>', 'Suffix for the new output folder name')
|
|
742
|
+
.option('--output-dir <path>', 'Explicit output directory (overrides suffix-based naming)')
|
|
743
|
+
.option('--only <filename|songKey>', 'Restrict this run to a single entry')
|
|
744
|
+
.option('-f, --force', 'Re-run and overwrite an existing output for an entry', false)
|
|
745
|
+
.option('-n, --dry-run', 'Print the planned ffmpeg operations per entry, write nothing', false)
|
|
746
|
+
.option('--pexels-key <key>', 'Pexels API key')
|
|
747
|
+
.option('--pixabay-key <key>', 'Pixabay API key')
|
|
748
|
+
.option('--media-dir <path>', 'Override .media-cache directory location')
|
|
749
|
+
.option('-q, --quiet', 'Minimal output mode', false)
|
|
750
|
+
.action(async (directoryArg, options, command) => {
|
|
751
|
+
const parentOpts = command?.parent?.opts() || {};
|
|
752
|
+
const dryRun = Boolean(options.dryRun || parentOpts.dryRun);
|
|
753
|
+
const force = Boolean(options.force || parentOpts.force);
|
|
754
|
+
const metadata = options.metadata || parentOpts.metadata;
|
|
755
|
+
const targetDir = path.resolve(options.dir || parentOpts.dir || directoryArg || process.cwd());
|
|
756
|
+
const isQuiet = Boolean(options.quiet || parentOpts.quiet);
|
|
757
|
+
|
|
758
|
+
if (!isQuiet) {
|
|
759
|
+
console.log();
|
|
760
|
+
console.log(pc.bold(pc.cyan(`š¬ Video Actions Pipeline`)) + ` ${pc.dim(`v${pkg.version}`)}`);
|
|
761
|
+
console.log(pc.dim(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`));
|
|
762
|
+
console.log(`${pc.bold('š Source Folder :')} ${pc.white(targetDir)}`);
|
|
763
|
+
if (metadata) {
|
|
764
|
+
console.log(`${pc.bold('š Metadata File :')} ${pc.white(path.resolve(metadata))}`);
|
|
765
|
+
}
|
|
766
|
+
if (options.actionsFile) {
|
|
767
|
+
console.log(`${pc.bold('š Actions Spec :')} ${pc.white(path.resolve(options.actionsFile))}`);
|
|
768
|
+
}
|
|
769
|
+
if (options.outputDir) {
|
|
770
|
+
console.log(`${pc.bold('š Target Output :')} ${pc.white(path.resolve(options.outputDir))}`);
|
|
771
|
+
}
|
|
772
|
+
if (options.outputSuffix) {
|
|
773
|
+
console.log(`${pc.bold('š·ļø Output Suffix :')} ${pc.cyan(options.outputSuffix)}`);
|
|
774
|
+
}
|
|
775
|
+
if (options.only) {
|
|
776
|
+
console.log(`${pc.bold('šÆ Filter Entry :')} ${pc.yellow(options.only)}`);
|
|
777
|
+
}
|
|
778
|
+
if (dryRun) {
|
|
779
|
+
console.log(`${pc.yellow(pc.bold('š Mode :'))} ${pc.yellow('DRY RUN (no file writes or transcoding)')}`);
|
|
780
|
+
}
|
|
781
|
+
if (force) {
|
|
782
|
+
console.log(`${pc.magenta(pc.bold('ā” Force :'))} ${pc.magenta('ENABLED (overwriting existing output files)')}`);
|
|
783
|
+
}
|
|
784
|
+
console.log();
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
try {
|
|
788
|
+
const result = await batchApplyActions(targetDir, {
|
|
789
|
+
metadataFile: metadata,
|
|
790
|
+
actionsFile: options.actionsFile,
|
|
791
|
+
outputDir: options.outputDir,
|
|
792
|
+
outputSuffix: options.outputSuffix,
|
|
793
|
+
only: options.only,
|
|
794
|
+
force,
|
|
795
|
+
dryRun,
|
|
796
|
+
pexelsKey: options.pexelsKey,
|
|
797
|
+
pixabayKey: options.pixabayKey,
|
|
798
|
+
cacheDir: options.mediaDir,
|
|
799
|
+
onEvent: (event) => {
|
|
800
|
+
if (isQuiet) return;
|
|
801
|
+
|
|
802
|
+
if (event.type === 'actions_plan') {
|
|
803
|
+
console.log(
|
|
804
|
+
pc.bold(`Found `) +
|
|
805
|
+
pc.green(pc.bold(`${event.total}`)) +
|
|
806
|
+
pc.bold(` item(s) to process.`)
|
|
807
|
+
);
|
|
808
|
+
console.log(`${pc.bold('š Output Directory:')} ${pc.cyan(event.outputDir)}`);
|
|
809
|
+
console.log();
|
|
810
|
+
} else if (event.type === 'entry_start') {
|
|
811
|
+
console.log(` š¬ [${event.index + 1}/${event.total}] Processing ${pc.bold(event.songKey)} (${event.actionsCount} action(s))...`);
|
|
812
|
+
} else if (event.type === 'action_start') {
|
|
813
|
+
console.log(` āļø [Action ${event.actionIndex + 1}/${event.totalActions}] Running ${pc.cyan(event.actionType)}...`);
|
|
814
|
+
} else if (event.type === 'action_complete') {
|
|
815
|
+
console.log(pc.green(` ā
Action complete:`), pc.dim(event.actionType));
|
|
816
|
+
} else if (event.type === 'entry_success') {
|
|
817
|
+
if (dryRun && event.steps) {
|
|
818
|
+
for (const step of event.steps) {
|
|
819
|
+
console.log(` ${pc.dim('ā³')} ${step}`);
|
|
820
|
+
}
|
|
821
|
+
} else {
|
|
822
|
+
console.log(pc.green(` ā
Finished ${event.songKey} ->`), pc.dim(event.outputPath));
|
|
823
|
+
}
|
|
824
|
+
console.log();
|
|
825
|
+
} else if (event.type === 'entry_skipped') {
|
|
826
|
+
console.log(pc.dim(` āļø [SKIPPED] ${event.songKey} (${event.reason})`));
|
|
827
|
+
console.log();
|
|
828
|
+
} else if (event.type === 'entry_error') {
|
|
829
|
+
console.error(pc.red(` ā Error on ${event.songKey}: ${event.error.message}`));
|
|
830
|
+
console.log();
|
|
831
|
+
} else if (event.type === 'action_warn') {
|
|
832
|
+
console.warn(pc.yellow(` ā ļø Warning: ${event.message}`));
|
|
833
|
+
} else if (event.type === 'metadata_cloned') {
|
|
834
|
+
console.log(pc.green(` š Cloned and updated metadata to:`), pc.dim(event.outputPath));
|
|
835
|
+
console.log();
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
if (!isQuiet) {
|
|
841
|
+
console.log(pc.dim(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`));
|
|
842
|
+
console.log(pc.bold(`š Actions Summary:`));
|
|
843
|
+
console.log(` ⢠Total items : ${pc.bold(result.total)}`);
|
|
844
|
+
console.log(` ⢠Processed : ${pc.green(pc.bold(result.processed.length))}`);
|
|
845
|
+
console.log(` ⢠Skipped : ${pc.dim(result.skipped.length)}`);
|
|
846
|
+
if (result.failed.length > 0) {
|
|
847
|
+
console.log(` ⢠Failed : ${pc.red(pc.bold(result.failed.length))}`);
|
|
848
|
+
}
|
|
849
|
+
console.log();
|
|
850
|
+
|
|
851
|
+
if (dryRun) {
|
|
852
|
+
console.log(pc.yellow(`⨠Dry run complete. No files were written or modified.`));
|
|
853
|
+
console.log();
|
|
854
|
+
} else if (result.failed.length > 0) {
|
|
855
|
+
console.log(pc.red(`ā ļø Completed with ${result.failed.length} failure(s).`));
|
|
856
|
+
process.exit(1);
|
|
857
|
+
} else {
|
|
858
|
+
console.log(pc.green(pc.bold(`š All video actions successfully applied!`)));
|
|
859
|
+
console.log(` ⢠Output directory: ${pc.underline(pc.cyan(result.outputDir))}`);
|
|
860
|
+
console.log();
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
} catch (err) {
|
|
864
|
+
console.error(pc.red(`\nā Error: ${err.message}\n`));
|
|
865
|
+
process.exit(1);
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
program
|
|
870
|
+
.command('preview-journey')
|
|
871
|
+
.description('Generate and preview the journey overlay graphic as a standalone PNG image')
|
|
872
|
+
.argument('<songKey>', 'Song key or filename to preview')
|
|
873
|
+
.option('-d, --dir <path>', 'Source folder containing metadata.json (default: current directory)')
|
|
874
|
+
.option('-m, --metadata <file>', 'Custom path to metadata.json file')
|
|
875
|
+
.option('--center-text <text>', 'Preview a specific center text without editing metadata.json')
|
|
876
|
+
.option('--concept-index <n>', 'Index of concept in centerConcepts to preview (default: 0)')
|
|
877
|
+
.option('-o, --output <file>', 'Output path for PNG file (default: ./journey-preview-<songKey>.png)')
|
|
878
|
+
.option('-q, --quiet', 'Minimal output mode', false)
|
|
879
|
+
.action(async (songKey, options) => {
|
|
880
|
+
const isQuiet = options.quiet;
|
|
881
|
+
|
|
882
|
+
if (!isQuiet) {
|
|
883
|
+
console.log();
|
|
884
|
+
console.log(pc.bold(pc.cyan(`š¼ļø Journey Overlay Preview`)) + ` ${pc.dim(`v${pkg.version}`)}`);
|
|
885
|
+
console.log(pc.dim(`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`));
|
|
886
|
+
console.log(`${pc.bold('šµ Song Key :')} ${pc.white(songKey)}`);
|
|
887
|
+
if (options.centerText) {
|
|
888
|
+
console.log(`${pc.bold('š¬ Center Text:')} ${pc.green(JSON.stringify(options.centerText))}`);
|
|
889
|
+
}
|
|
890
|
+
if (options.conceptIndex !== undefined) {
|
|
891
|
+
console.log(`${pc.bold('š¢ Concept Idx:')} ${pc.cyan(options.conceptIndex)}`);
|
|
892
|
+
}
|
|
893
|
+
console.log();
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
try {
|
|
897
|
+
const res = await previewJourney(songKey, {
|
|
898
|
+
dir: options.dir,
|
|
899
|
+
metadata: options.metadata,
|
|
900
|
+
centerText: options.centerText,
|
|
901
|
+
conceptIndex: options.conceptIndex,
|
|
902
|
+
output: options.output,
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
if (!isQuiet) {
|
|
906
|
+
console.log(pc.green(pc.bold(`š Preview generated!`)));
|
|
907
|
+
console.log(` ⢠File : ${pc.underline(pc.blue(res.outputPath))}`);
|
|
908
|
+
console.log(` ⢠Resolution: ${res.width}x${res.height}`);
|
|
909
|
+
console.log(` ⢠Tracks : ${res.tracksCount}`);
|
|
910
|
+
console.log();
|
|
911
|
+
}
|
|
912
|
+
} catch (err) {
|
|
913
|
+
console.error(pc.red(`\nā Error: ${err.message}\n`));
|
|
914
|
+
process.exit(1);
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
|
|
186
918
|
program.parse(process.argv);
|
|
919
|
+
|