@gannochenko/staticstripes 0.0.14 → 0.0.16
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/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.d.ts +3 -4
- package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.d.ts.map +1 -1
- package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.js +22 -32
- package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.js.map +1 -1
- package/dist/cli/credentials.d.ts +81 -0
- package/dist/cli/credentials.d.ts.map +1 -0
- package/dist/cli/credentials.js +109 -0
- package/dist/cli/credentials.js.map +1 -0
- package/dist/cli/instagram/instagram-upload-strategy.d.ts +3 -1
- package/dist/cli/instagram/instagram-upload-strategy.d.ts.map +1 -1
- package/dist/cli/instagram/instagram-upload-strategy.js +37 -26
- package/dist/cli/instagram/instagram-upload-strategy.js.map +1 -1
- package/dist/cli/s3/s3-upload-strategy.d.ts +3 -1
- package/dist/cli/s3/s3-upload-strategy.d.ts.map +1 -1
- package/dist/cli/s3/s3-upload-strategy.js +91 -44
- package/dist/cli/s3/s3-upload-strategy.js.map +1 -1
- package/dist/cli/youtube/youtube-upload-strategy.d.ts +3 -0
- package/dist/cli/youtube/youtube-upload-strategy.d.ts.map +1 -1
- package/dist/cli/youtube/youtube-upload-strategy.js +22 -20
- package/dist/cli/youtube/youtube-upload-strategy.js.map +1 -1
- package/dist/html-project-parser.d.ts +8 -0
- package/dist/html-project-parser.d.ts.map +1 -1
- package/dist/html-project-parser.js +71 -5
- package/dist/html-project-parser.js.map +1 -1
- package/dist/project.d.ts +3 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +6 -1
- package/dist/project.js.map +1 -1
- package/dist/type.d.ts +1 -1
- package/dist/type.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.ts +24 -55
- package/src/cli/credentials.ts +171 -0
- package/src/cli/instagram/instagram-upload-strategy.ts +38 -39
- package/src/cli/s3/s3-upload-strategy.ts +102 -57
- package/src/cli/youtube/youtube-upload-strategy.ts +22 -23
- package/src/html-project-parser.ts +84 -4
- package/src/project.ts +5 -0
- package/src/type.ts +1 -1
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
import { UploadStrategy } from '../upload-strategy';
|
|
2
2
|
import { Project } from '../../project';
|
|
3
3
|
import { Upload } from '../../type';
|
|
4
|
-
import { readFileSync, existsSync } from 'fs';
|
|
5
|
-
import { resolve } from 'path';
|
|
6
4
|
import ejs from 'ejs';
|
|
7
5
|
import { makeRequest } from '../../lib/net';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Instagram credentials format stored in .auth/<upload-name>.json
|
|
11
|
-
*/
|
|
12
|
-
interface InstagramCredentials {
|
|
13
|
-
accessToken: string; // Long-lived Instagram Graph API access token
|
|
14
|
-
igUserId: string; // Instagram User ID (not the username)
|
|
15
|
-
}
|
|
6
|
+
import { CredentialsManager, InstagramCredentials } from '../credentials';
|
|
16
7
|
|
|
17
8
|
/**
|
|
18
9
|
* Instagram upload strategy implementation
|
|
@@ -22,7 +13,7 @@ export class InstagramUploadStrategy implements UploadStrategy {
|
|
|
22
13
|
private readonly API_VERSION = 'v21.0';
|
|
23
14
|
private readonly GRAPH_API_BASE = 'https://graph.instagram.com';
|
|
24
15
|
|
|
25
|
-
constructor() {}
|
|
16
|
+
constructor(private credentialsManager?: CredentialsManager) {}
|
|
26
17
|
|
|
27
18
|
getTag(): string {
|
|
28
19
|
return 'instagram';
|
|
@@ -47,36 +38,32 @@ export class InstagramUploadStrategy implements UploadStrategy {
|
|
|
47
38
|
const { caption, shareToFeed, thumbOffset, coverUrl, videoUrl } =
|
|
48
39
|
upload.instagram;
|
|
49
40
|
|
|
50
|
-
// Load credentials from .auth/<upload-name>.json
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (!existsSync(credentialsPath)) {
|
|
55
|
-
throw new Error(
|
|
56
|
-
`❌ Error: Instagram credentials not found\n\n` +
|
|
57
|
-
`Expected location: ${credentialsPath}\n\n` +
|
|
58
|
-
`💡 Run authentication wizard:\n` +
|
|
59
|
-
` staticstripes auth --upload-name ${upload.name}\n\n` +
|
|
60
|
-
`📖 Or view detailed setup instructions:\n` +
|
|
61
|
-
` staticstripes auth-help instagram\n`,
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
console.log(`🔐 Loading credentials from: ${credentialsPath}`);
|
|
41
|
+
// Load credentials from local .auth/<upload-name>.json or global ~/.staticstripes/auth/<upload-name>.json
|
|
42
|
+
const manager =
|
|
43
|
+
this.credentialsManager ||
|
|
44
|
+
new CredentialsManager(projectPath, upload.name);
|
|
66
45
|
|
|
67
46
|
let credentials: InstagramCredentials;
|
|
68
47
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
throw new Error('Missing accessToken or igUserId');
|
|
74
|
-
}
|
|
48
|
+
credentials = manager.load<InstagramCredentials>([
|
|
49
|
+
'accessToken',
|
|
50
|
+
'igUserId',
|
|
51
|
+
]);
|
|
75
52
|
} catch (error) {
|
|
53
|
+
// Add helpful context about Instagram credentials
|
|
54
|
+
const errorMessage =
|
|
55
|
+
error instanceof Error ? error.message : String(error);
|
|
76
56
|
throw new Error(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
`
|
|
57
|
+
`${errorMessage}\n\n` +
|
|
58
|
+
`💡 Instagram credentials file should contain:\n` +
|
|
59
|
+
`{\n` +
|
|
60
|
+
` "accessToken": "YOUR_LONG_LIVED_ACCESS_TOKEN",\n` +
|
|
61
|
+
` "igUserId": "YOUR_INSTAGRAM_USER_ID"\n` +
|
|
62
|
+
`}\n\n` +
|
|
63
|
+
`📖 Run authentication wizard:\n` +
|
|
64
|
+
` staticstripes auth --upload-name ${upload.name}\n\n` +
|
|
65
|
+
`Or view detailed setup instructions:\n` +
|
|
66
|
+
` staticstripes auth-help instagram\n`,
|
|
80
67
|
);
|
|
81
68
|
}
|
|
82
69
|
|
|
@@ -108,6 +95,9 @@ export class InstagramUploadStrategy implements UploadStrategy {
|
|
|
108
95
|
// Determine title (use upload-specific title or fall back to project title)
|
|
109
96
|
const title = upload.title || project.getTitle();
|
|
110
97
|
|
|
98
|
+
// Get date from project
|
|
99
|
+
const date = project.getDate();
|
|
100
|
+
|
|
111
101
|
// Format tags with # and space-separated (Instagram style)
|
|
112
102
|
const formattedTags = upload.tags.map((tag) => `#${tag}`).join(' ');
|
|
113
103
|
|
|
@@ -116,6 +106,7 @@ export class InstagramUploadStrategy implements UploadStrategy {
|
|
|
116
106
|
|
|
117
107
|
const processedCaption = ejs.render(ejsCaption, {
|
|
118
108
|
title,
|
|
109
|
+
date,
|
|
119
110
|
tags: formattedTags,
|
|
120
111
|
});
|
|
121
112
|
|
|
@@ -329,18 +320,26 @@ export class InstagramUploadStrategy implements UploadStrategy {
|
|
|
329
320
|
throw new Error('S3 configuration missing');
|
|
330
321
|
}
|
|
331
322
|
|
|
332
|
-
const { endpoint, region, bucket,
|
|
323
|
+
const { endpoint, region, bucket, paths } = s3Upload.s3;
|
|
333
324
|
const output = project.getOutput(s3Upload.outputName);
|
|
334
325
|
if (!output) {
|
|
335
326
|
throw new Error(`Output "${s3Upload.outputName}" not found`);
|
|
336
327
|
}
|
|
337
328
|
|
|
329
|
+
// Get the file path
|
|
330
|
+
const filePath = paths.get('file');
|
|
331
|
+
if (!filePath) {
|
|
332
|
+
throw new Error('S3 upload missing "file" path');
|
|
333
|
+
}
|
|
334
|
+
|
|
338
335
|
// Interpolate path variables
|
|
339
336
|
const slug = this.slugify(project.getTitle());
|
|
340
337
|
const outputName = output.name;
|
|
341
|
-
const
|
|
338
|
+
const date = project.getDate() || '';
|
|
339
|
+
const interpolatedPath = filePath
|
|
342
340
|
.replace(/\$\{slug\}/g, slug)
|
|
343
|
-
.replace(/\$\{output\}/g, outputName)
|
|
341
|
+
.replace(/\$\{output\}/g, outputName)
|
|
342
|
+
.replace(/\$\{date\}/g, date);
|
|
344
343
|
|
|
345
344
|
// Construct URL
|
|
346
345
|
if (endpoint) {
|
|
@@ -3,22 +3,14 @@ import { Project } from '../../project';
|
|
|
3
3
|
import { Upload } from '../../type';
|
|
4
4
|
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
5
5
|
import { readFileSync, existsSync } from 'fs';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* S3 credentials format stored in .auth/<upload-name>.json
|
|
10
|
-
*/
|
|
11
|
-
interface S3Credentials {
|
|
12
|
-
accessKeyId: string;
|
|
13
|
-
secretAccessKey: string;
|
|
14
|
-
}
|
|
6
|
+
import { CredentialsManager, S3Credentials } from '../credentials';
|
|
15
7
|
|
|
16
8
|
/**
|
|
17
9
|
* S3 upload strategy implementation
|
|
18
10
|
* Supports generic S3-compatible storage (AWS S3, DigitalOcean Spaces, etc.)
|
|
19
11
|
*/
|
|
20
12
|
export class S3UploadStrategy implements UploadStrategy {
|
|
21
|
-
constructor() {}
|
|
13
|
+
constructor(private credentialsManager?: CredentialsManager) {}
|
|
22
14
|
|
|
23
15
|
getTag(): string {
|
|
24
16
|
return 's3';
|
|
@@ -40,7 +32,14 @@ export class S3UploadStrategy implements UploadStrategy {
|
|
|
40
32
|
);
|
|
41
33
|
}
|
|
42
34
|
|
|
43
|
-
const { endpoint, region, bucket,
|
|
35
|
+
const { endpoint, region, bucket, paths, acl } = upload.s3;
|
|
36
|
+
|
|
37
|
+
// Validate that we have a "file" path
|
|
38
|
+
if (!paths.has('file')) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`❌ Error: S3 upload "${upload.name}" missing required <path name="file"> element`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
44
43
|
|
|
45
44
|
// Validate ACL value if specified
|
|
46
45
|
const allowedAcls = ['private', 'public-read', 'authenticated-read'];
|
|
@@ -52,15 +51,24 @@ export class S3UploadStrategy implements UploadStrategy {
|
|
|
52
51
|
);
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
// Load credentials from .auth/<upload-name>.json
|
|
56
|
-
const
|
|
57
|
-
|
|
54
|
+
// Load credentials from local .auth/<upload-name>.json or global ~/.staticstripes/auth/<upload-name>.json
|
|
55
|
+
const manager =
|
|
56
|
+
this.credentialsManager ||
|
|
57
|
+
new CredentialsManager(projectPath, upload.name);
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
let credentials: S3Credentials;
|
|
60
|
+
try {
|
|
61
|
+
credentials = manager.load<S3Credentials>([
|
|
62
|
+
'accessKeyId',
|
|
63
|
+
'secretAccessKey',
|
|
64
|
+
]);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// Add helpful context about S3 credentials format
|
|
67
|
+
const errorMessage =
|
|
68
|
+
error instanceof Error ? error.message : String(error);
|
|
60
69
|
throw new Error(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
`💡 Create a JSON file with your S3 credentials:\n` +
|
|
70
|
+
`${errorMessage}\n\n` +
|
|
71
|
+
`💡 S3 credentials file should contain:\n` +
|
|
64
72
|
`{\n` +
|
|
65
73
|
` "accessKeyId": "YOUR_ACCESS_KEY",\n` +
|
|
66
74
|
` "secretAccessKey": "YOUR_SECRET_KEY"\n` +
|
|
@@ -71,24 +79,6 @@ export class S3UploadStrategy implements UploadStrategy {
|
|
|
71
79
|
);
|
|
72
80
|
}
|
|
73
81
|
|
|
74
|
-
console.log(`🔐 Loading credentials from: ${credentialsPath}`);
|
|
75
|
-
|
|
76
|
-
let credentials: S3Credentials;
|
|
77
|
-
try {
|
|
78
|
-
const credentialsJson = readFileSync(credentialsPath, 'utf-8');
|
|
79
|
-
credentials = JSON.parse(credentialsJson);
|
|
80
|
-
|
|
81
|
-
if (!credentials.accessKeyId || !credentials.secretAccessKey) {
|
|
82
|
-
throw new Error('Missing accessKeyId or secretAccessKey');
|
|
83
|
-
}
|
|
84
|
-
} catch (error) {
|
|
85
|
-
throw new Error(
|
|
86
|
-
`❌ Error: Failed to parse S3 credentials from ${credentialsPath}\n` +
|
|
87
|
-
`Ensure the file contains valid JSON with accessKeyId and secretAccessKey.\n` +
|
|
88
|
-
`Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
82
|
// Get the output file
|
|
93
83
|
const output = project.getOutput(upload.outputName);
|
|
94
84
|
if (!output) {
|
|
@@ -102,12 +92,23 @@ export class S3UploadStrategy implements UploadStrategy {
|
|
|
102
92
|
);
|
|
103
93
|
}
|
|
104
94
|
|
|
105
|
-
//
|
|
95
|
+
// Prepare interpolation variables
|
|
106
96
|
const slug = this.slugify(project.getTitle());
|
|
107
97
|
const outputName = output.name;
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
98
|
+
const date = project.getDate() || '';
|
|
99
|
+
const title = project.getTitle();
|
|
100
|
+
const tags = upload.tags;
|
|
101
|
+
|
|
102
|
+
// Helper function to interpolate path variables
|
|
103
|
+
const interpolatePath = (pathTemplate: string): string => {
|
|
104
|
+
return pathTemplate
|
|
105
|
+
.replace(/\$\{slug\}/g, slug)
|
|
106
|
+
.replace(/\$\{output\}/g, outputName)
|
|
107
|
+
.replace(/\$\{date\}/g, date);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Get and interpolate the file path
|
|
111
|
+
const filePath = interpolatePath(paths.get('file')!);
|
|
111
112
|
|
|
112
113
|
console.log(`\n📦 Preparing S3 upload...`);
|
|
113
114
|
console.log(` Bucket: ${bucket}`);
|
|
@@ -115,8 +116,13 @@ export class S3UploadStrategy implements UploadStrategy {
|
|
|
115
116
|
if (endpoint) {
|
|
116
117
|
console.log(` Endpoint: ${endpoint}`);
|
|
117
118
|
}
|
|
118
|
-
console.log(`
|
|
119
|
-
console.log(`
|
|
119
|
+
console.log(` File path: ${filePath}`);
|
|
120
|
+
console.log(` Video file: ${output.path}`);
|
|
121
|
+
if (paths.has('metadata')) {
|
|
122
|
+
const metadataPath = interpolatePath(paths.get('metadata')!);
|
|
123
|
+
console.log(` Metadata path: ${metadataPath}`);
|
|
124
|
+
}
|
|
125
|
+
console.log('');
|
|
120
126
|
|
|
121
127
|
// Configure S3 client
|
|
122
128
|
const s3Config: any = {
|
|
@@ -138,41 +144,80 @@ export class S3UploadStrategy implements UploadStrategy {
|
|
|
138
144
|
|
|
139
145
|
const s3Client = new S3Client(s3Config);
|
|
140
146
|
|
|
141
|
-
//
|
|
142
|
-
console.log(`📤 Uploading
|
|
147
|
+
// Upload video file
|
|
148
|
+
console.log(`📤 Uploading video file...`);
|
|
143
149
|
const fileBuffer = readFileSync(output.path);
|
|
144
150
|
|
|
145
|
-
|
|
146
|
-
const uploadParams: any = {
|
|
151
|
+
const fileUploadParams: any = {
|
|
147
152
|
Bucket: bucket,
|
|
148
|
-
Key:
|
|
153
|
+
Key: filePath,
|
|
149
154
|
Body: fileBuffer,
|
|
150
155
|
ContentType: 'video/mp4',
|
|
151
156
|
};
|
|
152
157
|
|
|
153
158
|
// Add ACL if specified in configuration
|
|
154
159
|
if (acl) {
|
|
155
|
-
|
|
160
|
+
fileUploadParams.ACL = acl;
|
|
156
161
|
}
|
|
157
162
|
|
|
158
|
-
const command = new PutObjectCommand(uploadParams);
|
|
159
|
-
|
|
160
163
|
try {
|
|
161
|
-
await s3Client.send(
|
|
164
|
+
await s3Client.send(new PutObjectCommand(fileUploadParams));
|
|
162
165
|
|
|
163
|
-
// Construct public URL
|
|
164
|
-
let
|
|
166
|
+
// Construct public URL for video
|
|
167
|
+
let fileUrl: string;
|
|
165
168
|
if (endpoint) {
|
|
166
169
|
// For DigitalOcean Spaces and similar services
|
|
167
170
|
// Format: https://{bucket}.{region}.{endpoint}/{path}
|
|
168
|
-
|
|
171
|
+
fileUrl = `https://${bucket}.${region}.${endpoint}/${filePath}`;
|
|
169
172
|
} else {
|
|
170
173
|
// For AWS S3
|
|
171
|
-
|
|
174
|
+
fileUrl = `https://${bucket}.s3.${region}.amazonaws.com/${filePath}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(`✅ Video uploaded successfully!`);
|
|
178
|
+
console.log(`🔗 Video URL: ${fileUrl}`);
|
|
179
|
+
|
|
180
|
+
// Upload metadata file if specified
|
|
181
|
+
if (paths.has('metadata')) {
|
|
182
|
+
console.log(`\n📤 Uploading metadata file...`);
|
|
183
|
+
const metadataPath = interpolatePath(paths.get('metadata')!);
|
|
184
|
+
|
|
185
|
+
// Create metadata JSON
|
|
186
|
+
const metadata = {
|
|
187
|
+
title,
|
|
188
|
+
date: date || null,
|
|
189
|
+
tags,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const metadataBuffer = Buffer.from(JSON.stringify(metadata, null, 2), 'utf-8');
|
|
193
|
+
|
|
194
|
+
const metadataUploadParams: any = {
|
|
195
|
+
Bucket: bucket,
|
|
196
|
+
Key: metadataPath,
|
|
197
|
+
Body: metadataBuffer,
|
|
198
|
+
ContentType: 'application/json',
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Add ACL if specified
|
|
202
|
+
if (acl) {
|
|
203
|
+
metadataUploadParams.ACL = acl;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await s3Client.send(new PutObjectCommand(metadataUploadParams));
|
|
207
|
+
|
|
208
|
+
// Construct public URL for metadata
|
|
209
|
+
let metadataUrl: string;
|
|
210
|
+
if (endpoint) {
|
|
211
|
+
metadataUrl = `https://${bucket}.${region}.${endpoint}/${metadataPath}`;
|
|
212
|
+
} else {
|
|
213
|
+
metadataUrl = `https://${bucket}.s3.${region}.amazonaws.com/${metadataPath}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
console.log(`✅ Metadata uploaded successfully!`);
|
|
217
|
+
console.log(`🔗 Metadata URL: ${metadataUrl}`);
|
|
172
218
|
}
|
|
173
219
|
|
|
174
|
-
console.log(
|
|
175
|
-
console.log(`🔗 Public URL: ${publicUrl}\n`);
|
|
220
|
+
console.log('');
|
|
176
221
|
} catch (error) {
|
|
177
222
|
throw new Error(
|
|
178
223
|
`❌ Error: Failed to upload to S3\n` +
|
|
@@ -4,7 +4,7 @@ import { YouTubeUpload } from '../../type';
|
|
|
4
4
|
import { resolve } from 'path';
|
|
5
5
|
import { YouTubeUploader } from '../../youtube-uploader';
|
|
6
6
|
import ejs from 'ejs';
|
|
7
|
-
import {
|
|
7
|
+
import { CredentialsManager, YouTubeCredentials } from '../credentials';
|
|
8
8
|
|
|
9
9
|
export interface YouTubeUploadOptions {
|
|
10
10
|
uploadName: string;
|
|
@@ -17,6 +17,8 @@ export interface YouTubeUploadOptions {
|
|
|
17
17
|
* YouTube upload strategy implementation
|
|
18
18
|
*/
|
|
19
19
|
export class YouTubeUploadStrategy implements UploadStrategy {
|
|
20
|
+
constructor(private credentialsManager?: CredentialsManager) {}
|
|
21
|
+
|
|
20
22
|
getTag(): string {
|
|
21
23
|
return 'youtube';
|
|
22
24
|
}
|
|
@@ -30,14 +32,23 @@ export class YouTubeUploadStrategy implements UploadStrategy {
|
|
|
30
32
|
upload: YouTubeUpload,
|
|
31
33
|
projectPath: string,
|
|
32
34
|
): Promise<void> {
|
|
33
|
-
//
|
|
34
|
-
const
|
|
35
|
-
|
|
35
|
+
// Load credentials from local .auth/<upload-name>.json or global ~/.staticstripes/auth/<upload-name>.json
|
|
36
|
+
const manager =
|
|
37
|
+
this.credentialsManager ||
|
|
38
|
+
new CredentialsManager(projectPath, upload.name);
|
|
36
39
|
|
|
37
|
-
|
|
40
|
+
let credentials: YouTubeCredentials;
|
|
41
|
+
try {
|
|
42
|
+
credentials = manager.load<YouTubeCredentials>([
|
|
43
|
+
'clientId',
|
|
44
|
+
'clientSecret',
|
|
45
|
+
]);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
// Add helpful context about YouTube credentials
|
|
48
|
+
const errorMessage =
|
|
49
|
+
error instanceof Error ? error.message : String(error);
|
|
38
50
|
throw new Error(
|
|
39
|
-
|
|
40
|
-
`Expected location: ${credentialsPath}\n\n` +
|
|
51
|
+
`${errorMessage}\n\n` +
|
|
41
52
|
`💡 Run authentication wizard:\n` +
|
|
42
53
|
` staticstripes auth --upload-name ${upload.name}\n\n` +
|
|
43
54
|
`📖 Or view setup instructions:\n` +
|
|
@@ -45,22 +56,6 @@ export class YouTubeUploadStrategy implements UploadStrategy {
|
|
|
45
56
|
);
|
|
46
57
|
}
|
|
47
58
|
|
|
48
|
-
let credentials: { clientId?: string; clientSecret?: string };
|
|
49
|
-
try {
|
|
50
|
-
const credentialsJson = readFileSync(credentialsPath, 'utf-8');
|
|
51
|
-
credentials = JSON.parse(credentialsJson);
|
|
52
|
-
|
|
53
|
-
if (!credentials.clientId || !credentials.clientSecret) {
|
|
54
|
-
throw new Error('Missing clientId or clientSecret');
|
|
55
|
-
}
|
|
56
|
-
} catch (error) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
`❌ Error: Failed to parse YouTube credentials from ${credentialsPath}\n` +
|
|
59
|
-
`Ensure the file contains clientId and clientSecret.\n` +
|
|
60
|
-
`Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
59
|
// Delegate to existing handler
|
|
65
60
|
await handleYouTubeUpload(project, {
|
|
66
61
|
uploadName: upload.name,
|
|
@@ -116,6 +111,9 @@ export async function handleYouTubeUpload(
|
|
|
116
111
|
const title = upload.title || project.getTitle();
|
|
117
112
|
console.log(`📝 Title: ${title}\n`);
|
|
118
113
|
|
|
114
|
+
// Get date from project
|
|
115
|
+
const date = project.getDate();
|
|
116
|
+
|
|
119
117
|
// Build the project to populate fragment times (needed for timecodes)
|
|
120
118
|
console.log('🔨 Building project to calculate timecodes...');
|
|
121
119
|
await project.build(upload.outputName);
|
|
@@ -134,6 +132,7 @@ export async function handleYouTubeUpload(
|
|
|
134
132
|
|
|
135
133
|
const processedDescription = ejs.render(ejsDescription, {
|
|
136
134
|
title,
|
|
135
|
+
date,
|
|
137
136
|
tags: formattedTags,
|
|
138
137
|
timecodes: timecodes.join('\n'),
|
|
139
138
|
});
|
|
@@ -105,6 +105,7 @@ export class HTMLProjectParser {
|
|
|
105
105
|
const outputs = this.processOutputs();
|
|
106
106
|
const ffmpegOptions = this.processFfmpegOptions();
|
|
107
107
|
const title = this.processTitle();
|
|
108
|
+
const date = this.processDate();
|
|
108
109
|
const globalTags = this.processGlobalTags();
|
|
109
110
|
const uploads = this.processUploads(title, globalTags);
|
|
110
111
|
const sequences = this.processSequences(assets);
|
|
@@ -118,6 +119,7 @@ export class HTMLProjectParser {
|
|
|
118
119
|
uploads,
|
|
119
120
|
aiProviders,
|
|
120
121
|
title,
|
|
122
|
+
date,
|
|
121
123
|
cssText,
|
|
122
124
|
this.projectPath,
|
|
123
125
|
);
|
|
@@ -894,7 +896,7 @@ export class HTMLProjectParser {
|
|
|
894
896
|
let endpoint: string | undefined;
|
|
895
897
|
let region = '';
|
|
896
898
|
let bucket = '';
|
|
897
|
-
|
|
899
|
+
const paths = new Map<string, string>();
|
|
898
900
|
let acl: string | undefined;
|
|
899
901
|
|
|
900
902
|
if ('children' in element && element.children) {
|
|
@@ -917,7 +919,23 @@ export class HTMLProjectParser {
|
|
|
917
919
|
break;
|
|
918
920
|
}
|
|
919
921
|
case 'path': {
|
|
920
|
-
path
|
|
922
|
+
// Extract path name from name attribute
|
|
923
|
+
const pathName = childAttrs.get('name') || 'file';
|
|
924
|
+
|
|
925
|
+
// Extract path value from text content
|
|
926
|
+
let pathValue = '';
|
|
927
|
+
if ('children' in childElement && childElement.children) {
|
|
928
|
+
for (const textNode of childElement.children) {
|
|
929
|
+
if (textNode.type === 'text' && 'data' in textNode) {
|
|
930
|
+
pathValue += textNode.data;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
pathValue = pathValue.trim();
|
|
936
|
+
if (pathValue) {
|
|
937
|
+
paths.set(pathName, pathValue);
|
|
938
|
+
}
|
|
921
939
|
break;
|
|
922
940
|
}
|
|
923
941
|
case 'acl': {
|
|
@@ -930,7 +948,7 @@ export class HTMLProjectParser {
|
|
|
930
948
|
}
|
|
931
949
|
|
|
932
950
|
// Validate required fields
|
|
933
|
-
if (!region || !bucket ||
|
|
951
|
+
if (!region || !bucket || paths.size === 0) {
|
|
934
952
|
console.warn(`S3 upload "${name}" missing required fields (region, bucket, or path)`);
|
|
935
953
|
return null;
|
|
936
954
|
}
|
|
@@ -949,7 +967,7 @@ export class HTMLProjectParser {
|
|
|
949
967
|
endpoint,
|
|
950
968
|
region,
|
|
951
969
|
bucket,
|
|
952
|
-
|
|
970
|
+
paths,
|
|
953
971
|
acl,
|
|
954
972
|
},
|
|
955
973
|
};
|
|
@@ -1242,6 +1260,31 @@ export class HTMLProjectParser {
|
|
|
1242
1260
|
return title.trim() || 'Untitled Project';
|
|
1243
1261
|
}
|
|
1244
1262
|
|
|
1263
|
+
/**
|
|
1264
|
+
* Processes the date from the parsed HTML
|
|
1265
|
+
*/
|
|
1266
|
+
private processDate(): string | undefined {
|
|
1267
|
+
const dateElements = this.findDateElements();
|
|
1268
|
+
|
|
1269
|
+
if (dateElements.length === 0) {
|
|
1270
|
+
return undefined;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// Get text content from first date element
|
|
1274
|
+
const dateElement = dateElements[0];
|
|
1275
|
+
let date = '';
|
|
1276
|
+
|
|
1277
|
+
if ('children' in dateElement && dateElement.children) {
|
|
1278
|
+
for (const textNode of dateElement.children) {
|
|
1279
|
+
if (textNode.type === 'text' && 'data' in textNode) {
|
|
1280
|
+
date += textNode.data;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
return date.trim() || undefined;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1245
1288
|
/**
|
|
1246
1289
|
* Finds all title elements in the HTML (top-level only, not inside uploads)
|
|
1247
1290
|
*/
|
|
@@ -1276,6 +1319,43 @@ export class HTMLProjectParser {
|
|
|
1276
1319
|
return results;
|
|
1277
1320
|
}
|
|
1278
1321
|
|
|
1322
|
+
/**
|
|
1323
|
+
* Finds all date elements in the HTML (top-level only)
|
|
1324
|
+
*/
|
|
1325
|
+
private findDateElements(): Element[] {
|
|
1326
|
+
const results: Element[] = [];
|
|
1327
|
+
|
|
1328
|
+
const traverse = (node: ASTNode, insideProject: boolean = false) => {
|
|
1329
|
+
if (node.type === 'tag') {
|
|
1330
|
+
const element = node as Element;
|
|
1331
|
+
|
|
1332
|
+
// Find top-level <date> tags (not inside <project>, <uploads>, etc.)
|
|
1333
|
+
if (element.name === 'date' && !insideProject) {
|
|
1334
|
+
results.push(element);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// Mark that we're inside a project/uploads/outputs section
|
|
1338
|
+
const isProjectSection =
|
|
1339
|
+
element.name === 'project' ||
|
|
1340
|
+
element.name === 'uploads' ||
|
|
1341
|
+
element.name === 'outputs';
|
|
1342
|
+
|
|
1343
|
+
if ('children' in node && node.children) {
|
|
1344
|
+
for (const child of node.children) {
|
|
1345
|
+
traverse(child, insideProject || isProjectSection);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
} else if ('children' in node && node.children) {
|
|
1349
|
+
for (const child of node.children) {
|
|
1350
|
+
traverse(child, insideProject);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
traverse(this.html.ast);
|
|
1356
|
+
return results;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1279
1359
|
/**
|
|
1280
1360
|
* Finds all uploads elements in the HTML
|
|
1281
1361
|
*/
|
package/src/project.ts
CHANGED
|
@@ -26,6 +26,7 @@ export class Project {
|
|
|
26
26
|
private uploads: Map<string, Upload>,
|
|
27
27
|
private aiProviders: Map<string, AIProvider>,
|
|
28
28
|
private title: string,
|
|
29
|
+
private date: string | undefined,
|
|
29
30
|
private cssText: string,
|
|
30
31
|
private projectPath: string,
|
|
31
32
|
) {
|
|
@@ -141,6 +142,10 @@ export class Project {
|
|
|
141
142
|
return this.title;
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
public getDate(): string | undefined {
|
|
146
|
+
return this.date;
|
|
147
|
+
}
|
|
148
|
+
|
|
144
149
|
public getCssText(): string {
|
|
145
150
|
return this.cssText;
|
|
146
151
|
}
|
package/src/type.ts
CHANGED
|
@@ -101,7 +101,7 @@ export type Upload = {
|
|
|
101
101
|
endpoint?: string; // e.g. "digitaloceanspaces.com"
|
|
102
102
|
region: string; // e.g. "ams3", "us-east-1"
|
|
103
103
|
bucket: string; // e.g. "my-bucket"
|
|
104
|
-
|
|
104
|
+
paths: Map<string, string>; // e.g. Map { "file" => "videos/${slug}/${output}.mp4", "metadata" => "videos/${slug}/metadata.json" }
|
|
105
105
|
acl?: string; // e.g. "public-read", "private"
|
|
106
106
|
};
|
|
107
107
|
// Instagram-specific configuration
|