@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x12i/youtube-video-uploader-cli",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "CLI tool to batch upload MP4 videos to YouTube via YouTube Data API v3 with OAuth2, metadata.json config, playlist assignment, and smart upload-state resume.",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -44,9 +44,15 @@
44
44
  "author": "",
45
45
  "license": "MIT",
46
46
  "dependencies": {
47
+ "@resvg/resvg-js": "^2.6.2",
47
48
  "commander": "^13.1.0",
49
+ "ffmpeg-static": "^5.2.0",
50
+ "ffprobe-static": "^3.1.0",
51
+ "fluent-ffmpeg": "^2.1.3",
48
52
  "googleapis": "^144.0.0",
53
+ "mime-types": "^2.1.35",
49
54
  "open": "^10.1.0",
55
+ "p-limit": "^6.1.0",
50
56
  "picocolors": "^1.1.1"
51
57
  }
52
58
  }
@@ -0,0 +1,274 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { shortenVideo, parseTimeSeconds, getVideoMetadata } from './shorten.js';
5
+ import { replaceBackground, extractAudio } from './replace-background.js';
6
+ import {
7
+ applyJourneyOverlay,
8
+ generateJourneySvg,
9
+ renderSvgToPng,
10
+ resolveJourneyTracks,
11
+ } from './journey-overlay.js';
12
+
13
+ export {
14
+ shortenVideo,
15
+ parseTimeSeconds,
16
+ getVideoMetadata,
17
+ replaceBackground,
18
+ extractAudio,
19
+ applyJourneyOverlay,
20
+ generateJourneySvg,
21
+ renderSvgToPng,
22
+ resolveJourneyTracks,
23
+ };
24
+
25
+ /**
26
+ * Run actions pipeline for a single entry.
27
+ *
28
+ * @param {object} entry - Video entry object
29
+ * @param {Array<object>} actions - List of actions to execute sequentially
30
+ * @param {object} context - Pipeline context options and metadata
31
+ * @returns {Promise<{
32
+ * songKey: string,
33
+ * filename: string,
34
+ * outputPath: string,
35
+ * actionsApplied: string[],
36
+ * skipped?: boolean,
37
+ * reason?: string,
38
+ * dryRun?: boolean,
39
+ * steps?: string[]
40
+ * }>}
41
+ */
42
+ export async function runActionsPipeline(entry, actions = [], context = {}) {
43
+ const {
44
+ outputDir,
45
+ rawMetadata = {},
46
+ journeyConfig = {},
47
+ dryRun = false,
48
+ force = false,
49
+ pexelsKey,
50
+ pixabayKey,
51
+ cacheDir,
52
+ fetchFn,
53
+ onEvent,
54
+ onWarn,
55
+ } = context;
56
+
57
+ const songKey = entry.songKey || entry.filename;
58
+ const filename = entry.filename;
59
+ const sourcePath = entry.sourcePath;
60
+ const targetPath = entry.targetPath || path.join(outputDir, filename);
61
+
62
+ // Check if output already exists and force is not set
63
+ if (fs.existsSync(targetPath) && !force && !dryRun) {
64
+ return {
65
+ songKey,
66
+ filename,
67
+ sourcePath,
68
+ outputPath: targetPath,
69
+ actionsApplied: [],
70
+ skipped: true,
71
+ reason: 'Output file already exists (use --force to overwrite)',
72
+ };
73
+ }
74
+
75
+ // If no actions defined, pass-through copy
76
+ if (!actions || actions.length === 0) {
77
+ if (dryRun) {
78
+ return {
79
+ songKey,
80
+ filename,
81
+ sourcePath,
82
+ outputPath: targetPath,
83
+ actionsApplied: [],
84
+ dryRun: true,
85
+ steps: [`Copy without modifications: ${sourcePath} -> ${targetPath}`],
86
+ };
87
+ }
88
+
89
+ if (onEvent) {
90
+ onEvent({ type: 'action_passthrough', songKey, filename, sourcePath, targetPath });
91
+ }
92
+
93
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
94
+ fs.copyFileSync(sourcePath, targetPath);
95
+
96
+ return {
97
+ songKey,
98
+ filename,
99
+ sourcePath,
100
+ outputPath: targetPath,
101
+ actionsApplied: [],
102
+ };
103
+ }
104
+
105
+ // Dry run mode: calculate plan for each action
106
+ if (dryRun) {
107
+ const steps = [];
108
+ let currentInput = sourcePath;
109
+
110
+ for (let i = 0; i < actions.length; i++) {
111
+ const action = actions[i];
112
+ const isLast = i === actions.length - 1;
113
+ const stepOutput = isLast ? targetPath : `temp_step_${i + 1}_${filename}`;
114
+
115
+ if (action.type === 'shorten') {
116
+ const res = await shortenVideo({
117
+ inputPath: currentInput,
118
+ outputPath: stepOutput,
119
+ ...action,
120
+ dryRun: true,
121
+ onWarn,
122
+ });
123
+ steps.push(`[${i + 1}/${actions.length}] ${res.command}`);
124
+ } else if (action.type === 'replace-background') {
125
+ const res = await replaceBackground({
126
+ inputPath: currentInput,
127
+ outputPath: stepOutput,
128
+ action,
129
+ entry,
130
+ context: { pexelsKey, pixabayKey, cacheDir, fetchFn },
131
+ dryRun: true,
132
+ });
133
+ steps.push(`[${i + 1}/${actions.length}] ${res.command}`);
134
+ } else if (action.type === 'journey-overlay') {
135
+ const res = await applyJourneyOverlay({
136
+ inputPath: currentInput,
137
+ outputPath: stepOutput,
138
+ action,
139
+ entry,
140
+ rawMetadata,
141
+ journeyConfig,
142
+ dryRun: true,
143
+ onWarn,
144
+ });
145
+ steps.push(`[${i + 1}/${actions.length}] ${res.command}`);
146
+ } else {
147
+ throw new Error(`Unknown action type: "${action.type}" on entry ${songKey}`);
148
+ }
149
+
150
+ currentInput = stepOutput;
151
+ }
152
+
153
+ return {
154
+ songKey,
155
+ filename,
156
+ sourcePath,
157
+ outputPath: targetPath,
158
+ actionsApplied: actions.map(a => a.type),
159
+ dryRun: true,
160
+ steps,
161
+ };
162
+ }
163
+
164
+ // Real execution
165
+ if (!fs.existsSync(sourcePath)) {
166
+ throw new Error(`Source video file not found: ${sourcePath}`);
167
+ }
168
+
169
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
170
+
171
+ const tempDir = path.join(
172
+ os.tmpdir(),
173
+ `actions_pipeline_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`
174
+ );
175
+ fs.mkdirSync(tempDir, { recursive: true });
176
+
177
+ const intermediateFiles = [];
178
+ let currentInput = sourcePath;
179
+
180
+ try {
181
+ for (let i = 0; i < actions.length; i++) {
182
+ const action = actions[i];
183
+ const isLast = i === actions.length - 1;
184
+ const stepOutput = isLast
185
+ ? targetPath
186
+ : path.join(tempDir, `step_${i}_${Date.now()}_${filename}`);
187
+
188
+ if (!isLast) {
189
+ intermediateFiles.push(stepOutput);
190
+ }
191
+
192
+ if (onEvent) {
193
+ onEvent({
194
+ type: 'action_start',
195
+ songKey,
196
+ filename,
197
+ actionIndex: i,
198
+ totalActions: actions.length,
199
+ actionType: action.type,
200
+ action,
201
+ });
202
+ }
203
+
204
+ if (action.type === 'shorten') {
205
+ await shortenVideo({
206
+ inputPath: currentInput,
207
+ outputPath: stepOutput,
208
+ ...action,
209
+ dryRun: false,
210
+ onWarn,
211
+ });
212
+ } else if (action.type === 'replace-background') {
213
+ await replaceBackground({
214
+ inputPath: currentInput,
215
+ outputPath: stepOutput,
216
+ action,
217
+ entry,
218
+ context: { pexelsKey, pixabayKey, cacheDir, fetchFn, tempDir },
219
+ dryRun: false,
220
+ });
221
+ } else if (action.type === 'journey-overlay') {
222
+ await applyJourneyOverlay({
223
+ inputPath: currentInput,
224
+ outputPath: stepOutput,
225
+ action,
226
+ entry,
227
+ rawMetadata,
228
+ journeyConfig,
229
+ context: { pexelsKey, pixabayKey, cacheDir, fetchFn, tempDir },
230
+ dryRun: false,
231
+ onWarn,
232
+ });
233
+ } else {
234
+ throw new Error(`Unknown action type: "${action.type}" on entry ${songKey}`);
235
+ }
236
+
237
+ if (onEvent) {
238
+ onEvent({
239
+ type: 'action_complete',
240
+ songKey,
241
+ filename,
242
+ actionIndex: i,
243
+ totalActions: actions.length,
244
+ actionType: action.type,
245
+ outputPath: stepOutput,
246
+ });
247
+ }
248
+
249
+ currentInput = stepOutput;
250
+ }
251
+
252
+ return {
253
+ songKey,
254
+ filename,
255
+ sourcePath,
256
+ outputPath: targetPath,
257
+ actionsApplied: actions.map(a => a.type),
258
+ };
259
+ } finally {
260
+ // Cleanup temporary files
261
+ for (const file of intermediateFiles) {
262
+ if (fs.existsSync(file)) {
263
+ try {
264
+ fs.unlinkSync(file);
265
+ } catch {}
266
+ }
267
+ }
268
+ if (fs.existsSync(tempDir)) {
269
+ try {
270
+ fs.rmSync(tempDir, { recursive: true, force: true });
271
+ } catch {}
272
+ }
273
+ }
274
+ }