@gannochenko/staticstripes 0.0.15 → 0.0.17
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/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +8 -2
- package/dist/cli/commands/generate.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 +7 -1
- package/dist/cli/s3/s3-upload-strategy.d.ts.map +1 -1
- package/dist/cli/s3/s3-upload-strategy.js +142 -43
- 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 +86 -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/time-utils.d.ts +7 -0
- package/dist/time-utils.d.ts.map +1 -0
- package/dist/time-utils.js +17 -0
- package/dist/time-utils.js.map +1 -0
- package/dist/type.d.ts +1 -1
- package/dist/type.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.ts +24 -55
- package/src/cli/commands/generate.ts +10 -2
- 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 +178 -57
- package/src/cli/youtube/youtube-upload-strategy.ts +22 -23
- package/src/html-project-parser.ts +99 -4
- package/src/project.ts +5 -0
- package/src/time-utils.ts +15 -0
- package/src/type.ts +1 -1
|
@@ -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,8 +896,9 @@ 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;
|
|
901
|
+
let thumbnailTimecode: number | undefined;
|
|
899
902
|
|
|
900
903
|
if ('children' in element && element.children) {
|
|
901
904
|
for (const child of element.children) {
|
|
@@ -917,20 +920,49 @@ export class HTMLProjectParser {
|
|
|
917
920
|
break;
|
|
918
921
|
}
|
|
919
922
|
case 'path': {
|
|
920
|
-
path
|
|
923
|
+
// Extract path name from name attribute
|
|
924
|
+
const pathName = childAttrs.get('name') || 'file';
|
|
925
|
+
|
|
926
|
+
// Extract path value from text content
|
|
927
|
+
let pathValue = '';
|
|
928
|
+
if ('children' in childElement && childElement.children) {
|
|
929
|
+
for (const textNode of childElement.children) {
|
|
930
|
+
if (textNode.type === 'text' && 'data' in textNode) {
|
|
931
|
+
pathValue += textNode.data;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
pathValue = pathValue.trim();
|
|
937
|
+
if (pathValue) {
|
|
938
|
+
paths.set(pathName, pathValue);
|
|
939
|
+
}
|
|
921
940
|
break;
|
|
922
941
|
}
|
|
923
942
|
case 'acl': {
|
|
924
943
|
acl = childAttrs.get('name');
|
|
925
944
|
break;
|
|
926
945
|
}
|
|
946
|
+
case 'thumbnail': {
|
|
947
|
+
const timecode = childAttrs.get('data-timecode');
|
|
948
|
+
if (timecode) {
|
|
949
|
+
// Parse timecode (e.g., "1000ms" or "1s")
|
|
950
|
+
const match = timecode.match(/^(\d+(?:\.\d+)?)(ms|s)$/);
|
|
951
|
+
if (match) {
|
|
952
|
+
const value = parseFloat(match[1]);
|
|
953
|
+
const unit = match[2];
|
|
954
|
+
thumbnailTimecode = unit === 's' ? value * 1000 : value;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
927
959
|
}
|
|
928
960
|
}
|
|
929
961
|
}
|
|
930
962
|
}
|
|
931
963
|
|
|
932
964
|
// Validate required fields
|
|
933
|
-
if (!region || !bucket ||
|
|
965
|
+
if (!region || !bucket || paths.size === 0) {
|
|
934
966
|
console.warn(`S3 upload "${name}" missing required fields (region, bucket, or path)`);
|
|
935
967
|
return null;
|
|
936
968
|
}
|
|
@@ -945,11 +977,12 @@ export class HTMLProjectParser {
|
|
|
945
977
|
category: '',
|
|
946
978
|
language: '',
|
|
947
979
|
description: '',
|
|
980
|
+
thumbnailTimecode,
|
|
948
981
|
s3: {
|
|
949
982
|
endpoint,
|
|
950
983
|
region,
|
|
951
984
|
bucket,
|
|
952
|
-
|
|
985
|
+
paths,
|
|
953
986
|
acl,
|
|
954
987
|
},
|
|
955
988
|
};
|
|
@@ -1242,6 +1275,31 @@ export class HTMLProjectParser {
|
|
|
1242
1275
|
return title.trim() || 'Untitled Project';
|
|
1243
1276
|
}
|
|
1244
1277
|
|
|
1278
|
+
/**
|
|
1279
|
+
* Processes the date from the parsed HTML
|
|
1280
|
+
*/
|
|
1281
|
+
private processDate(): string | undefined {
|
|
1282
|
+
const dateElements = this.findDateElements();
|
|
1283
|
+
|
|
1284
|
+
if (dateElements.length === 0) {
|
|
1285
|
+
return undefined;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// Get text content from first date element
|
|
1289
|
+
const dateElement = dateElements[0];
|
|
1290
|
+
let date = '';
|
|
1291
|
+
|
|
1292
|
+
if ('children' in dateElement && dateElement.children) {
|
|
1293
|
+
for (const textNode of dateElement.children) {
|
|
1294
|
+
if (textNode.type === 'text' && 'data' in textNode) {
|
|
1295
|
+
date += textNode.data;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
return date.trim() || undefined;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1245
1303
|
/**
|
|
1246
1304
|
* Finds all title elements in the HTML (top-level only, not inside uploads)
|
|
1247
1305
|
*/
|
|
@@ -1276,6 +1334,43 @@ export class HTMLProjectParser {
|
|
|
1276
1334
|
return results;
|
|
1277
1335
|
}
|
|
1278
1336
|
|
|
1337
|
+
/**
|
|
1338
|
+
* Finds all date elements in the HTML (top-level only)
|
|
1339
|
+
*/
|
|
1340
|
+
private findDateElements(): Element[] {
|
|
1341
|
+
const results: Element[] = [];
|
|
1342
|
+
|
|
1343
|
+
const traverse = (node: ASTNode, insideProject: boolean = false) => {
|
|
1344
|
+
if (node.type === 'tag') {
|
|
1345
|
+
const element = node as Element;
|
|
1346
|
+
|
|
1347
|
+
// Find top-level <date> tags (not inside <project>, <uploads>, etc.)
|
|
1348
|
+
if (element.name === 'date' && !insideProject) {
|
|
1349
|
+
results.push(element);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// Mark that we're inside a project/uploads/outputs section
|
|
1353
|
+
const isProjectSection =
|
|
1354
|
+
element.name === 'project' ||
|
|
1355
|
+
element.name === 'uploads' ||
|
|
1356
|
+
element.name === 'outputs';
|
|
1357
|
+
|
|
1358
|
+
if ('children' in node && node.children) {
|
|
1359
|
+
for (const child of node.children) {
|
|
1360
|
+
traverse(child, insideProject || isProjectSection);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
} else if ('children' in node && node.children) {
|
|
1364
|
+
for (const child of node.children) {
|
|
1365
|
+
traverse(child, insideProject);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
traverse(this.html.ast);
|
|
1371
|
+
return results;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1279
1374
|
/**
|
|
1280
1375
|
* Finds all uploads elements in the HTML
|
|
1281
1376
|
*/
|
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
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats milliseconds to HH:MM:SS format
|
|
3
|
+
* @param ms - Time in milliseconds
|
|
4
|
+
* @returns Formatted time string (e.g., "01:23:45" or "00:05:30")
|
|
5
|
+
*/
|
|
6
|
+
export function formatDuration(ms: number): string {
|
|
7
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
8
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
9
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
10
|
+
const seconds = totalSeconds % 60;
|
|
11
|
+
|
|
12
|
+
const pad = (num: number) => num.toString().padStart(2, '0');
|
|
13
|
+
|
|
14
|
+
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
|
|
15
|
+
}
|
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
|