@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.
@@ -38,11 +38,12 @@ export async function previewJourney(songKey, options = {}) {
38
38
  const journeyConfig = {
39
39
  enabled: true,
40
40
  mode: 'before-current',
41
- position: 'left',
42
- displayDuration: 5,
41
+ position: 'right',
42
+ displayDuration: 4,
43
43
  sidebarDisplayDuration: undefined,
44
- sidebarLabel: 'title',
44
+ sidebarLabel: 'stage',
45
45
  theme: 'dark',
46
+ style: 'badge',
46
47
  maxCenterChars: 60,
47
48
  maxVisibleRows: 8,
48
49
  ...(rawMetadata.journey || {}),
@@ -71,7 +72,7 @@ export async function previewJourney(songKey, options = {}) {
71
72
  currentSongKey: songKey,
72
73
  rawMetadata,
73
74
  mode: journeyConfig.mode,
74
- maxVisibleRows: journeyConfig.maxVisibleRows,
75
+ maxVisibleRows: journeyConfig.style === 'panel' ? journeyConfig.maxVisibleRows : null,
75
76
  sidebarLabel: journeyConfig.sidebarLabel,
76
77
  });
77
78
 
@@ -86,6 +87,10 @@ export async function previewJourney(songKey, options = {}) {
86
87
  showSidebar: journeyConfig.enabled !== false,
87
88
  theme: journeyConfig.theme,
88
89
  position: journeyConfig.position,
90
+ style: journeyConfig.style,
91
+ stage: songData.journeyStage || songData.albumRole,
92
+ step: typeof songData.trackNumber === 'number' ? songData.trackNumber : (songData.playlistPosition !== undefined ? songData.playlistPosition + 1 : undefined),
93
+ totalSteps: rawMetadata.album?.trackCount || rawMetadata.album?.arc?.length || (tracks[0]?.totalTracks) || 11,
89
94
  });
90
95
 
91
96
  const pngBuffer = renderSvgToPng(svgContent, width, height);
@@ -0,0 +1,252 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {
4
+ removeWatermark,
5
+ isWatermarkSupportedMedia,
6
+ defaultCleanOutputPath,
7
+ defaultCleanOutDir,
8
+ } from '../actions/remove-watermark.js';
9
+ import {
10
+ DEFAULT_WATERMARK_PROVIDER,
11
+ listWatermarkProviders,
12
+ } from '../providers/watermark/index.js';
13
+
14
+ /**
15
+ * Batch / single-file watermark removal without metadata.json.
16
+ *
17
+ * @param {string} input - File or directory path
18
+ * @param {object} [options]
19
+ * @param {string} [options.output] - Explicit output file (file input only)
20
+ * @param {string} [options.outDir] - Output directory (directory input, or optional for file)
21
+ * @param {string} [options.provider]
22
+ * @param {boolean} [options.overwrite=false]
23
+ * @param {boolean} [options.json=false]
24
+ * @param {number} [options.videoBitrateMbps]
25
+ * @param {boolean} [options.dryRun=false]
26
+ * @param {(event: object) => void} [options.onEvent]
27
+ * @returns {Promise<{
28
+ * input: string,
29
+ * mode: 'file' | 'directory',
30
+ * provider: string,
31
+ * dryRun: boolean,
32
+ * processed: Array<object>,
33
+ * skipped: Array<object>,
34
+ * failed: Array<object>
35
+ * }>}
36
+ */
37
+ export async function batchRemoveWatermark(input, options = {}) {
38
+ const {
39
+ output,
40
+ outDir,
41
+ provider = DEFAULT_WATERMARK_PROVIDER,
42
+ overwrite = false,
43
+ json = false,
44
+ videoBitrateMbps,
45
+ dryRun = false,
46
+ onEvent,
47
+ } = options;
48
+
49
+ const resolvedInput = path.resolve(input || '.');
50
+ if (!fs.existsSync(resolvedInput)) {
51
+ throw new Error(`Input path not found: ${resolvedInput}`);
52
+ }
53
+
54
+ const stat = fs.statSync(resolvedInput);
55
+ const actionOpts = { provider, overwrite, json, videoBitrateMbps };
56
+
57
+ if (stat.isFile()) {
58
+ return removeSingleFile(resolvedInput, { output, outDir, dryRun, actionOpts, onEvent });
59
+ }
60
+
61
+ if (stat.isDirectory()) {
62
+ return removeDirectory(resolvedInput, { outDir, dryRun, actionOpts, onEvent });
63
+ }
64
+
65
+ throw new Error(`Unsupported input path type: ${resolvedInput}`);
66
+ }
67
+
68
+ /**
69
+ * @param {string} inputPath
70
+ * @param {object} ctx
71
+ */
72
+ async function removeSingleFile(inputPath, ctx) {
73
+ const { output, outDir, dryRun, actionOpts, onEvent } = ctx;
74
+ let outputPath = output ? path.resolve(output) : null;
75
+ if (!outputPath && outDir) {
76
+ outputPath = path.join(path.resolve(outDir), path.basename(inputPath));
77
+ }
78
+ if (!outputPath) {
79
+ outputPath = defaultCleanOutputPath(inputPath);
80
+ }
81
+
82
+ const processed = [];
83
+ const skipped = [];
84
+ const failed = [];
85
+
86
+ if (onEvent) {
87
+ onEvent({
88
+ type: 'watermark_plan',
89
+ mode: 'file',
90
+ input: inputPath,
91
+ total: 1,
92
+ provider: actionOpts.provider,
93
+ dryRun,
94
+ });
95
+ }
96
+
97
+ try {
98
+ if (onEvent) {
99
+ onEvent({ type: 'entry_start', index: 0, total: 1, filename: path.basename(inputPath), inputPath });
100
+ }
101
+
102
+ if (!isWatermarkSupportedMedia(inputPath)) {
103
+ const skip = { filename: path.basename(inputPath), inputPath, reason: 'Unsupported media extension' };
104
+ skipped.push(skip);
105
+ if (onEvent) onEvent({ type: 'entry_skipped', ...skip });
106
+ } else if (fs.existsSync(outputPath) && !actionOpts.overwrite && !dryRun) {
107
+ const skip = {
108
+ filename: path.basename(inputPath),
109
+ inputPath,
110
+ outputPath,
111
+ reason: 'Output file already exists (use --overwrite / --force)',
112
+ };
113
+ skipped.push(skip);
114
+ if (onEvent) onEvent({ type: 'entry_skipped', ...skip });
115
+ } else {
116
+ const res = await removeWatermark({
117
+ inputPath,
118
+ outputPath,
119
+ action: actionOpts,
120
+ dryRun,
121
+ onStderr: (chunk) => {
122
+ if (onEvent) onEvent({ type: 'provider_stderr', chunk });
123
+ },
124
+ });
125
+ const item = {
126
+ filename: path.basename(inputPath),
127
+ inputPath,
128
+ outputPath,
129
+ command: res.command,
130
+ provider: res.provider,
131
+ dryRun: res.dryRun,
132
+ };
133
+ processed.push(item);
134
+ if (onEvent) onEvent({ type: 'entry_complete', ...item });
135
+ }
136
+ } catch (err) {
137
+ const fail = { filename: path.basename(inputPath), inputPath, error: err };
138
+ failed.push(fail);
139
+ if (onEvent) onEvent({ type: 'entry_failed', filename: fail.filename, inputPath, error: err });
140
+ }
141
+
142
+ return {
143
+ input: inputPath,
144
+ mode: 'file',
145
+ provider: actionOpts.provider,
146
+ dryRun,
147
+ processed,
148
+ skipped,
149
+ failed,
150
+ };
151
+ }
152
+
153
+ /**
154
+ * @param {string} inputDir
155
+ * @param {object} ctx
156
+ */
157
+ async function removeDirectory(inputDir, ctx) {
158
+ const { outDir, dryRun, actionOpts, onEvent } = ctx;
159
+ const resolvedOutDir = outDir ? path.resolve(outDir) : defaultCleanOutDir(inputDir);
160
+
161
+ const entries = fs.readdirSync(inputDir)
162
+ .map((name) => path.join(inputDir, name))
163
+ .filter((p) => fs.statSync(p).isFile() && isWatermarkSupportedMedia(p))
164
+ .sort((a, b) => a.localeCompare(b));
165
+
166
+ if (onEvent) {
167
+ onEvent({
168
+ type: 'watermark_plan',
169
+ mode: 'directory',
170
+ input: inputDir,
171
+ outDir: resolvedOutDir,
172
+ total: entries.length,
173
+ provider: actionOpts.provider,
174
+ dryRun,
175
+ });
176
+ }
177
+
178
+ // Directory batch: prefer provider --out-dir when not dry-run for a single spawn when overwrite/json only.
179
+ // We still process file-by-file so skip/exists and progress events stay consistent with apply-actions.
180
+ const processed = [];
181
+ const skipped = [];
182
+ const failed = [];
183
+
184
+ for (let i = 0; i < entries.length; i++) {
185
+ const inputPath = entries[i];
186
+ const outputPath = path.join(resolvedOutDir, path.basename(inputPath));
187
+
188
+ if (onEvent) {
189
+ onEvent({
190
+ type: 'entry_start',
191
+ index: i,
192
+ total: entries.length,
193
+ filename: path.basename(inputPath),
194
+ inputPath,
195
+ });
196
+ }
197
+
198
+ try {
199
+ if (fs.existsSync(outputPath) && !actionOpts.overwrite && !dryRun) {
200
+ const skip = {
201
+ filename: path.basename(inputPath),
202
+ inputPath,
203
+ outputPath,
204
+ reason: 'Output file already exists (use --overwrite / --force)',
205
+ };
206
+ skipped.push(skip);
207
+ if (onEvent) onEvent({ type: 'entry_skipped', ...skip });
208
+ continue;
209
+ }
210
+
211
+ const res = await removeWatermark({
212
+ inputPath,
213
+ outputPath,
214
+ action: actionOpts,
215
+ dryRun,
216
+ onStderr: (chunk) => {
217
+ if (onEvent) onEvent({ type: 'provider_stderr', chunk });
218
+ },
219
+ });
220
+ const item = {
221
+ filename: path.basename(inputPath),
222
+ inputPath,
223
+ outputPath,
224
+ command: res.command,
225
+ provider: res.provider,
226
+ dryRun: res.dryRun,
227
+ };
228
+ processed.push(item);
229
+ if (onEvent) onEvent({ type: 'entry_complete', ...item });
230
+ } catch (err) {
231
+ const fail = { filename: path.basename(inputPath), inputPath, error: err };
232
+ failed.push(fail);
233
+ if (onEvent) onEvent({ type: 'entry_failed', filename: fail.filename, inputPath, error: err });
234
+ }
235
+ }
236
+
237
+ return {
238
+ input: inputDir,
239
+ mode: 'directory',
240
+ provider: actionOpts.provider,
241
+ outDir: resolvedOutDir,
242
+ dryRun,
243
+ processed,
244
+ skipped,
245
+ failed,
246
+ };
247
+ }
248
+
249
+ export {
250
+ listWatermarkProviders,
251
+ DEFAULT_WATERMARK_PROVIDER,
252
+ };
package/src/index.d.ts CHANGED
@@ -193,6 +193,78 @@ export const QUOTA_PER_SWAP: number;
193
193
  export const DEFAULT_SWAP_STATE_FILENAME: string;
194
194
  export const SWAP_STATUSES: string[];
195
195
 
196
+ // ─── Access / Privacy ────────────────────────────────────────────────────────
197
+
198
+ export type PrivacyStatus = 'private' | 'unlisted' | 'public';
199
+
200
+ export const QUOTA_PER_PRIVACY_UPDATE: number;
201
+
202
+ export function normalizePrivacyStatus(value: unknown): PrivacyStatus;
203
+ export function getVideosPrivacy(youtube: youtube_v3.Youtube, videoIds: string[]): Promise<Map<string, PrivacyStatus>>;
204
+ export function getPlaylistPrivacy(youtube: youtube_v3.Youtube, playlistId: string): Promise<PrivacyStatus>;
205
+ export function setVideoPrivacy(youtube: youtube_v3.Youtube, videoId: string, privacyStatus: PrivacyStatus): Promise<void>;
206
+ export function setPlaylistPrivacy(youtube: youtube_v3.Youtube, playlistId: string, privacyStatus: PrivacyStatus): Promise<void>;
207
+
208
+ export interface AccessVideoPlan {
209
+ videoId: string;
210
+ title?: string;
211
+ position?: number;
212
+ currentPrivacy: PrivacyStatus | null;
213
+ targetPrivacy: PrivacyStatus;
214
+ changed: boolean;
215
+ }
216
+
217
+ export interface SetPlaylistAccessOptions {
218
+ youtube?: youtube_v3.Youtube;
219
+ playlistId: string;
220
+ privacyStatus: PrivacyStatus;
221
+ videoIds?: string[];
222
+ videoSource?: 'playlist' | 'history';
223
+ albumDir?: string;
224
+ overviewHistoryPath?: string;
225
+ overviewKeys?: string[];
226
+ updateMetadata?: boolean;
227
+ metadataFile?: string;
228
+ dryRun?: boolean;
229
+ onEvent?: (event: { type: string; [key: string]: any }) => void;
230
+ }
231
+
232
+ export interface SetPlaylistAccessResult {
233
+ dryRun: boolean;
234
+ playlistId: string;
235
+ privacyStatus: PrivacyStatus;
236
+ videoSource: 'playlist' | 'history';
237
+ playlist: {
238
+ playlistId: string;
239
+ currentPrivacy: PrivacyStatus | null;
240
+ targetPrivacy: PrivacyStatus;
241
+ changed: boolean;
242
+ };
243
+ videos: AccessVideoPlan[];
244
+ changedCount: number;
245
+ estimatedQuota: number;
246
+ updated: {
247
+ playlist: boolean;
248
+ videos: Array<{ videoId: string; privacyStatus: PrivacyStatus }>;
249
+ };
250
+ }
251
+
252
+ export function setPlaylistAccess(options: SetPlaylistAccessOptions): Promise<SetPlaylistAccessResult>;
253
+
254
+ export interface SetVideosAccessOptions {
255
+ youtube?: youtube_v3.Youtube;
256
+ videoIds: string[];
257
+ privacyStatus: PrivacyStatus;
258
+ dryRun?: boolean;
259
+ }
260
+
261
+ export function setVideosAccess(options: SetVideosAccessOptions): Promise<{
262
+ dryRun: boolean;
263
+ videos: AccessVideoPlan[];
264
+ changedCount?: number;
265
+ updated?: string[];
266
+ }>;
267
+
196
268
  // ─── Metadata & Title Updates ──────────────────────────────────────────────
197
269
 
198
270
  export interface BatchUpdateMetadataOptions {
@@ -200,6 +272,11 @@ export interface BatchUpdateMetadataOptions {
200
272
  titlePrefix?: string;
201
273
  playlistPostfix?: string;
202
274
  playlistTitle?: string;
275
+ replacePattern?: string;
276
+ replaceWith?: string;
277
+ replaceLiteral?: boolean;
278
+ replaceInSnippet?: boolean;
279
+ includePlaylistId?: string;
203
280
  metadataFile?: string;
204
281
  updateLocalMetadata?: boolean;
205
282
  syncMetadata?: boolean;
@@ -260,6 +337,9 @@ export function updatePlaylistTitle(
260
337
  newTitle: string
261
338
  ): Promise<{ playlistId: string; title: string; data: any }>;
262
339
 
340
+ export function applyTextReplace(text: string, pattern: string, replacement?: string, literal?: boolean): string;
341
+ export function deepReplaceInObject(value: unknown, pattern: string, replacement?: string, literal?: boolean): unknown;
342
+
263
343
  export function batchUpdateMetadata(
264
344
  targetFolder?: string,
265
345
  options?: BatchUpdateMetadataOptions
@@ -500,12 +580,22 @@ export interface JourneyOverlayAction {
500
580
  showSidebar?: boolean;
501
581
  }
502
582
 
503
- export type VideoAction = ShortenAction | ReplaceBackgroundAction | JourneyOverlayAction | { type: string; [key: string]: any };
583
+ export interface RemoveWatermarkAction {
584
+ type: 'remove-watermark';
585
+ provider?: string;
586
+ overwrite?: boolean;
587
+ json?: boolean;
588
+ videoBitrateMbps?: number;
589
+ }
590
+
591
+ export type VideoAction = ShortenAction | ReplaceBackgroundAction | JourneyOverlayAction | RemoveWatermarkAction | { type: string; [key: string]: any };
504
592
 
505
593
  export interface JourneyConfig {
506
594
  enabled?: boolean;
507
595
  mode?: 'before-current' | 'all';
508
- position?: 'left';
596
+ position?: 'left' | 'right';
597
+ style?: 'badge' | 'panel';
598
+ badgeText?: string;
509
599
  displayDuration?: number | 'full';
510
600
  sidebarDisplayDuration?: number | 'full';
511
601
  sidebarLabel?: 'title' | 'stage' | 'both';
@@ -614,7 +704,12 @@ export function generateJourneySvg(params: {
614
704
  centerText?: string;
615
705
  showSidebar?: boolean;
616
706
  theme?: 'dark' | 'light';
617
- position?: 'left';
707
+ position?: 'left' | 'right';
708
+ style?: 'badge' | 'panel';
709
+ stage?: string;
710
+ step?: number;
711
+ totalSteps?: number;
712
+ badgeText?: string;
618
713
  }): string;
619
714
 
620
715
  export function renderSvgToPng(svgContent: string, width?: number, height?: number): Buffer;
@@ -630,6 +725,64 @@ export function applyJourneyOverlay(params: {
630
725
  onWarn?: (msg: string) => void;
631
726
  }): Promise<{ outputPath: string; duration?: number; dryRun?: boolean; command?: string }>;
632
727
 
728
+ export function isWatermarkSupportedMedia(filePath: string): boolean;
729
+ export function defaultCleanOutputPath(inputPath: string): string;
730
+ export function defaultCleanOutDir(inputDir: string): string;
731
+
732
+ export function removeWatermark(params: {
733
+ inputPath: string;
734
+ outputPath: string;
735
+ action?: Partial<RemoveWatermarkAction>;
736
+ dryRun?: boolean;
737
+ onWarn?: (msg: string) => void;
738
+ onStderr?: (chunk: string) => void;
739
+ }): Promise<{ outputPath: string; dryRun?: boolean; command: string; provider: string }>;
740
+
741
+ export function listWatermarkProviders(): Array<{ id: string; license: string; packageName: string }>;
742
+ export function getWatermarkProvider(providerId?: string): {
743
+ id: string;
744
+ license: string;
745
+ packageName: string;
746
+ remove: (options: object) => Promise<any>;
747
+ };
748
+ export function removeWatermarkWithProvider(options: {
749
+ inputPath: string;
750
+ outputPath?: string;
751
+ outDir?: string;
752
+ provider?: string;
753
+ overwrite?: boolean;
754
+ json?: boolean;
755
+ videoBitrateMbps?: number;
756
+ dryRun?: boolean;
757
+ onStderr?: (chunk: string) => void;
758
+ }): Promise<{ outputPath?: string; outDir?: string; dryRun?: boolean; command: string; exitCode?: number; stdout?: string }>;
759
+
760
+ export const DEFAULT_WATERMARK_PROVIDER: string;
761
+ export const PERMISSIVE_LICENSES: readonly string[];
762
+
763
+ export function batchRemoveWatermark(
764
+ input?: string,
765
+ options?: {
766
+ output?: string;
767
+ outDir?: string;
768
+ provider?: string;
769
+ overwrite?: boolean;
770
+ json?: boolean;
771
+ videoBitrateMbps?: number;
772
+ dryRun?: boolean;
773
+ onEvent?: (event: { type: string; [key: string]: any }) => void;
774
+ }
775
+ ): Promise<{
776
+ input: string;
777
+ mode: 'file' | 'directory';
778
+ provider: string;
779
+ dryRun: boolean;
780
+ outDir?: string;
781
+ processed: Array<object>;
782
+ skipped: Array<object>;
783
+ failed: Array<object>;
784
+ }>;
785
+
633
786
  export function runActionsPipeline(
634
787
  entry: PlannedEntry | { songKey?: string; filename: string; sourcePath: string; targetPath?: string },
635
788
  actions?: VideoAction[],
package/src/index.js CHANGED
@@ -32,6 +32,8 @@ export {
32
32
  updatePlaylistSnippet,
33
33
  updatePlaylistTitle,
34
34
  batchUpdateMetadata,
35
+ applyTextReplace,
36
+ deepReplaceInObject,
35
37
  QUOTA_PER_VIDEO_UPDATE,
36
38
  QUOTA_PER_PLAYLIST_UPDATE,
37
39
  QUOTA_PER_LIST,
@@ -81,6 +83,10 @@ export {
81
83
  generateJourneySvg,
82
84
  renderSvgToPng,
83
85
  resolveJourneyTracks,
86
+ removeWatermark,
87
+ isWatermarkSupportedMedia,
88
+ defaultCleanOutputPath,
89
+ defaultCleanOutDir,
84
90
  runActionsPipeline,
85
91
  } from './actions/index.js';
86
92
  export {
@@ -97,5 +103,41 @@ export {
97
103
  export {
98
104
  previewJourney,
99
105
  } from './cli/preview-journey.js';
106
+ export {
107
+ batchRemoveWatermark,
108
+ } from './cli/remove-watermark.js';
109
+ export {
110
+ getWatermarkProvider,
111
+ listWatermarkProviders,
112
+ removeWatermarkWithProvider,
113
+ DEFAULT_WATERMARK_PROVIDER,
114
+ PERMISSIVE_LICENSES,
115
+ } from './providers/watermark/index.js';
116
+ export {
117
+ detectOutdatedUploads,
118
+ } from './playlist/detect-outdated.js';
119
+ export {
120
+ listPlaylistItems,
121
+ reorderPlaylist,
122
+ playlistOrderMatches,
123
+ } from './playlist/reorder.js';
124
+ export {
125
+ resolveDesiredPlaylistOrder,
126
+ } from './playlist/order.js';
127
+ export {
128
+ syncPlaylist,
129
+ } from './playlist/sync.js';
130
+ export {
131
+ normalizePrivacyStatus,
132
+ getVideosPrivacy,
133
+ getPlaylistPrivacy,
134
+ setVideoPrivacy,
135
+ setPlaylistPrivacy,
136
+ QUOTA_PER_PRIVACY_UPDATE,
137
+ } from './access/privacy.js';
138
+ export {
139
+ setPlaylistAccess,
140
+ setVideosAccess,
141
+ } from './access/set-access.js';
100
142
 
101
143