my-youtube-api 1.0.3 → 1.0.5
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 +3 -39
- package/dist/youtube-handlers/youtube-video-poster.js +10 -291
- 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 +13 -362
- 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,18 +1,6 @@
|
|
|
1
|
-
// src/youtube-handlers/youtube-video-poster.ts
|
|
2
1
|
import { google, youtube_v3 } from "googleapis";
|
|
3
2
|
import axios from "axios";
|
|
4
3
|
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";
|
|
16
4
|
|
|
17
5
|
export interface VideoMetadata {
|
|
18
6
|
title: string;
|
|
@@ -21,299 +9,37 @@ export interface VideoMetadata {
|
|
|
21
9
|
categoryId?: string;
|
|
22
10
|
privacyStatus?: "public" | "private" | "unlisted";
|
|
23
11
|
madeForKids?: boolean;
|
|
24
|
-
license?: "youtube" | "creativeCommon";
|
|
12
|
+
license?: "youtube" | "creativeCommon";
|
|
25
13
|
embeddable?: boolean;
|
|
26
14
|
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;
|
|
38
15
|
}
|
|
39
16
|
|
|
40
17
|
export class YoutubeVideoPoster {
|
|
41
18
|
private youtube: youtube_v3.Youtube;
|
|
42
19
|
|
|
43
20
|
constructor(private access_token: string) {
|
|
44
|
-
if (!access_token) {
|
|
45
|
-
throw new YouTubeAuthenticationError('Access token is required');
|
|
46
|
-
}
|
|
47
21
|
this.youtube = google.youtube({ version: "v3", auth: this.access_token });
|
|
48
22
|
}
|
|
49
23
|
|
|
50
|
-
/**
|
|
51
|
-
* Handle YouTube API errors and throw appropriate custom errors
|
|
52
|
-
*/
|
|
53
|
-
private handleYouTubeError(error: any, context: string): never {
|
|
54
|
-
// Handle Google API errors
|
|
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
|
-
}
|
|
115
|
-
|
|
116
|
-
// Handle network errors
|
|
117
|
-
if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
|
|
118
|
-
throw new YouTubeApiError(`${context}: Network error - ${error.message}`, 'NETWORK_ERROR', 503);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Handle media/upload specific errors
|
|
122
|
-
if (error.message?.includes('stream') || error.message?.includes('video') || error.message?.includes('upload')) {
|
|
123
|
-
throw new YouTubeUploadError(`${context}: ${error.message}`, error);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Default to generic API error
|
|
127
|
-
if (error instanceof YouTubeApiError) {
|
|
128
|
-
throw error;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
throw new YouTubeApiError(`${context}: ${error.message}`, 'UNKNOWN_ERROR', error.response?.status);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Validate video metadata
|
|
136
|
-
*/
|
|
137
|
-
private validateMetadata(metadata: VideoMetadata): void {
|
|
138
|
-
if (!metadata.title || metadata.title.trim().length === 0) {
|
|
139
|
-
throw new YouTubeValidationError('Video title is required');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (metadata.title.length > 100) {
|
|
143
|
-
throw new YouTubeValidationError('Video title must be 100 characters or less');
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (metadata.description && metadata.description.length > 5000) {
|
|
147
|
-
throw new YouTubeValidationError('Video description must be 5000 characters or less');
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (metadata.tags && metadata.tags.length > 500) {
|
|
151
|
-
throw new YouTubeValidationError('Maximum 500 tags allowed');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (metadata.tags) {
|
|
155
|
-
for (const tag of metadata.tags) {
|
|
156
|
-
if (tag.length > 30) {
|
|
157
|
-
throw new YouTubeValidationError(`Tag "${tag}" must be 30 characters or less`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const validPrivacyStatuses = ['public', 'private', 'unlisted'];
|
|
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
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Fetch video stream from URL with proper error handling
|
|
175
|
-
*/
|
|
176
|
-
private async fetchVideoStream(videoUrl: string): Promise<Readable> {
|
|
177
|
-
try {
|
|
178
|
-
console.log(`📹 Fetching video from: ${videoUrl}`);
|
|
179
|
-
|
|
180
|
-
const response = await axios.get(videoUrl, {
|
|
181
|
-
responseType: "stream",
|
|
182
|
-
timeout: 30000, // 30 second timeout
|
|
183
|
-
maxContentLength: 1024 * 1024 * 1024, // 1GB max file size
|
|
184
|
-
validateStatus: (status) => status === 200
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
if (!response.data || !(response.data instanceof Readable)) {
|
|
188
|
-
throw new YouTubeMediaError('Invalid video stream received from URL');
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return response.data;
|
|
192
|
-
|
|
193
|
-
} catch (error: any) {
|
|
194
|
-
if (error instanceof YouTubeApiError) {
|
|
195
|
-
throw error;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (error.response?.status === 404) {
|
|
199
|
-
throw new YouTubeMediaError(`Video not found at URL: ${videoUrl}`);
|
|
200
|
-
} else if (error.response?.status === 403) {
|
|
201
|
-
throw new YouTubeMediaError(`Access denied to video URL: ${videoUrl}`);
|
|
202
|
-
} else if (error.code === 'ECONNABORTED') {
|
|
203
|
-
throw new YouTubeTimeoutError(`Timeout while fetching video from URL: ${videoUrl}`);
|
|
204
|
-
} else {
|
|
205
|
-
throw new YouTubeMediaError(`Failed to fetch video from URL: ${error.message}`);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Wait for video processing to complete
|
|
212
|
-
*/
|
|
213
|
-
private async waitForVideoProcessing(
|
|
214
|
-
videoId: string,
|
|
215
|
-
timeoutMs: number = 300000 // 5 minutes
|
|
216
|
-
): Promise<void> {
|
|
217
|
-
const startTime = Date.now();
|
|
218
|
-
const maxAttempts = 30;
|
|
219
|
-
let attempts = 0;
|
|
220
|
-
|
|
221
|
-
console.log(`⏳ Waiting for video processing: ${videoId}`);
|
|
222
|
-
|
|
223
|
-
while (attempts < maxAttempts && Date.now() - startTime < timeoutMs) {
|
|
224
|
-
attempts++;
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
const statusResponse = await this.youtube.videos.list({
|
|
228
|
-
part: ['status'],
|
|
229
|
-
id: [videoId]
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
const video = statusResponse.data.items?.[0];
|
|
233
|
-
if (!video) {
|
|
234
|
-
throw new YouTubeProcessingError(`Video ${videoId} not found after upload`);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const status = video.status?.uploadStatus;
|
|
238
|
-
|
|
239
|
-
switch (status) {
|
|
240
|
-
case 'processed':
|
|
241
|
-
console.log(`✅ Video processing completed: ${videoId}`);
|
|
242
|
-
return;
|
|
243
|
-
case 'failed':
|
|
244
|
-
const failureReason = video.status?.failureReason || 'Unknown failure reason';
|
|
245
|
-
throw new YouTubeProcessingError(`Video processing failed: ${failureReason}`);
|
|
246
|
-
case 'rejected':
|
|
247
|
-
const rejectionReason = video.status?.rejectionReason || 'Unknown rejection reason';
|
|
248
|
-
throw new YouTubeProcessingError(`Video was rejected: ${rejectionReason}`);
|
|
249
|
-
case 'uploaded':
|
|
250
|
-
case 'processing':
|
|
251
|
-
// Wait and check again
|
|
252
|
-
const waitTime = Math.min(1000 * Math.pow(2, attempts), 10000); // Exponential backoff, max 10s
|
|
253
|
-
console.log(`⏳ Video status: ${status}, waiting ${waitTime}ms...`);
|
|
254
|
-
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
255
|
-
break;
|
|
256
|
-
default:
|
|
257
|
-
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
258
|
-
break;
|
|
259
|
-
}
|
|
260
|
-
} catch (error: any) {
|
|
261
|
-
if (error instanceof YouTubeApiError) {
|
|
262
|
-
throw error;
|
|
263
|
-
}
|
|
264
|
-
this.handleYouTubeError(error, "Failed to check video processing status");
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
throw new YouTubeTimeoutError(`Video processing timed out after ${timeoutMs}ms`);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Add video to playlist if specified
|
|
273
|
-
*/
|
|
274
|
-
private async addToPlaylist(videoId: string, playlistId: string): Promise<void> {
|
|
275
|
-
try {
|
|
276
|
-
await this.youtube.playlistItems.insert({
|
|
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
|
-
}
|
|
294
|
-
|
|
295
24
|
/**
|
|
296
25
|
* Upload a video from a Cloudinary URL to YouTube
|
|
26
|
+
* @param videoUrl URL of the video on Cloudinary
|
|
27
|
+
* @param metadata Video metadata
|
|
297
28
|
*/
|
|
298
|
-
public async uploadFromCloudUrl(videoUrl: string, metadata: VideoMetadata): Promise<
|
|
299
|
-
let videoStream: Readable | null = null;
|
|
300
|
-
|
|
29
|
+
public async uploadFromCloudUrl(videoUrl: string, metadata: VideoMetadata): Promise<youtube_v3.Schema$Video> {
|
|
301
30
|
try {
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
// Fetch video stream
|
|
306
|
-
videoStream = await this.fetchVideoStream(videoUrl);
|
|
31
|
+
// Fetch video as stream from Cloudinary
|
|
32
|
+
const response = await axios.get(videoUrl, { responseType: "stream" });
|
|
33
|
+
const videoStream = response.data as Readable;
|
|
307
34
|
|
|
308
35
|
// Prepare metadata payload
|
|
309
36
|
const metadataPayload: youtube_v3.Params$Resource$Videos$Insert = {
|
|
310
37
|
part: ["snippet", "status"],
|
|
311
|
-
notifySubscribers: metadata.notifySubscribers !== false,
|
|
312
38
|
requestBody: {
|
|
313
39
|
snippet: {
|
|
314
40
|
title: metadata.title.substring(0, 100),
|
|
315
41
|
description: (metadata.description || "").substring(0, 5000),
|
|
316
|
-
tags: (metadata.tags || []).slice(0,
|
|
42
|
+
tags: (metadata.tags || []).slice(0, 5),
|
|
317
43
|
categoryId: metadata.categoryId || "22"
|
|
318
44
|
},
|
|
319
45
|
status: {
|
|
@@ -327,88 +53,13 @@ export class YoutubeVideoPoster {
|
|
|
327
53
|
media: { body: videoStream }
|
|
328
54
|
};
|
|
329
55
|
|
|
330
|
-
console.log(`🚀 Uploading video to YouTube: "${metadata.title}"`);
|
|
331
|
-
|
|
332
56
|
// Upload video
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const videoId = uploadResponse.data.id;
|
|
337
|
-
|
|
338
|
-
if (!videoId) {
|
|
339
|
-
throw new YouTubeUploadError('Video ID not returned from YouTube API');
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
console.log(`✅ Video uploaded successfully: ${videoId}`);
|
|
343
|
-
|
|
344
|
-
// Wait for processing to complete
|
|
345
|
-
await this.waitForVideoProcessing(videoId);
|
|
346
|
-
|
|
347
|
-
// Add to playlist if specified
|
|
348
|
-
if (metadata.playlistId) {
|
|
349
|
-
await this.addToPlaylist(videoId, metadata.playlistId);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const result: YouTubeUploadResult = {
|
|
353
|
-
id: videoId,
|
|
354
|
-
status: 'processed',
|
|
355
|
-
url: `https://www.youtube.com/watch?v=${videoId}`,
|
|
356
|
-
title: metadata.title,
|
|
357
|
-
description: metadata.description,
|
|
358
|
-
// You can add thumbnail URL here if needed
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
console.log(`🎉 YouTube video published successfully: ${result.url}`);
|
|
362
|
-
|
|
363
|
-
return result;
|
|
364
|
-
|
|
365
|
-
} catch (error: any) {
|
|
366
|
-
// Clean up stream if it exists
|
|
367
|
-
if (videoStream) {
|
|
368
|
-
videoStream.destroy();
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (error instanceof YouTubeApiError) {
|
|
372
|
-
throw error;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
this.handleYouTubeError(error, "Unexpected error during YouTube video upload");
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Get video details
|
|
381
|
-
*/
|
|
382
|
-
public async getVideoDetails(videoId: string): Promise<youtube_v3.Schema$Video> {
|
|
383
|
-
try {
|
|
384
|
-
const response = await this.youtube.videos.list({
|
|
385
|
-
part: ['snippet', 'status', 'contentDetails', 'statistics'],
|
|
386
|
-
id: [videoId]
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
const video = response.data.items?.[0];
|
|
390
|
-
if (!video) {
|
|
391
|
-
throw new YouTubeValidationError(`Video not found: ${videoId}`);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return video;
|
|
57
|
+
const res = await this.youtube.videos.insert(metadataPayload);
|
|
58
|
+
return res.data;
|
|
395
59
|
|
|
396
60
|
} catch (error: any) {
|
|
397
|
-
|
|
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}`);
|
|
398
63
|
}
|
|
399
64
|
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Delete a video
|
|
403
|
-
*/
|
|
404
|
-
public async deleteVideo(videoId: string): Promise<void> {
|
|
405
|
-
try {
|
|
406
|
-
await this.youtube.videos.delete({
|
|
407
|
-
id: videoId
|
|
408
|
-
});
|
|
409
|
-
console.log(`✅ Video deleted successfully: ${videoId}`);
|
|
410
|
-
} catch (error: any) {
|
|
411
|
-
this.handleYouTubeError(error, "Failed to delete video");
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
65
|
+
}
|