@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.
Files changed (48) hide show
  1. package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.d.ts +3 -4
  2. package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.d.ts.map +1 -1
  3. package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.js +22 -32
  4. package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.js.map +1 -1
  5. package/dist/cli/commands/generate.d.ts.map +1 -1
  6. package/dist/cli/commands/generate.js +8 -2
  7. package/dist/cli/commands/generate.js.map +1 -1
  8. package/dist/cli/credentials.d.ts +81 -0
  9. package/dist/cli/credentials.d.ts.map +1 -0
  10. package/dist/cli/credentials.js +109 -0
  11. package/dist/cli/credentials.js.map +1 -0
  12. package/dist/cli/instagram/instagram-upload-strategy.d.ts +3 -1
  13. package/dist/cli/instagram/instagram-upload-strategy.d.ts.map +1 -1
  14. package/dist/cli/instagram/instagram-upload-strategy.js +37 -26
  15. package/dist/cli/instagram/instagram-upload-strategy.js.map +1 -1
  16. package/dist/cli/s3/s3-upload-strategy.d.ts +7 -1
  17. package/dist/cli/s3/s3-upload-strategy.d.ts.map +1 -1
  18. package/dist/cli/s3/s3-upload-strategy.js +142 -43
  19. package/dist/cli/s3/s3-upload-strategy.js.map +1 -1
  20. package/dist/cli/youtube/youtube-upload-strategy.d.ts +3 -0
  21. package/dist/cli/youtube/youtube-upload-strategy.d.ts.map +1 -1
  22. package/dist/cli/youtube/youtube-upload-strategy.js +22 -20
  23. package/dist/cli/youtube/youtube-upload-strategy.js.map +1 -1
  24. package/dist/html-project-parser.d.ts +8 -0
  25. package/dist/html-project-parser.d.ts.map +1 -1
  26. package/dist/html-project-parser.js +86 -5
  27. package/dist/html-project-parser.js.map +1 -1
  28. package/dist/project.d.ts +3 -1
  29. package/dist/project.d.ts.map +1 -1
  30. package/dist/project.js +6 -1
  31. package/dist/project.js.map +1 -1
  32. package/dist/time-utils.d.ts +7 -0
  33. package/dist/time-utils.d.ts.map +1 -0
  34. package/dist/time-utils.js +17 -0
  35. package/dist/time-utils.js.map +1 -0
  36. package/dist/type.d.ts +1 -1
  37. package/dist/type.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.ts +24 -55
  40. package/src/cli/commands/generate.ts +10 -2
  41. package/src/cli/credentials.ts +171 -0
  42. package/src/cli/instagram/instagram-upload-strategy.ts +38 -39
  43. package/src/cli/s3/s3-upload-strategy.ts +178 -57
  44. package/src/cli/youtube/youtube-upload-strategy.ts +22 -23
  45. package/src/html-project-parser.ts +99 -4
  46. package/src/project.ts +5 -0
  47. package/src/time-utils.ts +15 -0
  48. 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 { readFileSync, existsSync } from 'fs';
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
- // Read credentials from .auth file
34
- const authDir = resolve(projectPath, '.auth');
35
- const credentialsPath = resolve(authDir, `${upload.name}.json`);
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
- if (!existsSync(credentialsPath)) {
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
- `❌ Error: YouTube credentials not found\n\n` +
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
- let path = '';
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 = childAttrs.get('name') || '';
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 || !path) {
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
- path,
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
- path: string; // e.g. "videos/${slug}/${output}.mp4"
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