@x12i/youtube-video-uploader-cli 1.3.0 → 1.6.1
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 +76 -5
- package/bin/cli.js +344 -6
- package/package.json +5 -3
- package/src/access/privacy.js +113 -0
- package/src/access/set-access.js +247 -0
- package/src/actions/index.js +33 -0
- package/src/actions/journey-overlay.js +117 -61
- package/src/actions/remove-watermark.js +109 -0
- package/src/actions/replace-background.js +42 -4
- package/src/actions/shorten.js +49 -10
- package/src/batch/plan.js +4 -3
- package/src/cli/preview-journey.js +9 -4
- package/src/cli/remove-watermark.js +252 -0
- package/src/index.d.ts +156 -3
- package/src/index.js +42 -0
- package/src/media/compose.js +85 -12
- package/src/playlist/detect-outdated.js +50 -0
- package/src/playlist/order.js +57 -0
- package/src/playlist/reorder.js +90 -0
- package/src/playlist/sync.js +192 -0
- package/src/providers/watermark/index.js +113 -0
- package/src/providers/watermark/pilio.js +201 -0
- package/src/swap/steps.js +17 -12
- package/src/swap/swapSong.js +40 -2
- package/src/update.js +165 -25
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { VALID_PRIVACY_STATUSES } from '../metadata.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {'private' | 'unlisted' | 'public'} PrivacyStatus
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Normalize and validate a privacy status string.
|
|
9
|
+
* @param {string} value
|
|
10
|
+
* @returns {PrivacyStatus}
|
|
11
|
+
*/
|
|
12
|
+
export function normalizePrivacyStatus(value) {
|
|
13
|
+
const status = String(value || '').toLowerCase().trim();
|
|
14
|
+
if (!VALID_PRIVACY_STATUSES.has(status)) {
|
|
15
|
+
throw new Error(`Invalid privacy status: ${value}. Use private, unlisted, or public.`);
|
|
16
|
+
}
|
|
17
|
+
return /** @type {PrivacyStatus} */ (status);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Fetch privacy status for videos (batch up to 50 ids per call).
|
|
22
|
+
*
|
|
23
|
+
* @param {import('googleapis').youtube_v3.Youtube} youtube
|
|
24
|
+
* @param {string[]} videoIds
|
|
25
|
+
* @returns {Promise<Map<string, PrivacyStatus>>}
|
|
26
|
+
*/
|
|
27
|
+
export async function getVideosPrivacy(youtube, videoIds) {
|
|
28
|
+
const map = new Map();
|
|
29
|
+
const ids = [...new Set(videoIds.filter(Boolean))];
|
|
30
|
+
for (let i = 0; i < ids.length; i += 50) {
|
|
31
|
+
const chunk = ids.slice(i, i + 50);
|
|
32
|
+
const { data } = await youtube.videos.list({
|
|
33
|
+
part: ['status', 'snippet'],
|
|
34
|
+
id: chunk,
|
|
35
|
+
});
|
|
36
|
+
for (const item of data.items || []) {
|
|
37
|
+
map.set(item.id, /** @type {PrivacyStatus} */ (item.status?.privacyStatus || 'private'));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return map;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Fetch playlist privacy status.
|
|
45
|
+
*
|
|
46
|
+
* @param {import('googleapis').youtube_v3.Youtube} youtube
|
|
47
|
+
* @param {string} playlistId
|
|
48
|
+
* @returns {Promise<PrivacyStatus>}
|
|
49
|
+
*/
|
|
50
|
+
export async function getPlaylistPrivacy(youtube, playlistId) {
|
|
51
|
+
const { data } = await youtube.playlists.list({
|
|
52
|
+
part: ['status'],
|
|
53
|
+
id: [playlistId],
|
|
54
|
+
});
|
|
55
|
+
const item = data.items?.[0];
|
|
56
|
+
if (!item) {
|
|
57
|
+
throw new Error(`Playlist not found or inaccessible: ${playlistId}`);
|
|
58
|
+
}
|
|
59
|
+
return /** @type {PrivacyStatus} */ (item.status?.privacyStatus || 'private');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Update a single video's privacy status.
|
|
64
|
+
*
|
|
65
|
+
* @param {import('googleapis').youtube_v3.Youtube} youtube
|
|
66
|
+
* @param {string} videoId
|
|
67
|
+
* @param {PrivacyStatus} privacyStatus
|
|
68
|
+
* @returns {Promise<{ videoId: string, privacyStatus: PrivacyStatus }>}
|
|
69
|
+
*/
|
|
70
|
+
export async function setVideoPrivacy(youtube, videoId, privacyStatus) {
|
|
71
|
+
const status = normalizePrivacyStatus(privacyStatus);
|
|
72
|
+
await youtube.videos.update({
|
|
73
|
+
part: ['status'],
|
|
74
|
+
requestBody: {
|
|
75
|
+
id: videoId,
|
|
76
|
+
status: { privacyStatus: status },
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
return { videoId, privacyStatus: status };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Update a playlist's privacy status.
|
|
84
|
+
*
|
|
85
|
+
* @param {import('googleapis').youtube_v3.Youtube} youtube
|
|
86
|
+
* @param {string} playlistId
|
|
87
|
+
* @param {PrivacyStatus} privacyStatus
|
|
88
|
+
* @returns {Promise<{ playlistId: string, privacyStatus: PrivacyStatus }>}
|
|
89
|
+
*/
|
|
90
|
+
export async function setPlaylistPrivacy(youtube, playlistId, privacyStatus) {
|
|
91
|
+
const status = normalizePrivacyStatus(privacyStatus);
|
|
92
|
+
|
|
93
|
+
const { data } = await youtube.playlists.list({
|
|
94
|
+
part: ['snippet', 'status'],
|
|
95
|
+
id: [playlistId],
|
|
96
|
+
});
|
|
97
|
+
const existing = data.items?.[0];
|
|
98
|
+
if (!existing?.snippet) {
|
|
99
|
+
throw new Error(`Playlist not found or inaccessible: ${playlistId}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await youtube.playlists.update({
|
|
103
|
+
part: ['snippet', 'status'],
|
|
104
|
+
requestBody: {
|
|
105
|
+
id: playlistId,
|
|
106
|
+
snippet: existing.snippet,
|
|
107
|
+
status: { privacyStatus: status },
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
return { playlistId, privacyStatus: status };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const QUOTA_PER_PRIVACY_UPDATE = 50;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { UploadState } from '../state.js';
|
|
4
|
+
import { listPlaylistItems } from '../playlist/reorder.js';
|
|
5
|
+
import { resolveDesiredPlaylistOrder } from '../playlist/order.js';
|
|
6
|
+
import {
|
|
7
|
+
getVideosPrivacy,
|
|
8
|
+
getPlaylistPrivacy,
|
|
9
|
+
setVideoPrivacy,
|
|
10
|
+
setPlaylistPrivacy,
|
|
11
|
+
normalizePrivacyStatus,
|
|
12
|
+
QUOTA_PER_PRIVACY_UPDATE,
|
|
13
|
+
} from './privacy.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Set privacy/access for a playlist and its member videos.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} options
|
|
19
|
+
* @param {import('googleapis').youtube_v3.Youtube} [options.youtube]
|
|
20
|
+
* @param {string} options.playlistId
|
|
21
|
+
* @param {'private' | 'unlisted' | 'public'} options.privacyStatus
|
|
22
|
+
* @param {string[]} [options.videoIds] - Explicit video IDs (skips playlist listing)
|
|
23
|
+
* @param {'playlist' | 'history'} [options.videoSource='playlist'] - Resolve videos from YouTube playlist or upload history
|
|
24
|
+
* @param {string} [options.albumDir]
|
|
25
|
+
* @param {string} [options.overviewHistoryPath]
|
|
26
|
+
* @param {string[]} [options.overviewKeys]
|
|
27
|
+
* @param {boolean} [options.updateMetadata=false]
|
|
28
|
+
* @param {string} [options.metadataFile]
|
|
29
|
+
* @param {boolean} [options.dryRun=false]
|
|
30
|
+
* @param {(event: object) => void} [options.onEvent]
|
|
31
|
+
* @returns {Promise<object>}
|
|
32
|
+
*/
|
|
33
|
+
export async function setPlaylistAccess(options = {}) {
|
|
34
|
+
const {
|
|
35
|
+
youtube,
|
|
36
|
+
playlistId,
|
|
37
|
+
privacyStatus: rawPrivacy,
|
|
38
|
+
videoSource = 'playlist',
|
|
39
|
+
albumDir,
|
|
40
|
+
overviewHistoryPath,
|
|
41
|
+
overviewKeys = [],
|
|
42
|
+
videoIds: explicitVideoIds,
|
|
43
|
+
updateMetadata = false,
|
|
44
|
+
metadataFile,
|
|
45
|
+
dryRun = false,
|
|
46
|
+
onEvent = () => {},
|
|
47
|
+
} = options;
|
|
48
|
+
|
|
49
|
+
if (!playlistId) {
|
|
50
|
+
throw new Error('setPlaylistAccess requires playlistId');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const privacyStatus = normalizePrivacyStatus(rawPrivacy);
|
|
54
|
+
|
|
55
|
+
/** @type {Array<{ videoId: string, title?: string, position?: number }>} */
|
|
56
|
+
let targets = [];
|
|
57
|
+
|
|
58
|
+
if (Array.isArray(explicitVideoIds) && explicitVideoIds.length > 0) {
|
|
59
|
+
targets = explicitVideoIds.map((videoId, index) => ({ videoId, position: index }));
|
|
60
|
+
} else if (videoSource === 'playlist') {
|
|
61
|
+
if (!youtube && !dryRun) {
|
|
62
|
+
throw new Error('videoSource "playlist" requires a YouTube client');
|
|
63
|
+
}
|
|
64
|
+
if (youtube) {
|
|
65
|
+
const items = await listPlaylistItems(youtube, playlistId);
|
|
66
|
+
targets = items.map((item) => ({
|
|
67
|
+
videoId: item.videoId,
|
|
68
|
+
title: item.title,
|
|
69
|
+
position: item.position,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
} else if (videoSource === 'history') {
|
|
73
|
+
if (!albumDir) {
|
|
74
|
+
throw new Error('videoSource "history" requires albumDir');
|
|
75
|
+
}
|
|
76
|
+
const albumState = new UploadState(albumDir);
|
|
77
|
+
const videoIds = resolveDesiredPlaylistOrder({
|
|
78
|
+
albumDir,
|
|
79
|
+
albumState,
|
|
80
|
+
overviewHistoryPath,
|
|
81
|
+
overviewKeys,
|
|
82
|
+
});
|
|
83
|
+
targets = videoIds.map((videoId) => ({ videoId }));
|
|
84
|
+
} else {
|
|
85
|
+
throw new Error(`Unsupported videoSource: ${videoSource}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (targets.length === 0 && dryRun && videoSource === 'playlist') {
|
|
89
|
+
// Dry-run without youtube: cannot list playlist — caller should use history
|
|
90
|
+
throw new Error('Dry-run with videoSource "playlist" requires YouTube auth to list playlist items');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let currentPlaylistPrivacy = null;
|
|
94
|
+
let currentVideoPrivacy = new Map();
|
|
95
|
+
|
|
96
|
+
if (youtube) {
|
|
97
|
+
currentPlaylistPrivacy = await getPlaylistPrivacy(youtube, playlistId);
|
|
98
|
+
currentVideoPrivacy = await getVideosPrivacy(youtube, targets.map((t) => t.videoId));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const videoPlans = targets.map((target) => {
|
|
102
|
+
const current = currentVideoPrivacy.get(target.videoId) || null;
|
|
103
|
+
return {
|
|
104
|
+
...target,
|
|
105
|
+
currentPrivacy: current,
|
|
106
|
+
targetPrivacy: privacyStatus,
|
|
107
|
+
changed: current !== null ? current !== privacyStatus : true,
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const playlistChanged = currentPlaylistPrivacy !== null
|
|
112
|
+
? currentPlaylistPrivacy !== privacyStatus
|
|
113
|
+
: true;
|
|
114
|
+
|
|
115
|
+
const changedVideos = videoPlans.filter((v) => v.changed);
|
|
116
|
+
const estimatedQuota = (playlistChanged ? QUOTA_PER_PRIVACY_UPDATE : 0)
|
|
117
|
+
+ (changedVideos.length * QUOTA_PER_PRIVACY_UPDATE);
|
|
118
|
+
|
|
119
|
+
const plan = {
|
|
120
|
+
playlistId,
|
|
121
|
+
privacyStatus,
|
|
122
|
+
videoSource,
|
|
123
|
+
playlist: {
|
|
124
|
+
playlistId,
|
|
125
|
+
currentPrivacy: currentPlaylistPrivacy,
|
|
126
|
+
targetPrivacy: privacyStatus,
|
|
127
|
+
changed: playlistChanged,
|
|
128
|
+
},
|
|
129
|
+
videos: videoPlans,
|
|
130
|
+
changedCount: changedVideos.length + (playlistChanged ? 1 : 0),
|
|
131
|
+
estimatedQuota,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
onEvent({ type: 'access_plan', plan });
|
|
135
|
+
|
|
136
|
+
if (dryRun) {
|
|
137
|
+
return { dryRun: true, ...plan, updated: { playlist: false, videos: [] } };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!youtube) {
|
|
141
|
+
throw new Error('setPlaylistAccess requires a YouTube client when dryRun is false');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const updated = { playlist: false, videos: [] };
|
|
145
|
+
|
|
146
|
+
if (playlistChanged) {
|
|
147
|
+
onEvent({ type: 'access_playlist_start', playlistId, privacyStatus });
|
|
148
|
+
await setPlaylistPrivacy(youtube, playlistId, privacyStatus);
|
|
149
|
+
updated.playlist = true;
|
|
150
|
+
onEvent({ type: 'access_playlist_success', playlistId, privacyStatus });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (let i = 0; i < changedVideos.length; i += 1) {
|
|
154
|
+
const video = changedVideos[i];
|
|
155
|
+
onEvent({
|
|
156
|
+
type: 'access_video_start',
|
|
157
|
+
videoId: video.videoId,
|
|
158
|
+
title: video.title,
|
|
159
|
+
index: i,
|
|
160
|
+
total: changedVideos.length,
|
|
161
|
+
});
|
|
162
|
+
await setVideoPrivacy(youtube, video.videoId, privacyStatus);
|
|
163
|
+
updated.videos.push({ videoId: video.videoId, privacyStatus });
|
|
164
|
+
onEvent({ type: 'access_video_success', videoId: video.videoId, privacyStatus });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (updateMetadata && albumDir) {
|
|
168
|
+
const metadataPath = metadataFile
|
|
169
|
+
? path.resolve(metadataFile)
|
|
170
|
+
: path.join(path.resolve(albumDir), 'metadata.json');
|
|
171
|
+
if (fs.existsSync(metadataPath)) {
|
|
172
|
+
try {
|
|
173
|
+
const raw = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
174
|
+
if (raw.uploadPolicy) {
|
|
175
|
+
raw.uploadPolicy.initialVideoPrivacyStatus = privacyStatus;
|
|
176
|
+
raw.uploadPolicy.initialPlaylistPrivacyStatus = privacyStatus;
|
|
177
|
+
}
|
|
178
|
+
if (raw.playlist?.youtube?.status) {
|
|
179
|
+
raw.playlist.youtube.status.privacyStatus = privacyStatus;
|
|
180
|
+
} else if (raw.playlist?.status) {
|
|
181
|
+
raw.playlist.status.privacyStatus = privacyStatus;
|
|
182
|
+
}
|
|
183
|
+
if (raw.songs && typeof raw.songs === 'object') {
|
|
184
|
+
for (const song of Object.values(raw.songs)) {
|
|
185
|
+
if (song?.youtube?.status) {
|
|
186
|
+
song.youtube.status.privacyStatus = privacyStatus;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
fs.writeFileSync(metadataPath, JSON.stringify(raw, null, 2) + '\n', 'utf-8');
|
|
191
|
+
onEvent({ type: 'access_metadata_saved', metadataPath });
|
|
192
|
+
} catch (err) {
|
|
193
|
+
onEvent({ type: 'access_metadata_error', error: err });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
dryRun: false,
|
|
200
|
+
...plan,
|
|
201
|
+
updated,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Set privacy for explicit video IDs (no playlist update).
|
|
207
|
+
*
|
|
208
|
+
* @param {object} options
|
|
209
|
+
* @param {import('googleapis').youtube_v3.Youtube} options.youtube
|
|
210
|
+
* @param {string[]} options.videoIds
|
|
211
|
+
* @param {'private' | 'unlisted' | 'public'} options.privacyStatus
|
|
212
|
+
* @param {boolean} [options.dryRun=false]
|
|
213
|
+
* @returns {Promise<object>}
|
|
214
|
+
*/
|
|
215
|
+
export async function setVideosAccess(options = {}) {
|
|
216
|
+
const { youtube, videoIds = [], privacyStatus: rawPrivacy, dryRun = false } = options;
|
|
217
|
+
const privacyStatus = normalizePrivacyStatus(rawPrivacy);
|
|
218
|
+
const ids = [...new Set(videoIds.filter(Boolean))];
|
|
219
|
+
|
|
220
|
+
if (ids.length === 0) {
|
|
221
|
+
throw new Error('setVideosAccess requires at least one videoId');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const current = youtube ? await getVideosPrivacy(youtube, ids) : new Map();
|
|
225
|
+
const plans = ids.map((videoId) => ({
|
|
226
|
+
videoId,
|
|
227
|
+
currentPrivacy: current.get(videoId) || null,
|
|
228
|
+
targetPrivacy: privacyStatus,
|
|
229
|
+
changed: current.get(videoId) !== privacyStatus,
|
|
230
|
+
}));
|
|
231
|
+
|
|
232
|
+
if (dryRun) {
|
|
233
|
+
return { dryRun: true, videos: plans, changedCount: plans.filter((p) => p.changed).length };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!youtube) {
|
|
237
|
+
throw new Error('setVideosAccess requires a YouTube client');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const updated = [];
|
|
241
|
+
for (const plan of plans.filter((p) => p.changed)) {
|
|
242
|
+
await setVideoPrivacy(youtube, plan.videoId, privacyStatus);
|
|
243
|
+
updated.push(plan.videoId);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { dryRun: false, videos: plans, updated };
|
|
247
|
+
}
|
package/src/actions/index.js
CHANGED
|
@@ -9,6 +9,12 @@ import {
|
|
|
9
9
|
renderSvgToPng,
|
|
10
10
|
resolveJourneyTracks,
|
|
11
11
|
} from './journey-overlay.js';
|
|
12
|
+
import {
|
|
13
|
+
removeWatermark,
|
|
14
|
+
isWatermarkSupportedMedia,
|
|
15
|
+
defaultCleanOutputPath,
|
|
16
|
+
defaultCleanOutDir,
|
|
17
|
+
} from './remove-watermark.js';
|
|
12
18
|
|
|
13
19
|
export {
|
|
14
20
|
shortenVideo,
|
|
@@ -20,6 +26,10 @@ export {
|
|
|
20
26
|
generateJourneySvg,
|
|
21
27
|
renderSvgToPng,
|
|
22
28
|
resolveJourneyTracks,
|
|
29
|
+
removeWatermark,
|
|
30
|
+
isWatermarkSupportedMedia,
|
|
31
|
+
defaultCleanOutputPath,
|
|
32
|
+
defaultCleanOutDir,
|
|
23
33
|
};
|
|
24
34
|
|
|
25
35
|
/**
|
|
@@ -116,6 +126,7 @@ export async function runActionsPipeline(entry, actions = [], context = {}) {
|
|
|
116
126
|
const res = await shortenVideo({
|
|
117
127
|
inputPath: currentInput,
|
|
118
128
|
outputPath: stepOutput,
|
|
129
|
+
entry,
|
|
119
130
|
...action,
|
|
120
131
|
dryRun: true,
|
|
121
132
|
onWarn,
|
|
@@ -143,6 +154,15 @@ export async function runActionsPipeline(entry, actions = [], context = {}) {
|
|
|
143
154
|
onWarn,
|
|
144
155
|
});
|
|
145
156
|
steps.push(`[${i + 1}/${actions.length}] ${res.command}`);
|
|
157
|
+
} else if (action.type === 'remove-watermark') {
|
|
158
|
+
const res = await removeWatermark({
|
|
159
|
+
inputPath: currentInput,
|
|
160
|
+
outputPath: stepOutput,
|
|
161
|
+
action,
|
|
162
|
+
dryRun: true,
|
|
163
|
+
onWarn,
|
|
164
|
+
});
|
|
165
|
+
steps.push(`[${i + 1}/${actions.length}] ${res.command}`);
|
|
146
166
|
} else {
|
|
147
167
|
throw new Error(`Unknown action type: "${action.type}" on entry ${songKey}`);
|
|
148
168
|
}
|
|
@@ -205,6 +225,7 @@ export async function runActionsPipeline(entry, actions = [], context = {}) {
|
|
|
205
225
|
await shortenVideo({
|
|
206
226
|
inputPath: currentInput,
|
|
207
227
|
outputPath: stepOutput,
|
|
228
|
+
entry,
|
|
208
229
|
...action,
|
|
209
230
|
dryRun: false,
|
|
210
231
|
onWarn,
|
|
@@ -230,6 +251,18 @@ export async function runActionsPipeline(entry, actions = [], context = {}) {
|
|
|
230
251
|
dryRun: false,
|
|
231
252
|
onWarn,
|
|
232
253
|
});
|
|
254
|
+
} else if (action.type === 'remove-watermark') {
|
|
255
|
+
await removeWatermark({
|
|
256
|
+
inputPath: currentInput,
|
|
257
|
+
outputPath: stepOutput,
|
|
258
|
+
action: {
|
|
259
|
+
...action,
|
|
260
|
+
// Pipeline targets are always freshly planned; allow overwrite of step outputs.
|
|
261
|
+
overwrite: action.overwrite !== undefined ? action.overwrite : true,
|
|
262
|
+
},
|
|
263
|
+
dryRun: false,
|
|
264
|
+
onWarn,
|
|
265
|
+
});
|
|
233
266
|
} else {
|
|
234
267
|
throw new Error(`Unknown action type: "${action.type}" on entry ${songKey}`);
|
|
235
268
|
}
|