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.
@@ -1,4 +1,3 @@
1
- import { youtube_v3 } from "googleapis";
2
1
  export interface VideoMetadata {
3
2
  title: string;
4
3
  description?: string;
@@ -21,45 +20,23 @@ export interface YouTubeUploadResult {
21
20
  thumbnailUrl?: string;
22
21
  }
23
22
  export declare class YoutubeVideoPoster {
24
- private access_token;
23
+ private accessToken;
25
24
  private youtube;
26
- constructor(access_token: string);
25
+ constructor(accessToken: string);
27
26
  /**
28
- * Handle YouTube API errors and throw appropriate custom errors
27
+ * Upload a video from a Cloudinary URL to YouTube using direct API calls
29
28
  */
30
- private handleYouTubeError;
29
+ uploadFromCloudUrl(videoUrl: string, metadata: VideoMetadata): Promise<YouTubeUploadResult>;
31
30
  /**
32
- * Validate video metadata
33
- */
34
- private validateMetadata;
35
- /**
36
- * Fetch video stream from URL with proper error handling
37
- */
38
- private fetchVideoStream;
39
- /**
40
- * Wait for video processing to complete
31
+ * Wait for video processing to complete using direct API calls
41
32
  */
42
33
  private waitForVideoProcessing;
43
34
  /**
44
- * Add video to playlist if specified
45
- */
46
- private addToPlaylist;
47
- /**
48
- * Upload a video from a Cloudinary URL to YouTube
49
- */
50
- uploadFromCloudUrl2(videoUrl: string, metadata: VideoMetadata): Promise<YouTubeUploadResult>;
51
- /**
52
- * Upload a video from a Cloudinary URL to YouTube
53
- * @param videoUrl URL of the video on Cloudinary
54
- * @param metadata Video metadata
55
- */
56
- uploadFromCloudUrl(videoUrl: string, metadata: VideoMetadata): Promise<youtube_v3.Schema$Video>;
57
- /**
58
- * Get video details
35
+ * Alternative method using direct axios calls (if Google client still has issues)
59
36
  */
60
- getVideoDetails(videoId: string): Promise<youtube_v3.Schema$Video>;
37
+ uploadFromCloudUrlDirect(videoUrl: string, metadata: VideoMetadata): Promise<YouTubeUploadResult>;
61
38
  /**
62
- * Delete a video
39
+ * Wait for processing using direct API calls
63
40
  */
64
- deleteVideo(videoId: string): Promise<void>;
41
+ private waitForVideoProcessingDirect;
65
42
  }
@@ -2,157 +2,118 @@
2
2
  import { google } from "googleapis";
3
3
  import axios from "axios";
4
4
  import { Readable } from "stream";
5
- import { YouTubeApiError, YouTubeAuthenticationError, YouTubeQuotaError, YouTubeRateLimitError, YouTubeMediaError, YouTubeUploadError, YouTubeValidationError, YouTubeProcessingError, YouTubeTimeoutError } from "../errors/youtube-api-errors";
5
+ import { YouTubeApiError, YouTubeAuthenticationError, YouTubeQuotaError, YouTubeMediaError, YouTubeUploadError, YouTubeValidationError, YouTubeProcessingError, YouTubeTimeoutError } from "../errors/youtube-api-errors";
6
6
  export class YoutubeVideoPoster {
7
- constructor(access_token) {
8
- this.access_token = access_token;
9
- if (!access_token) {
7
+ constructor(accessToken) {
8
+ if (!accessToken) {
10
9
  throw new YouTubeAuthenticationError('Access token is required');
11
10
  }
12
- this.youtube = google.youtube({ version: "v3", auth: this.access_token });
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
+ });
13
21
  }
14
22
  /**
15
- * Handle YouTube API errors and throw appropriate custom errors
23
+ * Upload a video from a Cloudinary URL to YouTube using direct API calls
16
24
  */
17
- handleYouTubeError(error, context) {
18
- // Handle Google API errors
19
- if (error.errors && Array.isArray(error.errors)) {
20
- const apiError = error.errors[0];
21
- const message = `${context}: ${apiError.message}`;
22
- const reason = apiError.reason;
23
- const domain = apiError.domain;
24
- switch (reason) {
25
- case 'authError':
26
- case 'invalidCredentials':
27
- case 'required':
28
- throw new YouTubeAuthenticationError(message, apiError);
29
- case 'quotaExceeded':
30
- case 'dailyLimitExceeded':
31
- case 'userRateLimitExceeded':
32
- throw new YouTubeQuotaError(message, apiError);
33
- case 'rateLimitExceeded':
34
- case 'userRateLimitExceeded':
35
- throw new YouTubeRateLimitError(message, apiError);
36
- case 'invalidValue':
37
- case 'invalid':
38
- throw new YouTubeValidationError(message, apiError);
39
- case 'processingFailed':
40
- case 'failed':
41
- throw new YouTubeProcessingError(message, apiError);
42
- default:
43
- throw new YouTubeApiError(message, `YOUTUBE_${reason?.toUpperCase()}`, error.code, apiError);
44
- }
45
- }
46
- // Handle axios/network errors
47
- if (error.response?.data?.error) {
48
- const youtubeError = error.response.data.error;
49
- const message = `${context}: ${youtubeError.message}`;
50
- const code = youtubeError.code;
51
- switch (code) {
52
- case 401:
53
- case 403:
54
- throw new YouTubeAuthenticationError(message, youtubeError);
55
- case 429:
56
- throw new YouTubeRateLimitError(message, youtubeError);
57
- case 400:
58
- throw new YouTubeValidationError(message, youtubeError);
59
- case 500:
60
- case 503:
61
- throw new YouTubeProcessingError(message, youtubeError);
62
- default:
63
- throw new YouTubeApiError(message, `YOUTUBE_${code}`, code, youtubeError);
64
- }
65
- }
66
- // Handle timeout errors
67
- if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {
68
- throw new YouTubeTimeoutError(`${context}: Request timed out`, error);
69
- }
70
- // Handle network errors
71
- if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
72
- throw new YouTubeApiError(`${context}: Network error - ${error.message}`, 'NETWORK_ERROR', 503);
73
- }
74
- // Handle media/upload specific errors
75
- if (error.message?.includes('stream') || error.message?.includes('video') || error.message?.includes('upload')) {
76
- throw new YouTubeUploadError(`${context}: ${error.message}`, error);
77
- }
78
- // Default to generic API error
79
- if (error instanceof YouTubeApiError) {
80
- throw error;
81
- }
82
- throw new YouTubeApiError(`${context}: ${error.message}`, 'UNKNOWN_ERROR', error.response?.status);
83
- }
84
- /**
85
- * Validate video metadata
86
- */
87
- validateMetadata(metadata) {
88
- if (!metadata.title || metadata.title.trim().length === 0) {
89
- throw new YouTubeValidationError('Video title is required');
90
- }
91
- if (metadata.title.length > 100) {
92
- throw new YouTubeValidationError('Video title must be 100 characters or less');
93
- }
94
- if (metadata.description && metadata.description.length > 5000) {
95
- throw new YouTubeValidationError('Video description must be 5000 characters or less');
96
- }
97
- if (metadata.tags && metadata.tags.length > 500) {
98
- throw new YouTubeValidationError('Maximum 500 tags allowed');
99
- }
100
- if (metadata.tags) {
101
- for (const tag of metadata.tags) {
102
- if (tag.length > 30) {
103
- throw new YouTubeValidationError(`Tag "${tag}" must be 30 characters or less`);
104
- }
105
- }
106
- }
107
- const validPrivacyStatuses = ['public', 'private', 'unlisted'];
108
- if (metadata.privacyStatus && !validPrivacyStatuses.includes(metadata.privacyStatus)) {
109
- throw new YouTubeValidationError(`Privacy status must be one of: ${validPrivacyStatuses.join(', ')}`);
110
- }
111
- const validLicenses = ['youtube', 'creativeCommon'];
112
- if (metadata.license && !validLicenses.includes(metadata.license)) {
113
- throw new YouTubeValidationError(`License must be one of: ${validLicenses.join(', ')}`);
114
- }
115
- }
116
- /**
117
- * Fetch video stream from URL with proper error handling
118
- */
119
- async fetchVideoStream(videoUrl) {
25
+ async uploadFromCloudUrl(videoUrl, metadata) {
26
+ let videoStream = null;
120
27
  try {
121
28
  console.log(`📹 Fetching video from: ${videoUrl}`);
29
+ // Fetch video stream
122
30
  const response = await axios.get(videoUrl, {
123
31
  responseType: "stream",
124
- timeout: 30000, // 30 second timeout
125
- maxContentLength: 1024 * 1024 * 1024, // 1GB max file size
126
- validateStatus: (status) => status === 200
32
+ timeout: 30000,
33
+ maxContentLength: 1024 * 1024 * 1024, // 1GB max
127
34
  });
128
- if (!response.data || !(response.data instanceof Readable)) {
35
+ videoStream = response.data;
36
+ if (!videoStream || !(videoStream instanceof Readable)) {
129
37
  throw new YouTubeMediaError('Invalid video stream received from URL');
130
38
  }
131
- return response.data;
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({
58
+ part: ["snippet", "status"],
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;
132
82
  }
133
83
  catch (error) {
134
- if (error instanceof YouTubeApiError) {
135
- throw error;
84
+ // Clean up stream
85
+ if (videoStream) {
86
+ videoStream.destroy();
136
87
  }
137
- if (error.response?.status === 404) {
138
- throw new YouTubeMediaError(`Video not found at URL: ${videoUrl}`);
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.');
139
93
  }
140
- else if (error.response?.status === 403) {
141
- throw new YouTubeMediaError(`Access denied to video URL: ${videoUrl}`);
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
+ }
142
101
  }
143
- else if (error.code === 'ECONNABORTED') {
144
- throw new YouTubeTimeoutError(`Timeout while fetching video from URL: ${videoUrl}`);
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.');
145
107
  }
146
108
  else {
147
- throw new YouTubeMediaError(`Failed to fetch video from URL: ${error.message}`);
109
+ throw new YouTubeApiError(`YouTube API error: ${error.message}`);
148
110
  }
149
111
  }
150
112
  }
151
113
  /**
152
- * Wait for video processing to complete
114
+ * Wait for video processing to complete using direct API calls
153
115
  */
154
- async waitForVideoProcessing(videoId, timeoutMs = 300000 // 5 minutes
155
- ) {
116
+ async waitForVideoProcessing(videoId, timeoutMs = 300000) {
156
117
  const startTime = Date.now();
157
118
  const maxAttempts = 30;
158
119
  let attempts = 0;
@@ -181,8 +142,7 @@ export class YoutubeVideoPoster {
181
142
  throw new YouTubeProcessingError(`Video was rejected: ${rejectionReason}`);
182
143
  case 'uploaded':
183
144
  case 'processing':
184
- // Wait and check again
185
- const waitTime = Math.min(1000 * Math.pow(2, attempts), 10000); // Exponential backoff, max 10s
145
+ const waitTime = Math.min(1000 * Math.pow(2, attempts), 10000);
186
146
  console.log(`⏳ Video status: ${status}, waiting ${waitTime}ms...`);
187
147
  await new Promise(resolve => setTimeout(resolve, waitTime));
188
148
  break;
@@ -195,173 +155,121 @@ export class YoutubeVideoPoster {
195
155
  if (error instanceof YouTubeApiError) {
196
156
  throw error;
197
157
  }
198
- this.handleYouTubeError(error, "Failed to check video processing status");
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));
199
161
  }
200
162
  }
201
163
  throw new YouTubeTimeoutError(`Video processing timed out after ${timeoutMs}ms`);
202
164
  }
203
165
  /**
204
- * Add video to playlist if specified
166
+ * Alternative method using direct axios calls (if Google client still has issues)
205
167
  */
206
- async addToPlaylist(videoId, playlistId) {
168
+ async uploadFromCloudUrlDirect(videoUrl, metadata) {
207
169
  try {
208
- await this.youtube.playlistItems.insert({
209
- part: ['snippet'],
210
- requestBody: {
211
- snippet: {
212
- playlistId: playlistId,
213
- resourceId: {
214
- kind: 'youtube#video',
215
- videoId: videoId
216
- }
217
- }
218
- }
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,
219
175
  });
220
- console.log(`✅ Video ${videoId} added to playlist ${playlistId}`);
221
- }
222
- catch (error) {
223
- console.warn(`⚠️ Failed to add video to playlist: ${error.message}`);
224
- // Don't throw error for playlist addition failure as video upload was successful
225
- }
226
- }
227
- /**
228
- * Upload a video from a Cloudinary URL to YouTube
229
- */
230
- async uploadFromCloudUrl2(videoUrl, metadata) {
231
- let videoStream = null;
232
- try {
233
- // Validate metadata first
234
- this.validateMetadata(metadata);
235
- // Fetch video stream
236
- videoStream = await this.fetchVideoStream(videoUrl);
237
- // Prepare metadata payload
176
+ const videoBuffer = Buffer.from(videoResponse.data);
177
+ // Prepare the metadata
238
178
  const metadataPayload = {
239
- part: ["snippet", "status"],
240
- notifySubscribers: metadata.notifySubscribers !== false,
241
- requestBody: {
242
- snippet: {
243
- title: metadata.title.substring(0, 100),
244
- description: (metadata.description || "").substring(0, 5000),
245
- tags: (metadata.tags || []).slice(0, 500),
246
- categoryId: metadata.categoryId || "22"
247
- },
248
- status: {
249
- privacyStatus: metadata.privacyStatus || "private",
250
- embeddable: metadata.embeddable !== false,
251
- publicStatsViewable: metadata.publicStatsViewable !== false,
252
- license: metadata.license || "youtube",
253
- selfDeclaredMadeForKids: metadata.madeForKids || false
254
- }
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"
255
184
  },
256
- 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
+ }
257
192
  };
258
- console.log(`🚀 Uploading video to YouTube: "${metadata.title}"`);
259
- // Upload video
260
- const uploadResponse = await this.youtube.videos.insert(metadataPayload)
261
- .catch(error => this.handleYouTubeError(error, "Failed to upload video to YouTube"));
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
+ });
262
206
  const videoId = uploadResponse.data.id;
263
207
  if (!videoId) {
264
208
  throw new YouTubeUploadError('Video ID not returned from YouTube API');
265
209
  }
266
- console.log(`✅ Video uploaded successfully: ${videoId}`);
267
- // Wait for processing to complete
268
- await this.waitForVideoProcessing(videoId);
269
- // Add to playlist if specified
270
- if (metadata.playlistId) {
271
- await this.addToPlaylist(videoId, metadata.playlistId);
272
- }
273
- const result = {
210
+ console.log(`✅ Video uploaded successfully via direct method: ${videoId}`);
211
+ // Wait for processing
212
+ await this.waitForVideoProcessingDirect(videoId);
213
+ return {
274
214
  id: videoId,
275
215
  status: 'processed',
276
216
  url: `https://www.youtube.com/watch?v=${videoId}`,
277
217
  title: metadata.title,
278
218
  description: metadata.description,
279
- // You can add thumbnail URL here if needed
280
219
  };
281
- console.log(`🎉 YouTube video published successfully: ${result.url}`);
282
- return result;
283
220
  }
284
221
  catch (error) {
285
- // Clean up stream if it exists
286
- if (videoStream) {
287
- videoStream.destroy();
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.');
288
225
  }
289
- if (error instanceof YouTubeApiError) {
290
- throw error;
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}`);
291
231
  }
292
- this.handleYouTubeError(error, "Unexpected error during YouTube video upload");
293
232
  }
294
233
  }
295
234
  /**
296
- * Upload a video from a Cloudinary URL to YouTube
297
- * @param videoUrl URL of the video on Cloudinary
298
- * @param metadata Video metadata
235
+ * Wait for processing using direct API calls
299
236
  */
300
- async uploadFromCloudUrl(videoUrl, metadata) {
301
- try {
302
- // Fetch video as stream from Cloudinary
303
- const response = await axios.get(videoUrl, { responseType: "stream" });
304
- const videoStream = response.data;
305
- // Prepare metadata payload
306
- const metadataPayload = {
307
- part: ["snippet", "status"],
308
- requestBody: {
309
- snippet: {
310
- title: metadata.title.substring(0, 100),
311
- description: (metadata.description || "").substring(0, 5000),
312
- tags: (metadata.tags || []).slice(0, 5),
313
- categoryId: metadata.categoryId || "22"
314
- },
315
- status: {
316
- privacyStatus: metadata.privacyStatus || "private",
317
- embeddable: metadata.embeddable !== false,
318
- publicStatsViewable: metadata.publicStatsViewable !== false,
319
- license: metadata.license || "youtube",
320
- selfDeclaredMadeForKids: metadata.madeForKids || false
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}`
321
246
  }
322
- },
323
- media: { body: videoStream }
324
- };
325
- // Upload video
326
- const res = await this.youtube.videos.insert(metadataPayload);
327
- return res.data;
328
- }
329
- catch (error) {
330
- console.error("YouTube upload failed:", error.response?.data || error.message);
331
- throw new Error(`YouTube upload failed: ${error.response?.data?.error?.message || error.message}`);
332
- }
333
- }
334
- /**
335
- * Get video details
336
- */
337
- async getVideoDetails(videoId) {
338
- try {
339
- const response = await this.youtube.videos.list({
340
- part: ['snippet', 'status', 'contentDetails', 'statistics'],
341
- id: [videoId]
342
- });
343
- const video = response.data.items?.[0];
344
- if (!video) {
345
- throw new YouTubeValidationError(`Video not found: ${videoId}`);
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));
346
271
  }
347
- return video;
348
- }
349
- catch (error) {
350
- this.handleYouTubeError(error, "Failed to get video details");
351
- }
352
- }
353
- /**
354
- * Delete a video
355
- */
356
- async deleteVideo(videoId) {
357
- try {
358
- await this.youtube.videos.delete({
359
- id: videoId
360
- });
361
- console.log(`✅ Video deleted successfully: ${videoId}`);
362
- }
363
- catch (error) {
364
- this.handleYouTubeError(error, "Failed to delete video");
365
272
  }
273
+ throw new YouTubeTimeoutError(`Video processing timed out after ${timeoutMs}ms`);
366
274
  }
367
275
  }
@@ -0,0 +1,21 @@
1
+ interface Channel {
2
+ channelId: string;
3
+ title: string;
4
+ description: string;
5
+ thumbnail: string;
6
+ }
7
+ export declare class YoutubeAccountHandler {
8
+ private account_access_token;
9
+ /**
10
+ * Creates a new YoutubeAccountHandler instance
11
+ * @param account_access_token - The current access token for YouTube API authentication
12
+ */
13
+ constructor(account_access_token: string);
14
+ /**
15
+ * Fetches the list of YouTube channels associated with the authenticated account
16
+ * @returns Promise resolving to an array of Channel objects containing channel details
17
+ * @throws {AxiosError} If the API request fails due to authentication or other errors
18
+ */
19
+ fetchChannels(): Promise<Channel[]>;
20
+ }
21
+ export {};
@@ -0,0 +1,33 @@
1
+ import axios from "axios";
2
+ import { YOUTUBE_BASE_URL } from "../error-utils";
3
+ export class YoutubeAccountHandler {
4
+ /**
5
+ * Creates a new YoutubeAccountHandler instance
6
+ * @param account_access_token - The current access token for YouTube API authentication
7
+ */
8
+ constructor(account_access_token) {
9
+ this.account_access_token = account_access_token;
10
+ }
11
+ /**
12
+ * Fetches the list of YouTube channels associated with the authenticated account
13
+ * @returns Promise resolving to an array of Channel objects containing channel details
14
+ * @throws {AxiosError} If the API request fails due to authentication or other errors
15
+ */
16
+ async fetchChannels() {
17
+ const url = `${YOUTUBE_BASE_URL}/youtube/v3/channels`;
18
+ const params = {
19
+ part: "snippet,contentDetails,statistics",
20
+ mine: true,
21
+ };
22
+ const access_token = this.account_access_token;
23
+ const headers = { Authorization: `Bearer ${access_token}` };
24
+ const res = await axios.get(url, { params, headers });
25
+ const channels = res.data.items.map((ch) => ({
26
+ channelId: ch.id,
27
+ title: ch.snippet.title,
28
+ description: ch.snippet.description,
29
+ thumbnail: ch.snippet.thumbnails?.default?.url,
30
+ }));
31
+ return channels;
32
+ }
33
+ }