@x12i/youtube-video-uploader-cli 1.4.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 +59 -1
- 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 +31 -0
- package/src/actions/remove-watermark.js +109 -0
- package/src/cli/remove-watermark.js +252 -0
- package/src/index.d.ts +147 -1
- package/src/index.js +42 -0
- 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
package/src/update.js
CHANGED
|
@@ -4,11 +4,50 @@ import { google } from 'googleapis';
|
|
|
4
4
|
import { loadMetadata } from './metadata.js';
|
|
5
5
|
import { UploadState } from './state.js';
|
|
6
6
|
import { authenticate } from './auth.js';
|
|
7
|
+
import { listPlaylistItems } from './playlist/reorder.js';
|
|
7
8
|
|
|
8
9
|
export const QUOTA_PER_VIDEO_UPDATE = 50;
|
|
9
10
|
export const QUOTA_PER_PLAYLIST_UPDATE = 50;
|
|
10
11
|
export const QUOTA_PER_LIST = 1;
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Apply a find/replace to a string.
|
|
15
|
+
* @param {string} text
|
|
16
|
+
* @param {string} pattern
|
|
17
|
+
* @param {string} [replacement]
|
|
18
|
+
* @param {boolean} [literal=false]
|
|
19
|
+
*/
|
|
20
|
+
export function applyTextReplace(text, pattern, replacement = '', literal = false) {
|
|
21
|
+
if (!pattern || text == null) return text;
|
|
22
|
+
const value = String(text);
|
|
23
|
+
if (literal) {
|
|
24
|
+
return value.split(pattern).join(replacement);
|
|
25
|
+
}
|
|
26
|
+
return value.replace(new RegExp(pattern, 'g'), replacement);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Deep-replace matching strings anywhere in a JSON-serializable object.
|
|
31
|
+
* @param {unknown} value
|
|
32
|
+
* @param {string} pattern
|
|
33
|
+
* @param {string} [replacement]
|
|
34
|
+
* @param {boolean} [literal=false]
|
|
35
|
+
*/
|
|
36
|
+
export function deepReplaceInObject(value, pattern, replacement = '', literal = false) {
|
|
37
|
+
if (typeof value === 'string') {
|
|
38
|
+
return applyTextReplace(value, pattern, replacement, literal);
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
return value.map((item) => deepReplaceInObject(item, pattern, replacement, literal));
|
|
42
|
+
}
|
|
43
|
+
if (value && typeof value === 'object') {
|
|
44
|
+
return Object.fromEntries(
|
|
45
|
+
Object.entries(value).map(([key, item]) => [key, deepReplaceInObject(item, pattern, replacement, literal)])
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
12
51
|
/**
|
|
13
52
|
* Update snippet fields of a YouTube video while preserving other metadata.
|
|
14
53
|
*
|
|
@@ -132,6 +171,9 @@ export async function updatePlaylistTitle(youtube, playlistId, newTitle) {
|
|
|
132
171
|
* @param {string} [options.playlistTitle] - Override playlist title
|
|
133
172
|
* @param {string} [options.replacePattern] - Regex or substring to find in titles
|
|
134
173
|
* @param {string} [options.replaceWith] - Replacement string
|
|
174
|
+
* @param {boolean} [options.replaceLiteral=false] - Treat replacePattern as a plain substring
|
|
175
|
+
* @param {boolean} [options.replaceInSnippet=false] - Also replace in descriptions/tags on YouTube
|
|
176
|
+
* @param {string} [options.includePlaylistId] - Include every video in this playlist (e.g. overview prefix videos)
|
|
135
177
|
* @param {string} [options.metadataFile] - Custom path to metadata.json
|
|
136
178
|
* @param {boolean} [options.updateLocalMetadata=true] - Update metadata.json on disk
|
|
137
179
|
* @param {boolean} [options.syncMetadata=false] - Push titles from metadata.json directly to YouTube
|
|
@@ -156,6 +198,14 @@ export async function batchUpdateMetadata(targetFolder, options = {}) {
|
|
|
156
198
|
const dryRun = Boolean(options.dryRun);
|
|
157
199
|
const onEvent = options.onEvent || (() => {});
|
|
158
200
|
const updateLocal = options.updateLocalMetadata !== false;
|
|
201
|
+
const replaceLiteral = Boolean(options.replaceLiteral);
|
|
202
|
+
const replaceInSnippet = Boolean(options.replaceInSnippet);
|
|
203
|
+
const includePlaylistId = options.includePlaylistId || null;
|
|
204
|
+
|
|
205
|
+
const replaceText = (text) => {
|
|
206
|
+
if (!options.replacePattern) return text;
|
|
207
|
+
return applyTextReplace(text, options.replacePattern, options.replaceWith || '', replaceLiteral);
|
|
208
|
+
};
|
|
159
209
|
|
|
160
210
|
const metadataPath = options.metadataFile
|
|
161
211
|
? path.resolve(options.metadataFile)
|
|
@@ -175,10 +225,30 @@ export async function batchUpdateMetadata(targetFolder, options = {}) {
|
|
|
175
225
|
const cachedPlaylist = state.getPlaylist(playlistKey);
|
|
176
226
|
const playlistId = options.singlePlaylistId || rawMetadata?.playlistId || rawMetadata?.playlist?.playlistId || cachedPlaylist?.playlistId || state.getPlaylist('gray_to_light')?.playlistId || null;
|
|
177
227
|
|
|
228
|
+
let youtube = null;
|
|
229
|
+
const needsYoutubeForPlan = Boolean(includePlaylistId && options.replacePattern);
|
|
230
|
+
if (needsYoutubeForPlan) {
|
|
231
|
+
const auth = await authenticate({
|
|
232
|
+
tokenFile: options.tokenFile,
|
|
233
|
+
clientSecretsFile: options.clientSecretsFile,
|
|
234
|
+
clientId: options.clientId,
|
|
235
|
+
clientSecret: options.clientSecret,
|
|
236
|
+
});
|
|
237
|
+
youtube = google.youtube({ version: 'v3', auth });
|
|
238
|
+
}
|
|
239
|
+
|
|
178
240
|
// Build playlist plan
|
|
179
241
|
let playlistPlan = null;
|
|
180
242
|
if (playlistId) {
|
|
181
243
|
let currentTitle = rawMetadata?.playlist?.youtube?.snippet?.title || rawMetadata?.playlist?.snippet?.title || '';
|
|
244
|
+
if (!currentTitle && youtube) {
|
|
245
|
+
try {
|
|
246
|
+
const { data } = await youtube.playlists.list({ part: ['snippet'], id: [playlistId] });
|
|
247
|
+
currentTitle = data.items?.[0]?.snippet?.title || '';
|
|
248
|
+
} catch {
|
|
249
|
+
// Non-fatal during planning
|
|
250
|
+
}
|
|
251
|
+
}
|
|
182
252
|
let newTitle = currentTitle;
|
|
183
253
|
|
|
184
254
|
if (options.syncMetadata) {
|
|
@@ -192,8 +262,7 @@ export async function batchUpdateMetadata(targetFolder, options = {}) {
|
|
|
192
262
|
}
|
|
193
263
|
|
|
194
264
|
if (options.replacePattern) {
|
|
195
|
-
|
|
196
|
-
newTitle = newTitle.replace(reg, options.replaceWith || '');
|
|
265
|
+
newTitle = replaceText(newTitle);
|
|
197
266
|
}
|
|
198
267
|
|
|
199
268
|
if (currentTitle || newTitle || options.playlistPostfix) {
|
|
@@ -207,6 +276,7 @@ export async function batchUpdateMetadata(targetFolder, options = {}) {
|
|
|
207
276
|
}
|
|
208
277
|
|
|
209
278
|
// Build video plans
|
|
279
|
+
/** @type {Array<{ filename: string, videoId: string, oldTitle: string, newTitle: string, changed: boolean, snippetUpdates?: object }>} */
|
|
210
280
|
const videoPlans = [];
|
|
211
281
|
const songsObj = rawMetadata?.songs || {};
|
|
212
282
|
const orderedKeys = Array.isArray(rawMetadata?.playlist?.orderedSongKeys)
|
|
@@ -233,6 +303,9 @@ export async function batchUpdateMetadata(targetFolder, options = {}) {
|
|
|
233
303
|
|
|
234
304
|
let currentTitle = songData.youtube?.snippet?.title || path.basename(filename, path.extname(filename));
|
|
235
305
|
let newTitle = currentTitle;
|
|
306
|
+
const snippet = songData.youtube?.snippet || {};
|
|
307
|
+
let newDescription = snippet.description || '';
|
|
308
|
+
let newTags = Array.isArray(snippet.tags) ? [...snippet.tags] : [];
|
|
236
309
|
|
|
237
310
|
if (options.syncMetadata) {
|
|
238
311
|
newTitle = currentTitle;
|
|
@@ -250,8 +323,20 @@ export async function batchUpdateMetadata(targetFolder, options = {}) {
|
|
|
250
323
|
}
|
|
251
324
|
|
|
252
325
|
if (options.replacePattern) {
|
|
253
|
-
|
|
254
|
-
|
|
326
|
+
newTitle = replaceText(newTitle);
|
|
327
|
+
if (replaceInSnippet) {
|
|
328
|
+
newDescription = replaceText(newDescription);
|
|
329
|
+
newTags = newTags.map((tag) => replaceText(tag));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const snippetUpdates = {};
|
|
334
|
+
if (newTitle !== currentTitle) snippetUpdates.title = newTitle;
|
|
335
|
+
if (replaceInSnippet && newDescription !== (snippet.description || '')) {
|
|
336
|
+
snippetUpdates.description = newDescription;
|
|
337
|
+
}
|
|
338
|
+
if (replaceInSnippet && JSON.stringify(newTags) !== JSON.stringify(snippet.tags || [])) {
|
|
339
|
+
snippetUpdates.tags = newTags;
|
|
255
340
|
}
|
|
256
341
|
|
|
257
342
|
videoPlans.push({
|
|
@@ -259,7 +344,8 @@ export async function batchUpdateMetadata(targetFolder, options = {}) {
|
|
|
259
344
|
videoId,
|
|
260
345
|
oldTitle: currentTitle,
|
|
261
346
|
newTitle,
|
|
262
|
-
changed: (
|
|
347
|
+
changed: Object.keys(snippetUpdates).length > 0,
|
|
348
|
+
snippetUpdates: Object.keys(snippetUpdates).length > 0 ? snippetUpdates : undefined,
|
|
263
349
|
});
|
|
264
350
|
}
|
|
265
351
|
|
|
@@ -278,8 +364,7 @@ export async function batchUpdateMetadata(targetFolder, options = {}) {
|
|
|
278
364
|
}
|
|
279
365
|
}
|
|
280
366
|
if (options.replacePattern) {
|
|
281
|
-
|
|
282
|
-
newTitle = newTitle.replace(reg, options.replaceWith || '');
|
|
367
|
+
newTitle = replaceText(newTitle);
|
|
283
368
|
}
|
|
284
369
|
videoPlans.push({
|
|
285
370
|
filename: upload.filename,
|
|
@@ -287,6 +372,41 @@ export async function batchUpdateMetadata(targetFolder, options = {}) {
|
|
|
287
372
|
oldTitle: currentTitle,
|
|
288
373
|
newTitle,
|
|
289
374
|
changed: (newTitle !== currentTitle),
|
|
375
|
+
snippetUpdates: newTitle !== currentTitle ? { title: newTitle } : undefined,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (includePlaylistId && youtube && options.replacePattern) {
|
|
382
|
+
const items = await listPlaylistItems(youtube, includePlaylistId);
|
|
383
|
+
const plannedIds = new Set(videoPlans.map((plan) => plan.videoId));
|
|
384
|
+
const missingIds = items.map((item) => item.videoId).filter((videoId) => videoId && !plannedIds.has(videoId));
|
|
385
|
+
for (let i = 0; i < missingIds.length; i += 50) {
|
|
386
|
+
const chunk = missingIds.slice(i, i + 50);
|
|
387
|
+
const { data } = await youtube.videos.list({ part: ['snippet'], id: chunk });
|
|
388
|
+
for (const item of data.items || []) {
|
|
389
|
+
const currentTitle = item.snippet?.title || '';
|
|
390
|
+
const newTitle = replaceText(currentTitle);
|
|
391
|
+
const snippetUpdates = {};
|
|
392
|
+
if (newTitle !== currentTitle) snippetUpdates.title = newTitle;
|
|
393
|
+
if (replaceInSnippet) {
|
|
394
|
+
const newDescription = replaceText(item.snippet?.description || '');
|
|
395
|
+
const newTags = (item.snippet?.tags || []).map((tag) => replaceText(tag));
|
|
396
|
+
if (newDescription !== (item.snippet?.description || '')) {
|
|
397
|
+
snippetUpdates.description = newDescription;
|
|
398
|
+
}
|
|
399
|
+
if (JSON.stringify(newTags) !== JSON.stringify(item.snippet?.tags || [])) {
|
|
400
|
+
snippetUpdates.tags = newTags;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
videoPlans.push({
|
|
404
|
+
filename: item.id,
|
|
405
|
+
videoId: item.id,
|
|
406
|
+
oldTitle: currentTitle,
|
|
407
|
+
newTitle,
|
|
408
|
+
changed: Object.keys(snippetUpdates).length > 0,
|
|
409
|
+
snippetUpdates: Object.keys(snippetUpdates).length > 0 ? snippetUpdates : undefined,
|
|
290
410
|
});
|
|
291
411
|
}
|
|
292
412
|
}
|
|
@@ -325,14 +445,15 @@ export async function batchUpdateMetadata(targetFolder, options = {}) {
|
|
|
325
445
|
}
|
|
326
446
|
|
|
327
447
|
// Authenticate
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
448
|
+
if (!youtube) {
|
|
449
|
+
const auth = await authenticate({
|
|
450
|
+
tokenFile: options.tokenFile,
|
|
451
|
+
clientSecretsFile: options.clientSecretsFile,
|
|
452
|
+
clientId: options.clientId,
|
|
453
|
+
clientSecret: options.clientSecret,
|
|
454
|
+
});
|
|
455
|
+
youtube = google.youtube({ version: 'v3', auth });
|
|
456
|
+
}
|
|
336
457
|
|
|
337
458
|
// Update Playlist
|
|
338
459
|
if (playlistPlan && playlistPlan.changed) {
|
|
@@ -351,7 +472,11 @@ export async function batchUpdateMetadata(targetFolder, options = {}) {
|
|
|
351
472
|
const v = changedVideos[i];
|
|
352
473
|
onEvent({ type: 'video_update_start', item: v, index: i, total: changedVideos.length });
|
|
353
474
|
try {
|
|
354
|
-
|
|
475
|
+
if (v.snippetUpdates && Object.keys(v.snippetUpdates).length > 0) {
|
|
476
|
+
await updateVideoSnippet(youtube, v.videoId, v.snippetUpdates);
|
|
477
|
+
} else {
|
|
478
|
+
await updateVideoTitle(youtube, v.videoId, v.newTitle);
|
|
479
|
+
}
|
|
355
480
|
onEvent({ type: 'video_update_success', item: v, index: i, total: changedVideos.length });
|
|
356
481
|
} catch (err) {
|
|
357
482
|
onEvent({ type: 'video_update_error', item: v, error: err });
|
|
@@ -362,17 +487,32 @@ export async function batchUpdateMetadata(targetFolder, options = {}) {
|
|
|
362
487
|
// Update metadata.json on disk if present and requested
|
|
363
488
|
if (updateLocal && rawMetadata && fs.existsSync(metadataPath)) {
|
|
364
489
|
try {
|
|
365
|
-
if (
|
|
366
|
-
|
|
367
|
-
rawMetadata
|
|
368
|
-
|
|
369
|
-
|
|
490
|
+
if (options.replacePattern) {
|
|
491
|
+
rawMetadata = deepReplaceInObject(
|
|
492
|
+
rawMetadata,
|
|
493
|
+
options.replacePattern,
|
|
494
|
+
options.replaceWith || '',
|
|
495
|
+
replaceLiteral
|
|
496
|
+
);
|
|
497
|
+
} else {
|
|
498
|
+
if (playlistPlan && playlistPlan.changed) {
|
|
499
|
+
if (rawMetadata.playlist?.youtube?.snippet) {
|
|
500
|
+
rawMetadata.playlist.youtube.snippet.title = playlistPlan.newTitle;
|
|
501
|
+
} else if (rawMetadata.playlist?.snippet) {
|
|
502
|
+
rawMetadata.playlist.snippet.title = playlistPlan.newTitle;
|
|
503
|
+
}
|
|
370
504
|
}
|
|
371
|
-
}
|
|
372
505
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
506
|
+
for (const v of videoPlans) {
|
|
507
|
+
if (v.changed && rawMetadata.songs?.[v.filename]?.youtube?.snippet) {
|
|
508
|
+
rawMetadata.songs[v.filename].youtube.snippet.title = v.newTitle;
|
|
509
|
+
if (v.snippetUpdates?.description) {
|
|
510
|
+
rawMetadata.songs[v.filename].youtube.snippet.description = v.snippetUpdates.description;
|
|
511
|
+
}
|
|
512
|
+
if (v.snippetUpdates?.tags) {
|
|
513
|
+
rawMetadata.songs[v.filename].youtube.snippet.tags = v.snippetUpdates.tags;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
376
516
|
}
|
|
377
517
|
}
|
|
378
518
|
|