@memberjunction/actions-bizapps-social 2.111.0 → 2.112.0
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +6 -6
- package/dist/base/base-social.action.d.ts.map +1 -1
- package/dist/base/base-social.action.js +18 -24
- package/dist/base/base-social.action.js.map +1 -1
- package/dist/providers/buffer/buffer-base.action.d.ts.map +1 -1
- package/dist/providers/buffer/buffer-base.action.js +35 -34
- package/dist/providers/buffer/buffer-base.action.js.map +1 -1
- package/dist/providers/facebook/actions/boost-post.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/boost-post.action.js +33 -33
- package/dist/providers/facebook/actions/boost-post.action.js.map +1 -1
- package/dist/providers/facebook/actions/create-album.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/create-album.action.js +34 -36
- package/dist/providers/facebook/actions/create-album.action.js.map +1 -1
- package/dist/providers/facebook/actions/create-post.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/create-post.action.js +20 -20
- package/dist/providers/facebook/actions/create-post.action.js.map +1 -1
- package/dist/providers/facebook/actions/get-page-insights.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/get-page-insights.action.js +25 -27
- package/dist/providers/facebook/actions/get-page-insights.action.js.map +1 -1
- package/dist/providers/facebook/actions/get-page-posts.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/get-page-posts.action.js +19 -23
- package/dist/providers/facebook/actions/get-page-posts.action.js.map +1 -1
- package/dist/providers/facebook/actions/get-post-insights.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/get-post-insights.action.js +28 -32
- package/dist/providers/facebook/actions/get-post-insights.action.js.map +1 -1
- package/dist/providers/facebook/actions/respond-to-comments.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/respond-to-comments.action.js +42 -44
- package/dist/providers/facebook/actions/respond-to-comments.action.js.map +1 -1
- package/dist/providers/facebook/actions/schedule-post.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/schedule-post.action.js +29 -29
- package/dist/providers/facebook/actions/schedule-post.action.js.map +1 -1
- package/dist/providers/facebook/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/search-posts.action.js +37 -39
- package/dist/providers/facebook/actions/search-posts.action.js.map +1 -1
- package/dist/providers/facebook/facebook-base.action.d.ts.map +1 -1
- package/dist/providers/facebook/facebook-base.action.js +44 -59
- package/dist/providers/facebook/facebook-base.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js +33 -31
- package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/create-scheduled-post.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/create-scheduled-post.action.js +28 -32
- package/dist/providers/hootsuite/actions/create-scheduled-post.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/delete-scheduled-post.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js +19 -19
- package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/get-analytics.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/get-analytics.action.js +24 -26
- package/dist/providers/hootsuite/actions/get-analytics.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/get-scheduled-posts.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js +22 -22
- package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/get-social-profiles.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/get-social-profiles.action.js +32 -34
- package/dist/providers/hootsuite/actions/get-social-profiles.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/search-posts.action.js +43 -52
- package/dist/providers/hootsuite/actions/search-posts.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/update-scheduled-post.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/update-scheduled-post.action.js +30 -28
- package/dist/providers/hootsuite/actions/update-scheduled-post.action.js.map +1 -1
- package/dist/providers/hootsuite/hootsuite-base.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/hootsuite-base.action.js +18 -20
- package/dist/providers/hootsuite/hootsuite-base.action.js.map +1 -1
- package/dist/providers/instagram/actions/create-post.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/create-post.action.js +27 -26
- package/dist/providers/instagram/actions/create-post.action.js.map +1 -1
- package/dist/providers/instagram/actions/create-story.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/create-story.action.js +35 -35
- package/dist/providers/instagram/actions/create-story.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-account-insights.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-account-insights.action.js +38 -59
- package/dist/providers/instagram/actions/get-account-insights.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-business-posts.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-business-posts.action.js +29 -29
- package/dist/providers/instagram/actions/get-business-posts.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-comments.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-comments.action.js +36 -36
- package/dist/providers/instagram/actions/get-comments.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-post-insights.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-post-insights.action.js +23 -25
- package/dist/providers/instagram/actions/get-post-insights.action.js.map +1 -1
- package/dist/providers/instagram/actions/schedule-post.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/schedule-post.action.js +25 -25
- package/dist/providers/instagram/actions/schedule-post.action.js.map +1 -1
- package/dist/providers/instagram/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/search-posts.action.js +56 -60
- package/dist/providers/instagram/actions/search-posts.action.js.map +1 -1
- package/dist/providers/instagram/instagram-base.action.d.ts.map +1 -1
- package/dist/providers/instagram/instagram-base.action.js +25 -27
- package/dist/providers/instagram/instagram-base.action.js.map +1 -1
- package/dist/providers/linkedin/actions/create-article.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/create-article.action.js +55 -45
- package/dist/providers/linkedin/actions/create-article.action.js.map +1 -1
- package/dist/providers/linkedin/actions/create-post.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/create-post.action.js +31 -29
- package/dist/providers/linkedin/actions/create-post.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-followers.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-followers.action.js +28 -28
- package/dist/providers/linkedin/actions/get-followers.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-organization-posts.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-organization-posts.action.js +20 -20
- package/dist/providers/linkedin/actions/get-organization-posts.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-personal-posts.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-personal-posts.action.js +19 -19
- package/dist/providers/linkedin/actions/get-personal-posts.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-post-analytics.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-post-analytics.action.js +25 -23
- package/dist/providers/linkedin/actions/get-post-analytics.action.js.map +1 -1
- package/dist/providers/linkedin/actions/schedule-post.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/schedule-post.action.js +32 -30
- package/dist/providers/linkedin/actions/schedule-post.action.js.map +1 -1
- package/dist/providers/linkedin/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/search-posts.action.js +28 -30
- package/dist/providers/linkedin/actions/search-posts.action.js.map +1 -1
- package/dist/providers/linkedin/linkedin-base.action.d.ts.map +1 -1
- package/dist/providers/linkedin/linkedin-base.action.js +33 -38
- package/dist/providers/linkedin/linkedin-base.action.js.map +1 -1
- package/dist/providers/tiktok/tiktok-base.action.d.ts.map +1 -1
- package/dist/providers/tiktok/tiktok-base.action.js +25 -26
- package/dist/providers/tiktok/tiktok-base.action.js.map +1 -1
- package/dist/providers/twitter/actions/create-thread.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/create-thread.action.js +25 -29
- package/dist/providers/twitter/actions/create-thread.action.js.map +1 -1
- package/dist/providers/twitter/actions/create-tweet.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/create-tweet.action.js +23 -23
- package/dist/providers/twitter/actions/create-tweet.action.js.map +1 -1
- package/dist/providers/twitter/actions/delete-tweet.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/delete-tweet.action.js +19 -19
- package/dist/providers/twitter/actions/delete-tweet.action.js.map +1 -1
- package/dist/providers/twitter/actions/get-analytics.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/get-analytics.action.js +40 -47
- package/dist/providers/twitter/actions/get-analytics.action.js.map +1 -1
- package/dist/providers/twitter/actions/get-mentions.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/get-mentions.action.js +30 -31
- package/dist/providers/twitter/actions/get-mentions.action.js.map +1 -1
- package/dist/providers/twitter/actions/get-timeline.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/get-timeline.action.js +29 -29
- package/dist/providers/twitter/actions/get-timeline.action.js.map +1 -1
- package/dist/providers/twitter/actions/schedule-tweet.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/schedule-tweet.action.js +26 -26
- package/dist/providers/twitter/actions/schedule-tweet.action.js.map +1 -1
- package/dist/providers/twitter/actions/search-tweets.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/search-tweets.action.js +56 -58
- package/dist/providers/twitter/actions/search-tweets.action.js.map +1 -1
- package/dist/providers/twitter/twitter-base.action.d.ts.map +1 -1
- package/dist/providers/twitter/twitter-base.action.js +58 -68
- package/dist/providers/twitter/twitter-base.action.js.map +1 -1
- package/dist/providers/youtube/youtube-base.action.d.ts +1 -1
- package/dist/providers/youtube/youtube-base.action.d.ts.map +1 -1
- package/dist/providers/youtube/youtube-base.action.js +22 -25
- package/dist/providers/youtube/youtube-base.action.js.map +1 -1
- package/package.json +5 -6
- package/src/base/base-social.action.ts +217 -224
- package/src/providers/buffer/buffer-base.action.ts +435 -441
- package/src/providers/facebook/actions/boost-post.action.ts +350 -386
- package/src/providers/facebook/actions/create-album.action.ts +291 -307
- package/src/providers/facebook/actions/create-post.action.ts +224 -227
- package/src/providers/facebook/actions/get-page-insights.action.ts +383 -403
- package/src/providers/facebook/actions/get-page-posts.action.ts +214 -225
- package/src/providers/facebook/actions/get-post-insights.action.ts +300 -316
- package/src/providers/facebook/actions/respond-to-comments.action.ts +319 -336
- package/src/providers/facebook/actions/schedule-post.action.ts +289 -292
- package/src/providers/facebook/actions/search-posts.action.ts +399 -413
- package/src/providers/facebook/facebook-base.action.ts +653 -670
- package/src/providers/hootsuite/actions/bulk-schedule-posts.action.ts +257 -257
- package/src/providers/hootsuite/actions/create-scheduled-post.action.ts +184 -189
- package/src/providers/hootsuite/actions/delete-scheduled-post.action.ts +160 -161
- package/src/providers/hootsuite/actions/get-analytics.action.ts +249 -254
- package/src/providers/hootsuite/actions/get-scheduled-posts.action.ts +206 -207
- package/src/providers/hootsuite/actions/get-social-profiles.action.ts +206 -205
- package/src/providers/hootsuite/actions/search-posts.action.ts +351 -369
- package/src/providers/hootsuite/actions/update-scheduled-post.action.ts +211 -209
- package/src/providers/hootsuite/hootsuite-base.action.ts +301 -307
- package/src/providers/instagram/actions/create-post.action.ts +276 -296
- package/src/providers/instagram/actions/create-story.action.ts +378 -394
- package/src/providers/instagram/actions/get-account-insights.action.ts +384 -420
- package/src/providers/instagram/actions/get-business-posts.action.ts +233 -242
- package/src/providers/instagram/actions/get-comments.action.ts +365 -377
- package/src/providers/instagram/actions/get-post-insights.action.ts +265 -273
- package/src/providers/instagram/actions/schedule-post.action.ts +233 -235
- package/src/providers/instagram/actions/search-posts.action.ts +512 -538
- package/src/providers/instagram/instagram-base.action.ts +368 -393
- package/src/providers/linkedin/actions/create-article.action.ts +275 -266
- package/src/providers/linkedin/actions/create-post.action.ts +179 -177
- package/src/providers/linkedin/actions/get-followers.action.ts +211 -211
- package/src/providers/linkedin/actions/get-organization-posts.action.ts +146 -147
- package/src/providers/linkedin/actions/get-personal-posts.action.ts +138 -139
- package/src/providers/linkedin/actions/get-post-analytics.action.ts +190 -189
- package/src/providers/linkedin/actions/schedule-post.action.ts +191 -189
- package/src/providers/linkedin/actions/search-posts.action.ts +275 -283
- package/src/providers/linkedin/linkedin-base.action.ts +407 -421
- package/src/providers/tiktok/tiktok-base.action.ts +305 -320
- package/src/providers/twitter/actions/create-thread.action.ts +203 -207
- package/src/providers/twitter/actions/create-tweet.action.ts +187 -188
- package/src/providers/twitter/actions/delete-tweet.action.ts +128 -129
- package/src/providers/twitter/actions/get-analytics.action.ts +402 -411
- package/src/providers/twitter/actions/get-mentions.action.ts +218 -219
- package/src/providers/twitter/actions/get-timeline.action.ts +232 -233
- package/src/providers/twitter/actions/schedule-tweet.action.ts +221 -222
- package/src/providers/twitter/actions/search-tweets.action.ts +540 -543
- package/src/providers/twitter/twitter-base.action.ts +541 -560
- package/src/providers/youtube/youtube-base.action.ts +320 -333
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RegisterClass } from '@memberjunction/global';
|
|
2
2
|
import { BaseSocialMediaAction } from '../../base/base-social.action';
|
|
3
|
-
import { UserInfo } from '@memberjunction/
|
|
3
|
+
import { UserInfo } from '@memberjunction/global';
|
|
4
4
|
import axios, { AxiosInstance, AxiosError } from 'axios';
|
|
5
5
|
import { SocialPost, MediaFile } from '../../base/base-social.action';
|
|
6
6
|
import { BaseAction } from '@memberjunction/actions';
|
|
@@ -11,364 +11,351 @@ import { BaseAction } from '@memberjunction/actions';
|
|
|
11
11
|
*/
|
|
12
12
|
@RegisterClass(BaseAction, 'YouTubeBaseAction')
|
|
13
13
|
export abstract class YouTubeBaseAction extends BaseSocialMediaAction {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
protected override get platformName(): string {
|
|
15
|
+
return 'YouTube';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
protected override get apiBaseUrl(): string {
|
|
19
|
+
return 'https://www.googleapis.com/youtube/v3';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Axios instance for API requests
|
|
24
|
+
*/
|
|
25
|
+
protected axiosInstance: AxiosInstance;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Initialize the axios instance with base configuration
|
|
29
|
+
*/
|
|
30
|
+
protected initializeAxios(): void {
|
|
31
|
+
this.axiosInstance = axios.create({
|
|
32
|
+
baseURL: this.apiBaseUrl,
|
|
33
|
+
headers: {
|
|
34
|
+
Accept: 'application/json',
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* YouTube-specific OAuth token refresh
|
|
42
|
+
*/
|
|
43
|
+
protected async refreshAccessToken(): Promise<void> {
|
|
44
|
+
const refreshToken = this.getRefreshToken();
|
|
45
|
+
if (!refreshToken) {
|
|
46
|
+
throw new Error('No refresh token available for YouTube');
|
|
16
47
|
}
|
|
17
48
|
|
|
18
|
-
|
|
19
|
-
|
|
49
|
+
try {
|
|
50
|
+
const response = await axios.post('https://oauth2.googleapis.com/token', {
|
|
51
|
+
client_id: this.getCustomAttribute(2), // Store client ID in CustomAttribute2
|
|
52
|
+
client_secret: this.getCustomAttribute(3), // Store client secret in CustomAttribute3
|
|
53
|
+
refresh_token: refreshToken,
|
|
54
|
+
grant_type: 'refresh_token',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const { access_token, expires_in } = response.data;
|
|
58
|
+
await this.updateStoredTokens(access_token, undefined, expires_in);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
throw new Error(`Failed to refresh YouTube access token: ${error.message}`);
|
|
20
61
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
'Content-Type': 'application/json'
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* YouTube-specific OAuth token refresh
|
|
42
|
-
*/
|
|
43
|
-
protected async refreshAccessToken(): Promise<void> {
|
|
44
|
-
const refreshToken = this.getRefreshToken();
|
|
45
|
-
if (!refreshToken) {
|
|
46
|
-
throw new Error('No refresh token available for YouTube');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const response = await axios.post('https://oauth2.googleapis.com/token', {
|
|
51
|
-
client_id: this.getCustomAttribute(2), // Store client ID in CustomAttribute2
|
|
52
|
-
client_secret: this.getCustomAttribute(3), // Store client secret in CustomAttribute3
|
|
53
|
-
refresh_token: refreshToken,
|
|
54
|
-
grant_type: 'refresh_token'
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const { access_token, expires_in } = response.data;
|
|
58
|
-
await this.updateStoredTokens(access_token, undefined, expires_in);
|
|
59
|
-
} catch (error) {
|
|
60
|
-
throw new Error(`Failed to refresh YouTube access token: ${error.message}`);
|
|
61
|
-
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Make authenticated request to YouTube API
|
|
66
|
+
*/
|
|
67
|
+
protected async makeYouTubeRequest<T = any>(
|
|
68
|
+
endpoint: string,
|
|
69
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
|
70
|
+
data?: any,
|
|
71
|
+
params?: Record<string, any>,
|
|
72
|
+
contextUser?: UserInfo
|
|
73
|
+
): Promise<T> {
|
|
74
|
+
if (!this.axiosInstance) {
|
|
75
|
+
this.initializeAxios();
|
|
62
76
|
}
|
|
63
77
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!this.axiosInstance) {
|
|
75
|
-
this.initializeAxios();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return this.makeAuthenticatedRequest(async (token) => {
|
|
79
|
-
try {
|
|
80
|
-
const response = await this.axiosInstance.request<T>({
|
|
81
|
-
url: endpoint,
|
|
82
|
-
method,
|
|
83
|
-
data,
|
|
84
|
-
params,
|
|
85
|
-
headers: {
|
|
86
|
-
'Authorization': `Bearer ${token}`
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
return response.data;
|
|
91
|
-
} catch (error) {
|
|
92
|
-
if (axios.isAxiosError(error)) {
|
|
93
|
-
this.handleYouTubeApiError(error);
|
|
94
|
-
}
|
|
95
|
-
throw error;
|
|
96
|
-
}
|
|
78
|
+
return this.makeAuthenticatedRequest(async (token) => {
|
|
79
|
+
try {
|
|
80
|
+
const response = await this.axiosInstance.request<T>({
|
|
81
|
+
url: endpoint,
|
|
82
|
+
method,
|
|
83
|
+
data,
|
|
84
|
+
params,
|
|
85
|
+
headers: {
|
|
86
|
+
Authorization: `Bearer ${token}`,
|
|
87
|
+
},
|
|
97
88
|
});
|
|
98
|
-
}
|
|
99
89
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const response = error.response;
|
|
105
|
-
if (!response) {
|
|
106
|
-
throw new Error('Network error occurred');
|
|
90
|
+
return response.data;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (axios.isAxiosError(error)) {
|
|
93
|
+
this.handleYouTubeApiError(error);
|
|
107
94
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const retryAfter = response.headers['retry-after'];
|
|
121
|
-
throw new Error(`Rate limit exceeded. Retry after ${retryAfter || '60'} seconds`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
throw new Error(`YouTube API error (${errorCode}): ${errorMessage}`);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Handle YouTube API errors
|
|
102
|
+
*/
|
|
103
|
+
protected handleYouTubeApiError(error: AxiosError): void {
|
|
104
|
+
const response = error.response;
|
|
105
|
+
if (!response) {
|
|
106
|
+
throw new Error('Network error occurred');
|
|
125
107
|
}
|
|
126
108
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
protected async uploadVideo(
|
|
131
|
-
videoFile: MediaFile,
|
|
132
|
-
metadata: {
|
|
133
|
-
title: string;
|
|
134
|
-
description?: string;
|
|
135
|
-
tags?: string[];
|
|
136
|
-
categoryId?: string;
|
|
137
|
-
privacyStatus?: 'private' | 'unlisted' | 'public';
|
|
138
|
-
}
|
|
139
|
-
): Promise<string> {
|
|
140
|
-
// YouTube requires a resumable upload for videos
|
|
141
|
-
const uploadUrl = await this.initiateResumableUpload(metadata);
|
|
142
|
-
const videoId = await this.performResumableUpload(uploadUrl, videoFile);
|
|
143
|
-
return videoId;
|
|
144
|
-
}
|
|
109
|
+
const errorData: any = response.data;
|
|
110
|
+
const errorMessage = errorData?.error?.message || response.statusText;
|
|
111
|
+
const errorCode = errorData?.error?.code || response.status;
|
|
145
112
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
private async initiateResumableUpload(metadata: any): Promise<string> {
|
|
150
|
-
const response = await this.makeYouTubeRequest<any>(
|
|
151
|
-
'/videos',
|
|
152
|
-
'POST',
|
|
153
|
-
{
|
|
154
|
-
snippet: {
|
|
155
|
-
title: metadata.title,
|
|
156
|
-
description: metadata.description || '',
|
|
157
|
-
tags: metadata.tags || [],
|
|
158
|
-
categoryId: metadata.categoryId || '22' // Default to People & Blogs
|
|
159
|
-
},
|
|
160
|
-
status: {
|
|
161
|
-
privacyStatus: metadata.privacyStatus || 'private'
|
|
162
|
-
}
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
uploadType: 'resumable',
|
|
166
|
-
part: 'snippet,status'
|
|
167
|
-
}
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
return response.headers.location;
|
|
113
|
+
// Check for quota exceeded
|
|
114
|
+
if (errorCode === 403 && errorMessage.includes('quota')) {
|
|
115
|
+
throw new Error(`YouTube API quota exceeded. ${errorMessage}`);
|
|
171
116
|
}
|
|
172
117
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const videoData = Buffer.isBuffer(videoFile.data)
|
|
178
|
-
? videoFile.data
|
|
179
|
-
: Buffer.from(videoFile.data, 'base64');
|
|
180
|
-
|
|
181
|
-
const response = await axios.put(uploadUrl, videoData, {
|
|
182
|
-
headers: {
|
|
183
|
-
'Content-Type': videoFile.mimeType,
|
|
184
|
-
'Content-Length': videoData.length.toString(),
|
|
185
|
-
'Authorization': `Bearer ${this.getAccessToken()}`
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
return response.data.id;
|
|
118
|
+
// Check for rate limiting
|
|
119
|
+
if (errorCode === 429) {
|
|
120
|
+
const retryAfter = response.headers['retry-after'];
|
|
121
|
+
throw new Error(`Rate limit exceeded. Retry after ${retryAfter || '60'} seconds`);
|
|
190
122
|
}
|
|
191
123
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
124
|
+
throw new Error(`YouTube API error (${errorCode}): ${errorMessage}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Upload video to YouTube
|
|
129
|
+
*/
|
|
130
|
+
protected async uploadVideo(
|
|
131
|
+
videoFile: MediaFile,
|
|
132
|
+
metadata: {
|
|
133
|
+
title: string;
|
|
134
|
+
description?: string;
|
|
135
|
+
tags?: string[];
|
|
136
|
+
categoryId?: string;
|
|
137
|
+
privacyStatus?: 'private' | 'unlisted' | 'public';
|
|
199
138
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
139
|
+
): Promise<string> {
|
|
140
|
+
// YouTube requires a resumable upload for videos
|
|
141
|
+
const uploadUrl = await this.initiateResumableUpload(metadata);
|
|
142
|
+
const videoId = await this.performResumableUpload(uploadUrl, videoFile);
|
|
143
|
+
return videoId;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Initiate resumable upload session
|
|
148
|
+
*/
|
|
149
|
+
private async initiateResumableUpload(metadata: any): Promise<string> {
|
|
150
|
+
const response = await this.makeYouTubeRequest<any>(
|
|
151
|
+
'/videos',
|
|
152
|
+
'POST',
|
|
153
|
+
{
|
|
154
|
+
snippet: {
|
|
155
|
+
title: metadata.title,
|
|
156
|
+
description: metadata.description || '',
|
|
157
|
+
tags: metadata.tags || [],
|
|
158
|
+
categoryId: metadata.categoryId || '22', // Default to People & Blogs
|
|
159
|
+
},
|
|
160
|
+
status: {
|
|
161
|
+
privacyStatus: metadata.privacyStatus || 'private',
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
uploadType: 'resumable',
|
|
166
|
+
part: 'snippet,status',
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return response.headers.location;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Perform the actual video upload
|
|
175
|
+
*/
|
|
176
|
+
private async performResumableUpload(uploadUrl: string, videoFile: MediaFile): Promise<string> {
|
|
177
|
+
const videoData = Buffer.isBuffer(videoFile.data) ? videoFile.data : Buffer.from(videoFile.data, 'base64');
|
|
178
|
+
|
|
179
|
+
const response = await axios.put(uploadUrl, videoData, {
|
|
180
|
+
headers: {
|
|
181
|
+
'Content-Type': videoFile.mimeType,
|
|
182
|
+
'Content-Length': videoData.length.toString(),
|
|
183
|
+
Authorization: `Bearer ${this.getAccessToken()}`,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return response.data.id;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Upload single media file (thumbnail)
|
|
192
|
+
*/
|
|
193
|
+
protected async uploadSingleMedia(file: MediaFile): Promise<string> {
|
|
194
|
+
// For YouTube, this would typically be used for thumbnails
|
|
195
|
+
// The actual implementation would upload to YouTube's thumbnail endpoint
|
|
196
|
+
throw new Error('Direct media upload not supported. Use uploadVideo for videos or setThumbnail for thumbnails.');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Convert YouTube video to standard social post format
|
|
201
|
+
*/
|
|
202
|
+
protected normalizePost(youtubeVideo: any): SocialPost {
|
|
203
|
+
return {
|
|
204
|
+
id: youtubeVideo.id,
|
|
205
|
+
platform: 'YouTube',
|
|
206
|
+
profileId: youtubeVideo.snippet.channelId,
|
|
207
|
+
content: youtubeVideo.snippet.description || '',
|
|
208
|
+
mediaUrls: [`https://www.youtube.com/watch?v=${youtubeVideo.id}`],
|
|
209
|
+
publishedAt: new Date(youtubeVideo.snippet.publishedAt),
|
|
210
|
+
scheduledFor: youtubeVideo.status.publishAt ? new Date(youtubeVideo.status.publishAt) : undefined,
|
|
211
|
+
analytics: this.extractVideoAnalytics(youtubeVideo),
|
|
212
|
+
platformSpecificData: {
|
|
213
|
+
title: youtubeVideo.snippet.title,
|
|
214
|
+
tags: youtubeVideo.snippet.tags || [],
|
|
215
|
+
categoryId: youtubeVideo.snippet.categoryId,
|
|
216
|
+
duration: youtubeVideo.contentDetails?.duration,
|
|
217
|
+
definition: youtubeVideo.contentDetails?.definition,
|
|
218
|
+
privacyStatus: youtubeVideo.status.privacyStatus,
|
|
219
|
+
embeddable: youtubeVideo.status.embeddable,
|
|
220
|
+
thumbnails: youtubeVideo.snippet.thumbnails,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Extract analytics from video statistics
|
|
227
|
+
*/
|
|
228
|
+
private extractVideoAnalytics(video: any): any {
|
|
229
|
+
if (!video.statistics) {
|
|
230
|
+
return undefined;
|
|
225
231
|
}
|
|
226
232
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
233
|
+
return this.normalizeAnalytics({
|
|
234
|
+
impressions: parseInt(video.statistics.viewCount || '0'),
|
|
235
|
+
engagements: parseInt(video.statistics.likeCount || '0') + parseInt(video.statistics.commentCount || '0'),
|
|
236
|
+
clicks: 0, // YouTube doesn't provide click data
|
|
237
|
+
shares: 0, // YouTube doesn't provide share count
|
|
238
|
+
comments: parseInt(video.statistics.commentCount || '0'),
|
|
239
|
+
likes: parseInt(video.statistics.likeCount || '0'),
|
|
240
|
+
reach: parseInt(video.statistics.viewCount || '0'),
|
|
241
|
+
saves: parseInt(video.statistics.favoriteCount || '0'),
|
|
242
|
+
videoViews: parseInt(video.statistics.viewCount || '0'),
|
|
243
|
+
dislikes: parseInt(video.statistics.dislikeCount || '0'),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Search for videos
|
|
249
|
+
*/
|
|
250
|
+
protected async searchPosts(params: any): Promise<SocialPost[]> {
|
|
251
|
+
const searchParams: any = {
|
|
252
|
+
part: 'snippet',
|
|
253
|
+
type: 'video',
|
|
254
|
+
maxResults: params.limit || 50,
|
|
255
|
+
order: params.sortBy || 'relevance',
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// Add search query
|
|
259
|
+
if (params.query) {
|
|
260
|
+
searchParams.q = params.query;
|
|
248
261
|
}
|
|
249
262
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
protected async searchPosts(params: any): Promise<SocialPost[]> {
|
|
254
|
-
const searchParams: any = {
|
|
255
|
-
part: 'snippet',
|
|
256
|
-
type: 'video',
|
|
257
|
-
maxResults: params.limit || 50,
|
|
258
|
-
order: params.sortBy || 'relevance'
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
// Add search query
|
|
262
|
-
if (params.query) {
|
|
263
|
-
searchParams.q = params.query;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Add channel filter if searching within a specific channel
|
|
267
|
-
if (params.channelId || this.getCustomAttribute(1)) {
|
|
268
|
-
searchParams.channelId = params.channelId || this.getCustomAttribute(1);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Add date filters
|
|
272
|
-
if (params.startDate) {
|
|
273
|
-
searchParams.publishedAfter = this.formatDate(params.startDate);
|
|
274
|
-
}
|
|
275
|
-
if (params.endDate) {
|
|
276
|
-
searchParams.publishedBefore = this.formatDate(params.endDate);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Add pagination
|
|
280
|
-
if (params.pageToken) {
|
|
281
|
-
searchParams.pageToken = params.pageToken;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const response = await this.makeYouTubeRequest<any>(
|
|
285
|
-
'/search',
|
|
286
|
-
'GET',
|
|
287
|
-
undefined,
|
|
288
|
-
searchParams
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
// Get full video details for search results
|
|
292
|
-
const videoIds = response.items.map((item: any) => item.id.videoId).join(',');
|
|
293
|
-
const videosResponse = await this.makeYouTubeRequest<any>(
|
|
294
|
-
'/videos',
|
|
295
|
-
'GET',
|
|
296
|
-
undefined,
|
|
297
|
-
{
|
|
298
|
-
part: 'snippet,statistics,status,contentDetails',
|
|
299
|
-
id: videoIds
|
|
300
|
-
}
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
return videosResponse.items.map((video: any) => this.normalizePost(video));
|
|
263
|
+
// Add channel filter if searching within a specific channel
|
|
264
|
+
if (params.channelId || this.getCustomAttribute(1)) {
|
|
265
|
+
searchParams.channelId = params.channelId || this.getCustomAttribute(1);
|
|
304
266
|
}
|
|
305
267
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
protected getQuotaCost(operation: string): number {
|
|
310
|
-
const quotaCosts: Record<string, number> = {
|
|
311
|
-
'videos.list': 1,
|
|
312
|
-
'videos.insert': 1600,
|
|
313
|
-
'videos.update': 50,
|
|
314
|
-
'videos.delete': 50,
|
|
315
|
-
'search.list': 100,
|
|
316
|
-
'channels.list': 1,
|
|
317
|
-
'playlists.list': 1,
|
|
318
|
-
'playlists.insert': 50,
|
|
319
|
-
'playlistItems.insert': 50,
|
|
320
|
-
'comments.list': 1,
|
|
321
|
-
'commentThreads.list': 1
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
return quotaCosts[operation] || 1;
|
|
268
|
+
// Add date filters
|
|
269
|
+
if (params.startDate) {
|
|
270
|
+
searchParams.publishedAfter = this.formatDate(params.startDate);
|
|
325
271
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
* Parse ISO 8601 duration to seconds
|
|
329
|
-
*/
|
|
330
|
-
protected parseDuration(isoDuration: string): number {
|
|
331
|
-
const matches = isoDuration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
|
|
332
|
-
if (!matches) return 0;
|
|
333
|
-
|
|
334
|
-
const hours = parseInt(matches[1] || '0');
|
|
335
|
-
const minutes = parseInt(matches[2] || '0');
|
|
336
|
-
const seconds = parseInt(matches[3] || '0');
|
|
337
|
-
|
|
338
|
-
return hours * 3600 + minutes * 60 + seconds;
|
|
272
|
+
if (params.endDate) {
|
|
273
|
+
searchParams.publishedBefore = this.formatDate(params.endDate);
|
|
339
274
|
}
|
|
340
275
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
protected formatBytes(bytes: number): string {
|
|
345
|
-
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
346
|
-
if (bytes === 0) return '0 Bytes';
|
|
347
|
-
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
348
|
-
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
|
276
|
+
// Add pagination
|
|
277
|
+
if (params.pageToken) {
|
|
278
|
+
searchParams.pageToken = params.pageToken;
|
|
349
279
|
}
|
|
350
280
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
281
|
+
const response = await this.makeYouTubeRequest<any>('/search', 'GET', undefined, searchParams);
|
|
282
|
+
|
|
283
|
+
// Get full video details for search results
|
|
284
|
+
const videoIds = response.items.map((item: any) => item.id.videoId).join(',');
|
|
285
|
+
const videosResponse = await this.makeYouTubeRequest<any>('/videos', 'GET', undefined, {
|
|
286
|
+
part: 'snippet,statistics,status,contentDetails',
|
|
287
|
+
id: videoIds,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return videosResponse.items.map((video: any) => this.normalizePost(video));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Get quota cost for an operation
|
|
295
|
+
*/
|
|
296
|
+
protected getQuotaCost(operation: string): number {
|
|
297
|
+
const quotaCosts: Record<string, number> = {
|
|
298
|
+
'videos.list': 1,
|
|
299
|
+
'videos.insert': 1600,
|
|
300
|
+
'videos.update': 50,
|
|
301
|
+
'videos.delete': 50,
|
|
302
|
+
'search.list': 100,
|
|
303
|
+
'channels.list': 1,
|
|
304
|
+
'playlists.list': 1,
|
|
305
|
+
'playlists.insert': 50,
|
|
306
|
+
'playlistItems.insert': 50,
|
|
307
|
+
'comments.list': 1,
|
|
308
|
+
'commentThreads.list': 1,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
return quotaCosts[operation] || 1;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Parse ISO 8601 duration to seconds
|
|
316
|
+
*/
|
|
317
|
+
protected parseDuration(isoDuration: string): number {
|
|
318
|
+
const matches = isoDuration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
|
|
319
|
+
if (!matches) return 0;
|
|
320
|
+
|
|
321
|
+
const hours = parseInt(matches[1] || '0');
|
|
322
|
+
const minutes = parseInt(matches[2] || '0');
|
|
323
|
+
const seconds = parseInt(matches[3] || '0');
|
|
324
|
+
|
|
325
|
+
return hours * 3600 + minutes * 60 + seconds;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Format bytes to human readable size
|
|
330
|
+
*/
|
|
331
|
+
protected formatBytes(bytes: number): string {
|
|
332
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
333
|
+
if (bytes === 0) return '0 Bytes';
|
|
334
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
335
|
+
return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + ' ' + sizes[i];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Validate video file
|
|
340
|
+
*/
|
|
341
|
+
protected validateVideoFile(file: MediaFile): void {
|
|
342
|
+
const allowedTypes = [
|
|
343
|
+
'video/mp4',
|
|
344
|
+
'video/x-msvideo', // AVI
|
|
345
|
+
'video/quicktime', // MOV
|
|
346
|
+
'video/x-ms-wmv', // WMV
|
|
347
|
+
'video/x-flv', // FLV
|
|
348
|
+
'video/webm',
|
|
349
|
+
];
|
|
350
|
+
|
|
351
|
+
if (!allowedTypes.includes(file.mimeType)) {
|
|
352
|
+
throw new Error(`Unsupported video format: ${file.mimeType}`);
|
|
353
|
+
}
|
|
367
354
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
355
|
+
// YouTube max file size is 128GB or 12 hours
|
|
356
|
+
const maxSize = 128 * 1024 * 1024 * 1024; // 128GB
|
|
357
|
+
if (file.size > maxSize) {
|
|
358
|
+
throw new Error(`Video file too large. Maximum size is ${this.formatBytes(maxSize)}`);
|
|
373
359
|
}
|
|
374
|
-
}
|
|
360
|
+
}
|
|
361
|
+
}
|