@gannochenko/staticstripes 0.0.12 ā 0.0.14
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/Makefile +20 -0
- package/dist/asset-manager.d.ts +1 -0
- package/dist/asset-manager.d.ts.map +1 -1
- package/dist/asset-manager.js +3 -0
- package/dist/asset-manager.js.map +1 -1
- package/dist/cli/ai-generation-strategy-factory.d.ts +23 -0
- package/dist/cli/ai-generation-strategy-factory.d.ts.map +1 -0
- package/dist/cli/ai-generation-strategy-factory.js +44 -0
- package/dist/cli/ai-generation-strategy-factory.js.map +1 -0
- package/dist/cli/ai-generation-strategy.d.ts +33 -0
- package/dist/cli/ai-generation-strategy.d.ts.map +1 -0
- package/dist/cli/ai-generation-strategy.js +3 -0
- package/dist/cli/ai-generation-strategy.js.map +1 -0
- package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.d.ts +38 -0
- package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.d.ts.map +1 -0
- package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.js +174 -0
- package/dist/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.js.map +1 -0
- package/dist/cli/auth-strategy-factory.d.ts +31 -0
- package/dist/cli/auth-strategy-factory.d.ts.map +1 -0
- package/dist/cli/auth-strategy-factory.js +61 -0
- package/dist/cli/auth-strategy-factory.js.map +1 -0
- package/dist/cli/auth-strategy.d.ts +31 -0
- package/dist/cli/auth-strategy.d.ts.map +1 -0
- package/dist/cli/auth-strategy.js +3 -0
- package/dist/cli/auth-strategy.js.map +1 -0
- package/dist/cli/commands/auth.d.ts +6 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/auth.js +103 -0
- package/dist/cli/commands/auth.js.map +1 -0
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +69 -2
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/instagram/instagram-auth-strategy.d.ts +31 -0
- package/dist/cli/instagram/instagram-auth-strategy.d.ts.map +1 -0
- package/dist/cli/instagram/instagram-auth-strategy.js +505 -0
- package/dist/cli/instagram/instagram-auth-strategy.js.map +1 -0
- package/dist/cli/instagram/instagram-upload-strategy.d.ts +45 -0
- package/dist/cli/instagram/instagram-upload-strategy.d.ts.map +1 -0
- package/dist/cli/instagram/instagram-upload-strategy.js +303 -0
- package/dist/cli/instagram/instagram-upload-strategy.js.map +1 -0
- package/dist/cli/s3/s3-upload-strategy.d.ts.map +1 -1
- package/dist/cli/s3/s3-upload-strategy.js +7 -3
- package/dist/cli/s3/s3-upload-strategy.js.map +1 -1
- package/dist/cli/upload-strategy-factory.d.ts +1 -1
- package/dist/cli/upload-strategy-factory.d.ts.map +1 -1
- package/dist/cli/upload-strategy-factory.js +5 -5
- package/dist/cli/upload-strategy-factory.js.map +1 -1
- package/dist/cli/youtube/youtube-auth-strategy.d.ts +11 -0
- package/dist/cli/youtube/youtube-auth-strategy.d.ts.map +1 -0
- package/dist/cli/youtube/youtube-auth-strategy.js +320 -0
- package/dist/cli/youtube/youtube-auth-strategy.js.map +1 -0
- package/dist/cli/youtube/youtube-upload-strategy.d.ts +10 -3
- package/dist/cli/youtube/youtube-upload-strategy.d.ts.map +1 -1
- package/dist/cli/youtube/youtube-upload-strategy.js +96 -16
- package/dist/cli/youtube/youtube-upload-strategy.js.map +1 -1
- package/dist/cli.js +2 -3
- package/dist/cli.js.map +1 -1
- package/dist/html-project-parser.d.ts +40 -1
- package/dist/html-project-parser.d.ts.map +1 -1
- package/dist/html-project-parser.js +343 -9
- package/dist/html-project-parser.js.map +1 -1
- package/dist/lib/file.d.ts +2 -0
- package/dist/lib/file.d.ts.map +1 -0
- package/dist/lib/file.js +13 -0
- package/dist/lib/file.js.map +1 -0
- package/dist/lib/net.d.ts +19 -0
- package/dist/lib/net.d.ts.map +1 -0
- package/dist/lib/net.js +101 -0
- package/dist/lib/net.js.map +1 -0
- package/dist/project.d.ts +5 -2
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +9 -1
- package/dist/project.js.map +1 -1
- package/dist/type.d.ts +17 -0
- package/dist/type.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/asset-manager.ts +4 -0
- package/src/cli/ai-generation-strategy-factory.ts +48 -0
- package/src/cli/ai-generation-strategy.ts +35 -0
- package/src/cli/ai-music-api-ai/ai-music-api-ai-generation-strategy.ts +266 -0
- package/src/cli/auth-strategy-factory.ts +67 -0
- package/src/cli/auth-strategy.ts +37 -0
- package/src/cli/commands/auth.ts +120 -0
- package/src/cli/commands/generate.ts +55 -2
- package/src/cli/instagram/instagram-auth-strategy.ts +569 -0
- package/src/cli/instagram/instagram-upload-strategy.ts +398 -0
- package/src/cli/s3/s3-upload-strategy.ts +7 -3
- package/src/cli/upload-strategy-factory.ts +6 -9
- package/src/cli/youtube/youtube-auth-strategy.ts +323 -0
- package/src/cli/youtube/youtube-upload-strategy.ts +147 -16
- package/src/cli.ts +2 -4
- package/src/html-project-parser.ts +429 -8
- package/src/lib/file.ts +11 -0
- package/src/lib/net.ts +120 -0
- package/src/project.ts +10 -0
- package/src/type.ts +19 -0
- package/dist/cli/youtube/auth-commands.d.ts +0 -3
- package/dist/cli/youtube/auth-commands.d.ts.map +0 -1
- package/dist/cli/youtube/auth-commands.js +0 -273
- package/dist/cli/youtube/auth-commands.js.map +0 -1
- package/dist/cli/youtube/cli.d.ts +0 -7
- package/dist/cli/youtube/cli.d.ts.map +0 -1
- package/dist/cli/youtube/cli.js +0 -13
- package/dist/cli/youtube/cli.js.map +0 -1
- package/dist/cli/youtube/upload-handler.d.ts +0 -12
- package/dist/cli/youtube/upload-handler.d.ts.map +0 -1
- package/dist/cli/youtube/upload-handler.js +0 -66
- package/dist/cli/youtube/upload-handler.js.map +0 -1
- package/src/cli/youtube/auth-commands.ts +0 -312
- package/src/cli/youtube/cli.ts +0 -11
- package/src/cli/youtube/upload-handler.ts +0 -101
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { AuthStrategy, AuthOptions } from '../auth-strategy';
|
|
2
|
+
import { YouTubeUploader } from '../../youtube-uploader.js';
|
|
3
|
+
import open from 'open';
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import { parse as parseUrl } from 'url';
|
|
6
|
+
import * as readline from 'readline';
|
|
7
|
+
import { writeFileSync } from 'fs';
|
|
8
|
+
import { resolve } from 'path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* YouTube authentication strategy
|
|
12
|
+
* Uses OAuth 2.0 flow with local callback server
|
|
13
|
+
*/
|
|
14
|
+
export class YouTubeAuthStrategy implements AuthStrategy {
|
|
15
|
+
getTag(): string {
|
|
16
|
+
return 'youtube';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async execute(
|
|
20
|
+
uploadName: string,
|
|
21
|
+
projectPath: string,
|
|
22
|
+
_options?: AuthOptions,
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
console.log(`š YouTube Authentication Setup\n`);
|
|
25
|
+
|
|
26
|
+
const rl = readline.createInterface({
|
|
27
|
+
input: process.stdin,
|
|
28
|
+
output: process.stdout,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const question = (prompt: string): Promise<string> => {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
rl.question(prompt, (answer) => {
|
|
34
|
+
resolve(answer);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
console.log('ā'.repeat(60));
|
|
41
|
+
console.log('STEP 1: Enter YouTube API Credentials');
|
|
42
|
+
console.log('ā'.repeat(60));
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log('š” Run `staticstripes auth-help youtube` for setup instructions\n');
|
|
45
|
+
|
|
46
|
+
const clientId = await question('Enter your OAuth Client ID: ');
|
|
47
|
+
if (!clientId || clientId.trim().length < 10) {
|
|
48
|
+
throw new Error('Invalid Client ID');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const clientSecret = await question('Enter your OAuth Client Secret: ');
|
|
52
|
+
if (!clientSecret || clientSecret.trim().length < 10) {
|
|
53
|
+
throw new Error('Invalid Client Secret');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log('\nā'.repeat(60));
|
|
57
|
+
console.log('STEP 2: Authorize with Google');
|
|
58
|
+
console.log('ā'.repeat(60));
|
|
59
|
+
console.log('');
|
|
60
|
+
|
|
61
|
+
rl.close();
|
|
62
|
+
|
|
63
|
+
// Create uploader instance
|
|
64
|
+
const uploader = new YouTubeUploader(
|
|
65
|
+
clientId.trim(),
|
|
66
|
+
clientSecret.trim(),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Get authorization URL
|
|
70
|
+
const authUrl = uploader.getAuthUrl();
|
|
71
|
+
|
|
72
|
+
console.log('š Starting local server on http://localhost:3000...\n');
|
|
73
|
+
|
|
74
|
+
// Create a promise that resolves when we get the OAuth callback
|
|
75
|
+
const authPromise = new Promise<string>((resolve, reject) => {
|
|
76
|
+
// Track all connections to force-close them
|
|
77
|
+
const connections = new Set<any>();
|
|
78
|
+
|
|
79
|
+
const server = http.createServer((req, res) => {
|
|
80
|
+
const url = parseUrl(req.url || '', true);
|
|
81
|
+
|
|
82
|
+
if (url.pathname === '/oauth2callback') {
|
|
83
|
+
const code = url.query.code as string;
|
|
84
|
+
const error = url.query.error as string;
|
|
85
|
+
|
|
86
|
+
const closeServer = () => {
|
|
87
|
+
// Destroy all connections
|
|
88
|
+
connections.forEach((socket) => {
|
|
89
|
+
socket.destroy();
|
|
90
|
+
});
|
|
91
|
+
connections.clear();
|
|
92
|
+
server.close();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (error) {
|
|
96
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
97
|
+
res.end(`
|
|
98
|
+
<html>
|
|
99
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
100
|
+
<h1>ā Authorization Failed</h1>
|
|
101
|
+
<p>Error: ${error}</p>
|
|
102
|
+
<p>You can close this window.</p>
|
|
103
|
+
</body>
|
|
104
|
+
</html>
|
|
105
|
+
`);
|
|
106
|
+
res.on('finish', closeServer);
|
|
107
|
+
reject(new Error(`Authorization failed: ${error}`));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (code) {
|
|
112
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
113
|
+
res.end(`
|
|
114
|
+
<html>
|
|
115
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
116
|
+
<h1>Authorization Successful!</h1>
|
|
117
|
+
<p>You can close this window and return to the terminal.</p>
|
|
118
|
+
</body>
|
|
119
|
+
</html>
|
|
120
|
+
`);
|
|
121
|
+
res.on('finish', closeServer);
|
|
122
|
+
resolve(code);
|
|
123
|
+
} else {
|
|
124
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
125
|
+
res.end(`
|
|
126
|
+
<html>
|
|
127
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
128
|
+
<h1>ā No Authorization Code</h1>
|
|
129
|
+
<p>No code was received from Google.</p>
|
|
130
|
+
<p>You can close this window.</p>
|
|
131
|
+
</body>
|
|
132
|
+
</html>
|
|
133
|
+
`);
|
|
134
|
+
res.on('finish', closeServer);
|
|
135
|
+
reject(new Error('No authorization code received'));
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
res.writeHead(404);
|
|
139
|
+
res.end('Not found');
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Track connections
|
|
144
|
+
server.on('connection', (socket) => {
|
|
145
|
+
connections.add(socket);
|
|
146
|
+
socket.on('close', () => {
|
|
147
|
+
connections.delete(socket);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
server.listen(3000, () => {
|
|
152
|
+
console.log('ā
Server started successfully\n');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Set timeout to avoid hanging forever
|
|
156
|
+
setTimeout(
|
|
157
|
+
() => {
|
|
158
|
+
connections.forEach((socket) => {
|
|
159
|
+
socket.destroy();
|
|
160
|
+
});
|
|
161
|
+
connections.clear();
|
|
162
|
+
server.close();
|
|
163
|
+
reject(new Error('Authentication timeout (5 minutes)'));
|
|
164
|
+
},
|
|
165
|
+
5 * 60 * 1000,
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
console.log('š Opening browser for authorization...\n');
|
|
170
|
+
|
|
171
|
+
// Open browser automatically
|
|
172
|
+
try {
|
|
173
|
+
await open(authUrl);
|
|
174
|
+
console.log('ā
Browser opened successfully\n');
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.log('ā ļø Could not open browser automatically');
|
|
177
|
+
console.log('š Please visit this URL to authorize:\n');
|
|
178
|
+
console.log(authUrl);
|
|
179
|
+
console.log();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log('ā³ Waiting for authorization...\n');
|
|
183
|
+
|
|
184
|
+
// Wait for the OAuth callback
|
|
185
|
+
const code = await authPromise;
|
|
186
|
+
|
|
187
|
+
console.log('š Authorization code received\n');
|
|
188
|
+
console.log('š¾ Saving authentication tokens...\n');
|
|
189
|
+
|
|
190
|
+
// Complete authentication - saves OAuth tokens to .auth file
|
|
191
|
+
await uploader.authenticate(code, uploadName, projectPath);
|
|
192
|
+
|
|
193
|
+
// Now add clientId and clientSecret to the saved file
|
|
194
|
+
const authDir = resolve(projectPath, '.auth');
|
|
195
|
+
const credentialsPath = resolve(authDir, `${uploadName}.json`);
|
|
196
|
+
|
|
197
|
+
// Read the tokens that were just saved
|
|
198
|
+
const { readFileSync } = await import('fs');
|
|
199
|
+
const savedTokens = JSON.parse(readFileSync(credentialsPath, 'utf-8'));
|
|
200
|
+
|
|
201
|
+
// Add clientId and clientSecret
|
|
202
|
+
const fullCredentials = {
|
|
203
|
+
clientId: clientId.trim(),
|
|
204
|
+
clientSecret: clientSecret.trim(),
|
|
205
|
+
...savedTokens,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Save back with all credentials
|
|
209
|
+
writeFileSync(
|
|
210
|
+
credentialsPath,
|
|
211
|
+
JSON.stringify(fullCredentials, null, 2),
|
|
212
|
+
'utf-8',
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
console.log(`ā
Authentication complete for ${uploadName}!\n`);
|
|
216
|
+
console.log(`š Credentials saved to: ${credentialsPath}\n`);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
getSetupInstructions(): string {
|
|
223
|
+
return `
|
|
224
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
225
|
+
YouTube Authentication Setup
|
|
226
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
227
|
+
|
|
228
|
+
Interactive OAuth 2.0 flow - no environment variables needed!
|
|
229
|
+
|
|
230
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
231
|
+
STEP 1: Go to Google Cloud Console
|
|
232
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
233
|
+
URL: https://console.cloud.google.com/
|
|
234
|
+
|
|
235
|
+
1. Create or select a project
|
|
236
|
+
|
|
237
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
238
|
+
STEP 2: Enable YouTube Data API v3
|
|
239
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
240
|
+
1. Go to "APIs & Services" > "Library"
|
|
241
|
+
2. Search for "YouTube Data API v3"
|
|
242
|
+
3. Click "Enable"
|
|
243
|
+
|
|
244
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
245
|
+
STEP 3: Configure OAuth Consent Screen
|
|
246
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
247
|
+
1. Go to "APIs & Services" > "OAuth consent screen"
|
|
248
|
+
2. Choose "External" user type
|
|
249
|
+
3. Fill in:
|
|
250
|
+
⢠App name: "My YouTube Uploader"
|
|
251
|
+
⢠User support email: your.email@example.com
|
|
252
|
+
⢠Developer contact email: your.email@example.com
|
|
253
|
+
4. Click "Save and Continue"
|
|
254
|
+
5. Add scope: https://www.googleapis.com/auth/youtube.upload
|
|
255
|
+
6. Click "Save and Continue"
|
|
256
|
+
7. Add your email as a test user
|
|
257
|
+
8. Click "Save and Continue"
|
|
258
|
+
|
|
259
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
260
|
+
STEP 4: Create OAuth 2.0 Credentials
|
|
261
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
262
|
+
1. Go to "APIs & Services" > "Credentials"
|
|
263
|
+
2. Click "Create Credentials" > "OAuth client ID"
|
|
264
|
+
3. Choose "Web application"
|
|
265
|
+
4. Name: "YouTube Uploader"
|
|
266
|
+
5. Add redirect URI: http://localhost:3000/oauth2callback
|
|
267
|
+
(Make sure it's exactly this - no trailing slash!)
|
|
268
|
+
6. Click "Create"
|
|
269
|
+
7. Copy your Client ID (looks like: xxx.apps.googleusercontent.com)
|
|
270
|
+
8. Copy your Client Secret
|
|
271
|
+
|
|
272
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
273
|
+
STEP 5: Publish Your OAuth App (IMPORTANT!)
|
|
274
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
275
|
+
1. Go to "APIs & Services" > "OAuth consent screen"
|
|
276
|
+
2. Click "PUBLISH APP" button
|
|
277
|
+
3. This makes refresh tokens permanent (otherwise they expire in 7 days)
|
|
278
|
+
4. Note: For personal use, you don't need Google verification
|
|
279
|
+
|
|
280
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
281
|
+
STEP 6: Run Authentication Wizard
|
|
282
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
283
|
+
Run:
|
|
284
|
+
staticstripes auth --upload-name YOUR_UPLOAD_NAME
|
|
285
|
+
|
|
286
|
+
The wizard will:
|
|
287
|
+
1. Ask you to enter your OAuth Client ID
|
|
288
|
+
2. Ask you to enter your OAuth Client Secret
|
|
289
|
+
3. Start local server on port 3000
|
|
290
|
+
4. Open browser automatically for Google authorization
|
|
291
|
+
5. Automatically exchange authorization code for tokens
|
|
292
|
+
6. Save ALL credentials to .auth/YOUR_UPLOAD_NAME.json
|
|
293
|
+
|
|
294
|
+
Done! Interactive and secure - no environment variables needed!
|
|
295
|
+
|
|
296
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
297
|
+
TROUBLESHOOTING
|
|
298
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
299
|
+
ā "redirect_uri_mismatch"
|
|
300
|
+
ā Make sure redirect URI is exactly: http://localhost:3000/oauth2callback
|
|
301
|
+
ā No trailing slash, no typos!
|
|
302
|
+
|
|
303
|
+
ā "Invalid client" error
|
|
304
|
+
ā Double-check your Client ID and Client Secret
|
|
305
|
+
ā Make sure you copied them correctly
|
|
306
|
+
|
|
307
|
+
ā Tokens expire after 7 days
|
|
308
|
+
ā Publish your OAuth app (Step 5)
|
|
309
|
+
ā This makes refresh tokens last indefinitely
|
|
310
|
+
|
|
311
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
312
|
+
REFERENCE LINKS
|
|
313
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
314
|
+
⢠Google Cloud Console:
|
|
315
|
+
https://console.cloud.google.com/
|
|
316
|
+
|
|
317
|
+
⢠YouTube Data API docs:
|
|
318
|
+
https://developers.google.com/youtube/v3
|
|
319
|
+
|
|
320
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
321
|
+
`;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -1,30 +1,28 @@
|
|
|
1
1
|
import { UploadStrategy } from '../upload-strategy';
|
|
2
2
|
import { Project } from '../../project';
|
|
3
3
|
import { YouTubeUpload } from '../../type';
|
|
4
|
-
import {
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
import { YouTubeUploader } from '../../youtube-uploader';
|
|
6
|
+
import ejs from 'ejs';
|
|
7
|
+
import { readFileSync, existsSync } from 'fs';
|
|
8
|
+
|
|
9
|
+
export interface YouTubeUploadOptions {
|
|
10
|
+
uploadName: string;
|
|
11
|
+
projectPath: string;
|
|
12
|
+
clientId: string;
|
|
13
|
+
clientSecret: string;
|
|
14
|
+
}
|
|
5
15
|
|
|
6
16
|
/**
|
|
7
17
|
* YouTube upload strategy implementation
|
|
8
18
|
*/
|
|
9
19
|
export class YouTubeUploadStrategy implements UploadStrategy {
|
|
10
|
-
constructor(
|
|
11
|
-
private clientId: string,
|
|
12
|
-
private clientSecret: string,
|
|
13
|
-
) {}
|
|
14
|
-
|
|
15
20
|
getTag(): string {
|
|
16
21
|
return 'youtube';
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
validate(): void {
|
|
20
|
-
|
|
21
|
-
const error = new Error(
|
|
22
|
-
'ā Error: STATICSTRIPES_GOOGLE_CLIENT_ID and STATICSTRIPES_GOOGLE_CLIENT_SECRET environment variables are not set\n\n' +
|
|
23
|
-
'š” Run: staticstripes auth --help\n' +
|
|
24
|
-
' for complete setup instructions',
|
|
25
|
-
);
|
|
26
|
-
throw error;
|
|
27
|
-
}
|
|
25
|
+
// Validation now happens in execute() when we read credentials
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
async execute(
|
|
@@ -32,12 +30,145 @@ export class YouTubeUploadStrategy implements UploadStrategy {
|
|
|
32
30
|
upload: YouTubeUpload,
|
|
33
31
|
projectPath: string,
|
|
34
32
|
): Promise<void> {
|
|
33
|
+
// Read credentials from .auth file
|
|
34
|
+
const authDir = resolve(projectPath, '.auth');
|
|
35
|
+
const credentialsPath = resolve(authDir, `${upload.name}.json`);
|
|
36
|
+
|
|
37
|
+
if (!existsSync(credentialsPath)) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`ā Error: YouTube credentials not found\n\n` +
|
|
40
|
+
`Expected location: ${credentialsPath}\n\n` +
|
|
41
|
+
`š” Run authentication wizard:\n` +
|
|
42
|
+
` staticstripes auth --upload-name ${upload.name}\n\n` +
|
|
43
|
+
`š Or view setup instructions:\n` +
|
|
44
|
+
` staticstripes auth-help youtube\n`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
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
|
+
|
|
35
64
|
// Delegate to existing handler
|
|
36
65
|
await handleYouTubeUpload(project, {
|
|
37
66
|
uploadName: upload.name,
|
|
38
67
|
projectPath,
|
|
39
|
-
clientId:
|
|
40
|
-
clientSecret:
|
|
68
|
+
clientId: credentials.clientId,
|
|
69
|
+
clientSecret: credentials.clientSecret,
|
|
41
70
|
});
|
|
42
71
|
}
|
|
43
72
|
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Handles YouTube video upload process
|
|
76
|
+
*/
|
|
77
|
+
export async function handleYouTubeUpload(
|
|
78
|
+
project: Project,
|
|
79
|
+
options: YouTubeUploadOptions,
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
// Get upload configuration
|
|
82
|
+
const upload = project.getYouTubeUpload(options.uploadName);
|
|
83
|
+
if (!upload) {
|
|
84
|
+
const availableUploads = Array.from(project.getYouTubeUploads().keys());
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Upload "${options.uploadName}" not found in project.html\n` +
|
|
87
|
+
(availableUploads.length > 0
|
|
88
|
+
? `Available uploads: ${availableUploads.join(', ')}`
|
|
89
|
+
: 'No uploads defined in project.html'),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Get the output file
|
|
94
|
+
const output = project.getOutput(upload.outputName);
|
|
95
|
+
if (!output) {
|
|
96
|
+
throw new Error(`Output "${upload.outputName}" not found`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(`š¹ Video file: ${output.path}`);
|
|
100
|
+
console.log(`š¬ Upload config: ${options.uploadName}\n`);
|
|
101
|
+
|
|
102
|
+
// Create uploader and load tokens
|
|
103
|
+
const uploader = new YouTubeUploader(options.clientId, options.clientSecret);
|
|
104
|
+
const hasTokens = uploader.loadTokens(
|
|
105
|
+
options.uploadName,
|
|
106
|
+
options.projectPath,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (!hasTokens) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Not authenticated. Please run: staticstripes auth --upload-name ${options.uploadName}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Determine title (use upload-specific title or fall back to project title)
|
|
116
|
+
const title = upload.title || project.getTitle();
|
|
117
|
+
console.log(`š Title: ${title}\n`);
|
|
118
|
+
|
|
119
|
+
// Build the project to populate fragment times (needed for timecodes)
|
|
120
|
+
console.log('šØ Building project to calculate timecodes...');
|
|
121
|
+
await project.build(upload.outputName);
|
|
122
|
+
|
|
123
|
+
// Get timecodes and process description with EJS
|
|
124
|
+
const timecodes = project.getTimecodes();
|
|
125
|
+
|
|
126
|
+
// Format tags (space-separated, no hashtags for YouTube)
|
|
127
|
+
const formattedTags = upload.tags.join(' ');
|
|
128
|
+
|
|
129
|
+
// Convert ${variable} syntax to <%= variable %> for EJS compatibility
|
|
130
|
+
const ejsDescription = upload.description.replace(
|
|
131
|
+
/\$\{(\w+)\}/g,
|
|
132
|
+
'<%= $1 %>',
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const processedDescription = ejs.render(ejsDescription, {
|
|
136
|
+
title,
|
|
137
|
+
tags: formattedTags,
|
|
138
|
+
timecodes: timecodes.join('\n'),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Create a processed upload object with rendered description
|
|
142
|
+
const processedUpload = {
|
|
143
|
+
...upload,
|
|
144
|
+
description: processedDescription,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Upload video
|
|
148
|
+
const videoId = await uploader.uploadVideo(
|
|
149
|
+
output.path,
|
|
150
|
+
processedUpload,
|
|
151
|
+
title,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Handle thumbnail if specified
|
|
155
|
+
if (upload.thumbnailTimecode !== undefined) {
|
|
156
|
+
console.log(
|
|
157
|
+
`\nš¼ļø Extracting thumbnail at ${upload.thumbnailTimecode}ms...`,
|
|
158
|
+
);
|
|
159
|
+
const thumbnailPath = resolve(
|
|
160
|
+
options.projectPath,
|
|
161
|
+
'.cache',
|
|
162
|
+
'thumbnail.png',
|
|
163
|
+
);
|
|
164
|
+
await uploader.extractThumbnail(
|
|
165
|
+
output.path,
|
|
166
|
+
upload.thumbnailTimecode,
|
|
167
|
+
thumbnailPath,
|
|
168
|
+
);
|
|
169
|
+
await uploader.uploadThumbnail(videoId, thumbnailPath);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// TODO: Update project.html with video ID
|
|
173
|
+
console.log('\nā
Upload complete!');
|
|
174
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { registerGenerateCommand } from './cli/commands/generate.js';
|
|
|
7
7
|
import { registerBootstrapCommand } from './cli/commands/bootstrap.js';
|
|
8
8
|
import { registerAddAssetsCommand } from './cli/commands/add-assets.js';
|
|
9
9
|
import { registerUploadCommand } from './cli/commands/upload.js';
|
|
10
|
-
import {
|
|
10
|
+
import { registerAuthCommand } from './cli/commands/auth.js';
|
|
11
11
|
|
|
12
12
|
// Read version from package.json
|
|
13
13
|
// In built code, this file is at dist/cli.js, package.json is at ../package.json
|
|
@@ -64,8 +64,6 @@ registerGenerateCommand(program, () => isDebugMode, handleError);
|
|
|
64
64
|
registerBootstrapCommand(program, handleError);
|
|
65
65
|
registerAddAssetsCommand(program, handleError);
|
|
66
66
|
registerUploadCommand(program, handleError);
|
|
67
|
-
|
|
68
|
-
// Register provider-specific commands (auth, etc.)
|
|
69
|
-
registerYouTubeCommands(program);
|
|
67
|
+
registerAuthCommand(program, handleError);
|
|
70
68
|
|
|
71
69
|
program.parse(process.argv);
|