my-youtube-api 1.0.5 → 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.
@@ -1,4 +1,3 @@
1
- import { youtube_v3 } from "googleapis";
2
1
  export interface VideoMetadata {
3
2
  title: string;
4
3
  description?: string;
@@ -9,15 +8,35 @@ export interface VideoMetadata {
9
8
  license?: "youtube" | "creativeCommon";
10
9
  embeddable?: boolean;
11
10
  publicStatsViewable?: boolean;
11
+ playlistId?: string;
12
+ notifySubscribers?: boolean;
13
+ }
14
+ export interface YouTubeUploadResult {
15
+ id: string;
16
+ status: string;
17
+ url: string;
18
+ title: string;
19
+ description?: string;
20
+ thumbnailUrl?: string;
12
21
  }
13
22
  export declare class YoutubeVideoPoster {
14
- private access_token;
23
+ private accessToken;
15
24
  private youtube;
16
- constructor(access_token: string);
25
+ constructor(accessToken: string);
26
+ /**
27
+ * Upload a video from a Cloudinary URL to YouTube using direct API calls
28
+ */
29
+ uploadFromCloudUrl(videoUrl: string, metadata: VideoMetadata): Promise<YouTubeUploadResult>;
30
+ /**
31
+ * Wait for video processing to complete using direct API calls
32
+ */
33
+ private waitForVideoProcessing;
34
+ /**
35
+ * Alternative method using direct axios calls (if Google client still has issues)
36
+ */
37
+ uploadFromCloudUrlDirect(videoUrl: string, metadata: VideoMetadata): Promise<YouTubeUploadResult>;
17
38
  /**
18
- * Upload a video from a Cloudinary URL to YouTube
19
- * @param videoUrl URL of the video on Cloudinary
20
- * @param metadata Video metadata
39
+ * Wait for processing using direct API calls
21
40
  */
22
- uploadFromCloudUrl(videoUrl: string, metadata: VideoMetadata): Promise<youtube_v3.Schema$Video>;
41
+ private waitForVideoProcessingDirect;
23
42
  }
@@ -1,47 +1,275 @@
1
+ // src/youtube-handlers/youtube-video-poster.ts
1
2
  import { google } from "googleapis";
2
3
  import axios from "axios";
4
+ import { Readable } from "stream";
5
+ import { YouTubeApiError, YouTubeAuthenticationError, YouTubeQuotaError, YouTubeMediaError, YouTubeUploadError, YouTubeValidationError, YouTubeProcessingError, YouTubeTimeoutError } from "../errors/youtube-api-errors";
3
6
  export class YoutubeVideoPoster {
4
- constructor(access_token) {
5
- this.access_token = access_token;
6
- this.youtube = google.youtube({ version: "v3", auth: this.access_token });
7
+ constructor(accessToken) {
8
+ if (!accessToken) {
9
+ throw new YouTubeAuthenticationError('Access token is required');
10
+ }
11
+ this.accessToken = accessToken;
12
+ // ✅ CORRECT: Create OAuth2 client and set credentials
13
+ const oauth2Client = new google.auth.OAuth2();
14
+ oauth2Client.setCredentials({
15
+ access_token: accessToken
16
+ });
17
+ this.youtube = google.youtube({
18
+ version: "v3",
19
+ auth: oauth2Client
20
+ });
7
21
  }
8
22
  /**
9
- * Upload a video from a Cloudinary URL to YouTube
10
- * @param videoUrl URL of the video on Cloudinary
11
- * @param metadata Video metadata
23
+ * Upload a video from a Cloudinary URL to YouTube using direct API calls
12
24
  */
13
25
  async uploadFromCloudUrl(videoUrl, metadata) {
26
+ let videoStream = null;
14
27
  try {
15
- // Fetch video as stream from Cloudinary
16
- const response = await axios.get(videoUrl, { responseType: "stream" });
17
- const videoStream = response.data;
18
- // Prepare metadata payload
19
- const metadataPayload = {
28
+ console.log(`📹 Fetching video from: ${videoUrl}`);
29
+ // Fetch video stream
30
+ const response = await axios.get(videoUrl, {
31
+ responseType: "stream",
32
+ timeout: 30000,
33
+ maxContentLength: 1024 * 1024 * 1024, // 1GB max
34
+ });
35
+ videoStream = response.data;
36
+ if (!videoStream || !(videoStream instanceof Readable)) {
37
+ throw new YouTubeMediaError('Invalid video stream received from URL');
38
+ }
39
+ // Prepare metadata
40
+ const requestBody = {
41
+ snippet: {
42
+ title: metadata.title.substring(0, 100),
43
+ description: (metadata.description || "").substring(0, 5000),
44
+ tags: metadata.tags || [],
45
+ categoryId: metadata.categoryId || "22"
46
+ },
47
+ status: {
48
+ privacyStatus: metadata.privacyStatus || "private",
49
+ embeddable: metadata.embeddable !== false,
50
+ publicStatsViewable: metadata.publicStatsViewable !== false,
51
+ license: metadata.license || "youtube",
52
+ selfDeclaredMadeForKids: metadata.madeForKids || false
53
+ }
54
+ };
55
+ console.log(`🚀 Uploading video to YouTube: "${metadata.title}"`);
56
+ // Upload using Google APIs client with OAuth2
57
+ const uploadResponse = await this.youtube.videos.insert({
20
58
  part: ["snippet", "status"],
21
- requestBody: {
22
- snippet: {
23
- title: metadata.title.substring(0, 100),
24
- description: (metadata.description || "").substring(0, 5000),
25
- tags: (metadata.tags || []).slice(0, 5),
26
- categoryId: metadata.categoryId || "22"
27
- },
28
- status: {
29
- privacyStatus: metadata.privacyStatus || "private",
30
- embeddable: metadata.embeddable !== false,
31
- publicStatsViewable: metadata.publicStatsViewable !== false,
32
- license: metadata.license || "youtube",
33
- selfDeclaredMadeForKids: metadata.madeForKids || false
34
- }
59
+ requestBody: requestBody,
60
+ media: {
61
+ body: videoStream,
62
+ mimeType: 'video/*'
63
+ },
64
+ notifySubscribers: metadata.notifySubscribers !== false
65
+ });
66
+ const videoId = uploadResponse.data.id;
67
+ if (!videoId) {
68
+ throw new YouTubeUploadError('Video ID not returned from YouTube API');
69
+ }
70
+ console.log(`✅ Video uploaded successfully: ${videoId}`);
71
+ // Wait for processing to complete
72
+ await this.waitForVideoProcessing(videoId);
73
+ const result = {
74
+ id: videoId,
75
+ status: 'processed',
76
+ url: `https://www.youtube.com/watch?v=${videoId}`,
77
+ title: metadata.title,
78
+ description: metadata.description,
79
+ };
80
+ console.log(`🎉 YouTube video published successfully: ${result.url}`);
81
+ return result;
82
+ }
83
+ catch (error) {
84
+ // Clean up stream
85
+ if (videoStream) {
86
+ videoStream.destroy();
87
+ }
88
+ console.error("YouTube upload failed:", error);
89
+ // Handle specific Google API errors
90
+ if (error.code === 401) {
91
+ throw new YouTubeAuthenticationError('Authentication failed. Please reconnect your YouTube account. ' +
92
+ 'The access token may have expired or is invalid.');
93
+ }
94
+ else if (error.code === 403) {
95
+ if (error.message?.includes('quota')) {
96
+ throw new YouTubeQuotaError('YouTube API quota exceeded. Please try again tomorrow.');
97
+ }
98
+ else {
99
+ throw new YouTubeAuthenticationError('Access denied. Please check your YouTube permissions.');
100
+ }
101
+ }
102
+ else if (error.code === 400) {
103
+ throw new YouTubeValidationError(`Invalid request: ${error.message}`);
104
+ }
105
+ else if (error.code === 503) {
106
+ throw new YouTubeProcessingError('YouTube service temporarily unavailable. Please try again.');
107
+ }
108
+ else {
109
+ throw new YouTubeApiError(`YouTube API error: ${error.message}`);
110
+ }
111
+ }
112
+ }
113
+ /**
114
+ * Wait for video processing to complete using direct API calls
115
+ */
116
+ async waitForVideoProcessing(videoId, timeoutMs = 300000) {
117
+ const startTime = Date.now();
118
+ const maxAttempts = 30;
119
+ let attempts = 0;
120
+ console.log(`⏳ Waiting for video processing: ${videoId}`);
121
+ while (attempts < maxAttempts && Date.now() - startTime < timeoutMs) {
122
+ attempts++;
123
+ try {
124
+ const statusResponse = await this.youtube.videos.list({
125
+ part: ['status'],
126
+ id: [videoId]
127
+ });
128
+ const video = statusResponse.data.items?.[0];
129
+ if (!video) {
130
+ throw new YouTubeProcessingError(`Video ${videoId} not found after upload`);
131
+ }
132
+ const status = video.status?.uploadStatus;
133
+ switch (status) {
134
+ case 'processed':
135
+ console.log(`✅ Video processing completed: ${videoId}`);
136
+ return;
137
+ case 'failed':
138
+ const failureReason = video.status?.failureReason || 'Unknown failure reason';
139
+ throw new YouTubeProcessingError(`Video processing failed: ${failureReason}`);
140
+ case 'rejected':
141
+ const rejectionReason = video.status?.rejectionReason || 'Unknown rejection reason';
142
+ throw new YouTubeProcessingError(`Video was rejected: ${rejectionReason}`);
143
+ case 'uploaded':
144
+ case 'processing':
145
+ const waitTime = Math.min(1000 * Math.pow(2, attempts), 10000);
146
+ console.log(`⏳ Video status: ${status}, waiting ${waitTime}ms...`);
147
+ await new Promise(resolve => setTimeout(resolve, waitTime));
148
+ break;
149
+ default:
150
+ await new Promise(resolve => setTimeout(resolve, 5000));
151
+ break;
152
+ }
153
+ }
154
+ catch (error) {
155
+ if (error instanceof YouTubeApiError) {
156
+ throw error;
157
+ }
158
+ // Continue waiting on network errors
159
+ console.warn(`Error checking video status (attempt ${attempts}):`, error.message);
160
+ await new Promise(resolve => setTimeout(resolve, 5000));
161
+ }
162
+ }
163
+ throw new YouTubeTimeoutError(`Video processing timed out after ${timeoutMs}ms`);
164
+ }
165
+ /**
166
+ * Alternative method using direct axios calls (if Google client still has issues)
167
+ */
168
+ async uploadFromCloudUrlDirect(videoUrl, metadata) {
169
+ try {
170
+ console.log(`📹 Using direct upload method for: ${videoUrl}`);
171
+ // First, get the video file as buffer
172
+ const videoResponse = await axios.get(videoUrl, {
173
+ responseType: 'arraybuffer',
174
+ timeout: 30000,
175
+ });
176
+ const videoBuffer = Buffer.from(videoResponse.data);
177
+ // Prepare the metadata
178
+ const metadataPayload = {
179
+ snippet: {
180
+ title: metadata.title.substring(0, 100),
181
+ description: (metadata.description || "").substring(0, 5000),
182
+ tags: metadata.tags || [],
183
+ categoryId: metadata.categoryId || "22"
35
184
  },
36
- media: { body: videoStream }
185
+ status: {
186
+ privacyStatus: metadata.privacyStatus || "private",
187
+ embeddable: metadata.embeddable !== false,
188
+ publicStatsViewable: metadata.publicStatsViewable !== false,
189
+ license: metadata.license || "youtube",
190
+ selfDeclaredMadeForKids: metadata.madeForKids || false
191
+ }
192
+ };
193
+ // Upload using direct multipart request
194
+ const formData = new FormData();
195
+ formData.append('metadata', JSON.stringify(metadataPayload));
196
+ formData.append('video', new Blob([videoBuffer]), 'video.mp4');
197
+ const uploadResponse = await axios.post('https://www.googleapis.com/upload/youtube/v3/videos?part=snippet,status', formData, {
198
+ headers: {
199
+ 'Authorization': `Bearer ${this.accessToken}`,
200
+ 'Content-Type': 'multipart/related',
201
+ },
202
+ maxContentLength: Infinity,
203
+ maxBodyLength: Infinity,
204
+ timeout: 60000,
205
+ });
206
+ const videoId = uploadResponse.data.id;
207
+ if (!videoId) {
208
+ throw new YouTubeUploadError('Video ID not returned from YouTube API');
209
+ }
210
+ console.log(`✅ Video uploaded successfully via direct method: ${videoId}`);
211
+ // Wait for processing
212
+ await this.waitForVideoProcessingDirect(videoId);
213
+ return {
214
+ id: videoId,
215
+ status: 'processed',
216
+ url: `https://www.youtube.com/watch?v=${videoId}`,
217
+ title: metadata.title,
218
+ description: metadata.description,
37
219
  };
38
- // Upload video
39
- const res = await this.youtube.videos.insert(metadataPayload);
40
- return res.data;
41
220
  }
42
221
  catch (error) {
43
- console.error("YouTube upload failed:", error.response?.data || error.message);
44
- throw new Error(`YouTube upload failed: ${error.response?.data?.error?.message || error.message}`);
222
+ console.error("Direct YouTube upload failed:", error.response?.data || error.message);
223
+ if (error.response?.status === 401) {
224
+ throw new YouTubeAuthenticationError('Authentication failed. Please reconnect your YouTube account.');
225
+ }
226
+ else if (error.response?.status === 403) {
227
+ throw new YouTubeQuotaError('YouTube API quota exceeded.');
228
+ }
229
+ else {
230
+ throw new YouTubeApiError(`Upload failed: ${error.message}`);
231
+ }
232
+ }
233
+ }
234
+ /**
235
+ * Wait for processing using direct API calls
236
+ */
237
+ async waitForVideoProcessingDirect(videoId, timeoutMs = 300000) {
238
+ const startTime = Date.now();
239
+ let attempts = 0;
240
+ while (Date.now() - startTime < timeoutMs) {
241
+ attempts++;
242
+ try {
243
+ const response = await axios.get(`https://www.googleapis.com/youtube/v3/videos?part=status&id=${videoId}`, {
244
+ headers: {
245
+ 'Authorization': `Bearer ${this.accessToken}`
246
+ }
247
+ });
248
+ const video = response.data.items?.[0];
249
+ if (!video) {
250
+ throw new YouTubeProcessingError(`Video ${videoId} not found`);
251
+ }
252
+ const status = video.status?.uploadStatus;
253
+ if (status === 'processed') {
254
+ console.log(`✅ Video processing completed: ${videoId}`);
255
+ return;
256
+ }
257
+ else if (status === 'failed') {
258
+ throw new YouTubeProcessingError(`Video processing failed: ${video.status?.failureReason}`);
259
+ }
260
+ else if (status === 'rejected') {
261
+ throw new YouTubeProcessingError(`Video rejected: ${video.status?.rejectionReason}`);
262
+ }
263
+ // Wait with exponential backoff
264
+ const waitTime = Math.min(1000 * Math.pow(2, attempts), 10000);
265
+ console.log(`⏳ Video status: ${status}, waiting ${waitTime}ms...`);
266
+ await new Promise(resolve => setTimeout(resolve, waitTime));
267
+ }
268
+ catch (error) {
269
+ console.warn(`Error checking video status (attempt ${attempts}):`, error.message);
270
+ await new Promise(resolve => setTimeout(resolve, 5000));
271
+ }
45
272
  }
273
+ throw new YouTubeTimeoutError(`Video processing timed out after ${timeoutMs}ms`);
46
274
  }
47
275
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-youtube-api",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "main": "dist/index.js",
5
5
  "scripts": {
6
6
  "prepublishOnly": "npm run build",
@@ -1,6 +1,18 @@
1
+ // src/youtube-handlers/youtube-video-poster.ts
1
2
  import { google, youtube_v3 } from "googleapis";
2
3
  import axios from "axios";
3
4
  import { Readable } from "stream";
5
+ import {
6
+ YouTubeApiError,
7
+ YouTubeAuthenticationError,
8
+ YouTubeQuotaError,
9
+ YouTubeRateLimitError,
10
+ YouTubeMediaError,
11
+ YouTubeUploadError,
12
+ YouTubeValidationError,
13
+ YouTubeProcessingError,
14
+ YouTubeTimeoutError
15
+ } from "../errors/youtube-api-errors";
4
16
 
5
17
  export interface VideoMetadata {
6
18
  title: string;
@@ -9,57 +21,340 @@ export interface VideoMetadata {
9
21
  categoryId?: string;
10
22
  privacyStatus?: "public" | "private" | "unlisted";
11
23
  madeForKids?: boolean;
12
- license?: "youtube" | "creativeCommon";
24
+ license?: "youtube" | "creativeCommon";
13
25
  embeddable?: boolean;
14
26
  publicStatsViewable?: boolean;
27
+ playlistId?: string;
28
+ notifySubscribers?: boolean;
29
+ }
30
+
31
+ export interface YouTubeUploadResult {
32
+ id: string;
33
+ status: string;
34
+ url: string;
35
+ title: string;
36
+ description?: string;
37
+ thumbnailUrl?: string;
15
38
  }
16
39
 
17
40
  export class YoutubeVideoPoster {
41
+ private accessToken: string;
18
42
  private youtube: youtube_v3.Youtube;
19
43
 
20
- constructor(private access_token: string) {
21
- this.youtube = google.youtube({ version: "v3", auth: this.access_token });
44
+ constructor(accessToken: string) {
45
+ if (!accessToken) {
46
+ throw new YouTubeAuthenticationError('Access token is required');
47
+ }
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
+ });
22
60
  }
23
61
 
24
62
  /**
25
- * Upload a video from a Cloudinary URL to YouTube
26
- * @param videoUrl URL of the video on Cloudinary
27
- * @param metadata Video metadata
63
+ * Upload a video from a Cloudinary URL to YouTube using direct API calls
28
64
  */
29
- public async uploadFromCloudUrl(videoUrl: string, metadata: VideoMetadata): Promise<youtube_v3.Schema$Video> {
65
+ public async uploadFromCloudUrl(videoUrl: string, metadata: VideoMetadata): Promise<YouTubeUploadResult> {
66
+ let videoStream: Readable | null = null;
67
+
30
68
  try {
31
- // Fetch video as stream from Cloudinary
32
- const response = await axios.get(videoUrl, { responseType: "stream" });
33
- const videoStream = response.data as Readable;
69
+ console.log(`📹 Fetching video from: ${videoUrl}`);
70
+
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
+ });
77
+
78
+ videoStream = response.data;
79
+
80
+ if (!videoStream || !(videoStream instanceof Readable)) {
81
+ throw new YouTubeMediaError('Invalid video stream received from URL');
82
+ }
34
83
 
35
- // Prepare metadata payload
36
- const metadataPayload: youtube_v3.Params$Resource$Videos$Insert = {
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
+ };
100
+
101
+ console.log(`🚀 Uploading video to YouTube: "${metadata.title}"`);
102
+
103
+ // Upload using Google APIs client with OAuth2
104
+ const uploadResponse = await this.youtube.videos.insert({
37
105
  part: ["snippet", "status"],
38
- requestBody: {
39
- snippet: {
40
- title: metadata.title.substring(0, 100),
41
- description: (metadata.description || "").substring(0, 5000),
42
- tags: (metadata.tags || []).slice(0, 5),
43
- categoryId: metadata.categoryId || "22"
44
- },
45
- status: {
46
- privacyStatus: metadata.privacyStatus || "private",
47
- embeddable: metadata.embeddable !== false,
48
- publicStatsViewable: metadata.publicStatsViewable !== false,
49
- license: metadata.license || "youtube",
50
- selfDeclaredMadeForKids: metadata.madeForKids || false
51
- }
106
+ requestBody: requestBody,
107
+ media: {
108
+ body: videoStream,
109
+ mimeType: 'video/*'
52
110
  },
53
- media: { body: videoStream }
111
+ notifySubscribers: metadata.notifySubscribers !== false
112
+ });
113
+
114
+ const videoId = uploadResponse.data.id;
115
+
116
+ if (!videoId) {
117
+ throw new YouTubeUploadError('Video ID not returned from YouTube API');
118
+ }
119
+
120
+ console.log(`✅ Video uploaded successfully: ${videoId}`);
121
+
122
+ // Wait for processing to complete
123
+ await this.waitForVideoProcessing(videoId);
124
+
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,
54
131
  };
55
132
 
56
- // Upload video
57
- const res = await this.youtube.videos.insert(metadataPayload);
58
- return res.data;
133
+ console.log(`🎉 YouTube video published successfully: ${result.url}`);
134
+
135
+ return result;
59
136
 
60
137
  } catch (error: any) {
61
- console.error("YouTube upload failed:", error.response?.data || error.message);
62
- throw new Error(`YouTube upload failed: ${error.response?.data?.error?.message || error.message}`);
138
+ // Clean up stream
139
+ if (videoStream) {
140
+ videoStream.destroy();
141
+ }
142
+
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.');
161
+ } else {
162
+ throw new YouTubeApiError(`YouTube API error: ${error.message}`);
163
+ }
63
164
  }
64
165
  }
65
- }
166
+
167
+ /**
168
+ * Wait for video processing to complete using direct API calls
169
+ */
170
+ private async waitForVideoProcessing(videoId: string, timeoutMs: number = 300000): Promise<void> {
171
+ const startTime = Date.now();
172
+ const maxAttempts = 30;
173
+ let attempts = 0;
174
+
175
+ console.log(`⏳ Waiting for video processing: ${videoId}`);
176
+
177
+ while (attempts < maxAttempts && Date.now() - startTime < timeoutMs) {
178
+ attempts++;
179
+
180
+ try {
181
+ const statusResponse = await this.youtube.videos.list({
182
+ part: ['status'],
183
+ id: [videoId]
184
+ });
185
+
186
+ const video = statusResponse.data.items?.[0];
187
+ if (!video) {
188
+ throw new YouTubeProcessingError(`Video ${videoId} not found after upload`);
189
+ }
190
+
191
+ const status = video.status?.uploadStatus;
192
+
193
+ switch (status) {
194
+ case 'processed':
195
+ console.log(`✅ Video processing completed: ${videoId}`);
196
+ return;
197
+ case 'failed':
198
+ const failureReason = video.status?.failureReason || 'Unknown failure reason';
199
+ throw new YouTubeProcessingError(`Video processing failed: ${failureReason}`);
200
+ case 'rejected':
201
+ const rejectionReason = video.status?.rejectionReason || 'Unknown rejection reason';
202
+ throw new YouTubeProcessingError(`Video was rejected: ${rejectionReason}`);
203
+ case 'uploaded':
204
+ case 'processing':
205
+ const waitTime = Math.min(1000 * Math.pow(2, attempts), 10000);
206
+ console.log(`⏳ Video status: ${status}, waiting ${waitTime}ms...`);
207
+ await new Promise(resolve => setTimeout(resolve, waitTime));
208
+ break;
209
+ default:
210
+ await new Promise(resolve => setTimeout(resolve, 5000));
211
+ break;
212
+ }
213
+ } catch (error: any) {
214
+ if (error instanceof YouTubeApiError) {
215
+ throw error;
216
+ }
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));
220
+ }
221
+ }
222
+
223
+ throw new YouTubeTimeoutError(`Video processing timed out after ${timeoutMs}ms`);
224
+ }
225
+
226
+ /**
227
+ * Alternative method using direct axios calls (if Google client still has issues)
228
+ */
229
+ public async uploadFromCloudUrlDirect(videoUrl: string, metadata: VideoMetadata): Promise<YouTubeUploadResult> {
230
+ try {
231
+ console.log(`📹 Using direct upload method for: ${videoUrl}`);
232
+
233
+ // First, get the video file as buffer
234
+ const videoResponse = await axios.get(videoUrl, {
235
+ responseType: 'arraybuffer',
236
+ timeout: 30000,
237
+ });
238
+
239
+ const videoBuffer = Buffer.from(videoResponse.data);
240
+
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"
248
+ },
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
+ }
256
+ };
257
+
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
+ );
276
+
277
+ const videoId = uploadResponse.data.id;
278
+
279
+ if (!videoId) {
280
+ throw new YouTubeUploadError('Video ID not returned from YouTube API');
281
+ }
282
+
283
+ console.log(`✅ Video uploaded successfully via direct method: ${videoId}`);
284
+
285
+ // Wait for processing
286
+ await this.waitForVideoProcessingDirect(videoId);
287
+
288
+ return {
289
+ id: videoId,
290
+ status: 'processed',
291
+ url: `https://www.youtube.com/watch?v=${videoId}`,
292
+ title: metadata.title,
293
+ description: metadata.description,
294
+ };
295
+
296
+ } catch (error: any) {
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}`);
307
+ }
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Wait for processing using direct API calls
313
+ */
314
+ private async waitForVideoProcessingDirect(videoId: string, timeoutMs: number = 300000): Promise<void> {
315
+ const startTime = Date.now();
316
+ let attempts = 0;
317
+
318
+ while (Date.now() - startTime < timeoutMs) {
319
+ attempts++;
320
+
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
+ );
330
+
331
+ const video = response.data.items?.[0];
332
+ if (!video) {
333
+ throw new YouTubeProcessingError(`Video ${videoId} not found`);
334
+ }
335
+
336
+ const status = video.status?.uploadStatus;
337
+
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
+ }
346
+
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));
351
+
352
+ } catch (error: any) {
353
+ console.warn(`Error checking video status (attempt ${attempts}):`, error.message);
354
+ await new Promise(resolve => setTimeout(resolve, 5000));
355
+ }
356
+ }
357
+
358
+ throw new YouTubeTimeoutError(`Video processing timed out after ${timeoutMs}ms`);
359
+ }
360
+ }