@x12i/youtube-video-uploader-cli 1.2.0 → 1.3.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,152 @@
1
+ import path from 'node:path';
2
+ import { planBatchActions } from '../batch/plan.js';
3
+ import { cloneMetadataForOutput } from '../batch/clone-metadata.js';
4
+ import { runActionsPipeline } from '../actions/index.js';
5
+
6
+ /**
7
+ * Apply actions pipeline across videos in a directory.
8
+ *
9
+ * @param {string} targetDir
10
+ * @param {object} [options]
11
+ * @param {string} [options.metadataFile]
12
+ * @param {string} [options.actionsFile]
13
+ * @param {string} [options.outputDir]
14
+ * @param {string} [options.outputSuffix]
15
+ * @param {string} [options.only]
16
+ * @param {boolean} [options.force=false]
17
+ * @param {boolean} [options.dryRun=false]
18
+ * @param {string} [options.pexelsKey]
19
+ * @param {string} [options.pixabayKey]
20
+ * @param {string} [options.cacheDir]
21
+ * @param {typeof fetch} [options.fetchFn]
22
+ * @param {(event: { type: string, [key: string]: any }) => void} [options.onEvent]
23
+ * @returns {Promise<{
24
+ * sourceDir: string,
25
+ * outputDir: string,
26
+ * total: number,
27
+ * processed: Array<object>,
28
+ * skipped: Array<object>,
29
+ * failed: Array<object>,
30
+ * dryRun: boolean
31
+ * }>}
32
+ */
33
+ export async function batchApplyActions(targetDir, options = {}) {
34
+ const plan = planBatchActions(targetDir, options);
35
+ const onEvent = options.onEvent;
36
+
37
+ if (onEvent) {
38
+ onEvent({
39
+ type: 'actions_plan',
40
+ sourceDir: plan.sourceDir,
41
+ outputDir: plan.outputDir,
42
+ hasShorten: plan.hasShorten,
43
+ total: plan.entries.length,
44
+ entries: plan.entries,
45
+ dryRun: plan.dryRun,
46
+ });
47
+ }
48
+
49
+ const results = [];
50
+ const skipped = [];
51
+ const failed = [];
52
+
53
+ const context = {
54
+ sourceDir: plan.sourceDir,
55
+ outputDir: plan.outputDir,
56
+ rawMetadata: plan.rawMetadata,
57
+ journeyConfig: plan.journeyConfig,
58
+ dryRun: plan.dryRun,
59
+ force: plan.force,
60
+ pexelsKey: options.pexelsKey,
61
+ pixabayKey: options.pixabayKey,
62
+ cacheDir: options.cacheDir,
63
+ fetchFn: options.fetchFn,
64
+ onEvent,
65
+ onWarn: (msg) => {
66
+ if (onEvent) {
67
+ onEvent({ type: 'action_warn', message: msg });
68
+ }
69
+ },
70
+ };
71
+
72
+ for (let i = 0; i < plan.entries.length; i++) {
73
+ const entry = plan.entries[i];
74
+
75
+ if (onEvent) {
76
+ onEvent({
77
+ type: 'entry_start',
78
+ index: i,
79
+ total: plan.entries.length,
80
+ songKey: entry.songKey,
81
+ filename: entry.filename,
82
+ actionsCount: entry.actions.length,
83
+ });
84
+ }
85
+
86
+ try {
87
+ const res = await runActionsPipeline(entry, entry.actions, context);
88
+ if (res.skipped) {
89
+ skipped.push(res);
90
+ if (onEvent) {
91
+ onEvent({
92
+ type: 'entry_skipped',
93
+ songKey: entry.songKey,
94
+ filename: entry.filename,
95
+ reason: res.reason,
96
+ });
97
+ }
98
+ } else {
99
+ results.push(res);
100
+ if (onEvent) {
101
+ onEvent({
102
+ type: 'entry_success',
103
+ index: i,
104
+ total: plan.entries.length,
105
+ songKey: entry.songKey,
106
+ filename: entry.filename,
107
+ outputPath: res.outputPath,
108
+ actionsApplied: res.actionsApplied,
109
+ steps: res.steps,
110
+ });
111
+ }
112
+ }
113
+ } catch (err) {
114
+ failed.push({
115
+ songKey: entry.songKey,
116
+ filename: entry.filename,
117
+ error: err,
118
+ });
119
+ if (onEvent) {
120
+ onEvent({
121
+ type: 'entry_error',
122
+ index: i,
123
+ total: plan.entries.length,
124
+ songKey: entry.songKey,
125
+ filename: entry.filename,
126
+ error: err,
127
+ });
128
+ }
129
+ }
130
+ }
131
+
132
+ if (!plan.dryRun && results.length > 0) {
133
+ cloneMetadataForOutput(plan.rawMetadata, plan.outputDir, results);
134
+ if (onEvent) {
135
+ onEvent({
136
+ type: 'metadata_cloned',
137
+ outputDir: plan.outputDir,
138
+ outputPath: path.join(plan.outputDir, 'metadata.json'),
139
+ });
140
+ }
141
+ }
142
+
143
+ return {
144
+ sourceDir: plan.sourceDir,
145
+ outputDir: plan.outputDir,
146
+ total: plan.entries.length,
147
+ processed: results,
148
+ skipped,
149
+ failed,
150
+ dryRun: plan.dryRun,
151
+ };
152
+ }
@@ -0,0 +1,106 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {
4
+ generateJourneySvg,
5
+ renderSvgToPng,
6
+ resolveJourneyTracks,
7
+ truncate,
8
+ } from '../actions/journey-overlay.js';
9
+
10
+ /**
11
+ * Preview journey overlay as a standalone PNG image.
12
+ *
13
+ * @param {string} songKey - Song key or filename to preview
14
+ * @param {object} [options]
15
+ * @param {string} [options.dir] - Target source folder containing metadata.json
16
+ * @param {string} [options.metadata] - Path to custom metadata.json
17
+ * @param {string} [options.centerText] - Override center text for preview
18
+ * @param {string} [options.output] - Output path for the PNG file
19
+ * @param {number} [options.width=1920]
20
+ * @param {number} [options.height=1080]
21
+ * @returns {Promise<{ outputPath: string, width: number, height: number, tracksCount: number }>}
22
+ */
23
+ export async function previewJourney(songKey, options = {}) {
24
+ const targetDir = path.resolve(options.dir || process.cwd());
25
+ const metadataPath = options.metadata
26
+ ? path.resolve(options.metadata)
27
+ : path.join(targetDir, 'metadata.json');
28
+
29
+ let rawMetadata = {};
30
+ if (fs.existsSync(metadataPath)) {
31
+ try {
32
+ rawMetadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
33
+ } catch (err) {
34
+ throw new Error(`Failed to parse metadata file (${metadataPath}): ${err.message}`);
35
+ }
36
+ }
37
+
38
+ const journeyConfig = {
39
+ enabled: true,
40
+ mode: 'before-current',
41
+ position: 'left',
42
+ displayDuration: 5,
43
+ sidebarDisplayDuration: undefined,
44
+ sidebarLabel: 'title',
45
+ theme: 'dark',
46
+ maxCenterChars: 60,
47
+ maxVisibleRows: 8,
48
+ ...(rawMetadata.journey || {}),
49
+ };
50
+
51
+ const songData = rawMetadata.songs?.[songKey] || {};
52
+ const journeyAction = Array.isArray(songData.actions)
53
+ ? songData.actions.find(a => a.type === 'journey-overlay')
54
+ : null;
55
+
56
+ let centerText = options.centerText;
57
+ if (centerText === undefined) {
58
+ if (Array.isArray(journeyAction?.centerConcepts) && journeyAction.centerConcepts.length > 0) {
59
+ const idx = options.conceptIndex !== undefined ? parseInt(options.conceptIndex, 10) : 0;
60
+ centerText = journeyAction.centerConcepts[idx] || journeyAction.centerConcepts[0];
61
+ } else {
62
+ centerText = journeyAction?.centerText || '';
63
+ }
64
+ }
65
+
66
+ if (centerText && centerText.length > journeyConfig.maxCenterChars) {
67
+ centerText = truncate(centerText, journeyConfig.maxCenterChars);
68
+ }
69
+
70
+ const tracks = resolveJourneyTracks({
71
+ currentSongKey: songKey,
72
+ rawMetadata,
73
+ mode: journeyConfig.mode,
74
+ maxVisibleRows: journeyConfig.maxVisibleRows,
75
+ sidebarLabel: journeyConfig.sidebarLabel,
76
+ });
77
+
78
+ const width = options.width || 1920;
79
+ const height = options.height || 1080;
80
+
81
+ const svgContent = generateJourneySvg({
82
+ width,
83
+ height,
84
+ tracks,
85
+ centerText,
86
+ showSidebar: journeyConfig.enabled !== false,
87
+ theme: journeyConfig.theme,
88
+ position: journeyConfig.position,
89
+ });
90
+
91
+ const pngBuffer = renderSvgToPng(svgContent, width, height);
92
+
93
+ const outputPath = options.output
94
+ ? path.resolve(options.output)
95
+ : path.join(targetDir, `journey-preview-${path.basename(songKey, path.extname(songKey))}.png`);
96
+
97
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
98
+ fs.writeFileSync(outputPath, pngBuffer);
99
+
100
+ return {
101
+ outputPath,
102
+ width,
103
+ height,
104
+ tracksCount: tracks.length,
105
+ };
106
+ }