@memberjunction/actions-bizapps-social 2.112.0 → 2.113.1

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.
Files changed (204) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/dist/base/base-social.action.d.ts.map +1 -1
  4. package/dist/base/base-social.action.js +24 -18
  5. package/dist/base/base-social.action.js.map +1 -1
  6. package/dist/providers/buffer/buffer-base.action.d.ts.map +1 -1
  7. package/dist/providers/buffer/buffer-base.action.js +34 -35
  8. package/dist/providers/buffer/buffer-base.action.js.map +1 -1
  9. package/dist/providers/facebook/actions/boost-post.action.d.ts.map +1 -1
  10. package/dist/providers/facebook/actions/boost-post.action.js +33 -33
  11. package/dist/providers/facebook/actions/boost-post.action.js.map +1 -1
  12. package/dist/providers/facebook/actions/create-album.action.d.ts.map +1 -1
  13. package/dist/providers/facebook/actions/create-album.action.js +36 -34
  14. package/dist/providers/facebook/actions/create-album.action.js.map +1 -1
  15. package/dist/providers/facebook/actions/create-post.action.d.ts.map +1 -1
  16. package/dist/providers/facebook/actions/create-post.action.js +20 -20
  17. package/dist/providers/facebook/actions/create-post.action.js.map +1 -1
  18. package/dist/providers/facebook/actions/get-page-insights.action.d.ts.map +1 -1
  19. package/dist/providers/facebook/actions/get-page-insights.action.js +27 -25
  20. package/dist/providers/facebook/actions/get-page-insights.action.js.map +1 -1
  21. package/dist/providers/facebook/actions/get-page-posts.action.d.ts.map +1 -1
  22. package/dist/providers/facebook/actions/get-page-posts.action.js +23 -19
  23. package/dist/providers/facebook/actions/get-page-posts.action.js.map +1 -1
  24. package/dist/providers/facebook/actions/get-post-insights.action.d.ts.map +1 -1
  25. package/dist/providers/facebook/actions/get-post-insights.action.js +32 -28
  26. package/dist/providers/facebook/actions/get-post-insights.action.js.map +1 -1
  27. package/dist/providers/facebook/actions/respond-to-comments.action.d.ts.map +1 -1
  28. package/dist/providers/facebook/actions/respond-to-comments.action.js +44 -42
  29. package/dist/providers/facebook/actions/respond-to-comments.action.js.map +1 -1
  30. package/dist/providers/facebook/actions/schedule-post.action.d.ts.map +1 -1
  31. package/dist/providers/facebook/actions/schedule-post.action.js +29 -29
  32. package/dist/providers/facebook/actions/schedule-post.action.js.map +1 -1
  33. package/dist/providers/facebook/actions/search-posts.action.d.ts.map +1 -1
  34. package/dist/providers/facebook/actions/search-posts.action.js +39 -37
  35. package/dist/providers/facebook/actions/search-posts.action.js.map +1 -1
  36. package/dist/providers/facebook/facebook-base.action.d.ts.map +1 -1
  37. package/dist/providers/facebook/facebook-base.action.js +59 -44
  38. package/dist/providers/facebook/facebook-base.action.js.map +1 -1
  39. package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.d.ts.map +1 -1
  40. package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js +31 -33
  41. package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js.map +1 -1
  42. package/dist/providers/hootsuite/actions/create-scheduled-post.action.d.ts.map +1 -1
  43. package/dist/providers/hootsuite/actions/create-scheduled-post.action.js +32 -28
  44. package/dist/providers/hootsuite/actions/create-scheduled-post.action.js.map +1 -1
  45. package/dist/providers/hootsuite/actions/delete-scheduled-post.action.d.ts.map +1 -1
  46. package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js +19 -19
  47. package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js.map +1 -1
  48. package/dist/providers/hootsuite/actions/get-analytics.action.d.ts.map +1 -1
  49. package/dist/providers/hootsuite/actions/get-analytics.action.js +26 -24
  50. package/dist/providers/hootsuite/actions/get-analytics.action.js.map +1 -1
  51. package/dist/providers/hootsuite/actions/get-scheduled-posts.action.d.ts.map +1 -1
  52. package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js +22 -22
  53. package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js.map +1 -1
  54. package/dist/providers/hootsuite/actions/get-social-profiles.action.d.ts.map +1 -1
  55. package/dist/providers/hootsuite/actions/get-social-profiles.action.js +34 -32
  56. package/dist/providers/hootsuite/actions/get-social-profiles.action.js.map +1 -1
  57. package/dist/providers/hootsuite/actions/search-posts.action.d.ts.map +1 -1
  58. package/dist/providers/hootsuite/actions/search-posts.action.js +52 -43
  59. package/dist/providers/hootsuite/actions/search-posts.action.js.map +1 -1
  60. package/dist/providers/hootsuite/actions/update-scheduled-post.action.d.ts.map +1 -1
  61. package/dist/providers/hootsuite/actions/update-scheduled-post.action.js +28 -30
  62. package/dist/providers/hootsuite/actions/update-scheduled-post.action.js.map +1 -1
  63. package/dist/providers/hootsuite/hootsuite-base.action.d.ts.map +1 -1
  64. package/dist/providers/hootsuite/hootsuite-base.action.js +20 -18
  65. package/dist/providers/hootsuite/hootsuite-base.action.js.map +1 -1
  66. package/dist/providers/instagram/actions/create-post.action.d.ts.map +1 -1
  67. package/dist/providers/instagram/actions/create-post.action.js +26 -27
  68. package/dist/providers/instagram/actions/create-post.action.js.map +1 -1
  69. package/dist/providers/instagram/actions/create-story.action.d.ts.map +1 -1
  70. package/dist/providers/instagram/actions/create-story.action.js +35 -35
  71. package/dist/providers/instagram/actions/create-story.action.js.map +1 -1
  72. package/dist/providers/instagram/actions/get-account-insights.action.d.ts.map +1 -1
  73. package/dist/providers/instagram/actions/get-account-insights.action.js +59 -38
  74. package/dist/providers/instagram/actions/get-account-insights.action.js.map +1 -1
  75. package/dist/providers/instagram/actions/get-business-posts.action.d.ts.map +1 -1
  76. package/dist/providers/instagram/actions/get-business-posts.action.js +29 -29
  77. package/dist/providers/instagram/actions/get-business-posts.action.js.map +1 -1
  78. package/dist/providers/instagram/actions/get-comments.action.d.ts.map +1 -1
  79. package/dist/providers/instagram/actions/get-comments.action.js +36 -36
  80. package/dist/providers/instagram/actions/get-comments.action.js.map +1 -1
  81. package/dist/providers/instagram/actions/get-post-insights.action.d.ts.map +1 -1
  82. package/dist/providers/instagram/actions/get-post-insights.action.js +25 -23
  83. package/dist/providers/instagram/actions/get-post-insights.action.js.map +1 -1
  84. package/dist/providers/instagram/actions/schedule-post.action.d.ts.map +1 -1
  85. package/dist/providers/instagram/actions/schedule-post.action.js +25 -25
  86. package/dist/providers/instagram/actions/schedule-post.action.js.map +1 -1
  87. package/dist/providers/instagram/actions/search-posts.action.d.ts.map +1 -1
  88. package/dist/providers/instagram/actions/search-posts.action.js +60 -56
  89. package/dist/providers/instagram/actions/search-posts.action.js.map +1 -1
  90. package/dist/providers/instagram/instagram-base.action.d.ts.map +1 -1
  91. package/dist/providers/instagram/instagram-base.action.js +27 -25
  92. package/dist/providers/instagram/instagram-base.action.js.map +1 -1
  93. package/dist/providers/linkedin/actions/create-article.action.d.ts.map +1 -1
  94. package/dist/providers/linkedin/actions/create-article.action.js +45 -55
  95. package/dist/providers/linkedin/actions/create-article.action.js.map +1 -1
  96. package/dist/providers/linkedin/actions/create-post.action.d.ts.map +1 -1
  97. package/dist/providers/linkedin/actions/create-post.action.js +29 -31
  98. package/dist/providers/linkedin/actions/create-post.action.js.map +1 -1
  99. package/dist/providers/linkedin/actions/get-followers.action.d.ts.map +1 -1
  100. package/dist/providers/linkedin/actions/get-followers.action.js +28 -28
  101. package/dist/providers/linkedin/actions/get-followers.action.js.map +1 -1
  102. package/dist/providers/linkedin/actions/get-organization-posts.action.d.ts.map +1 -1
  103. package/dist/providers/linkedin/actions/get-organization-posts.action.js +20 -20
  104. package/dist/providers/linkedin/actions/get-organization-posts.action.js.map +1 -1
  105. package/dist/providers/linkedin/actions/get-personal-posts.action.d.ts.map +1 -1
  106. package/dist/providers/linkedin/actions/get-personal-posts.action.js +19 -19
  107. package/dist/providers/linkedin/actions/get-personal-posts.action.js.map +1 -1
  108. package/dist/providers/linkedin/actions/get-post-analytics.action.d.ts.map +1 -1
  109. package/dist/providers/linkedin/actions/get-post-analytics.action.js +23 -25
  110. package/dist/providers/linkedin/actions/get-post-analytics.action.js.map +1 -1
  111. package/dist/providers/linkedin/actions/schedule-post.action.d.ts.map +1 -1
  112. package/dist/providers/linkedin/actions/schedule-post.action.js +30 -32
  113. package/dist/providers/linkedin/actions/schedule-post.action.js.map +1 -1
  114. package/dist/providers/linkedin/actions/search-posts.action.d.ts.map +1 -1
  115. package/dist/providers/linkedin/actions/search-posts.action.js +30 -28
  116. package/dist/providers/linkedin/actions/search-posts.action.js.map +1 -1
  117. package/dist/providers/linkedin/linkedin-base.action.d.ts.map +1 -1
  118. package/dist/providers/linkedin/linkedin-base.action.js +38 -33
  119. package/dist/providers/linkedin/linkedin-base.action.js.map +1 -1
  120. package/dist/providers/tiktok/tiktok-base.action.d.ts.map +1 -1
  121. package/dist/providers/tiktok/tiktok-base.action.js +26 -25
  122. package/dist/providers/tiktok/tiktok-base.action.js.map +1 -1
  123. package/dist/providers/twitter/actions/create-thread.action.d.ts.map +1 -1
  124. package/dist/providers/twitter/actions/create-thread.action.js +29 -25
  125. package/dist/providers/twitter/actions/create-thread.action.js.map +1 -1
  126. package/dist/providers/twitter/actions/create-tweet.action.d.ts.map +1 -1
  127. package/dist/providers/twitter/actions/create-tweet.action.js +23 -23
  128. package/dist/providers/twitter/actions/create-tweet.action.js.map +1 -1
  129. package/dist/providers/twitter/actions/delete-tweet.action.d.ts.map +1 -1
  130. package/dist/providers/twitter/actions/delete-tweet.action.js +19 -19
  131. package/dist/providers/twitter/actions/delete-tweet.action.js.map +1 -1
  132. package/dist/providers/twitter/actions/get-analytics.action.d.ts.map +1 -1
  133. package/dist/providers/twitter/actions/get-analytics.action.js +47 -40
  134. package/dist/providers/twitter/actions/get-analytics.action.js.map +1 -1
  135. package/dist/providers/twitter/actions/get-mentions.action.d.ts.map +1 -1
  136. package/dist/providers/twitter/actions/get-mentions.action.js +31 -30
  137. package/dist/providers/twitter/actions/get-mentions.action.js.map +1 -1
  138. package/dist/providers/twitter/actions/get-timeline.action.d.ts.map +1 -1
  139. package/dist/providers/twitter/actions/get-timeline.action.js +29 -29
  140. package/dist/providers/twitter/actions/get-timeline.action.js.map +1 -1
  141. package/dist/providers/twitter/actions/schedule-tweet.action.d.ts.map +1 -1
  142. package/dist/providers/twitter/actions/schedule-tweet.action.js +26 -26
  143. package/dist/providers/twitter/actions/schedule-tweet.action.js.map +1 -1
  144. package/dist/providers/twitter/actions/search-tweets.action.d.ts.map +1 -1
  145. package/dist/providers/twitter/actions/search-tweets.action.js +58 -56
  146. package/dist/providers/twitter/actions/search-tweets.action.js.map +1 -1
  147. package/dist/providers/twitter/twitter-base.action.d.ts.map +1 -1
  148. package/dist/providers/twitter/twitter-base.action.js +68 -58
  149. package/dist/providers/twitter/twitter-base.action.js.map +1 -1
  150. package/dist/providers/youtube/youtube-base.action.d.ts +1 -1
  151. package/dist/providers/youtube/youtube-base.action.d.ts.map +1 -1
  152. package/dist/providers/youtube/youtube-base.action.js +25 -22
  153. package/dist/providers/youtube/youtube-base.action.js.map +1 -1
  154. package/package.json +6 -5
  155. package/src/base/base-social.action.ts +224 -217
  156. package/src/providers/buffer/buffer-base.action.ts +441 -435
  157. package/src/providers/facebook/actions/boost-post.action.ts +386 -350
  158. package/src/providers/facebook/actions/create-album.action.ts +307 -291
  159. package/src/providers/facebook/actions/create-post.action.ts +227 -224
  160. package/src/providers/facebook/actions/get-page-insights.action.ts +403 -383
  161. package/src/providers/facebook/actions/get-page-posts.action.ts +225 -214
  162. package/src/providers/facebook/actions/get-post-insights.action.ts +316 -300
  163. package/src/providers/facebook/actions/respond-to-comments.action.ts +336 -319
  164. package/src/providers/facebook/actions/schedule-post.action.ts +292 -289
  165. package/src/providers/facebook/actions/search-posts.action.ts +413 -399
  166. package/src/providers/facebook/facebook-base.action.ts +670 -653
  167. package/src/providers/hootsuite/actions/bulk-schedule-posts.action.ts +257 -257
  168. package/src/providers/hootsuite/actions/create-scheduled-post.action.ts +189 -184
  169. package/src/providers/hootsuite/actions/delete-scheduled-post.action.ts +161 -160
  170. package/src/providers/hootsuite/actions/get-analytics.action.ts +254 -249
  171. package/src/providers/hootsuite/actions/get-scheduled-posts.action.ts +207 -206
  172. package/src/providers/hootsuite/actions/get-social-profiles.action.ts +205 -206
  173. package/src/providers/hootsuite/actions/search-posts.action.ts +369 -351
  174. package/src/providers/hootsuite/actions/update-scheduled-post.action.ts +209 -211
  175. package/src/providers/hootsuite/hootsuite-base.action.ts +307 -301
  176. package/src/providers/instagram/actions/create-post.action.ts +296 -276
  177. package/src/providers/instagram/actions/create-story.action.ts +394 -378
  178. package/src/providers/instagram/actions/get-account-insights.action.ts +420 -384
  179. package/src/providers/instagram/actions/get-business-posts.action.ts +242 -233
  180. package/src/providers/instagram/actions/get-comments.action.ts +377 -365
  181. package/src/providers/instagram/actions/get-post-insights.action.ts +273 -265
  182. package/src/providers/instagram/actions/schedule-post.action.ts +235 -233
  183. package/src/providers/instagram/actions/search-posts.action.ts +538 -512
  184. package/src/providers/instagram/instagram-base.action.ts +393 -368
  185. package/src/providers/linkedin/actions/create-article.action.ts +266 -275
  186. package/src/providers/linkedin/actions/create-post.action.ts +177 -179
  187. package/src/providers/linkedin/actions/get-followers.action.ts +211 -211
  188. package/src/providers/linkedin/actions/get-organization-posts.action.ts +147 -146
  189. package/src/providers/linkedin/actions/get-personal-posts.action.ts +139 -138
  190. package/src/providers/linkedin/actions/get-post-analytics.action.ts +189 -190
  191. package/src/providers/linkedin/actions/schedule-post.action.ts +189 -191
  192. package/src/providers/linkedin/actions/search-posts.action.ts +283 -275
  193. package/src/providers/linkedin/linkedin-base.action.ts +421 -407
  194. package/src/providers/tiktok/tiktok-base.action.ts +320 -305
  195. package/src/providers/twitter/actions/create-thread.action.ts +207 -203
  196. package/src/providers/twitter/actions/create-tweet.action.ts +188 -187
  197. package/src/providers/twitter/actions/delete-tweet.action.ts +129 -128
  198. package/src/providers/twitter/actions/get-analytics.action.ts +411 -402
  199. package/src/providers/twitter/actions/get-mentions.action.ts +219 -218
  200. package/src/providers/twitter/actions/get-timeline.action.ts +233 -232
  201. package/src/providers/twitter/actions/schedule-tweet.action.ts +222 -221
  202. package/src/providers/twitter/actions/search-tweets.action.ts +543 -540
  203. package/src/providers/twitter/twitter-base.action.ts +560 -541
  204. 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/global';
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
- 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');
14
+ protected override get platformName(): string {
15
+ return 'YouTube';
47
16
  }
48
17
 
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();
18
+ protected override get apiBaseUrl(): string {
19
+ return 'https://www.googleapis.com/youtube/v3';
76
20
  }
77
21
 
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
- },
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
- return response.data;
91
- } catch (error) {
92
- if (axios.isAxiosError(error)) {
93
- this.handleYouTubeApiError(error);
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
- const errorData: any = response.data;
110
- const errorMessage = errorData?.error?.message || response.statusText;
111
- const errorCode = errorData?.error?.code || response.status;
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
- // Check for quota exceeded
114
- if (errorCode === 403 && errorMessage.includes('quota')) {
115
- throw new Error(`YouTube API quota exceeded. ${errorMessage}`);
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
- // 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`);
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
- 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';
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
- ): 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;
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
- 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;
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
- // Add channel filter if searching within a specific channel
264
- if (params.channelId || this.getCustomAttribute(1)) {
265
- searchParams.channelId = params.channelId || this.getCustomAttribute(1);
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
- // Add date filters
269
- if (params.startDate) {
270
- searchParams.publishedAfter = this.formatDate(params.startDate);
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
- if (params.endDate) {
273
- searchParams.publishedBefore = this.formatDate(params.endDate);
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
- // Add pagination
277
- if (params.pageToken) {
278
- searchParams.pageToken = params.pageToken;
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
- 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}`);
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
- // 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)}`);
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
+ }