@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.
- package/README.md +351 -27
- package/bin/cli.js +620 -2
- package/package.json +7 -1
- package/src/actions/index.js +276 -0
- package/src/actions/journey-overlay.js +623 -0
- package/src/actions/replace-background.js +192 -0
- package/src/actions/shorten.js +310 -0
- package/src/batch/clone-metadata.js +51 -0
- package/src/batch/plan.js +195 -0
- package/src/cli/apply-actions.js +152 -0
- package/src/cli/preview-journey.js +111 -0
- package/src/index.d.ts +532 -3
- package/src/index.js +82 -1
- package/src/media/cache.js +134 -0
- package/src/media/compose.js +300 -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/update.js +392 -0
- package/src/uploader.js +20 -0
package/README.md
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
# @x12i/youtube-video-uploader-cli 📺
|
|
2
2
|
|
|
3
|
-
> Automated batch video uploader for YouTube Data API v3 with OAuth2 authentication, playlist
|
|
3
|
+
> Automated batch video uploader for YouTube Data API v3 with OAuth2 authentication, album/playlist metadata, safe-quota batching, playlist video swap, and smart upload-state resume.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## 🚀 Features
|
|
8
8
|
|
|
9
|
-
- **🔐 Interactive OAuth2 Authentication**:
|
|
10
|
-
- **💾 Token Persistence**: Caches tokens
|
|
11
|
-
- **📄 Metadata-Driven Batch Uploads**:
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
9
|
+
- **🔐 Interactive OAuth2 Authentication**: Local loopback server to capture the callback redirect, with a terminal prompt fallback. Scopes: `youtube.upload` + `youtube.force-ssl`.
|
|
10
|
+
- **💾 Token Persistence**: Caches tokens in `token.json` and refreshes automatically.
|
|
11
|
+
- **📄 Metadata-Driven Batch Uploads**: Flat `videos[]` schema **or** rich album/`songs` + `playlist` + `uploadPolicy` + `{{template}}` variables.
|
|
12
|
+
- **📑 Playlist Auto-Create**: Creates a playlist from `playlist.snippet` when no `playlistId` is set, then inserts videos at their metadata positions.
|
|
13
|
+
- **⚡ Smart Skip & Resume**: Saves status to `.upload-history.json` and skips already-uploaded files on rerun.
|
|
14
|
+
- **🛡️ Safe Quota Batching**: `--safe-quota`, `--limit`, and `--max-quota` defer excess uploads for the next day (~1,600 units/video + 50/playlist item).
|
|
15
|
+
- **📊 Quota Estimation**: Warns when estimated units approach the standard 10,000/day limit.
|
|
16
|
+
- **🔁 Playlist Video Swap**: `swap` subcommand replaces a playlist entry in place; old video is unlisted, never deleted. Resumable via `.swap-history.json`.
|
|
17
|
+
- **🏷️ Metadata & Title Updates**: `update-metadata` / `update-titles` subcommand to batch update video titles and playlist titles on YouTube and in `metadata.json` (e.g. adding postfixes, prefixes, or syncing).
|
|
18
|
+
- **🎬 Automated Stock Media Sourcing**: Search Pexels and Pixabay for high-res stock videos/photos, rank and auto-select best matches, cache responses, and compose final `.mp4` files from audio tracks via ffmpeg.
|
|
19
|
+
- **🔍 Dry Run Mode**: Validate metadata and estimate quota before uploading or updating.
|
|
16
20
|
|
|
17
21
|
---
|
|
18
22
|
|
|
@@ -39,6 +43,9 @@ Before using the uploader, you need OAuth2 credentials from Google Cloud:
|
|
|
39
43
|
```bash
|
|
40
44
|
# In the folder containing your .mp4 files and metadata.json:
|
|
41
45
|
npx @x12i/youtube-video-uploader-cli
|
|
46
|
+
|
|
47
|
+
# Cap under daily quota (~5–6 videos), defer the rest:
|
|
48
|
+
npx @x12i/youtube-video-uploader-cli --safe-quota
|
|
42
49
|
```
|
|
43
50
|
|
|
44
51
|
### Install globally
|
|
@@ -52,12 +59,12 @@ yt-uploader [folder]
|
|
|
52
59
|
yvu-cli [folder]
|
|
53
60
|
```
|
|
54
61
|
|
|
62
|
+
Credentials can also come from environment variables (`YOUTUBE_CLIENT_ID` / `YOUTUBE_CLIENT_SECRET`, or `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`) or a nearby `.env` file.
|
|
63
|
+
|
|
55
64
|
---
|
|
56
65
|
|
|
57
66
|
## 📂 Folder Structure & `metadata.json`
|
|
58
67
|
|
|
59
|
-
Place your videos and a `metadata.json` file in your target folder:
|
|
60
|
-
|
|
61
68
|
```text
|
|
62
69
|
my-videos/
|
|
63
70
|
├── metadata.json
|
|
@@ -66,7 +73,7 @@ my-videos/
|
|
|
66
73
|
└── track3.mp4
|
|
67
74
|
```
|
|
68
75
|
|
|
69
|
-
### `
|
|
76
|
+
### Flat `videos[]` schema
|
|
70
77
|
|
|
71
78
|
```json
|
|
72
79
|
{
|
|
@@ -90,6 +97,72 @@ my-videos/
|
|
|
90
97
|
}
|
|
91
98
|
```
|
|
92
99
|
|
|
100
|
+
### Rich album / `songs` schema
|
|
101
|
+
|
|
102
|
+
Supports album metadata, upload policy, playlist auto-create, ordered keys, and `{{playlistUrl}}` / `{{channelUrl}}` templates:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"channel": {
|
|
107
|
+
"name": "My Artist",
|
|
108
|
+
"channelUrl": "https://www.youtube.com/@myartist"
|
|
109
|
+
},
|
|
110
|
+
"album": {
|
|
111
|
+
"title": "GRAY TO LIGHT",
|
|
112
|
+
"artist": "My Artist"
|
|
113
|
+
},
|
|
114
|
+
"templateVariables": {
|
|
115
|
+
"channelUrl": "https://www.youtube.com/@myartist",
|
|
116
|
+
"playlistUrl": null
|
|
117
|
+
},
|
|
118
|
+
"uploadPolicy": {
|
|
119
|
+
"initialVideoPrivacyStatus": "private",
|
|
120
|
+
"categoryId": "10",
|
|
121
|
+
"notifySubscribersOnInitialUpload": false
|
|
122
|
+
},
|
|
123
|
+
"playlist": {
|
|
124
|
+
"key": "gray_to_light",
|
|
125
|
+
"orderedSongKeys": ["01-first.mp4", "02-second.mp4"],
|
|
126
|
+
"youtube": {
|
|
127
|
+
"snippet": {
|
|
128
|
+
"title": "GRAY TO LIGHT — Full Album",
|
|
129
|
+
"description": "Full album playlist.\n\nChannel: {{channelUrl}}"
|
|
130
|
+
},
|
|
131
|
+
"status": { "privacyStatus": "private" }
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"songs": {
|
|
135
|
+
"01-first.mp4": {
|
|
136
|
+
"trackNumber": 1,
|
|
137
|
+
"playlistPosition": 0,
|
|
138
|
+
"youtube": {
|
|
139
|
+
"snippet": {
|
|
140
|
+
"title": "First Song — My Artist | GRAY TO LIGHT",
|
|
141
|
+
"description": "Track 1.\n\nPlaylist: {{playlistUrl}}",
|
|
142
|
+
"tags": ["album", "music"]
|
|
143
|
+
},
|
|
144
|
+
"status": { "privacyStatus": "private" }
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
"02-second.mp4": {
|
|
148
|
+
"trackNumber": 2,
|
|
149
|
+
"playlistPosition": 1,
|
|
150
|
+
"youtube": {
|
|
151
|
+
"snippet": {
|
|
152
|
+
"title": "Second Song — My Artist | GRAY TO LIGHT",
|
|
153
|
+
"description": "Track 2.",
|
|
154
|
+
"tags": ["album", "music"]
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
If `playlistId` is missing but `playlist.youtube.snippet` is present, the uploader creates the playlist on first run and stores its ID in `.upload-history.json`. Template variables are re-interpolated after creation so `{{playlistUrl}}` resolves correctly.
|
|
163
|
+
|
|
164
|
+
If no `videos` / `songs` entries exist, all `.mp4` files in the folder are auto-discovered.
|
|
165
|
+
|
|
93
166
|
#### Category IDs:
|
|
94
167
|
- `"10"` = Music (default)
|
|
95
168
|
- `"22"` = People & Blogs
|
|
@@ -100,6 +173,8 @@ my-videos/
|
|
|
100
173
|
|
|
101
174
|
## ⚙️ CLI Options Reference
|
|
102
175
|
|
|
176
|
+
### Batch upload (default command)
|
|
177
|
+
|
|
103
178
|
```text
|
|
104
179
|
Usage: youtube-video-uploader [directory] [options]
|
|
105
180
|
|
|
@@ -111,7 +186,10 @@ Options:
|
|
|
111
186
|
-d, --dir <path> Explicit target directory path
|
|
112
187
|
-m, --metadata <file> Custom path to metadata.json file
|
|
113
188
|
-f, --force Force re-upload of already uploaded videos (default: false)
|
|
114
|
-
-n, --dry-run Preview video uploads and estimate quota without uploading
|
|
189
|
+
-n, --dry-run Preview video uploads and estimate quota without uploading
|
|
190
|
+
-l, --limit <number> Maximum number of videos to upload in this run
|
|
191
|
+
--safe-quota Cap batch under ~9,950 daily units (~5–6 videos); defer the rest
|
|
192
|
+
--max-quota <units> Hard API quota unit cap for this run
|
|
115
193
|
--client-id <id> Google OAuth Client ID
|
|
116
194
|
--client-secret <secret> Google OAuth Client Secret
|
|
117
195
|
--client-secrets-file <file> Path to client_secret.json downloaded from Google Cloud
|
|
@@ -120,6 +198,58 @@ Options:
|
|
|
120
198
|
-h, --help Display help
|
|
121
199
|
```
|
|
122
200
|
|
|
201
|
+
### `swap` subcommand
|
|
202
|
+
|
|
203
|
+
```text
|
|
204
|
+
Usage: youtube-video-uploader swap <videoFile> [options]
|
|
205
|
+
|
|
206
|
+
Arguments:
|
|
207
|
+
videoFile Path to the new MP4 video file
|
|
208
|
+
|
|
209
|
+
Options:
|
|
210
|
+
--playlist <id> Playlist ID containing the old video (required)
|
|
211
|
+
--old-video <videoId> Video ID of the old video to replace (required)
|
|
212
|
+
-k, --key <songKey> Stable song key for swap state (default: video file basename)
|
|
213
|
+
--title <title> Override the new video title (default: carried over from old)
|
|
214
|
+
--state-file <file> Swap state filename (default: .swap-history.json)
|
|
215
|
+
--token-file <file> OAuth token path (default: token.json in cwd)
|
|
216
|
+
--client-secrets-file <file> Path to client_secret.json
|
|
217
|
+
--client-id <id> Google OAuth Client ID
|
|
218
|
+
--client-secret <secret> Google OAuth Client Secret
|
|
219
|
+
--fresh Discard prior swap state for this key and start over
|
|
220
|
+
-n, --dry-run Print the swap plan without any API calls
|
|
221
|
+
-q, --quiet Minimal output mode
|
|
222
|
+
-h, --help Display help
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### `update-metadata` / `update-titles` subcommand
|
|
226
|
+
|
|
227
|
+
```text
|
|
228
|
+
Usage: youtube-video-uploader update-metadata [directory] [options]
|
|
229
|
+
|
|
230
|
+
Arguments:
|
|
231
|
+
directory Target folder containing metadata.json and upload history (default: ".")
|
|
232
|
+
|
|
233
|
+
Options:
|
|
234
|
+
-d, --dir <path> Explicit target directory path
|
|
235
|
+
-m, --metadata <file> Custom path to metadata.json file
|
|
236
|
+
--title-postfix <postfix> Postfix to append to each video title (e.g. " (long version)")
|
|
237
|
+
--playlist-postfix <postfix> Postfix to append to playlist title (e.g. " (long versions)")
|
|
238
|
+
--title-prefix <prefix> Prefix to prepend to each video title
|
|
239
|
+
--playlist-title <title> Explicit new title for the playlist
|
|
240
|
+
--video <videoId> Target a specific YouTube video ID
|
|
241
|
+
--title <title> Explicit new title for the targeted video
|
|
242
|
+
--playlist <playlistId> Target a specific YouTube playlist ID
|
|
243
|
+
--no-update-file Do not update metadata.json on disk
|
|
244
|
+
-n, --dry-run Preview changes without making any API calls
|
|
245
|
+
--token-file <file> OAuth token path (default: token.json in target dir)
|
|
246
|
+
--client-secrets-file <file> Path to client_secret.json
|
|
247
|
+
--client-id <id> Google OAuth Client ID
|
|
248
|
+
--client-secret <secret> Google OAuth Client Secret
|
|
249
|
+
-q, --quiet Minimal output mode
|
|
250
|
+
-h, --help Display help
|
|
251
|
+
```
|
|
252
|
+
|
|
123
253
|
---
|
|
124
254
|
|
|
125
255
|
## 💡 Examples
|
|
@@ -129,7 +259,18 @@ Options:
|
|
|
129
259
|
npx @x12i/youtube-video-uploader-cli ./my-videos --dry-run
|
|
130
260
|
```
|
|
131
261
|
|
|
132
|
-
### 2.
|
|
262
|
+
### 2. Safe-quota batch (upload today’s slice, defer the rest)
|
|
263
|
+
```bash
|
|
264
|
+
npx @x12i/youtube-video-uploader-cli ./my-videos --safe-quota
|
|
265
|
+
# Re-run the same command tomorrow for the next batch
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### 3. Hard limit of N videos
|
|
269
|
+
```bash
|
|
270
|
+
npx @x12i/youtube-video-uploader-cli ./my-videos --limit 5
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### 4. Upload with credentials from environment variables
|
|
133
274
|
```bash
|
|
134
275
|
export YOUTUBE_CLIENT_ID="your-client-id.apps.googleusercontent.com"
|
|
135
276
|
export YOUTUBE_CLIENT_SECRET="your-client-secret"
|
|
@@ -137,7 +278,7 @@ export YOUTUBE_CLIENT_SECRET="your-client-secret"
|
|
|
137
278
|
npx @x12i/youtube-video-uploader-cli ./my-videos
|
|
138
279
|
```
|
|
139
280
|
|
|
140
|
-
###
|
|
281
|
+
### 5. Upload with downloaded `client_secret.json`
|
|
141
282
|
```bash
|
|
142
283
|
npx @x12i/youtube-video-uploader-cli ./my-videos --client-secrets-file ./client_secret.json
|
|
143
284
|
```
|
|
@@ -151,6 +292,8 @@ Re-uploads a song (e.g. a new version with lyrics subtitles) and swaps it into a
|
|
|
151
292
|
```bash
|
|
152
293
|
yt-uploader swap ./song-v2.mp4 --playlist PLxxxx --old-video VIDEOID
|
|
153
294
|
yt-uploader swap ./song-v2.mp4 --playlist PLxxxx --old-video VIDEOID --dry-run
|
|
295
|
+
yt-uploader swap ./song-v2.mp4 --playlist PLxxxx --old-video VIDEOID --key "01-first.mp4" --title "First Song (Lyrics)"
|
|
296
|
+
yt-uploader swap ./song-v2.mp4 --playlist PLxxxx --old-video VIDEOID --fresh
|
|
154
297
|
```
|
|
155
298
|
|
|
156
299
|
The swap runs as a resumable five-step state machine (state persisted to `.swap-history.json` after every step):
|
|
@@ -161,31 +304,212 @@ The swap runs as a resumable five-step state machine (state persisted to `.swap-
|
|
|
161
304
|
4. **REMOVE** — remove the old playlist item
|
|
162
305
|
5. **FINALIZE** — old video → unlisted, new video → public
|
|
163
306
|
|
|
164
|
-
If any step fails, re-run the same command — completed steps are skipped automatically (no duplicate uploads or playlist entries). A missing old video aborts with a "needs manual review" flag instead of guessing.
|
|
307
|
+
If any step fails, re-run the same command — completed steps are skipped automatically (no duplicate uploads or playlist entries). A missing old video aborts with a "needs manual review" flag instead of guessing. Use `--fresh` to discard prior state for that key.
|
|
165
308
|
|
|
166
309
|
**Quota cost:** ~150 shared-pool units + 1 upload-bucket unit per swap (well within the 10,000/day default pool).
|
|
167
310
|
|
|
168
311
|
**Notes:**
|
|
169
|
-
- OAuth scopes are
|
|
312
|
+
- OAuth scopes are `youtube.upload` + `youtube.force-ssl` — existing `token.json` files need a one-time re-consent if issued under older scopes.
|
|
170
313
|
- The target playlist must use **Manual** ordering for position-preserving inserts (set in YouTube Studio).
|
|
171
314
|
- Videos from an unverified Google Cloud project are forced private until the project passes YouTube's API audit.
|
|
172
315
|
|
|
173
|
-
|
|
316
|
+
---
|
|
174
317
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
318
|
+
## 🎬 Automated Stock Media Sourcing (Pexels + Pixabay)
|
|
319
|
+
|
|
320
|
+
If you have audio tracks (`.wav`, `.mp3`) and no visuals, the uploader can automatically query Pexels and Pixabay for relevant stock video/photo backgrounds, select the best candidate, download it, and compose `.mp4` videos via FFmpeg before uploading.
|
|
321
|
+
|
|
322
|
+
### Environment Variables & Credentials
|
|
323
|
+
|
|
324
|
+
| Variable | Description | Aliases |
|
|
325
|
+
|---|---|---|
|
|
326
|
+
| `PEXELS_KEY` | Pexels API Key | `PEXEL_KEY` |
|
|
327
|
+
| `PIXABAY_KEY` | Pixabay API Key | `PICABAY_KEY` |
|
|
328
|
+
| `MEDIA_PROVIDER_ORDER` | Fallback search order (default: `pexels,pixabay`) | |
|
|
329
|
+
|
|
330
|
+
API keys can also be specified via CLI flags (`--pexels-key <key>`, `--pixabay-key <key>`) or in a `.env` file.
|
|
331
|
+
|
|
332
|
+
### CLI Commands
|
|
333
|
+
|
|
334
|
+
#### 1. Preview search results (`search-media`)
|
|
335
|
+
Preview ranked candidates without downloading anything:
|
|
336
|
+
```bash
|
|
337
|
+
yt-uploader search-media "calm night drive neon city" --type video --count 5
|
|
338
|
+
yt-uploader search-media "sunset over ocean" --provider pixabay --orientation landscape --json
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
#### 2. Resolve media & write to `metadata.json` (`resolve-media`)
|
|
342
|
+
Search, download winning media to `.media-cache/assets/`, and optionally compose the final `.mp4`:
|
|
343
|
+
```bash
|
|
344
|
+
# Resolve and download assets, update metadata.json
|
|
345
|
+
yt-uploader resolve-media ./my-songs
|
|
346
|
+
|
|
347
|
+
# Resolve, download, and build the final .mp4 with ffmpeg immediately
|
|
348
|
+
yt-uploader resolve-media ./my-songs --compose
|
|
349
|
+
|
|
350
|
+
# Dry-run preview
|
|
351
|
+
yt-uploader resolve-media ./my-songs --dry-run
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
#### 3. Upload with automatic media resolution (`--auto-media`)
|
|
355
|
+
Run media resolution and ffmpeg composition automatically for missing video files before running the batch upload:
|
|
356
|
+
```bash
|
|
357
|
+
yt-uploader ./my-songs --auto-media
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
#### 4. Standalone Video Composer (`compose-video`)
|
|
361
|
+
Mux any background video or photo with an audio file:
|
|
362
|
+
```bash
|
|
363
|
+
yt-uploader compose-video --background ./bg.mp4 --audio ./song.wav --output ./final.mp4
|
|
364
|
+
yt-uploader compose-video --background ./photo.jpg --audio ./song.wav --output ./final.mp4 --mode ken-burns
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### `metadata.json` Configuration
|
|
368
|
+
|
|
369
|
+
Add optional top-level `mediaPolicy` or per-song `media` overrides:
|
|
370
|
+
|
|
371
|
+
```json
|
|
372
|
+
{
|
|
373
|
+
"mediaPolicy": {
|
|
374
|
+
"provider": "auto",
|
|
375
|
+
"providerOrder": ["pexels", "pixabay"],
|
|
376
|
+
"mergeStrategy": "fallback",
|
|
377
|
+
"type": "video",
|
|
378
|
+
"orientation": "landscape",
|
|
379
|
+
"minWidth": 1280,
|
|
380
|
+
"minHeight": 720,
|
|
381
|
+
"safeSearch": true,
|
|
382
|
+
"avoidReuse": true,
|
|
383
|
+
"composeMode": "video-loop",
|
|
384
|
+
"cacheDir": ".media-cache"
|
|
385
|
+
},
|
|
386
|
+
"songs": {
|
|
387
|
+
"01_track.wav": {
|
|
388
|
+
"youtube": {
|
|
389
|
+
"snippet": {
|
|
390
|
+
"title": "Night Lights"
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
"media": {
|
|
394
|
+
"query": "neon city driving rain",
|
|
395
|
+
"type": "video",
|
|
396
|
+
"orientation": "landscape"
|
|
397
|
+
}
|
|
184
398
|
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
After resolution, `resolvedMedia` is saved directly to `metadata.json` and `ATTRIBUTIONS.md` is generated automatically with full licensing credits.
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## 🎬 Batch Video Actions Pipeline
|
|
408
|
+
|
|
409
|
+
The actions pipeline allows non-destructive video modifications (shortening, background replacement, and playlist journey overlays) processed into a **new sibling folder** with cloned `metadata.json`:
|
|
410
|
+
|
|
411
|
+
### Available Actions
|
|
412
|
+
|
|
413
|
+
1. **`shorten`**: Cut video at `cutPoint`, smoothly fade out audio and video into the cut, then append a silent padding buffer.
|
|
414
|
+
2. **`replace-background`**: Keep existing audio and swap the visual background with a freshly sourced stock video or photo.
|
|
415
|
+
3. **`journey-overlay`**: Burn in a modern journey overlay graphic (either a sleek top-right step badge or playlist panel) and optional cycling center concepts.
|
|
416
|
+
|
|
417
|
+
### `metadata.json` Actions Configuration
|
|
418
|
+
|
|
419
|
+
```json
|
|
420
|
+
{
|
|
421
|
+
"defaultActions": [
|
|
422
|
+
{ "type": "journey-overlay" }
|
|
423
|
+
],
|
|
424
|
+
"journey": {
|
|
425
|
+
"enabled": true,
|
|
426
|
+
"style": "badge",
|
|
427
|
+
"position": "right",
|
|
428
|
+
"sidebarLabel": "stage",
|
|
429
|
+
"sidebarDisplayDuration": 4,
|
|
430
|
+
"theme": "dark",
|
|
431
|
+
"maxCenterChars": 60
|
|
185
432
|
},
|
|
433
|
+
"songs": {
|
|
434
|
+
"01-first.mp4": {
|
|
435
|
+
"journeyStage": "Decision",
|
|
436
|
+
"actions": [
|
|
437
|
+
{ "type": "shorten", "cutPoint": "02:15", "fadeDuration": 3, "paddingDuration": 2 },
|
|
438
|
+
{
|
|
439
|
+
"type": "journey-overlay",
|
|
440
|
+
"conceptsDisplayMode": "full",
|
|
441
|
+
"conceptDisplaySeconds": 7,
|
|
442
|
+
"conceptFadeSeconds": 1,
|
|
443
|
+
"centerConcepts": [
|
|
444
|
+
"You don't feel ready.",
|
|
445
|
+
"Move anyway."
|
|
446
|
+
]
|
|
447
|
+
}
|
|
448
|
+
]
|
|
449
|
+
},
|
|
450
|
+
"02-second.mp4": {
|
|
451
|
+
"journeyStage": "Sensation",
|
|
452
|
+
"actions": [
|
|
453
|
+
{ "type": "replace-background", "query": "empty highway sunrise" }
|
|
454
|
+
]
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### CLI Commands
|
|
461
|
+
|
|
462
|
+
#### Apply Actions (`apply-actions`)
|
|
463
|
+
```bash
|
|
464
|
+
# Process all videos and output to sibling folder (e.g. ./my-videos (short))
|
|
465
|
+
yt-uploader apply-actions ./my-videos
|
|
466
|
+
|
|
467
|
+
# Preview planned ffmpeg operations without modifying disk
|
|
468
|
+
yt-uploader apply-actions ./my-videos --dry-run
|
|
469
|
+
|
|
470
|
+
# Run only a specific video entry
|
|
471
|
+
yt-uploader apply-actions ./my-videos --only 01-first.mp4
|
|
472
|
+
|
|
473
|
+
# Overwrite existing outputs
|
|
474
|
+
yt-uploader apply-actions ./my-videos --force
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
#### Preview Journey Overlay (`preview-journey`)
|
|
478
|
+
Generate a PNG preview of the journey graphic to iterate on sidebar and center text appearance without re-compositing videos:
|
|
479
|
+
```bash
|
|
480
|
+
yt-uploader preview-journey 01-first.mp4 --dir ./my-videos
|
|
481
|
+
yt-uploader preview-journey 01-first.mp4 --center-text "Custom prompt question?" -o ./preview.png
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## 🛠️ Programmatic Node.js API
|
|
487
|
+
|
|
488
|
+
```javascript
|
|
489
|
+
import {
|
|
490
|
+
searchMedia,
|
|
491
|
+
resolveMediaForMetadata,
|
|
492
|
+
composeVideo,
|
|
493
|
+
batchUpload,
|
|
494
|
+
} from '@x12i/youtube-video-uploader-cli';
|
|
495
|
+
|
|
496
|
+
// 1. Search stock media
|
|
497
|
+
const results = await searchMedia({
|
|
498
|
+
query: 'calm night drive neon city',
|
|
499
|
+
type: 'video',
|
|
500
|
+
provider: 'auto',
|
|
186
501
|
});
|
|
187
502
|
|
|
188
|
-
|
|
503
|
+
// 2. Resolve media for metadata.json and compose videos
|
|
504
|
+
const { resolved, skipped, errors } = await resolveMediaForMetadata('./my-videos', {
|
|
505
|
+
compose: true,
|
|
506
|
+
provider: 'auto',
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// 3. Batch upload
|
|
510
|
+
const uploadResult = await batchUpload('./my-videos', {
|
|
511
|
+
safeQuota: true,
|
|
512
|
+
});
|
|
189
513
|
```
|
|
190
514
|
|
|
191
515
|
---
|