@classytic/social 0.1.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.
Files changed (121) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +21 -0
  3. package/README.md +368 -0
  4. package/dist/base-Bw7e52V8.mjs +246 -0
  5. package/dist/base-Bw7e52V8.mjs.map +1 -0
  6. package/dist/base-DBtKFiSX.d.mts +226 -0
  7. package/dist/base-DBtKFiSX.d.mts.map +1 -0
  8. package/dist/chunk-DQk6qfdC.mjs +18 -0
  9. package/dist/client/index.d.mts +44 -0
  10. package/dist/client/index.d.mts.map +1 -0
  11. package/dist/client/index.mjs +154 -0
  12. package/dist/client/index.mjs.map +1 -0
  13. package/dist/common/index.d.mts +3 -0
  14. package/dist/common/index.mjs +7 -0
  15. package/dist/contracts-Cdwa4zlg.d.mts +121 -0
  16. package/dist/contracts-Cdwa4zlg.d.mts.map +1 -0
  17. package/dist/contracts-lCa069IK.mjs +221 -0
  18. package/dist/contracts-lCa069IK.mjs.map +1 -0
  19. package/dist/env-Bl0cwwjC.mjs +955 -0
  20. package/dist/env-Bl0cwwjC.mjs.map +1 -0
  21. package/dist/env-DxOZHf0p.d.mts +394 -0
  22. package/dist/env-DxOZHf0p.d.mts.map +1 -0
  23. package/dist/errors-Cm6LeKf7.mjs +32 -0
  24. package/dist/errors-Cm6LeKf7.mjs.map +1 -0
  25. package/dist/facebook-l_4CghaA.mjs +95 -0
  26. package/dist/facebook-l_4CghaA.mjs.map +1 -0
  27. package/dist/http-DpcLSR1M.mjs +197 -0
  28. package/dist/http-DpcLSR1M.mjs.map +1 -0
  29. package/dist/index.d.mts +42 -0
  30. package/dist/index.d.mts.map +1 -0
  31. package/dist/index.mjs +71 -0
  32. package/dist/index.mjs.map +1 -0
  33. package/dist/instagram-BGaeUFU2.mjs +90 -0
  34. package/dist/instagram-BGaeUFU2.mjs.map +1 -0
  35. package/dist/linkedin-70whtVKa.mjs +101 -0
  36. package/dist/linkedin-70whtVKa.mjs.map +1 -0
  37. package/dist/meta-D3vcJU1c.mjs +126 -0
  38. package/dist/meta-D3vcJU1c.mjs.map +1 -0
  39. package/dist/pkce-jq5II68b.mjs +72 -0
  40. package/dist/pkce-jq5II68b.mjs.map +1 -0
  41. package/dist/polling-DZ1apXtA.mjs +25 -0
  42. package/dist/polling-DZ1apXtA.mjs.map +1 -0
  43. package/dist/providers/facebook.d.mts +135 -0
  44. package/dist/providers/facebook.d.mts.map +1 -0
  45. package/dist/providers/facebook.mjs +450 -0
  46. package/dist/providers/facebook.mjs.map +1 -0
  47. package/dist/providers/instagram.d.mts +122 -0
  48. package/dist/providers/instagram.d.mts.map +1 -0
  49. package/dist/providers/instagram.mjs +496 -0
  50. package/dist/providers/instagram.mjs.map +1 -0
  51. package/dist/providers/linkedin.d.mts +145 -0
  52. package/dist/providers/linkedin.d.mts.map +1 -0
  53. package/dist/providers/linkedin.mjs +574 -0
  54. package/dist/providers/linkedin.mjs.map +1 -0
  55. package/dist/providers/reddit.d.mts +102 -0
  56. package/dist/providers/reddit.d.mts.map +1 -0
  57. package/dist/providers/reddit.mjs +657 -0
  58. package/dist/providers/reddit.mjs.map +1 -0
  59. package/dist/providers/telegram.d.mts +139 -0
  60. package/dist/providers/telegram.d.mts.map +1 -0
  61. package/dist/providers/telegram.mjs +517 -0
  62. package/dist/providers/telegram.mjs.map +1 -0
  63. package/dist/providers/tiktok.d.mts +116 -0
  64. package/dist/providers/tiktok.d.mts.map +1 -0
  65. package/dist/providers/tiktok.mjs +676 -0
  66. package/dist/providers/tiktok.mjs.map +1 -0
  67. package/dist/providers/twitter.d.mts +150 -0
  68. package/dist/providers/twitter.d.mts.map +1 -0
  69. package/dist/providers/twitter.mjs +628 -0
  70. package/dist/providers/twitter.mjs.map +1 -0
  71. package/dist/providers/whatsapp.d.mts +79 -0
  72. package/dist/providers/whatsapp.d.mts.map +1 -0
  73. package/dist/providers/whatsapp.mjs +376 -0
  74. package/dist/providers/whatsapp.mjs.map +1 -0
  75. package/dist/providers/youtube.d.mts +153 -0
  76. package/dist/providers/youtube.d.mts.map +1 -0
  77. package/dist/providers/youtube.mjs +902 -0
  78. package/dist/providers/youtube.mjs.map +1 -0
  79. package/dist/reddit-B10kS4Se.mjs +126 -0
  80. package/dist/reddit-B10kS4Se.mjs.map +1 -0
  81. package/dist/schemas/index.d.mts +819 -0
  82. package/dist/schemas/index.d.mts.map +1 -0
  83. package/dist/schemas/index.mjs +31 -0
  84. package/dist/schemas/index.mjs.map +1 -0
  85. package/dist/security-BXhfebWm.d.mts +338 -0
  86. package/dist/security-BXhfebWm.d.mts.map +1 -0
  87. package/dist/shared-Fvc6xQku.mjs +100 -0
  88. package/dist/shared-Fvc6xQku.mjs.map +1 -0
  89. package/dist/telegram-FaUHpZgB.mjs +107 -0
  90. package/dist/telegram-FaUHpZgB.mjs.map +1 -0
  91. package/dist/tiktok-B_bMk4G-.mjs +94 -0
  92. package/dist/tiktok-B_bMk4G-.mjs.map +1 -0
  93. package/dist/twitter-BC22zfuc.mjs +98 -0
  94. package/dist/twitter-BC22zfuc.mjs.map +1 -0
  95. package/dist/types-BFE4psYI.d.mts +102 -0
  96. package/dist/types-BFE4psYI.d.mts.map +1 -0
  97. package/dist/types-Bv27tcT0.d.mts +230 -0
  98. package/dist/types-Bv27tcT0.d.mts.map +1 -0
  99. package/dist/types-BwkKyqpi.d.mts +253 -0
  100. package/dist/types-BwkKyqpi.d.mts.map +1 -0
  101. package/dist/types-CJrHMDV9.mjs +27 -0
  102. package/dist/types-CJrHMDV9.mjs.map +1 -0
  103. package/dist/types-ClbVc2rc.d.mts +117 -0
  104. package/dist/types-ClbVc2rc.d.mts.map +1 -0
  105. package/dist/types-D91N16Ym.d.mts +242 -0
  106. package/dist/types-D91N16Ym.d.mts.map +1 -0
  107. package/dist/types-DfLp_ibQ.d.mts +178 -0
  108. package/dist/types-DfLp_ibQ.d.mts.map +1 -0
  109. package/dist/types-DfjDgEoJ.d.mts +88 -0
  110. package/dist/types-DfjDgEoJ.d.mts.map +1 -0
  111. package/dist/types-Dp5Z9VBr.mjs +23 -0
  112. package/dist/types-Dp5Z9VBr.mjs.map +1 -0
  113. package/dist/types-hriBJTsU.d.mts +129 -0
  114. package/dist/types-hriBJTsU.d.mts.map +1 -0
  115. package/dist/types-rn6UuLL8.d.mts +184 -0
  116. package/dist/types-rn6UuLL8.d.mts.map +1 -0
  117. package/dist/whatsapp-CFp7ryR4.mjs +101 -0
  118. package/dist/whatsapp-CFp7ryR4.mjs.map +1 -0
  119. package/dist/youtube-Bs0fdY7H.mjs +98 -0
  120. package/dist/youtube-Bs0fdY7H.mjs.map +1 -0
  121. package/package.json +148 -0
@@ -0,0 +1,902 @@
1
+ import { t as PlatformProvider } from "../base-Bw7e52V8.mjs";
2
+ import { t as SocialError } from "../errors-Cm6LeKf7.mjs";
3
+ import { t as YouTubeCredentialsSchema } from "../youtube-Bs0fdY7H.mjs";
4
+ import { google } from "googleapis";
5
+ import { createReadStream } from "fs";
6
+ import { stat } from "fs/promises";
7
+ import { Readable } from "stream";
8
+
9
+ //#region src/providers/youtube/index.ts
10
+ /**
11
+ * YouTube Platform Provider
12
+ * =========================
13
+ * YouTube integration with OAuth2, video upload, and content management.
14
+ *
15
+ * Features:
16
+ * - OAuth2 authorization flow
17
+ * - Resumable video uploads
18
+ * - Upload progress tracking
19
+ * - Video CRUD (get, list, update, delete)
20
+ * - Credential validation
21
+ */
22
+ var YouTubeProvider = class extends PlatformProvider {
23
+ defaultRedirectUri;
24
+ scopes;
25
+ constructor(config = {}) {
26
+ super(config);
27
+ this.name = "youtube";
28
+ this.displayName = "YouTube";
29
+ this.authType = "oauth2";
30
+ this.defaultRedirectUri = config.redirectUri || `http://localhost:${config.port || 4e3}/api/oauth/youtube/callback`;
31
+ this.scopes = [
32
+ "https://www.googleapis.com/auth/youtube.upload",
33
+ "https://www.googleapis.com/auth/youtube",
34
+ "https://www.googleapis.com/auth/userinfo.profile"
35
+ ];
36
+ }
37
+ /**
38
+ * Create OAuth2 client with dynamic credentials
39
+ * @private
40
+ */
41
+ _createOAuthClient(clientId, clientSecret, redirectUri) {
42
+ return new google.auth.OAuth2(clientId, clientSecret, redirectUri || this.defaultRedirectUri);
43
+ }
44
+ /**
45
+ * Get OAuth authorization URL
46
+ * @param state - State parameter for CSRF protection
47
+ * @param credentials - OAuth credentials {clientId, clientSecret}
48
+ */
49
+ getAuthUrl(state, credentials = {}, _options) {
50
+ return this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri).generateAuthUrl({
51
+ access_type: "offline",
52
+ scope: this.scopes,
53
+ state,
54
+ prompt: "consent"
55
+ });
56
+ }
57
+ /**
58
+ * Exchange authorization code for tokens
59
+ * @param code - Authorization code
60
+ * @param credentials - OAuth credentials {clientId, clientSecret}
61
+ */
62
+ async exchangeCode(code, credentials = {}) {
63
+ const { tokens } = await this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri).getToken(code);
64
+ return {
65
+ access_token: tokens.access_token,
66
+ refresh_token: tokens.refresh_token,
67
+ expires_in: tokens.expiry_date,
68
+ scope: tokens.scope,
69
+ token_type: tokens.token_type
70
+ };
71
+ }
72
+ /**
73
+ * Refresh access token
74
+ * @param refreshToken - Refresh token
75
+ * @param credentials - OAuth credentials {clientId, clientSecret}
76
+ */
77
+ async refreshToken(refreshToken, credentials = {}) {
78
+ const client = this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri);
79
+ client.setCredentials({ refresh_token: refreshToken });
80
+ const tokenResponse = await client.getAccessToken();
81
+ const newCreds = client.credentials;
82
+ return {
83
+ access_token: tokenResponse.token ?? newCreds.access_token,
84
+ expires_in: newCreds.expiry_date,
85
+ token_type: newCreds.token_type
86
+ };
87
+ }
88
+ /**
89
+ * Get account information
90
+ * @param accessToken - Access token
91
+ * @param credentials - OAuth credentials {clientId, clientSecret}
92
+ */
93
+ async getAccountInfo(accessToken, credentials = {}) {
94
+ const client = this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri);
95
+ client.setCredentials({ access_token: accessToken });
96
+ const youtube = google.youtube({
97
+ version: "v3",
98
+ auth: client
99
+ });
100
+ try {
101
+ const response = await youtube.channels.list({
102
+ part: [
103
+ "snippet",
104
+ "contentDetails",
105
+ "statistics"
106
+ ],
107
+ mine: true
108
+ });
109
+ if (!response.data.items || response.data.items.length === 0) throw new SocialError("youtube", "No YouTube channel found for this account", {
110
+ statusCode: 400,
111
+ hint: "Make sure your Google account has a YouTube channel."
112
+ });
113
+ const channel = response.data.items[0];
114
+ return {
115
+ id: channel.id,
116
+ name: channel.snippet.title,
117
+ description: channel.snippet.description,
118
+ customUrl: channel.snippet.customUrl,
119
+ profileImage: channel.snippet.thumbnails?.default?.url,
120
+ subscriberCount: channel.statistics?.subscriberCount,
121
+ videoCount: channel.statistics?.videoCount,
122
+ viewCount: channel.statistics?.viewCount
123
+ };
124
+ } catch (error) {
125
+ throw new SocialError("youtube", `Failed to get account info: ${error.message}`, { originalError: error });
126
+ }
127
+ }
128
+ /**
129
+ * Test credential validity
130
+ */
131
+ async testCredential(credentialData) {
132
+ try {
133
+ if (credentialData.oauthTokenData) {
134
+ const tokenData = JSON.parse(credentialData.oauthTokenData);
135
+ const accountInfo = await this.getAccountInfo(tokenData.access_token);
136
+ return {
137
+ status: "OK",
138
+ message: "YouTube credential is valid",
139
+ data: {
140
+ channelId: accountInfo.id,
141
+ channelTitle: accountInfo.name,
142
+ subscriberCount: accountInfo.subscriberCount
143
+ }
144
+ };
145
+ }
146
+ if (!this.config.clientId || !this.config.clientSecret) return {
147
+ status: "Error",
148
+ message: "YouTube OAuth credentials not configured"
149
+ };
150
+ return {
151
+ status: "OK",
152
+ message: "Credential needs OAuth authorization",
153
+ authUrl: this.getAuthUrl("test")
154
+ };
155
+ } catch (error) {
156
+ return {
157
+ status: "Error",
158
+ message: error.message || "Failed to validate YouTube credential"
159
+ };
160
+ }
161
+ }
162
+ /**
163
+ * Build video metadata for YouTube uploads.
164
+ * Shared between initUploadSession() and uploadVideo().
165
+ *
166
+ * Supports all YouTube Data API v3 fields:
167
+ * - snippet: title, description, tags, categoryId, defaultLanguage
168
+ * - status: privacyStatus, publishAt, license, embeddable, publicStatsViewable, selfDeclaredMadeForKids
169
+ * - recordingDetails: recordingDate
170
+ */
171
+ _buildVideoMetadata(params) {
172
+ const { title, description = "", tags = [], privacy = "private", categoryId = "22", scheduledAt, options } = params;
173
+ const metadata = {
174
+ snippet: {
175
+ title: title.substring(0, 100),
176
+ description: description.substring(0, 5e3),
177
+ tags: tags.slice(0, 500),
178
+ categoryId
179
+ },
180
+ status: { privacyStatus: scheduledAt ? "private" : privacy }
181
+ };
182
+ if (options?.defaultLanguage) metadata.snippet.defaultLanguage = options.defaultLanguage;
183
+ if (options?.license !== void 0) metadata.status.license = options.license;
184
+ if (options?.embeddable !== void 0) metadata.status.embeddable = options.embeddable;
185
+ if (options?.publicStatsViewable !== void 0) metadata.status.publicStatsViewable = options.publicStatsViewable;
186
+ if (options?.selfDeclaredMadeForKids !== void 0) metadata.status.selfDeclaredMadeForKids = options.selfDeclaredMadeForKids;
187
+ if (options?.recordingDate) metadata.recordingDetails = { recordingDate: options.recordingDate };
188
+ if (scheduledAt) {
189
+ const publishDate = new Date(scheduledAt);
190
+ if (publishDate <= /* @__PURE__ */ new Date()) throw new SocialError("youtube", "Scheduled time must be in the future", {
191
+ statusCode: 400,
192
+ retryable: false
193
+ });
194
+ metadata.status.publishAt = publishDate.toISOString();
195
+ }
196
+ return metadata;
197
+ }
198
+ /**
199
+ * Initialize a resumable upload session (metadata only, no file).
200
+ * Returns a self-authenticating upload URI that the frontend can PUT to directly.
201
+ *
202
+ * Flow: Server calls this → returns uploadUri → Frontend PUTs file to uploadUri
203
+ * The upload URI is valid for ~24 hours and requires no additional auth.
204
+ */
205
+ async initUploadSession(params) {
206
+ const { title, description, tags, privacy, categoryId, scheduledAt, credentials, tokens, options } = params;
207
+ try {
208
+ const videoMetadata = this._buildVideoMetadata({
209
+ title,
210
+ description,
211
+ tags,
212
+ privacy,
213
+ categoryId,
214
+ scheduledAt,
215
+ options
216
+ });
217
+ const initResponse = await fetch("https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&part=snippet,status,recordingDetails", {
218
+ method: "POST",
219
+ headers: {
220
+ "Authorization": `Bearer ${tokens.access_token}`,
221
+ "Content-Type": "application/json; charset=UTF-8"
222
+ },
223
+ body: JSON.stringify(videoMetadata)
224
+ });
225
+ if (!initResponse.ok) throw new SocialError("youtube", `Upload session init failed: ${(await initResponse.json().catch(() => ({ error: { message: initResponse.statusText } }))).error?.message || initResponse.statusText}`, { statusCode: initResponse.status });
226
+ const uploadUri = initResponse.headers.get("location");
227
+ if (!uploadUri) throw new SocialError("youtube", "No resumable upload URI returned by YouTube", { statusCode: 502 });
228
+ return { uploadUri };
229
+ } catch (error) {
230
+ if (error instanceof SocialError) throw error;
231
+ throw new SocialError("youtube", `Upload session init failed: ${error.message}`, { originalError: error });
232
+ }
233
+ }
234
+ /**
235
+ * Upload video to YouTube (server-side).
236
+ *
237
+ * Supports two modes:
238
+ * - filePath: Read from local file (original behavior)
239
+ * - videoUrl: Stream-through from URL (no disk IO, for GCS-stored media)
240
+ */
241
+ async uploadVideo(params) {
242
+ const { filePath, videoUrl, title, description = "", tags = [], privacy = "private", categoryId = "22", credentials, tokens, scheduledAt, onProgress } = params;
243
+ try {
244
+ const client = this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri);
245
+ client.setCredentials({
246
+ access_token: tokens.access_token,
247
+ refresh_token: tokens.refresh_token
248
+ });
249
+ const youtube = google.youtube({
250
+ version: "v3",
251
+ auth: client
252
+ });
253
+ let mediaBody;
254
+ let fileSize;
255
+ if (filePath) {
256
+ fileSize = (await stat(filePath)).size;
257
+ mediaBody = createReadStream(filePath);
258
+ } else if (videoUrl) {
259
+ const response = await fetch(videoUrl);
260
+ if (!response.ok) throw new SocialError("youtube", `Failed to fetch video from URL: ${response.status} ${response.statusText}`, { statusCode: 502 });
261
+ const contentLength = response.headers.get("content-length");
262
+ fileSize = contentLength ? parseInt(contentLength, 10) : void 0;
263
+ mediaBody = Readable.fromWeb(response.body);
264
+ } else throw new SocialError("youtube", "uploadVideo requires either filePath or videoUrl", { statusCode: 400 });
265
+ const videoMetadata = this._buildVideoMetadata({
266
+ title: title || "",
267
+ description,
268
+ tags,
269
+ privacy,
270
+ categoryId,
271
+ scheduledAt
272
+ });
273
+ const notifySubscribers = params.notifySubscribers;
274
+ const video = (await youtube.videos.insert({
275
+ part: [
276
+ "snippet",
277
+ "status",
278
+ "recordingDetails"
279
+ ],
280
+ notifySubscribers: notifySubscribers !== void 0 ? notifySubscribers : true,
281
+ requestBody: videoMetadata,
282
+ media: { body: mediaBody }
283
+ }, { onUploadProgress: (evt) => {
284
+ if (onProgress && evt.bytesRead && fileSize) onProgress(evt.bytesRead / fileSize * 100);
285
+ } })).data;
286
+ return {
287
+ platformVideoId: video.id,
288
+ platformUrl: `https://www.youtube.com/watch?v=${video.id}`,
289
+ status: scheduledAt ? "scheduled" : "published",
290
+ uploadedAt: /* @__PURE__ */ new Date(),
291
+ scheduledAt: scheduledAt ? new Date(scheduledAt) : null,
292
+ metadata: {
293
+ title: video.snippet.title,
294
+ privacy: video.status.privacyStatus,
295
+ publishAt: video.status.publishAt || null
296
+ }
297
+ };
298
+ } catch (error) {
299
+ if (error instanceof SocialError) throw error;
300
+ console.error("[YouTubeProvider] Upload failed:", error.message);
301
+ throw new SocialError("youtube", `Upload failed: ${error.message}`, { originalError: error });
302
+ }
303
+ }
304
+ /**
305
+ * Parse a YouTube API video item into a normalized YouTubeVideo.
306
+ */
307
+ _parseVideoItem(item) {
308
+ return {
309
+ id: item.id,
310
+ title: item.snippet?.title ?? "",
311
+ description: item.snippet?.description ?? "",
312
+ publishedAt: item.snippet?.publishedAt ?? "",
313
+ thumbnailUrl: item.snippet?.thumbnails?.default?.url ?? null,
314
+ channelId: item.snippet?.channelId ?? "",
315
+ channelTitle: item.snippet?.channelTitle ?? "",
316
+ tags: item.snippet?.tags ?? [],
317
+ categoryId: item.snippet?.categoryId ?? "",
318
+ privacyStatus: item.status?.privacyStatus ?? "",
319
+ duration: item.contentDetails?.duration ?? null,
320
+ viewCount: item.statistics?.viewCount ?? null,
321
+ likeCount: item.statistics?.likeCount ?? null,
322
+ commentCount: item.statistics?.commentCount ?? null,
323
+ defaultLanguage: item.snippet?.defaultLanguage,
324
+ embeddable: item.status?.embeddable,
325
+ license: item.status?.license,
326
+ madeForKids: item.status?.madeForKids,
327
+ publishAt: item.status?.publishAt ?? null,
328
+ favoriteCount: item.statistics?.favoriteCount ?? null
329
+ };
330
+ }
331
+ /**
332
+ * Get a single video by ID.
333
+ *
334
+ * @param accessToken - OAuth access token
335
+ * @param videoId - YouTube video ID
336
+ * @param credentials - OAuth credentials { clientId, clientSecret }
337
+ * @returns Normalized video data with snippet, stats, and content details
338
+ */
339
+ async getVideo(accessToken, videoId, credentials = {}) {
340
+ const client = this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri);
341
+ client.setCredentials({ access_token: accessToken });
342
+ const youtube = google.youtube({
343
+ version: "v3",
344
+ auth: client
345
+ });
346
+ try {
347
+ const items = (await youtube.videos.list({
348
+ part: [
349
+ "snippet",
350
+ "contentDetails",
351
+ "statistics",
352
+ "status"
353
+ ],
354
+ id: [videoId]
355
+ })).data.items;
356
+ if (!items || items.length === 0) throw new SocialError("youtube", `Video not found: ${videoId}`, {
357
+ statusCode: 404,
358
+ hint: "Check that the video ID is correct and the video is accessible with your credentials."
359
+ });
360
+ return this._parseVideoItem(items[0]);
361
+ } catch (error) {
362
+ if (error instanceof SocialError) throw error;
363
+ throw new SocialError("youtube", `Failed to get video: ${error.message}`, { originalError: error });
364
+ }
365
+ }
366
+ /**
367
+ * List videos from the authenticated channel.
368
+ *
369
+ * Uses playlistItems.list on the channel's uploads playlist (1 quota unit per call)
370
+ * instead of search.list (100 quota units per call — avoid).
371
+ *
372
+ * For text search within videos use videos.list with a separate search flow.
373
+ */
374
+ async listVideos(accessToken, credentials = {}, options = {}) {
375
+ const { maxResults = 10, pageToken } = options;
376
+ const client = this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri);
377
+ client.setCredentials({ access_token: accessToken });
378
+ const youtube = google.youtube({
379
+ version: "v3",
380
+ auth: client
381
+ });
382
+ try {
383
+ const uploadsPlaylistId = (await youtube.channels.list({
384
+ part: ["contentDetails"],
385
+ mine: true
386
+ })).data.items?.[0]?.contentDetails?.relatedPlaylists?.uploads;
387
+ if (!uploadsPlaylistId) return {
388
+ videos: [],
389
+ nextPageToken: null,
390
+ prevPageToken: null,
391
+ totalResults: 0
392
+ };
393
+ const playlistData = (await youtube.playlistItems.list({
394
+ part: ["snippet", "contentDetails"],
395
+ playlistId: uploadsPlaylistId,
396
+ maxResults: Math.min(maxResults, 50),
397
+ pageToken
398
+ })).data;
399
+ const videoIds = (playlistData.items ?? []).map((item) => item.contentDetails?.videoId).filter(Boolean);
400
+ if (videoIds.length === 0) return {
401
+ videos: [],
402
+ nextPageToken: null,
403
+ prevPageToken: null,
404
+ totalResults: 0
405
+ };
406
+ const videos = ((await youtube.videos.list({
407
+ part: [
408
+ "snippet",
409
+ "contentDetails",
410
+ "statistics",
411
+ "status"
412
+ ],
413
+ id: videoIds
414
+ })).data.items ?? []).map((item) => this._parseVideoItem(item));
415
+ return {
416
+ videos,
417
+ nextPageToken: playlistData.nextPageToken ?? null,
418
+ prevPageToken: playlistData.prevPageToken ?? null,
419
+ totalResults: playlistData.pageInfo?.totalResults ?? videos.length
420
+ };
421
+ } catch (error) {
422
+ if (error instanceof SocialError) throw error;
423
+ throw new SocialError("youtube", `Failed to list videos: ${error.message}`, { originalError: error });
424
+ }
425
+ }
426
+ /**
427
+ * Update video metadata (title, description, tags, category, privacy).
428
+ *
429
+ * Only the fields provided in `params` are updated. Omitted fields are left unchanged.
430
+ * Note: You must have ownership of the video.
431
+ *
432
+ * @param accessToken - OAuth access token
433
+ * @param videoId - YouTube video ID to update
434
+ * @param params - Fields to update
435
+ * @param credentials - OAuth credentials { clientId, clientSecret }
436
+ */
437
+ async updateVideo(accessToken, videoId, params, credentials = {}) {
438
+ const client = this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri);
439
+ client.setCredentials({ access_token: accessToken });
440
+ const youtube = google.youtube({
441
+ version: "v3",
442
+ auth: client
443
+ });
444
+ try {
445
+ const requestBody = {
446
+ id: videoId,
447
+ snippet: {},
448
+ status: {}
449
+ };
450
+ if (params.title !== void 0) requestBody.snippet.title = params.title;
451
+ if (params.description !== void 0) requestBody.snippet.description = params.description;
452
+ if (params.tags !== void 0) requestBody.snippet.tags = params.tags;
453
+ if (params.categoryId !== void 0) requestBody.snippet.categoryId = params.categoryId;
454
+ if (params.defaultLanguage !== void 0) requestBody.snippet.defaultLanguage = params.defaultLanguage;
455
+ if (params.privacy !== void 0) requestBody.status.privacyStatus = params.privacy;
456
+ if (params.embeddable !== void 0) requestBody.status.embeddable = params.embeddable;
457
+ if (params.license !== void 0) requestBody.status.license = params.license;
458
+ if (params.publicStatsViewable !== void 0) requestBody.status.publicStatsViewable = params.publicStatsViewable;
459
+ if (params.selfDeclaredMadeForKids !== void 0) requestBody.status.selfDeclaredMadeForKids = params.selfDeclaredMadeForKids;
460
+ const item = (await youtube.videos.update({
461
+ part: ["snippet", "status"],
462
+ requestBody
463
+ })).data;
464
+ return {
465
+ id: item.id,
466
+ title: item.snippet?.title ?? "",
467
+ description: item.snippet?.description ?? "",
468
+ publishedAt: "",
469
+ thumbnailUrl: null,
470
+ channelId: "",
471
+ channelTitle: "",
472
+ tags: item.snippet?.tags ?? [],
473
+ categoryId: item.snippet?.categoryId ?? "",
474
+ privacyStatus: item.status?.privacyStatus ?? "",
475
+ duration: null,
476
+ viewCount: null,
477
+ likeCount: null,
478
+ commentCount: null
479
+ };
480
+ } catch (error) {
481
+ if (error instanceof SocialError) throw error;
482
+ throw new SocialError("youtube", `Failed to update video: ${error.message}`, { originalError: error });
483
+ }
484
+ }
485
+ /**
486
+ * Delete a video by ID.
487
+ *
488
+ * @param accessToken - OAuth access token
489
+ * @param videoId - YouTube video ID to delete
490
+ * @param credentials - OAuth credentials { clientId, clientSecret }
491
+ */
492
+ async deleteVideo(accessToken, videoId, credentials = {}) {
493
+ const client = this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri);
494
+ client.setCredentials({ access_token: accessToken });
495
+ const youtube = google.youtube({
496
+ version: "v3",
497
+ auth: client
498
+ });
499
+ try {
500
+ await youtube.videos.delete({ id: videoId });
501
+ } catch (error) {
502
+ if (error instanceof SocialError) throw error;
503
+ throw new SocialError("youtube", `Failed to delete video: ${error.message}`, { originalError: error });
504
+ }
505
+ }
506
+ _parsePlaylist(item) {
507
+ return {
508
+ id: item.id,
509
+ title: item.snippet?.title ?? "",
510
+ description: item.snippet?.description ?? "",
511
+ publishedAt: item.snippet?.publishedAt ?? "",
512
+ channelId: item.snippet?.channelId ?? "",
513
+ thumbnailUrl: item.snippet?.thumbnails?.default?.url ?? null,
514
+ itemCount: item.contentDetails?.itemCount ?? 0,
515
+ privacyStatus: item.status?.privacyStatus ?? "",
516
+ defaultLanguage: item.snippet?.defaultLanguage
517
+ };
518
+ }
519
+ _parsePlaylistItem(item) {
520
+ return {
521
+ id: item.id,
522
+ playlistId: item.snippet?.playlistId ?? "",
523
+ videoId: item.snippet?.resourceId?.videoId ?? "",
524
+ title: item.snippet?.title ?? "",
525
+ description: item.snippet?.description ?? "",
526
+ thumbnailUrl: item.snippet?.thumbnails?.default?.url ?? null,
527
+ position: item.snippet?.position ?? 0
528
+ };
529
+ }
530
+ async createPlaylist(accessToken, params, credentials = {}) {
531
+ const client = this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri);
532
+ client.setCredentials({ access_token: accessToken });
533
+ const youtube = google.youtube({
534
+ version: "v3",
535
+ auth: client
536
+ });
537
+ try {
538
+ const requestBody = {
539
+ snippet: { title: params.title },
540
+ status: {}
541
+ };
542
+ if (params.description !== void 0) requestBody.snippet.description = params.description;
543
+ if (params.tags) requestBody.snippet.tags = params.tags;
544
+ if (params.defaultLanguage) requestBody.snippet.defaultLanguage = params.defaultLanguage;
545
+ if (params.privacy) requestBody.status.privacyStatus = params.privacy;
546
+ const response = await youtube.playlists.insert({
547
+ part: ["snippet", "status"],
548
+ requestBody
549
+ });
550
+ return this._parsePlaylist(response.data);
551
+ } catch (error) {
552
+ if (error instanceof SocialError) throw error;
553
+ throw new SocialError("youtube", `Failed to create playlist: ${error.message}`, { originalError: error });
554
+ }
555
+ }
556
+ async getPlaylist(accessToken, playlistId, credentials = {}) {
557
+ const client = this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri);
558
+ client.setCredentials({ access_token: accessToken });
559
+ const youtube = google.youtube({
560
+ version: "v3",
561
+ auth: client
562
+ });
563
+ try {
564
+ const items = (await youtube.playlists.list({
565
+ part: [
566
+ "snippet",
567
+ "contentDetails",
568
+ "status"
569
+ ],
570
+ id: [playlistId]
571
+ })).data.items;
572
+ if (!items || items.length === 0) throw new SocialError("youtube", `Playlist not found: ${playlistId}`, { statusCode: 404 });
573
+ return this._parsePlaylist(items[0]);
574
+ } catch (error) {
575
+ if (error instanceof SocialError) throw error;
576
+ throw new SocialError("youtube", `Failed to get playlist: ${error.message}`, { originalError: error });
577
+ }
578
+ }
579
+ async listPlaylists(accessToken, credentials = {}, options = {}) {
580
+ const { maxResults = 25, pageToken, mine = true, channelId } = options;
581
+ const client = this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri);
582
+ client.setCredentials({ access_token: accessToken });
583
+ const youtube = google.youtube({
584
+ version: "v3",
585
+ auth: client
586
+ });
587
+ try {
588
+ const params = {
589
+ part: [
590
+ "snippet",
591
+ "contentDetails",
592
+ "status"
593
+ ],
594
+ maxResults: Math.min(maxResults, 50)
595
+ };
596
+ if (channelId) params.channelId = channelId;
597
+ else if (mine) params.mine = true;
598
+ if (pageToken) params.pageToken = pageToken;
599
+ const data = (await youtube.playlists.list(params)).data;
600
+ return {
601
+ playlists: (data.items ?? []).map((item) => this._parsePlaylist(item)),
602
+ nextPageToken: data.nextPageToken ?? null,
603
+ prevPageToken: data.prevPageToken ?? null,
604
+ totalResults: data.pageInfo?.totalResults ?? 0
605
+ };
606
+ } catch (error) {
607
+ if (error instanceof SocialError) throw error;
608
+ throw new SocialError("youtube", `Failed to list playlists: ${error.message}`, { originalError: error });
609
+ }
610
+ }
611
+ async updatePlaylist(accessToken, playlistId, params, credentials = {}) {
612
+ const client = this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri);
613
+ client.setCredentials({ access_token: accessToken });
614
+ const youtube = google.youtube({
615
+ version: "v3",
616
+ auth: client
617
+ });
618
+ try {
619
+ const requestBody = {
620
+ id: playlistId,
621
+ snippet: {},
622
+ status: {}
623
+ };
624
+ if (params.title !== void 0) requestBody.snippet.title = params.title;
625
+ if (params.description !== void 0) requestBody.snippet.description = params.description;
626
+ if (params.tags !== void 0) requestBody.snippet.tags = params.tags;
627
+ if (params.defaultLanguage !== void 0) requestBody.snippet.defaultLanguage = params.defaultLanguage;
628
+ if (params.privacy !== void 0) requestBody.status.privacyStatus = params.privacy;
629
+ const response = await youtube.playlists.update({
630
+ part: ["snippet", "status"],
631
+ requestBody
632
+ });
633
+ return this._parsePlaylist(response.data);
634
+ } catch (error) {
635
+ if (error instanceof SocialError) throw error;
636
+ throw new SocialError("youtube", `Failed to update playlist: ${error.message}`, { originalError: error });
637
+ }
638
+ }
639
+ async deletePlaylist(accessToken, playlistId, credentials = {}) {
640
+ const client = this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri);
641
+ client.setCredentials({ access_token: accessToken });
642
+ const youtube = google.youtube({
643
+ version: "v3",
644
+ auth: client
645
+ });
646
+ try {
647
+ await youtube.playlists.delete({ id: playlistId });
648
+ } catch (error) {
649
+ if (error instanceof SocialError) throw error;
650
+ throw new SocialError("youtube", `Failed to delete playlist: ${error.message}`, { originalError: error });
651
+ }
652
+ }
653
+ async addToPlaylist(accessToken, playlistId, videoId, credentials = {}, options = {}) {
654
+ const client = this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri);
655
+ client.setCredentials({ access_token: accessToken });
656
+ const youtube = google.youtube({
657
+ version: "v3",
658
+ auth: client
659
+ });
660
+ try {
661
+ const requestBody = { snippet: {
662
+ playlistId,
663
+ resourceId: {
664
+ kind: "youtube#video",
665
+ videoId
666
+ }
667
+ } };
668
+ if (options.position !== void 0) requestBody.snippet.position = options.position;
669
+ if (options.note) requestBody.contentDetails = { note: options.note };
670
+ const response = await youtube.playlistItems.insert({
671
+ part: ["snippet", "contentDetails"],
672
+ requestBody
673
+ });
674
+ return this._parsePlaylistItem(response.data);
675
+ } catch (error) {
676
+ if (error instanceof SocialError) throw error;
677
+ throw new SocialError("youtube", `Failed to add to playlist: ${error.message}`, { originalError: error });
678
+ }
679
+ }
680
+ async removeFromPlaylist(accessToken, playlistItemId, credentials = {}) {
681
+ const client = this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri);
682
+ client.setCredentials({ access_token: accessToken });
683
+ const youtube = google.youtube({
684
+ version: "v3",
685
+ auth: client
686
+ });
687
+ try {
688
+ await youtube.playlistItems.delete({ id: playlistItemId });
689
+ } catch (error) {
690
+ if (error instanceof SocialError) throw error;
691
+ throw new SocialError("youtube", `Failed to remove from playlist: ${error.message}`, { originalError: error });
692
+ }
693
+ }
694
+ async listPlaylistItems(accessToken, playlistId, credentials = {}, options = {}) {
695
+ const { maxResults = 25, pageToken } = options;
696
+ const client = this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri);
697
+ client.setCredentials({ access_token: accessToken });
698
+ const youtube = google.youtube({
699
+ version: "v3",
700
+ auth: client
701
+ });
702
+ try {
703
+ const params = {
704
+ part: ["snippet", "contentDetails"],
705
+ playlistId,
706
+ maxResults: Math.min(maxResults, 50)
707
+ };
708
+ if (pageToken) params.pageToken = pageToken;
709
+ const data = (await youtube.playlistItems.list(params)).data;
710
+ return {
711
+ items: (data.items ?? []).map((item) => this._parsePlaylistItem(item)),
712
+ nextPageToken: data.nextPageToken ?? null,
713
+ prevPageToken: data.prevPageToken ?? null,
714
+ totalResults: data.pageInfo?.totalResults ?? 0
715
+ };
716
+ } catch (error) {
717
+ if (error instanceof SocialError) throw error;
718
+ throw new SocialError("youtube", `Failed to list playlist items: ${error.message}`, { originalError: error });
719
+ }
720
+ }
721
+ /**
722
+ * Search YouTube videos with comprehensive filters.
723
+ * Uses a 2-step approach: search.list → videos.list for full details.
724
+ */
725
+ async searchVideos(accessToken, credentials = {}, options = {}) {
726
+ const { q, publishedAfter, publishedBefore, videoCategoryId, channelId, order = "relevance", safeSearch, videoType, regionCode, maxResults = 25, pageToken } = options;
727
+ const client = this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri);
728
+ client.setCredentials({ access_token: accessToken });
729
+ const youtube = google.youtube({
730
+ version: "v3",
731
+ auth: client
732
+ });
733
+ try {
734
+ const searchParams = {
735
+ part: ["id"],
736
+ type: ["video"],
737
+ maxResults: Math.min(maxResults, 50),
738
+ order
739
+ };
740
+ if (q) searchParams.q = q;
741
+ if (publishedAfter) searchParams.publishedAfter = publishedAfter;
742
+ if (publishedBefore) searchParams.publishedBefore = publishedBefore;
743
+ if (videoCategoryId) searchParams.videoCategoryId = videoCategoryId;
744
+ if (channelId) searchParams.channelId = channelId;
745
+ if (safeSearch) searchParams.safeSearch = safeSearch;
746
+ if (videoType) searchParams.videoType = videoType;
747
+ if (regionCode) searchParams.regionCode = regionCode;
748
+ if (pageToken) searchParams.pageToken = pageToken;
749
+ if (!channelId && !q) searchParams.forMine = true;
750
+ const searchData = (await youtube.search.list(searchParams)).data;
751
+ const videoIds = (searchData.items ?? []).map((item) => item.id?.videoId).filter(Boolean);
752
+ if (videoIds.length === 0) return {
753
+ videos: [],
754
+ nextPageToken: null,
755
+ prevPageToken: null,
756
+ totalResults: 0
757
+ };
758
+ return {
759
+ videos: ((await youtube.videos.list({
760
+ part: [
761
+ "snippet",
762
+ "contentDetails",
763
+ "statistics",
764
+ "status"
765
+ ],
766
+ id: videoIds
767
+ })).data.items ?? []).map((item) => this._parseVideoItem(item)),
768
+ nextPageToken: searchData.nextPageToken ?? null,
769
+ prevPageToken: searchData.prevPageToken ?? null,
770
+ totalResults: searchData.pageInfo?.totalResults ?? 0
771
+ };
772
+ } catch (error) {
773
+ if (error instanceof SocialError) throw error;
774
+ throw new SocialError("youtube", `Search failed: ${error.message}`, { originalError: error });
775
+ }
776
+ }
777
+ /**
778
+ * Get available video categories for a region.
779
+ * @param regionCode - ISO 3166-1 alpha-2 country code (e.g. 'US', 'BD')
780
+ */
781
+ async getVideoCategories(accessToken, regionCode, credentials = {}) {
782
+ const client = this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri);
783
+ client.setCredentials({ access_token: accessToken });
784
+ const youtube = google.youtube({
785
+ version: "v3",
786
+ auth: client
787
+ });
788
+ try {
789
+ return ((await youtube.videoCategories.list({
790
+ part: ["snippet"],
791
+ regionCode
792
+ })).data.items ?? []).map((item) => ({
793
+ id: item.id,
794
+ title: item.snippet?.title ?? "",
795
+ assignable: item.snippet?.assignable ?? false
796
+ }));
797
+ } catch (error) {
798
+ if (error instanceof SocialError) throw error;
799
+ throw new SocialError("youtube", `Failed to get video categories: ${error.message}`, { originalError: error });
800
+ }
801
+ }
802
+ /**
803
+ * Rate a video (like, dislike, or remove rating).
804
+ */
805
+ async rateVideo(accessToken, videoId, rating, credentials = {}) {
806
+ const client = this._createOAuthClient(credentials.clientId, credentials.clientSecret, credentials.redirectUri);
807
+ client.setCredentials({ access_token: accessToken });
808
+ const youtube = google.youtube({
809
+ version: "v3",
810
+ auth: client
811
+ });
812
+ try {
813
+ await youtube.videos.rate({
814
+ id: videoId,
815
+ rating
816
+ });
817
+ } catch (error) {
818
+ if (error instanceof SocialError) throw error;
819
+ throw new SocialError("youtube", `Failed to rate video: ${error.message}`, { originalError: error });
820
+ }
821
+ }
822
+ /**
823
+ * Return the zod schema for runtime validation. Consumers can call
824
+ * `z.toJSONSchema(provider.getCredentialZodSchema())` to render UI forms
825
+ * or generate OpenAPI specs.
826
+ */
827
+ getCredentialZodSchema() {
828
+ return YouTubeCredentialsSchema;
829
+ }
830
+ /**
831
+ * Get credential schema for UI (legacy CredentialField[] shape).
832
+ */
833
+ getCredentialSchema() {
834
+ return [{
835
+ name: "clientId",
836
+ displayName: "Client ID",
837
+ type: "text",
838
+ required: true,
839
+ description: "Google OAuth2 Client ID from Google Cloud Console"
840
+ }, {
841
+ name: "clientSecret",
842
+ displayName: "Client Secret",
843
+ type: "password",
844
+ required: true,
845
+ description: "Google OAuth2 Client Secret"
846
+ }];
847
+ }
848
+ /**
849
+ * Get provider metadata for frontend display
850
+ */
851
+ getMetadata() {
852
+ return {
853
+ name: this.name,
854
+ displayName: this.displayName,
855
+ authType: this.authType,
856
+ icon: "youtube",
857
+ brandColor: "#FF0000",
858
+ supportsScheduling: true,
859
+ supportsEnvironment: false,
860
+ description: "Upload and manage videos on YouTube",
861
+ scopes: this.scopes,
862
+ scopeDescriptions: {
863
+ "https://www.googleapis.com/auth/youtube.upload": "Upload videos to your channel",
864
+ "https://www.googleapis.com/auth/youtube": "Manage your YouTube account",
865
+ "https://www.googleapis.com/auth/userinfo.profile": "View your basic profile info"
866
+ },
867
+ setupGuide: [
868
+ {
869
+ step: 1,
870
+ title: "Create Google Cloud Project",
871
+ description: "Go to console.cloud.google.com and create a new project or select an existing one"
872
+ },
873
+ {
874
+ step: 2,
875
+ title: "Enable YouTube Data API v3",
876
+ description: "Navigate to APIs & Services > Library, search for \"YouTube Data API v3\" and enable it"
877
+ },
878
+ {
879
+ step: 3,
880
+ title: "Create OAuth Credentials",
881
+ description: "Go to APIs & Services > Credentials, click \"Create Credentials\" > \"OAuth 2.0 Client ID\" for Web application"
882
+ },
883
+ {
884
+ step: 4,
885
+ title: "Set Redirect URI",
886
+ description: `Add this as an authorized redirect URI: ${this.defaultRedirectUri}`
887
+ },
888
+ {
889
+ step: 5,
890
+ title: "Configure Consent Screen",
891
+ description: "Set up the OAuth consent screen with your app name and add your email as a test user"
892
+ }
893
+ ],
894
+ redirectUriPattern: this.defaultRedirectUri,
895
+ credentialSchema: this.getCredentialSchema()
896
+ };
897
+ }
898
+ };
899
+
900
+ //#endregion
901
+ export { YouTubeProvider };
902
+ //# sourceMappingURL=youtube.mjs.map