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
|
@@ -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
|
|
23
|
+
private accessToken;
|
|
25
24
|
private youtube;
|
|
26
|
-
constructor(
|
|
25
|
+
constructor(accessToken: string);
|
|
27
26
|
/**
|
|
28
|
-
*
|
|
27
|
+
* Upload a video from a Cloudinary URL to YouTube using direct API calls
|
|
29
28
|
*/
|
|
30
|
-
|
|
29
|
+
uploadFromCloudUrl(videoUrl: string, metadata: VideoMetadata): Promise<YouTubeUploadResult>;
|
|
31
30
|
/**
|
|
32
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
37
|
+
uploadFromCloudUrlDirect(videoUrl: string, metadata: VideoMetadata): Promise<YouTubeUploadResult>;
|
|
61
38
|
/**
|
|
62
|
-
*
|
|
39
|
+
* Wait for processing using direct API calls
|
|
63
40
|
*/
|
|
64
|
-
|
|
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,
|
|
5
|
+
import { YouTubeApiError, YouTubeAuthenticationError, YouTubeQuotaError, YouTubeMediaError, YouTubeUploadError, YouTubeValidationError, YouTubeProcessingError, YouTubeTimeoutError } from "../errors/youtube-api-errors";
|
|
6
6
|
export class YoutubeVideoPoster {
|
|
7
|
-
constructor(
|
|
8
|
-
|
|
9
|
-
if (!access_token) {
|
|
7
|
+
constructor(accessToken) {
|
|
8
|
+
if (!accessToken) {
|
|
10
9
|
throw new YouTubeAuthenticationError('Access token is required');
|
|
11
10
|
}
|
|
12
|
-
this.
|
|
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
|
-
*
|
|
23
|
+
* Upload a video from a Cloudinary URL to YouTube using direct API calls
|
|
16
24
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
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,
|
|
125
|
-
maxContentLength: 1024 * 1024 * 1024, // 1GB max
|
|
126
|
-
validateStatus: (status) => status === 200
|
|
32
|
+
timeout: 30000,
|
|
33
|
+
maxContentLength: 1024 * 1024 * 1024, // 1GB max
|
|
127
34
|
});
|
|
128
|
-
|
|
35
|
+
videoStream = response.data;
|
|
36
|
+
if (!videoStream || !(videoStream instanceof Readable)) {
|
|
129
37
|
throw new YouTubeMediaError('Invalid video stream received from URL');
|
|
130
38
|
}
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
84
|
+
// Clean up stream
|
|
85
|
+
if (videoStream) {
|
|
86
|
+
videoStream.destroy();
|
|
136
87
|
}
|
|
137
|
-
|
|
138
|
-
|
|
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.
|
|
141
|
-
|
|
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 ===
|
|
144
|
-
throw new
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
166
|
+
* Alternative method using direct axios calls (if Google client still has issues)
|
|
205
167
|
*/
|
|
206
|
-
async
|
|
168
|
+
async uploadFromCloudUrlDirect(videoUrl, metadata) {
|
|
207
169
|
try {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|
268
|
-
await this.
|
|
269
|
-
|
|
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
|
-
|
|
286
|
-
if (
|
|
287
|
-
|
|
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
|
|
290
|
-
throw
|
|
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
|
-
*
|
|
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
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
+
}
|