@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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generic credentials interface for upload services
|
|
7
|
+
*/
|
|
8
|
+
export interface Credentials {
|
|
9
|
+
[key: string]: any;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* S3-specific credentials format
|
|
14
|
+
*/
|
|
15
|
+
export interface S3Credentials extends Credentials {
|
|
16
|
+
accessKeyId: string;
|
|
17
|
+
secretAccessKey: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* YouTube-specific credentials format
|
|
22
|
+
*/
|
|
23
|
+
export interface YouTubeCredentials extends Credentials {
|
|
24
|
+
clientId: string;
|
|
25
|
+
clientSecret: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Instagram-specific credentials format
|
|
30
|
+
*/
|
|
31
|
+
export interface InstagramCredentials extends Credentials {
|
|
32
|
+
accessToken: string;
|
|
33
|
+
igUserId: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* AI Music API credentials format
|
|
38
|
+
*/
|
|
39
|
+
export interface MusicAPICredentials extends Credentials {
|
|
40
|
+
apiKey: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Credentials manager that handles loading credentials from local or global locations
|
|
45
|
+
*
|
|
46
|
+
* Search priority:
|
|
47
|
+
* 1. Local: <projectPath>/.auth/<credentialName>.json
|
|
48
|
+
* 2. Global: $HOME/.staticstripes/auth/<credentialName>.json
|
|
49
|
+
*/
|
|
50
|
+
export class CredentialsManager {
|
|
51
|
+
private projectPath: string;
|
|
52
|
+
private credentialName: string;
|
|
53
|
+
|
|
54
|
+
constructor(projectPath: string, credentialName: string) {
|
|
55
|
+
this.projectPath = projectPath;
|
|
56
|
+
this.credentialName = credentialName;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the local credentials path
|
|
61
|
+
*/
|
|
62
|
+
private getLocalPath(): string {
|
|
63
|
+
const authDir = resolve(this.projectPath, '.auth');
|
|
64
|
+
return resolve(authDir, `${this.credentialName}.json`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the global credentials path
|
|
69
|
+
* Works cross-platform (Windows, macOS, Linux)
|
|
70
|
+
*/
|
|
71
|
+
private getGlobalPath(): string {
|
|
72
|
+
const homeDir = homedir();
|
|
73
|
+
const globalAuthDir = resolve(homeDir, '.staticstripes', 'auth');
|
|
74
|
+
return resolve(globalAuthDir, `${this.credentialName}.json`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Find and return the credentials file path
|
|
79
|
+
* Returns the path and whether it was found locally or globally
|
|
80
|
+
*/
|
|
81
|
+
private findCredentialsPath(): { path: string; location: 'local' | 'global' } | null {
|
|
82
|
+
const localPath = this.getLocalPath();
|
|
83
|
+
if (existsSync(localPath)) {
|
|
84
|
+
return { path: localPath, location: 'local' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const globalPath = this.getGlobalPath();
|
|
88
|
+
if (existsSync(globalPath)) {
|
|
89
|
+
return { path: globalPath, location: 'global' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Load credentials from local or global location
|
|
97
|
+
*
|
|
98
|
+
* @param requiredFields - Array of required field names to validate
|
|
99
|
+
* @returns The credentials object
|
|
100
|
+
* @throws Error if credentials not found or invalid
|
|
101
|
+
*/
|
|
102
|
+
load<T extends Credentials>(requiredFields: string[] = []): T {
|
|
103
|
+
const found = this.findCredentialsPath();
|
|
104
|
+
|
|
105
|
+
if (!found) {
|
|
106
|
+
const localPath = this.getLocalPath();
|
|
107
|
+
const globalPath = this.getGlobalPath();
|
|
108
|
+
|
|
109
|
+
throw new Error(
|
|
110
|
+
`ā Error: Credentials not found for "${this.credentialName}"\n\n` +
|
|
111
|
+
`Searched in:\n` +
|
|
112
|
+
` 1. Local: ${localPath}\n` +
|
|
113
|
+
` 2. Global: ${globalPath}\n\n` +
|
|
114
|
+
`š” Create a JSON file in one of these locations with your credentials.\n` +
|
|
115
|
+
` The global location is useful for sharing credentials across projects.\n`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const { path: credentialsPath, location } = found;
|
|
120
|
+
console.log(`š Loading credentials from ${location} storage: ${credentialsPath}`);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const credentialsJson = readFileSync(credentialsPath, 'utf-8');
|
|
124
|
+
const credentials = JSON.parse(credentialsJson) as T;
|
|
125
|
+
|
|
126
|
+
// Validate required fields
|
|
127
|
+
const missingFields = requiredFields.filter(
|
|
128
|
+
(field) => !credentials[field],
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (missingFields.length > 0) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Missing required fields: ${missingFields.join(', ')}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return credentials;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`ā Error: Failed to parse credentials from ${credentialsPath}\n` +
|
|
141
|
+
`Ensure the file contains valid JSON.\n` +
|
|
142
|
+
`Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if credentials exist (locally or globally)
|
|
149
|
+
*/
|
|
150
|
+
exists(): boolean {
|
|
151
|
+
return this.findCredentialsPath() !== null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get information about where credentials are stored
|
|
156
|
+
*/
|
|
157
|
+
getInfo(): {
|
|
158
|
+
localPath: string;
|
|
159
|
+
globalPath: string;
|
|
160
|
+
exists: boolean;
|
|
161
|
+
location?: 'local' | 'global';
|
|
162
|
+
} {
|
|
163
|
+
const found = this.findCredentialsPath();
|
|
164
|
+
return {
|
|
165
|
+
localPath: this.getLocalPath(),
|
|
166
|
+
globalPath: this.getGlobalPath(),
|
|
167
|
+
exists: found !== null,
|
|
168
|
+
location: found?.location,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -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) {
|
|
@@ -2,23 +2,20 @@ import { UploadStrategy } from '../upload-strategy';
|
|
|
2
2
|
import { Project } from '../../project';
|
|
3
3
|
import { Upload } from '../../type';
|
|
4
4
|
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
5
|
-
import { readFileSync, existsSync } from 'fs';
|
|
6
|
-
import {
|
|
5
|
+
import { readFileSync, existsSync, mkdirSync } from 'fs';
|
|
6
|
+
import { CredentialsManager, S3Credentials } from '../credentials';
|
|
7
|
+
import { exec } from 'child_process';
|
|
8
|
+
import { promisify } from 'util';
|
|
9
|
+
import { dirname, resolve } from 'path';
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
* S3 credentials format stored in .auth/<upload-name>.json
|
|
10
|
-
*/
|
|
11
|
-
interface S3Credentials {
|
|
12
|
-
accessKeyId: string;
|
|
13
|
-
secretAccessKey: string;
|
|
14
|
-
}
|
|
11
|
+
const execAsync = promisify(exec);
|
|
15
12
|
|
|
16
13
|
/**
|
|
17
14
|
* S3 upload strategy implementation
|
|
18
15
|
* Supports generic S3-compatible storage (AWS S3, DigitalOcean Spaces, etc.)
|
|
19
16
|
*/
|
|
20
17
|
export class S3UploadStrategy implements UploadStrategy {
|
|
21
|
-
constructor() {}
|
|
18
|
+
constructor(private credentialsManager?: CredentialsManager) {}
|
|
22
19
|
|
|
23
20
|
getTag(): string {
|
|
24
21
|
return 's3';
|
|
@@ -40,7 +37,14 @@ export class S3UploadStrategy implements UploadStrategy {
|
|
|
40
37
|
);
|
|
41
38
|
}
|
|
42
39
|
|
|
43
|
-
const { endpoint, region, bucket,
|
|
40
|
+
const { endpoint, region, bucket, paths, acl } = upload.s3;
|
|
41
|
+
|
|
42
|
+
// Validate that we have a "file" path
|
|
43
|
+
if (!paths.has('file')) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`ā Error: S3 upload "${upload.name}" missing required <path name="file"> element`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
44
48
|
|
|
45
49
|
// Validate ACL value if specified
|
|
46
50
|
const allowedAcls = ['private', 'public-read', 'authenticated-read'];
|
|
@@ -52,15 +56,24 @@ export class S3UploadStrategy implements UploadStrategy {
|
|
|
52
56
|
);
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
// Load credentials from .auth/<upload-name>.json
|
|
56
|
-
const
|
|
57
|
-
|
|
59
|
+
// Load credentials from local .auth/<upload-name>.json or global ~/.staticstripes/auth/<upload-name>.json
|
|
60
|
+
const manager =
|
|
61
|
+
this.credentialsManager ||
|
|
62
|
+
new CredentialsManager(projectPath, upload.name);
|
|
58
63
|
|
|
59
|
-
|
|
64
|
+
let credentials: S3Credentials;
|
|
65
|
+
try {
|
|
66
|
+
credentials = manager.load<S3Credentials>([
|
|
67
|
+
'accessKeyId',
|
|
68
|
+
'secretAccessKey',
|
|
69
|
+
]);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// Add helpful context about S3 credentials format
|
|
72
|
+
const errorMessage =
|
|
73
|
+
error instanceof Error ? error.message : String(error);
|
|
60
74
|
throw new Error(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
`š” Create a JSON file with your S3 credentials:\n` +
|
|
75
|
+
`${errorMessage}\n\n` +
|
|
76
|
+
`š” S3 credentials file should contain:\n` +
|
|
64
77
|
`{\n` +
|
|
65
78
|
` "accessKeyId": "YOUR_ACCESS_KEY",\n` +
|
|
66
79
|
` "secretAccessKey": "YOUR_SECRET_KEY"\n` +
|
|
@@ -71,24 +84,6 @@ export class S3UploadStrategy implements UploadStrategy {
|
|
|
71
84
|
);
|
|
72
85
|
}
|
|
73
86
|
|
|
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
87
|
// Get the output file
|
|
93
88
|
const output = project.getOutput(upload.outputName);
|
|
94
89
|
if (!output) {
|
|
@@ -102,12 +97,23 @@ export class S3UploadStrategy implements UploadStrategy {
|
|
|
102
97
|
);
|
|
103
98
|
}
|
|
104
99
|
|
|
105
|
-
//
|
|
100
|
+
// Prepare interpolation variables
|
|
106
101
|
const slug = this.slugify(project.getTitle());
|
|
107
102
|
const outputName = output.name;
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
const date = project.getDate() || '';
|
|
104
|
+
const title = project.getTitle();
|
|
105
|
+
const tags = upload.tags;
|
|
106
|
+
|
|
107
|
+
// Helper function to interpolate path variables
|
|
108
|
+
const interpolatePath = (pathTemplate: string): string => {
|
|
109
|
+
return pathTemplate
|
|
110
|
+
.replace(/\$\{slug\}/g, slug)
|
|
111
|
+
.replace(/\$\{output\}/g, outputName)
|
|
112
|
+
.replace(/\$\{date\}/g, date);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Get and interpolate the file path
|
|
116
|
+
const filePath = interpolatePath(paths.get('file')!);
|
|
111
117
|
|
|
112
118
|
console.log(`\nš¦ Preparing S3 upload...`);
|
|
113
119
|
console.log(` Bucket: ${bucket}`);
|
|
@@ -115,8 +121,18 @@ export class S3UploadStrategy implements UploadStrategy {
|
|
|
115
121
|
if (endpoint) {
|
|
116
122
|
console.log(` Endpoint: ${endpoint}`);
|
|
117
123
|
}
|
|
118
|
-
console.log(`
|
|
119
|
-
console.log(`
|
|
124
|
+
console.log(` File path: ${filePath}`);
|
|
125
|
+
console.log(` Video file: ${output.path}`);
|
|
126
|
+
if (paths.has('metadata')) {
|
|
127
|
+
const metadataPath = interpolatePath(paths.get('metadata')!);
|
|
128
|
+
console.log(` Metadata path: ${metadataPath}`);
|
|
129
|
+
}
|
|
130
|
+
if (upload.thumbnailTimecode !== undefined && paths.has('thumbnail')) {
|
|
131
|
+
const thumbnailPath = interpolatePath(paths.get('thumbnail')!);
|
|
132
|
+
console.log(` Thumbnail path: ${thumbnailPath}`);
|
|
133
|
+
console.log(` Thumbnail timecode: ${upload.thumbnailTimecode}ms`);
|
|
134
|
+
}
|
|
135
|
+
console.log('');
|
|
120
136
|
|
|
121
137
|
// Configure S3 client
|
|
122
138
|
const s3Config: any = {
|
|
@@ -138,41 +154,126 @@ export class S3UploadStrategy implements UploadStrategy {
|
|
|
138
154
|
|
|
139
155
|
const s3Client = new S3Client(s3Config);
|
|
140
156
|
|
|
141
|
-
//
|
|
142
|
-
console.log(`š¤ Uploading
|
|
157
|
+
// Upload video file
|
|
158
|
+
console.log(`š¤ Uploading video file...`);
|
|
143
159
|
const fileBuffer = readFileSync(output.path);
|
|
144
160
|
|
|
145
|
-
|
|
146
|
-
const uploadParams: any = {
|
|
161
|
+
const fileUploadParams: any = {
|
|
147
162
|
Bucket: bucket,
|
|
148
|
-
Key:
|
|
163
|
+
Key: filePath,
|
|
149
164
|
Body: fileBuffer,
|
|
150
165
|
ContentType: 'video/mp4',
|
|
151
166
|
};
|
|
152
167
|
|
|
153
168
|
// Add ACL if specified in configuration
|
|
154
169
|
if (acl) {
|
|
155
|
-
|
|
170
|
+
fileUploadParams.ACL = acl;
|
|
156
171
|
}
|
|
157
172
|
|
|
158
|
-
const command = new PutObjectCommand(uploadParams);
|
|
159
|
-
|
|
160
173
|
try {
|
|
161
|
-
await s3Client.send(
|
|
174
|
+
await s3Client.send(new PutObjectCommand(fileUploadParams));
|
|
162
175
|
|
|
163
|
-
// Construct public URL
|
|
164
|
-
let
|
|
176
|
+
// Construct public URL for video
|
|
177
|
+
let fileUrl: string;
|
|
165
178
|
if (endpoint) {
|
|
166
179
|
// For DigitalOcean Spaces and similar services
|
|
167
180
|
// Format: https://{bucket}.{region}.{endpoint}/{path}
|
|
168
|
-
|
|
181
|
+
fileUrl = `https://${bucket}.${region}.${endpoint}/${filePath}`;
|
|
169
182
|
} else {
|
|
170
183
|
// For AWS S3
|
|
171
|
-
|
|
184
|
+
fileUrl = `https://${bucket}.s3.${region}.amazonaws.com/${filePath}`;
|
|
172
185
|
}
|
|
173
186
|
|
|
174
|
-
console.log(
|
|
175
|
-
console.log(`š
|
|
187
|
+
console.log(`ā
Video uploaded successfully!`);
|
|
188
|
+
console.log(`š Video URL: ${fileUrl}`);
|
|
189
|
+
|
|
190
|
+
// Upload thumbnail if specified
|
|
191
|
+
if (upload.thumbnailTimecode !== undefined && paths.has('thumbnail')) {
|
|
192
|
+
console.log(
|
|
193
|
+
`\nš¼ļø Extracting thumbnail at ${upload.thumbnailTimecode}ms...`,
|
|
194
|
+
);
|
|
195
|
+
const thumbnailPath = resolve(
|
|
196
|
+
dirname(projectPath),
|
|
197
|
+
'.cache',
|
|
198
|
+
'thumbnail.jpeg',
|
|
199
|
+
);
|
|
200
|
+
await this.extractThumbnail(
|
|
201
|
+
output.path,
|
|
202
|
+
upload.thumbnailTimecode,
|
|
203
|
+
thumbnailPath,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
console.log(`š¤ Uploading thumbnail...`);
|
|
207
|
+
const s3ThumbnailPath = interpolatePath(paths.get('thumbnail')!);
|
|
208
|
+
const thumbnailBuffer = readFileSync(thumbnailPath);
|
|
209
|
+
|
|
210
|
+
const thumbnailUploadParams: any = {
|
|
211
|
+
Bucket: bucket,
|
|
212
|
+
Key: s3ThumbnailPath,
|
|
213
|
+
Body: thumbnailBuffer,
|
|
214
|
+
ContentType: 'image/jpeg',
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Add ACL if specified
|
|
218
|
+
if (acl) {
|
|
219
|
+
thumbnailUploadParams.ACL = acl;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await s3Client.send(new PutObjectCommand(thumbnailUploadParams));
|
|
223
|
+
|
|
224
|
+
// Construct public URL for thumbnail
|
|
225
|
+
let thumbnailUrl: string;
|
|
226
|
+
if (endpoint) {
|
|
227
|
+
thumbnailUrl = `https://${bucket}.${region}.${endpoint}/${s3ThumbnailPath}`;
|
|
228
|
+
} else {
|
|
229
|
+
thumbnailUrl = `https://${bucket}.s3.${region}.amazonaws.com/${s3ThumbnailPath}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log(`ā
Thumbnail uploaded successfully!`);
|
|
233
|
+
console.log(`š Thumbnail URL: ${thumbnailUrl}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Upload metadata file if specified
|
|
237
|
+
if (paths.has('metadata')) {
|
|
238
|
+
console.log(`\nš¤ Uploading metadata file...`);
|
|
239
|
+
const metadataPath = interpolatePath(paths.get('metadata')!);
|
|
240
|
+
|
|
241
|
+
// Create metadata JSON
|
|
242
|
+
const metadata = {
|
|
243
|
+
title,
|
|
244
|
+
date: date || null,
|
|
245
|
+
tags,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const metadataBuffer = Buffer.from(JSON.stringify(metadata, null, 2), 'utf-8');
|
|
249
|
+
|
|
250
|
+
const metadataUploadParams: any = {
|
|
251
|
+
Bucket: bucket,
|
|
252
|
+
Key: metadataPath,
|
|
253
|
+
Body: metadataBuffer,
|
|
254
|
+
ContentType: 'application/json',
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Add ACL if specified
|
|
258
|
+
if (acl) {
|
|
259
|
+
metadataUploadParams.ACL = acl;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
await s3Client.send(new PutObjectCommand(metadataUploadParams));
|
|
263
|
+
|
|
264
|
+
// Construct public URL for metadata
|
|
265
|
+
let metadataUrl: string;
|
|
266
|
+
if (endpoint) {
|
|
267
|
+
metadataUrl = `https://${bucket}.${region}.${endpoint}/${metadataPath}`;
|
|
268
|
+
} else {
|
|
269
|
+
metadataUrl = `https://${bucket}.s3.${region}.amazonaws.com/${metadataPath}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log(`ā
Metadata uploaded successfully!`);
|
|
273
|
+
console.log(`š Metadata URL: ${metadataUrl}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log('');
|
|
176
277
|
} catch (error) {
|
|
177
278
|
throw new Error(
|
|
178
279
|
`ā Error: Failed to upload to S3\n` +
|
|
@@ -195,4 +296,24 @@ export class S3UploadStrategy implements UploadStrategy {
|
|
|
195
296
|
.replace(/^-+/, '') // Trim - from start
|
|
196
297
|
.replace(/-+$/, ''); // Trim - from end
|
|
197
298
|
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Extracts a frame from video at specific timecode using ffmpeg
|
|
302
|
+
*/
|
|
303
|
+
private async extractThumbnail(
|
|
304
|
+
videoPath: string,
|
|
305
|
+
timecode: number,
|
|
306
|
+
outputPath: string,
|
|
307
|
+
): Promise<void> {
|
|
308
|
+
// Ensure the output directory exists
|
|
309
|
+
const outputDir = dirname(outputPath);
|
|
310
|
+
if (!existsSync(outputDir)) {
|
|
311
|
+
mkdirSync(outputDir, { recursive: true });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const timeInSeconds = timecode / 1000;
|
|
315
|
+
const command = `ffmpeg -y -ss ${timeInSeconds} -i "${videoPath}" -frames:v 1 -q:v 2 "${outputPath}"`;
|
|
316
|
+
|
|
317
|
+
await execAsync(command);
|
|
318
|
+
}
|
|
198
319
|
}
|