@memberjunction/actions-bizapps-social 2.112.0 → 2.113.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 +13 -0
- package/dist/base/base-social.action.d.ts.map +1 -1
- package/dist/base/base-social.action.js +24 -18
- 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 +34 -35
- 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 +36 -34
- 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 +27 -25
- 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 +23 -19
- 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 +32 -28
- 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 +44 -42
- 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 +39 -37
- 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 +59 -44
- 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 +31 -33
- 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 +32 -28
- 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 +26 -24
- 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 +34 -32
- 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 +52 -43
- 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 +28 -30
- 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 +20 -18
- 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 +26 -27
- 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 +59 -38
- 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 +25 -23
- 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 +60 -56
- 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 +27 -25
- 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 +45 -55
- 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 +29 -31
- 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 +23 -25
- 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 +30 -32
- 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 +30 -28
- 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 +38 -33
- 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 +26 -25
- 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 +29 -25
- 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 +47 -40
- 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 +31 -30
- 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 +58 -56
- 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 +68 -58
- 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 +25 -22
- package/dist/providers/youtube/youtube-base.action.js.map +1 -1
- package/package.json +6 -5
- package/src/base/base-social.action.ts +224 -217
- package/src/providers/buffer/buffer-base.action.ts +441 -435
- package/src/providers/facebook/actions/boost-post.action.ts +386 -350
- package/src/providers/facebook/actions/create-album.action.ts +307 -291
- package/src/providers/facebook/actions/create-post.action.ts +227 -224
- package/src/providers/facebook/actions/get-page-insights.action.ts +403 -383
- package/src/providers/facebook/actions/get-page-posts.action.ts +225 -214
- package/src/providers/facebook/actions/get-post-insights.action.ts +316 -300
- package/src/providers/facebook/actions/respond-to-comments.action.ts +336 -319
- package/src/providers/facebook/actions/schedule-post.action.ts +292 -289
- package/src/providers/facebook/actions/search-posts.action.ts +413 -399
- package/src/providers/facebook/facebook-base.action.ts +670 -653
- package/src/providers/hootsuite/actions/bulk-schedule-posts.action.ts +257 -257
- package/src/providers/hootsuite/actions/create-scheduled-post.action.ts +189 -184
- package/src/providers/hootsuite/actions/delete-scheduled-post.action.ts +161 -160
- package/src/providers/hootsuite/actions/get-analytics.action.ts +254 -249
- package/src/providers/hootsuite/actions/get-scheduled-posts.action.ts +207 -206
- package/src/providers/hootsuite/actions/get-social-profiles.action.ts +205 -206
- package/src/providers/hootsuite/actions/search-posts.action.ts +369 -351
- package/src/providers/hootsuite/actions/update-scheduled-post.action.ts +209 -211
- package/src/providers/hootsuite/hootsuite-base.action.ts +307 -301
- package/src/providers/instagram/actions/create-post.action.ts +296 -276
- package/src/providers/instagram/actions/create-story.action.ts +394 -378
- package/src/providers/instagram/actions/get-account-insights.action.ts +420 -384
- package/src/providers/instagram/actions/get-business-posts.action.ts +242 -233
- package/src/providers/instagram/actions/get-comments.action.ts +377 -365
- package/src/providers/instagram/actions/get-post-insights.action.ts +273 -265
- package/src/providers/instagram/actions/schedule-post.action.ts +235 -233
- package/src/providers/instagram/actions/search-posts.action.ts +538 -512
- package/src/providers/instagram/instagram-base.action.ts +393 -368
- package/src/providers/linkedin/actions/create-article.action.ts +266 -275
- package/src/providers/linkedin/actions/create-post.action.ts +177 -179
- package/src/providers/linkedin/actions/get-followers.action.ts +211 -211
- package/src/providers/linkedin/actions/get-organization-posts.action.ts +147 -146
- package/src/providers/linkedin/actions/get-personal-posts.action.ts +139 -138
- package/src/providers/linkedin/actions/get-post-analytics.action.ts +189 -190
- package/src/providers/linkedin/actions/schedule-post.action.ts +189 -191
- package/src/providers/linkedin/actions/search-posts.action.ts +283 -275
- package/src/providers/linkedin/linkedin-base.action.ts +421 -407
- package/src/providers/tiktok/tiktok-base.action.ts +320 -305
- package/src/providers/twitter/actions/create-thread.action.ts +207 -203
- package/src/providers/twitter/actions/create-tweet.action.ts +188 -187
- package/src/providers/twitter/actions/delete-tweet.action.ts +129 -128
- package/src/providers/twitter/actions/get-analytics.action.ts +411 -402
- package/src/providers/twitter/actions/get-mentions.action.ts +219 -218
- package/src/providers/twitter/actions/get-timeline.action.ts +233 -232
- package/src/providers/twitter/actions/schedule-tweet.action.ts +222 -221
- package/src/providers/twitter/actions/search-tweets.action.ts +543 -540
- package/src/providers/twitter/twitter-base.action.ts +560 -541
- package/src/providers/youtube/youtube-base.action.ts +333 -320
|
@@ -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/core';
|
|
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,351 +11,364 @@ import { BaseAction } from '@memberjunction/actions';
|
|
|
11
11
|
*/
|
|
12
12
|
@RegisterClass(BaseAction, 'YouTubeBaseAction')
|
|
13
13
|
export abstract class YouTubeBaseAction extends BaseSocialMediaAction {
|
|
14
|
-
|
|
15
|
-
|
|
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');
|
|
14
|
+
protected override get platformName(): string {
|
|
15
|
+
return 'YouTube';
|
|
47
16
|
}
|
|
48
17
|
|
|
49
|
-
|
|
50
|
-
|
|
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();
|
|
18
|
+
protected override get apiBaseUrl(): string {
|
|
19
|
+
return 'https://www.googleapis.com/youtube/v3';
|
|
76
20
|
}
|
|
77
21
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
+
}
|
|
88
37
|
});
|
|
38
|
+
}
|
|
89
39
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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}`);
|
|
94
61
|
}
|
|
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');
|
|
107
62
|
}
|
|
108
63
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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();
|
|
76
|
+
}
|
|
112
77
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
}
|
|
97
|
+
});
|
|
116
98
|
}
|
|
117
99
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const errorData: any = response.data;
|
|
110
|
+
const errorMessage = errorData?.error?.message || response.statusText;
|
|
111
|
+
const errorCode = errorData?.error?.code || response.status;
|
|
112
|
+
|
|
113
|
+
// Check for quota exceeded
|
|
114
|
+
if (errorCode === 403 && errorMessage.includes('quota')) {
|
|
115
|
+
throw new Error(`YouTube API quota exceeded. ${errorMessage}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
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`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
throw new Error(`YouTube API error (${errorCode}): ${errorMessage}`);
|
|
122
125
|
}
|
|
123
126
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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';
|
|
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;
|
|
138
144
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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;
|
|
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;
|
|
231
171
|
}
|
|
232
172
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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;
|
|
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)
|
|
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;
|
|
261
190
|
}
|
|
262
191
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Upload single media file (thumbnail)
|
|
194
|
+
*/
|
|
195
|
+
protected async uploadSingleMedia(file: MediaFile): Promise<string> {
|
|
196
|
+
// For YouTube, this would typically be used for thumbnails
|
|
197
|
+
// The actual implementation would upload to YouTube's thumbnail endpoint
|
|
198
|
+
throw new Error('Direct media upload not supported. Use uploadVideo for videos or setThumbnail for thumbnails.');
|
|
266
199
|
}
|
|
267
200
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
201
|
+
/**
|
|
202
|
+
* Convert YouTube video to standard social post format
|
|
203
|
+
*/
|
|
204
|
+
protected normalizePost(youtubeVideo: any): SocialPost {
|
|
205
|
+
return {
|
|
206
|
+
id: youtubeVideo.id,
|
|
207
|
+
platform: 'YouTube',
|
|
208
|
+
profileId: youtubeVideo.snippet.channelId,
|
|
209
|
+
content: youtubeVideo.snippet.description || '',
|
|
210
|
+
mediaUrls: [`https://www.youtube.com/watch?v=${youtubeVideo.id}`],
|
|
211
|
+
publishedAt: new Date(youtubeVideo.snippet.publishedAt),
|
|
212
|
+
scheduledFor: youtubeVideo.status.publishAt ? new Date(youtubeVideo.status.publishAt) : undefined,
|
|
213
|
+
analytics: this.extractVideoAnalytics(youtubeVideo),
|
|
214
|
+
platformSpecificData: {
|
|
215
|
+
title: youtubeVideo.snippet.title,
|
|
216
|
+
tags: youtubeVideo.snippet.tags || [],
|
|
217
|
+
categoryId: youtubeVideo.snippet.categoryId,
|
|
218
|
+
duration: youtubeVideo.contentDetails?.duration,
|
|
219
|
+
definition: youtubeVideo.contentDetails?.definition,
|
|
220
|
+
privacyStatus: youtubeVideo.status.privacyStatus,
|
|
221
|
+
embeddable: youtubeVideo.status.embeddable,
|
|
222
|
+
thumbnails: youtubeVideo.snippet.thumbnails
|
|
223
|
+
}
|
|
224
|
+
};
|
|
271
225
|
}
|
|
272
|
-
|
|
273
|
-
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Extract analytics from video statistics
|
|
229
|
+
*/
|
|
230
|
+
private extractVideoAnalytics(video: any): any {
|
|
231
|
+
if (!video.statistics) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return this.normalizeAnalytics({
|
|
236
|
+
impressions: parseInt(video.statistics.viewCount || '0'),
|
|
237
|
+
engagements: parseInt(video.statistics.likeCount || '0') +
|
|
238
|
+
parseInt(video.statistics.commentCount || '0'),
|
|
239
|
+
clicks: 0, // YouTube doesn't provide click data
|
|
240
|
+
shares: 0, // YouTube doesn't provide share count
|
|
241
|
+
comments: parseInt(video.statistics.commentCount || '0'),
|
|
242
|
+
likes: parseInt(video.statistics.likeCount || '0'),
|
|
243
|
+
reach: parseInt(video.statistics.viewCount || '0'),
|
|
244
|
+
saves: parseInt(video.statistics.favoriteCount || '0'),
|
|
245
|
+
videoViews: parseInt(video.statistics.viewCount || '0'),
|
|
246
|
+
dislikes: parseInt(video.statistics.dislikeCount || '0')
|
|
247
|
+
});
|
|
274
248
|
}
|
|
275
249
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
250
|
+
/**
|
|
251
|
+
* Search for videos
|
|
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));
|
|
279
304
|
}
|
|
280
305
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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}`);
|
|
306
|
+
/**
|
|
307
|
+
* Get quota cost for an operation
|
|
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;
|
|
353
325
|
}
|
|
354
326
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Format bytes to human readable size
|
|
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];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Validate video file
|
|
353
|
+
*/
|
|
354
|
+
protected validateVideoFile(file: MediaFile): void {
|
|
355
|
+
const allowedTypes = [
|
|
356
|
+
'video/mp4',
|
|
357
|
+
'video/x-msvideo', // AVI
|
|
358
|
+
'video/quicktime', // MOV
|
|
359
|
+
'video/x-ms-wmv', // WMV
|
|
360
|
+
'video/x-flv', // FLV
|
|
361
|
+
'video/webm'
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
if (!allowedTypes.includes(file.mimeType)) {
|
|
365
|
+
throw new Error(`Unsupported video format: ${file.mimeType}`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// YouTube max file size is 128GB or 12 hours
|
|
369
|
+
const maxSize = 128 * 1024 * 1024 * 1024; // 128GB
|
|
370
|
+
if (file.size > maxSize) {
|
|
371
|
+
throw new Error(`Video file too large. Maximum size is ${this.formatBytes(maxSize)}`);
|
|
372
|
+
}
|
|
359
373
|
}
|
|
360
|
-
|
|
361
|
-
}
|
|
374
|
+
}
|