@x12i/youtube-video-uploader-cli 1.2.0 → 1.4.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.
@@ -0,0 +1,276 @@
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
+ entry,
120
+ ...action,
121
+ dryRun: true,
122
+ onWarn,
123
+ });
124
+ steps.push(`[${i + 1}/${actions.length}] ${res.command}`);
125
+ } else if (action.type === 'replace-background') {
126
+ const res = await replaceBackground({
127
+ inputPath: currentInput,
128
+ outputPath: stepOutput,
129
+ action,
130
+ entry,
131
+ context: { pexelsKey, pixabayKey, cacheDir, fetchFn },
132
+ dryRun: true,
133
+ });
134
+ steps.push(`[${i + 1}/${actions.length}] ${res.command}`);
135
+ } else if (action.type === 'journey-overlay') {
136
+ const res = await applyJourneyOverlay({
137
+ inputPath: currentInput,
138
+ outputPath: stepOutput,
139
+ action,
140
+ entry,
141
+ rawMetadata,
142
+ journeyConfig,
143
+ dryRun: true,
144
+ onWarn,
145
+ });
146
+ steps.push(`[${i + 1}/${actions.length}] ${res.command}`);
147
+ } else {
148
+ throw new Error(`Unknown action type: "${action.type}" on entry ${songKey}`);
149
+ }
150
+
151
+ currentInput = stepOutput;
152
+ }
153
+
154
+ return {
155
+ songKey,
156
+ filename,
157
+ sourcePath,
158
+ outputPath: targetPath,
159
+ actionsApplied: actions.map(a => a.type),
160
+ dryRun: true,
161
+ steps,
162
+ };
163
+ }
164
+
165
+ // Real execution
166
+ if (!fs.existsSync(sourcePath)) {
167
+ throw new Error(`Source video file not found: ${sourcePath}`);
168
+ }
169
+
170
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
171
+
172
+ const tempDir = path.join(
173
+ os.tmpdir(),
174
+ `actions_pipeline_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`
175
+ );
176
+ fs.mkdirSync(tempDir, { recursive: true });
177
+
178
+ const intermediateFiles = [];
179
+ let currentInput = sourcePath;
180
+
181
+ try {
182
+ for (let i = 0; i < actions.length; i++) {
183
+ const action = actions[i];
184
+ const isLast = i === actions.length - 1;
185
+ const stepOutput = isLast
186
+ ? targetPath
187
+ : path.join(tempDir, `step_${i}_${Date.now()}_${filename}`);
188
+
189
+ if (!isLast) {
190
+ intermediateFiles.push(stepOutput);
191
+ }
192
+
193
+ if (onEvent) {
194
+ onEvent({
195
+ type: 'action_start',
196
+ songKey,
197
+ filename,
198
+ actionIndex: i,
199
+ totalActions: actions.length,
200
+ actionType: action.type,
201
+ action,
202
+ });
203
+ }
204
+
205
+ if (action.type === 'shorten') {
206
+ await shortenVideo({
207
+ inputPath: currentInput,
208
+ outputPath: stepOutput,
209
+ entry,
210
+ ...action,
211
+ dryRun: false,
212
+ onWarn,
213
+ });
214
+ } else if (action.type === 'replace-background') {
215
+ await replaceBackground({
216
+ inputPath: currentInput,
217
+ outputPath: stepOutput,
218
+ action,
219
+ entry,
220
+ context: { pexelsKey, pixabayKey, cacheDir, fetchFn, tempDir },
221
+ dryRun: false,
222
+ });
223
+ } else if (action.type === 'journey-overlay') {
224
+ await applyJourneyOverlay({
225
+ inputPath: currentInput,
226
+ outputPath: stepOutput,
227
+ action,
228
+ entry,
229
+ rawMetadata,
230
+ journeyConfig,
231
+ context: { pexelsKey, pixabayKey, cacheDir, fetchFn, tempDir },
232
+ dryRun: false,
233
+ onWarn,
234
+ });
235
+ } else {
236
+ throw new Error(`Unknown action type: "${action.type}" on entry ${songKey}`);
237
+ }
238
+
239
+ if (onEvent) {
240
+ onEvent({
241
+ type: 'action_complete',
242
+ songKey,
243
+ filename,
244
+ actionIndex: i,
245
+ totalActions: actions.length,
246
+ actionType: action.type,
247
+ outputPath: stepOutput,
248
+ });
249
+ }
250
+
251
+ currentInput = stepOutput;
252
+ }
253
+
254
+ return {
255
+ songKey,
256
+ filename,
257
+ sourcePath,
258
+ outputPath: targetPath,
259
+ actionsApplied: actions.map(a => a.type),
260
+ };
261
+ } finally {
262
+ // Cleanup temporary files
263
+ for (const file of intermediateFiles) {
264
+ if (fs.existsSync(file)) {
265
+ try {
266
+ fs.unlinkSync(file);
267
+ } catch {}
268
+ }
269
+ }
270
+ if (fs.existsSync(tempDir)) {
271
+ try {
272
+ fs.rmSync(tempDir, { recursive: true, force: true });
273
+ } catch {}
274
+ }
275
+ }
276
+ }