my-youtube-api 1.0.4 → 1.0.6
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/youtube-handlers/youtube-video-poster.d.ts +9 -32
- package/dist/youtube-handlers/youtube-video-poster.js +172 -264
- package/dist/youtube-handlers2/youtube-account-handler.d.ts +21 -0
- package/dist/youtube-handlers2/youtube-account-handler.js +33 -0
- package/dist/youtube-handlers2/youtube-channel-handler.d.ts +65 -0
- package/dist/youtube-handlers2/youtube-channel-handler.js +193 -0
- package/dist/youtube-handlers2/youtube-channel-handler2.d.ts +65 -0
- package/dist/youtube-handlers2/youtube-channel-handler2.js +177 -0
- package/dist/youtube-handlers2/youtube-video-handler.d.ts +30 -0
- package/dist/youtube-handlers2/youtube-video-handler.js +55 -0
- package/dist/youtube-handlers2/youtube-video-poster.d.ts +65 -0
- package/dist/youtube-handlers2/youtube-video-poster.js +367 -0
- package/package.json +1 -1
- package/src/youtube-handlers/youtube-video-poster.ts +192 -288
- package/src/youtube-handlers2/youtube-video-poster.ts +456 -0
- package/src/youtube-handlers copy/youtube-video-poster.ts +0 -65
- /package/src/{youtube-handlers copy → youtube-handlers2}/youtube-account-handler.ts +0 -0
- /package/src/{youtube-handlers copy → youtube-handlers2}/youtube-channel-handler.ts +0 -0
- /package/src/{youtube-handlers copy → youtube-handlers2}/youtube-channel-handler2.ts +0 -0
- /package/src/{youtube-handlers copy → youtube-handlers2}/youtube-video-handler.ts +0 -0
|
@@ -38,182 +38,136 @@ export interface YouTubeUploadResult {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export class YoutubeVideoPoster {
|
|
41
|
+
private accessToken: string;
|
|
41
42
|
private youtube: youtube_v3.Youtube;
|
|
42
43
|
|
|
43
|
-
constructor(
|
|
44
|
-
if (!
|
|
44
|
+
constructor(accessToken: string) {
|
|
45
|
+
if (!accessToken) {
|
|
45
46
|
throw new YouTubeAuthenticationError('Access token is required');
|
|
46
47
|
}
|
|
47
|
-
this.
|
|
48
|
+
this.accessToken = accessToken;
|
|
49
|
+
|
|
50
|
+
// ✅ CORRECT: Create OAuth2 client and set credentials
|
|
51
|
+
const oauth2Client = new google.auth.OAuth2();
|
|
52
|
+
oauth2Client.setCredentials({
|
|
53
|
+
access_token: accessToken
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this.youtube = google.youtube({
|
|
57
|
+
version: "v3",
|
|
58
|
+
auth: oauth2Client
|
|
59
|
+
});
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
/**
|
|
51
|
-
*
|
|
63
|
+
* Upload a video from a Cloudinary URL to YouTube using direct API calls
|
|
52
64
|
*/
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (error.errors && Array.isArray(error.errors)) {
|
|
56
|
-
const apiError = error.errors[0];
|
|
57
|
-
const message = `${context}: ${apiError.message}`;
|
|
58
|
-
const reason = apiError.reason;
|
|
59
|
-
const domain = apiError.domain;
|
|
60
|
-
|
|
61
|
-
switch (reason) {
|
|
62
|
-
case 'authError':
|
|
63
|
-
case 'invalidCredentials':
|
|
64
|
-
case 'required':
|
|
65
|
-
throw new YouTubeAuthenticationError(message, apiError);
|
|
66
|
-
|
|
67
|
-
case 'quotaExceeded':
|
|
68
|
-
case 'dailyLimitExceeded':
|
|
69
|
-
case 'userRateLimitExceeded':
|
|
70
|
-
throw new YouTubeQuotaError(message, apiError);
|
|
71
|
-
|
|
72
|
-
case 'rateLimitExceeded':
|
|
73
|
-
case 'userRateLimitExceeded':
|
|
74
|
-
throw new YouTubeRateLimitError(message, apiError);
|
|
75
|
-
|
|
76
|
-
case 'invalidValue':
|
|
77
|
-
case 'invalid':
|
|
78
|
-
throw new YouTubeValidationError(message, apiError);
|
|
79
|
-
|
|
80
|
-
case 'processingFailed':
|
|
81
|
-
case 'failed':
|
|
82
|
-
throw new YouTubeProcessingError(message, apiError);
|
|
83
|
-
|
|
84
|
-
default:
|
|
85
|
-
throw new YouTubeApiError(message, `YOUTUBE_${reason?.toUpperCase()}`, error.code, apiError);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Handle axios/network errors
|
|
90
|
-
if (error.response?.data?.error) {
|
|
91
|
-
const youtubeError = error.response.data.error;
|
|
92
|
-
const message = `${context}: ${youtubeError.message}`;
|
|
93
|
-
const code = youtubeError.code;
|
|
94
|
-
|
|
95
|
-
switch (code) {
|
|
96
|
-
case 401:
|
|
97
|
-
case 403:
|
|
98
|
-
throw new YouTubeAuthenticationError(message, youtubeError);
|
|
99
|
-
case 429:
|
|
100
|
-
throw new YouTubeRateLimitError(message, youtubeError);
|
|
101
|
-
case 400:
|
|
102
|
-
throw new YouTubeValidationError(message, youtubeError);
|
|
103
|
-
case 500:
|
|
104
|
-
case 503:
|
|
105
|
-
throw new YouTubeProcessingError(message, youtubeError);
|
|
106
|
-
default:
|
|
107
|
-
throw new YouTubeApiError(message, `YOUTUBE_${code}`, code, youtubeError);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Handle timeout errors
|
|
112
|
-
if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {
|
|
113
|
-
throw new YouTubeTimeoutError(`${context}: Request timed out`, error);
|
|
114
|
-
}
|
|
65
|
+
public async uploadFromCloudUrl(videoUrl: string, metadata: VideoMetadata): Promise<YouTubeUploadResult> {
|
|
66
|
+
let videoStream: Readable | null = null;
|
|
115
67
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
throw new YouTubeApiError(`${context}: Network error - ${error.message}`, 'NETWORK_ERROR', 503);
|
|
119
|
-
}
|
|
68
|
+
try {
|
|
69
|
+
console.log(`📹 Fetching video from: ${videoUrl}`);
|
|
120
70
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
71
|
+
// Fetch video stream
|
|
72
|
+
const response = await axios.get(videoUrl, {
|
|
73
|
+
responseType: "stream",
|
|
74
|
+
timeout: 30000,
|
|
75
|
+
maxContentLength: 1024 * 1024 * 1024, // 1GB max
|
|
76
|
+
});
|
|
125
77
|
|
|
126
|
-
|
|
127
|
-
if (error instanceof YouTubeApiError) {
|
|
128
|
-
throw error;
|
|
129
|
-
}
|
|
78
|
+
videoStream = response.data;
|
|
130
79
|
|
|
131
|
-
|
|
132
|
-
|
|
80
|
+
if (!videoStream || !(videoStream instanceof Readable)) {
|
|
81
|
+
throw new YouTubeMediaError('Invalid video stream received from URL');
|
|
82
|
+
}
|
|
133
83
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
84
|
+
// Prepare metadata
|
|
85
|
+
const requestBody = {
|
|
86
|
+
snippet: {
|
|
87
|
+
title: metadata.title.substring(0, 100),
|
|
88
|
+
description: (metadata.description || "").substring(0, 5000),
|
|
89
|
+
tags: metadata.tags || [],
|
|
90
|
+
categoryId: metadata.categoryId || "22"
|
|
91
|
+
},
|
|
92
|
+
status: {
|
|
93
|
+
privacyStatus: metadata.privacyStatus || "private",
|
|
94
|
+
embeddable: metadata.embeddable !== false,
|
|
95
|
+
publicStatsViewable: metadata.publicStatsViewable !== false,
|
|
96
|
+
license: metadata.license || "youtube",
|
|
97
|
+
selfDeclaredMadeForKids: metadata.madeForKids || false
|
|
98
|
+
}
|
|
99
|
+
};
|
|
141
100
|
|
|
142
|
-
|
|
143
|
-
throw new YouTubeValidationError('Video title must be 100 characters or less');
|
|
144
|
-
}
|
|
101
|
+
console.log(`🚀 Uploading video to YouTube: "${metadata.title}"`);
|
|
145
102
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
103
|
+
// Upload using Google APIs client with OAuth2
|
|
104
|
+
const uploadResponse = await this.youtube.videos.insert({
|
|
105
|
+
part: ["snippet", "status"],
|
|
106
|
+
requestBody: requestBody,
|
|
107
|
+
media: {
|
|
108
|
+
body: videoStream,
|
|
109
|
+
mimeType: 'video/*'
|
|
110
|
+
},
|
|
111
|
+
notifySubscribers: metadata.notifySubscribers !== false
|
|
112
|
+
});
|
|
149
113
|
|
|
150
|
-
|
|
151
|
-
throw new YouTubeValidationError('Maximum 500 tags allowed');
|
|
152
|
-
}
|
|
114
|
+
const videoId = uploadResponse.data.id;
|
|
153
115
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (tag.length > 30) {
|
|
157
|
-
throw new YouTubeValidationError(`Tag "${tag}" must be 30 characters or less`);
|
|
158
|
-
}
|
|
116
|
+
if (!videoId) {
|
|
117
|
+
throw new YouTubeUploadError('Video ID not returned from YouTube API');
|
|
159
118
|
}
|
|
160
|
-
}
|
|
161
119
|
|
|
162
|
-
|
|
163
|
-
if (metadata.privacyStatus && !validPrivacyStatuses.includes(metadata.privacyStatus)) {
|
|
164
|
-
throw new YouTubeValidationError(`Privacy status must be one of: ${validPrivacyStatuses.join(', ')}`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const validLicenses = ['youtube', 'creativeCommon'];
|
|
168
|
-
if (metadata.license && !validLicenses.includes(metadata.license)) {
|
|
169
|
-
throw new YouTubeValidationError(`License must be one of: ${validLicenses.join(', ')}`);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
120
|
+
console.log(`✅ Video uploaded successfully: ${videoId}`);
|
|
172
121
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
*/
|
|
176
|
-
private async fetchVideoStream(videoUrl: string): Promise<Readable> {
|
|
177
|
-
try {
|
|
178
|
-
console.log(`📹 Fetching video from: ${videoUrl}`);
|
|
122
|
+
// Wait for processing to complete
|
|
123
|
+
await this.waitForVideoProcessing(videoId);
|
|
179
124
|
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
125
|
+
const result: YouTubeUploadResult = {
|
|
126
|
+
id: videoId,
|
|
127
|
+
status: 'processed',
|
|
128
|
+
url: `https://www.youtube.com/watch?v=${videoId}`,
|
|
129
|
+
title: metadata.title,
|
|
130
|
+
description: metadata.description,
|
|
131
|
+
};
|
|
186
132
|
|
|
187
|
-
|
|
188
|
-
throw new YouTubeMediaError('Invalid video stream received from URL');
|
|
189
|
-
}
|
|
133
|
+
console.log(`🎉 YouTube video published successfully: ${result.url}`);
|
|
190
134
|
|
|
191
|
-
return
|
|
135
|
+
return result;
|
|
192
136
|
|
|
193
137
|
} catch (error: any) {
|
|
194
|
-
|
|
195
|
-
|
|
138
|
+
// Clean up stream
|
|
139
|
+
if (videoStream) {
|
|
140
|
+
videoStream.destroy();
|
|
196
141
|
}
|
|
197
142
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
143
|
+
console.error("YouTube upload failed:", error);
|
|
144
|
+
|
|
145
|
+
// Handle specific Google API errors
|
|
146
|
+
if (error.code === 401) {
|
|
147
|
+
throw new YouTubeAuthenticationError(
|
|
148
|
+
'Authentication failed. Please reconnect your YouTube account. ' +
|
|
149
|
+
'The access token may have expired or is invalid.'
|
|
150
|
+
);
|
|
151
|
+
} else if (error.code === 403) {
|
|
152
|
+
if (error.message?.includes('quota')) {
|
|
153
|
+
throw new YouTubeQuotaError('YouTube API quota exceeded. Please try again tomorrow.');
|
|
154
|
+
} else {
|
|
155
|
+
throw new YouTubeAuthenticationError('Access denied. Please check your YouTube permissions.');
|
|
156
|
+
}
|
|
157
|
+
} else if (error.code === 400) {
|
|
158
|
+
throw new YouTubeValidationError(`Invalid request: ${error.message}`);
|
|
159
|
+
} else if (error.code === 503) {
|
|
160
|
+
throw new YouTubeProcessingError('YouTube service temporarily unavailable. Please try again.');
|
|
204
161
|
} else {
|
|
205
|
-
throw new
|
|
162
|
+
throw new YouTubeApiError(`YouTube API error: ${error.message}`);
|
|
206
163
|
}
|
|
207
164
|
}
|
|
208
165
|
}
|
|
209
166
|
|
|
210
167
|
/**
|
|
211
|
-
* Wait for video processing to complete
|
|
168
|
+
* Wait for video processing to complete using direct API calls
|
|
212
169
|
*/
|
|
213
|
-
private async waitForVideoProcessing(
|
|
214
|
-
videoId: string,
|
|
215
|
-
timeoutMs: number = 300000 // 5 minutes
|
|
216
|
-
): Promise<void> {
|
|
170
|
+
private async waitForVideoProcessing(videoId: string, timeoutMs: number = 300000): Promise<void> {
|
|
217
171
|
const startTime = Date.now();
|
|
218
172
|
const maxAttempts = 30;
|
|
219
173
|
let attempts = 0;
|
|
@@ -248,8 +202,7 @@ export class YoutubeVideoPoster {
|
|
|
248
202
|
throw new YouTubeProcessingError(`Video was rejected: ${rejectionReason}`);
|
|
249
203
|
case 'uploaded':
|
|
250
204
|
case 'processing':
|
|
251
|
-
|
|
252
|
-
const waitTime = Math.min(1000 * Math.pow(2, attempts), 10000); // Exponential backoff, max 10s
|
|
205
|
+
const waitTime = Math.min(1000 * Math.pow(2, attempts), 10000);
|
|
253
206
|
console.log(`⏳ Video status: ${status}, waiting ${waitTime}ms...`);
|
|
254
207
|
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
255
208
|
break;
|
|
@@ -261,7 +214,9 @@ export class YoutubeVideoPoster {
|
|
|
261
214
|
if (error instanceof YouTubeApiError) {
|
|
262
215
|
throw error;
|
|
263
216
|
}
|
|
264
|
-
|
|
217
|
+
// Continue waiting on network errors
|
|
218
|
+
console.warn(`Error checking video status (attempt ${attempts}):`, error.message);
|
|
219
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
265
220
|
}
|
|
266
221
|
}
|
|
267
222
|
|
|
@@ -269,69 +224,55 @@ export class YoutubeVideoPoster {
|
|
|
269
224
|
}
|
|
270
225
|
|
|
271
226
|
/**
|
|
272
|
-
*
|
|
227
|
+
* Alternative method using direct axios calls (if Google client still has issues)
|
|
273
228
|
*/
|
|
274
|
-
|
|
229
|
+
public async uploadFromCloudUrlDirect(videoUrl: string, metadata: VideoMetadata): Promise<YouTubeUploadResult> {
|
|
275
230
|
try {
|
|
276
|
-
|
|
277
|
-
part: ['snippet'],
|
|
278
|
-
requestBody: {
|
|
279
|
-
snippet: {
|
|
280
|
-
playlistId: playlistId,
|
|
281
|
-
resourceId: {
|
|
282
|
-
kind: 'youtube#video',
|
|
283
|
-
videoId: videoId
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
console.log(`✅ Video ${videoId} added to playlist ${playlistId}`);
|
|
289
|
-
} catch (error: any) {
|
|
290
|
-
console.warn(`⚠️ Failed to add video to playlist: ${error.message}`);
|
|
291
|
-
// Don't throw error for playlist addition failure as video upload was successful
|
|
292
|
-
}
|
|
293
|
-
}
|
|
231
|
+
console.log(`📹 Using direct upload method for: ${videoUrl}`);
|
|
294
232
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
try {
|
|
302
|
-
// Validate metadata first
|
|
303
|
-
this.validateMetadata(metadata);
|
|
233
|
+
// First, get the video file as buffer
|
|
234
|
+
const videoResponse = await axios.get(videoUrl, {
|
|
235
|
+
responseType: 'arraybuffer',
|
|
236
|
+
timeout: 30000,
|
|
237
|
+
});
|
|
304
238
|
|
|
305
|
-
|
|
306
|
-
videoStream = await this.fetchVideoStream(videoUrl);
|
|
239
|
+
const videoBuffer = Buffer.from(videoResponse.data);
|
|
307
240
|
|
|
308
|
-
// Prepare metadata
|
|
309
|
-
const metadataPayload
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
description: (metadata.description || "").substring(0, 5000),
|
|
316
|
-
tags: (metadata.tags || []).slice(0, 500),
|
|
317
|
-
categoryId: metadata.categoryId || "22"
|
|
318
|
-
},
|
|
319
|
-
status: {
|
|
320
|
-
privacyStatus: metadata.privacyStatus || "private",
|
|
321
|
-
embeddable: metadata.embeddable !== false,
|
|
322
|
-
publicStatsViewable: metadata.publicStatsViewable !== false,
|
|
323
|
-
license: metadata.license || "youtube",
|
|
324
|
-
selfDeclaredMadeForKids: metadata.madeForKids || false
|
|
325
|
-
}
|
|
241
|
+
// Prepare the metadata
|
|
242
|
+
const metadataPayload = {
|
|
243
|
+
snippet: {
|
|
244
|
+
title: metadata.title.substring(0, 100),
|
|
245
|
+
description: (metadata.description || "").substring(0, 5000),
|
|
246
|
+
tags: metadata.tags || [],
|
|
247
|
+
categoryId: metadata.categoryId || "22"
|
|
326
248
|
},
|
|
327
|
-
|
|
249
|
+
status: {
|
|
250
|
+
privacyStatus: metadata.privacyStatus || "private",
|
|
251
|
+
embeddable: metadata.embeddable !== false,
|
|
252
|
+
publicStatsViewable: metadata.publicStatsViewable !== false,
|
|
253
|
+
license: metadata.license || "youtube",
|
|
254
|
+
selfDeclaredMadeForKids: metadata.madeForKids || false
|
|
255
|
+
}
|
|
328
256
|
};
|
|
329
257
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
258
|
+
// Upload using direct multipart request
|
|
259
|
+
const formData = new FormData();
|
|
260
|
+
formData.append('metadata', JSON.stringify(metadataPayload));
|
|
261
|
+
formData.append('video', new Blob([videoBuffer]), 'video.mp4');
|
|
262
|
+
|
|
263
|
+
const uploadResponse = await axios.post(
|
|
264
|
+
'https://www.googleapis.com/upload/youtube/v3/videos?part=snippet,status',
|
|
265
|
+
formData,
|
|
266
|
+
{
|
|
267
|
+
headers: {
|
|
268
|
+
'Authorization': `Bearer ${this.accessToken}`,
|
|
269
|
+
'Content-Type': 'multipart/related',
|
|
270
|
+
},
|
|
271
|
+
maxContentLength: Infinity,
|
|
272
|
+
maxBodyLength: Infinity,
|
|
273
|
+
timeout: 60000,
|
|
274
|
+
}
|
|
275
|
+
);
|
|
335
276
|
|
|
336
277
|
const videoId = uploadResponse.data.id;
|
|
337
278
|
|
|
@@ -339,118 +280,81 @@ export class YoutubeVideoPoster {
|
|
|
339
280
|
throw new YouTubeUploadError('Video ID not returned from YouTube API');
|
|
340
281
|
}
|
|
341
282
|
|
|
342
|
-
console.log(`✅ Video uploaded successfully: ${videoId}`);
|
|
283
|
+
console.log(`✅ Video uploaded successfully via direct method: ${videoId}`);
|
|
343
284
|
|
|
344
|
-
// Wait for processing
|
|
345
|
-
await this.
|
|
346
|
-
|
|
347
|
-
// Add to playlist if specified
|
|
348
|
-
if (metadata.playlistId) {
|
|
349
|
-
await this.addToPlaylist(videoId, metadata.playlistId);
|
|
350
|
-
}
|
|
285
|
+
// Wait for processing
|
|
286
|
+
await this.waitForVideoProcessingDirect(videoId);
|
|
351
287
|
|
|
352
|
-
|
|
288
|
+
return {
|
|
353
289
|
id: videoId,
|
|
354
290
|
status: 'processed',
|
|
355
291
|
url: `https://www.youtube.com/watch?v=${videoId}`,
|
|
356
292
|
title: metadata.title,
|
|
357
293
|
description: metadata.description,
|
|
358
|
-
// You can add thumbnail URL here if needed
|
|
359
294
|
};
|
|
360
295
|
|
|
361
|
-
console.log(`🎉 YouTube video published successfully: ${result.url}`);
|
|
362
|
-
|
|
363
|
-
return result;
|
|
364
|
-
|
|
365
296
|
} catch (error: any) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
297
|
+
console.error("Direct YouTube upload failed:", error.response?.data || error.message);
|
|
298
|
+
|
|
299
|
+
if (error.response?.status === 401) {
|
|
300
|
+
throw new YouTubeAuthenticationError(
|
|
301
|
+
'Authentication failed. Please reconnect your YouTube account.'
|
|
302
|
+
);
|
|
303
|
+
} else if (error.response?.status === 403) {
|
|
304
|
+
throw new YouTubeQuotaError('YouTube API quota exceeded.');
|
|
305
|
+
} else {
|
|
306
|
+
throw new YouTubeApiError(`Upload failed: ${error.message}`);
|
|
373
307
|
}
|
|
374
|
-
|
|
375
|
-
this.handleYouTubeError(error, "Unexpected error during YouTube video upload");
|
|
376
308
|
}
|
|
377
309
|
}
|
|
378
310
|
|
|
379
311
|
/**
|
|
380
|
-
*
|
|
381
|
-
* @param videoUrl URL of the video on Cloudinary
|
|
382
|
-
* @param metadata Video metadata
|
|
312
|
+
* Wait for processing using direct API calls
|
|
383
313
|
*/
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const response = await axios.get(videoUrl, { responseType: "stream" });
|
|
388
|
-
const videoStream = response.data as Readable;
|
|
314
|
+
private async waitForVideoProcessingDirect(videoId: string, timeoutMs: number = 300000): Promise<void> {
|
|
315
|
+
const startTime = Date.now();
|
|
316
|
+
let attempts = 0;
|
|
389
317
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
part: ["snippet", "status"],
|
|
393
|
-
requestBody: {
|
|
394
|
-
snippet: {
|
|
395
|
-
title: metadata.title.substring(0, 100),
|
|
396
|
-
description: (metadata.description || "").substring(0, 5000),
|
|
397
|
-
tags: (metadata.tags || []).slice(0, 5),
|
|
398
|
-
categoryId: metadata.categoryId || "22"
|
|
399
|
-
},
|
|
400
|
-
status: {
|
|
401
|
-
privacyStatus: metadata.privacyStatus || "private",
|
|
402
|
-
embeddable: metadata.embeddable !== false,
|
|
403
|
-
publicStatsViewable: metadata.publicStatsViewable !== false,
|
|
404
|
-
license: metadata.license || "youtube",
|
|
405
|
-
selfDeclaredMadeForKids: metadata.madeForKids || false
|
|
406
|
-
}
|
|
407
|
-
},
|
|
408
|
-
media: { body: videoStream }
|
|
409
|
-
};
|
|
318
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
319
|
+
attempts++;
|
|
410
320
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
321
|
+
try {
|
|
322
|
+
const response = await axios.get(
|
|
323
|
+
`https://www.googleapis.com/youtube/v3/videos?part=status&id=${videoId}`,
|
|
324
|
+
{
|
|
325
|
+
headers: {
|
|
326
|
+
'Authorization': `Bearer ${this.accessToken}`
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
);
|
|
414
330
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
331
|
+
const video = response.data.items?.[0];
|
|
332
|
+
if (!video) {
|
|
333
|
+
throw new YouTubeProcessingError(`Video ${videoId} not found`);
|
|
334
|
+
}
|
|
420
335
|
|
|
421
|
-
|
|
422
|
-
* Get video details
|
|
423
|
-
*/
|
|
424
|
-
public async getVideoDetails(videoId: string): Promise<youtube_v3.Schema$Video> {
|
|
425
|
-
try {
|
|
426
|
-
const response = await this.youtube.videos.list({
|
|
427
|
-
part: ['snippet', 'status', 'contentDetails', 'statistics'],
|
|
428
|
-
id: [videoId]
|
|
429
|
-
});
|
|
336
|
+
const status = video.status?.uploadStatus;
|
|
430
337
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
338
|
+
if (status === 'processed') {
|
|
339
|
+
console.log(`✅ Video processing completed: ${videoId}`);
|
|
340
|
+
return;
|
|
341
|
+
} else if (status === 'failed') {
|
|
342
|
+
throw new YouTubeProcessingError(`Video processing failed: ${video.status?.failureReason}`);
|
|
343
|
+
} else if (status === 'rejected') {
|
|
344
|
+
throw new YouTubeProcessingError(`Video rejected: ${video.status?.rejectionReason}`);
|
|
345
|
+
}
|
|
435
346
|
|
|
436
|
-
|
|
347
|
+
// Wait with exponential backoff
|
|
348
|
+
const waitTime = Math.min(1000 * Math.pow(2, attempts), 10000);
|
|
349
|
+
console.log(`⏳ Video status: ${status}, waiting ${waitTime}ms...`);
|
|
350
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
437
351
|
|
|
438
|
-
|
|
439
|
-
|
|
352
|
+
} catch (error: any) {
|
|
353
|
+
console.warn(`Error checking video status (attempt ${attempts}):`, error.message);
|
|
354
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
355
|
+
}
|
|
440
356
|
}
|
|
441
|
-
}
|
|
442
357
|
|
|
443
|
-
|
|
444
|
-
* Delete a video
|
|
445
|
-
*/
|
|
446
|
-
public async deleteVideo(videoId: string): Promise<void> {
|
|
447
|
-
try {
|
|
448
|
-
await this.youtube.videos.delete({
|
|
449
|
-
id: videoId
|
|
450
|
-
});
|
|
451
|
-
console.log(`✅ Video deleted successfully: ${videoId}`);
|
|
452
|
-
} catch (error: any) {
|
|
453
|
-
this.handleYouTubeError(error, "Failed to delete video");
|
|
454
|
-
}
|
|
358
|
+
throw new YouTubeTimeoutError(`Video processing timed out after ${timeoutMs}ms`);
|
|
455
359
|
}
|
|
456
360
|
}
|