@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.
- package/CHANGELOG.md +65 -0
- package/LICENSE +21 -0
- package/README.md +368 -0
- package/dist/base-Bw7e52V8.mjs +246 -0
- package/dist/base-Bw7e52V8.mjs.map +1 -0
- package/dist/base-DBtKFiSX.d.mts +226 -0
- package/dist/base-DBtKFiSX.d.mts.map +1 -0
- package/dist/chunk-DQk6qfdC.mjs +18 -0
- package/dist/client/index.d.mts +44 -0
- package/dist/client/index.d.mts.map +1 -0
- package/dist/client/index.mjs +154 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/common/index.d.mts +3 -0
- package/dist/common/index.mjs +7 -0
- package/dist/contracts-Cdwa4zlg.d.mts +121 -0
- package/dist/contracts-Cdwa4zlg.d.mts.map +1 -0
- package/dist/contracts-lCa069IK.mjs +221 -0
- package/dist/contracts-lCa069IK.mjs.map +1 -0
- package/dist/env-Bl0cwwjC.mjs +955 -0
- package/dist/env-Bl0cwwjC.mjs.map +1 -0
- package/dist/env-DxOZHf0p.d.mts +394 -0
- package/dist/env-DxOZHf0p.d.mts.map +1 -0
- package/dist/errors-Cm6LeKf7.mjs +32 -0
- package/dist/errors-Cm6LeKf7.mjs.map +1 -0
- package/dist/facebook-l_4CghaA.mjs +95 -0
- package/dist/facebook-l_4CghaA.mjs.map +1 -0
- package/dist/http-DpcLSR1M.mjs +197 -0
- package/dist/http-DpcLSR1M.mjs.map +1 -0
- package/dist/index.d.mts +42 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +71 -0
- package/dist/index.mjs.map +1 -0
- package/dist/instagram-BGaeUFU2.mjs +90 -0
- package/dist/instagram-BGaeUFU2.mjs.map +1 -0
- package/dist/linkedin-70whtVKa.mjs +101 -0
- package/dist/linkedin-70whtVKa.mjs.map +1 -0
- package/dist/meta-D3vcJU1c.mjs +126 -0
- package/dist/meta-D3vcJU1c.mjs.map +1 -0
- package/dist/pkce-jq5II68b.mjs +72 -0
- package/dist/pkce-jq5II68b.mjs.map +1 -0
- package/dist/polling-DZ1apXtA.mjs +25 -0
- package/dist/polling-DZ1apXtA.mjs.map +1 -0
- package/dist/providers/facebook.d.mts +135 -0
- package/dist/providers/facebook.d.mts.map +1 -0
- package/dist/providers/facebook.mjs +450 -0
- package/dist/providers/facebook.mjs.map +1 -0
- package/dist/providers/instagram.d.mts +122 -0
- package/dist/providers/instagram.d.mts.map +1 -0
- package/dist/providers/instagram.mjs +496 -0
- package/dist/providers/instagram.mjs.map +1 -0
- package/dist/providers/linkedin.d.mts +145 -0
- package/dist/providers/linkedin.d.mts.map +1 -0
- package/dist/providers/linkedin.mjs +574 -0
- package/dist/providers/linkedin.mjs.map +1 -0
- package/dist/providers/reddit.d.mts +102 -0
- package/dist/providers/reddit.d.mts.map +1 -0
- package/dist/providers/reddit.mjs +657 -0
- package/dist/providers/reddit.mjs.map +1 -0
- package/dist/providers/telegram.d.mts +139 -0
- package/dist/providers/telegram.d.mts.map +1 -0
- package/dist/providers/telegram.mjs +517 -0
- package/dist/providers/telegram.mjs.map +1 -0
- package/dist/providers/tiktok.d.mts +116 -0
- package/dist/providers/tiktok.d.mts.map +1 -0
- package/dist/providers/tiktok.mjs +676 -0
- package/dist/providers/tiktok.mjs.map +1 -0
- package/dist/providers/twitter.d.mts +150 -0
- package/dist/providers/twitter.d.mts.map +1 -0
- package/dist/providers/twitter.mjs +628 -0
- package/dist/providers/twitter.mjs.map +1 -0
- package/dist/providers/whatsapp.d.mts +79 -0
- package/dist/providers/whatsapp.d.mts.map +1 -0
- package/dist/providers/whatsapp.mjs +376 -0
- package/dist/providers/whatsapp.mjs.map +1 -0
- package/dist/providers/youtube.d.mts +153 -0
- package/dist/providers/youtube.d.mts.map +1 -0
- package/dist/providers/youtube.mjs +902 -0
- package/dist/providers/youtube.mjs.map +1 -0
- package/dist/reddit-B10kS4Se.mjs +126 -0
- package/dist/reddit-B10kS4Se.mjs.map +1 -0
- package/dist/schemas/index.d.mts +819 -0
- package/dist/schemas/index.d.mts.map +1 -0
- package/dist/schemas/index.mjs +31 -0
- package/dist/schemas/index.mjs.map +1 -0
- package/dist/security-BXhfebWm.d.mts +338 -0
- package/dist/security-BXhfebWm.d.mts.map +1 -0
- package/dist/shared-Fvc6xQku.mjs +100 -0
- package/dist/shared-Fvc6xQku.mjs.map +1 -0
- package/dist/telegram-FaUHpZgB.mjs +107 -0
- package/dist/telegram-FaUHpZgB.mjs.map +1 -0
- package/dist/tiktok-B_bMk4G-.mjs +94 -0
- package/dist/tiktok-B_bMk4G-.mjs.map +1 -0
- package/dist/twitter-BC22zfuc.mjs +98 -0
- package/dist/twitter-BC22zfuc.mjs.map +1 -0
- package/dist/types-BFE4psYI.d.mts +102 -0
- package/dist/types-BFE4psYI.d.mts.map +1 -0
- package/dist/types-Bv27tcT0.d.mts +230 -0
- package/dist/types-Bv27tcT0.d.mts.map +1 -0
- package/dist/types-BwkKyqpi.d.mts +253 -0
- package/dist/types-BwkKyqpi.d.mts.map +1 -0
- package/dist/types-CJrHMDV9.mjs +27 -0
- package/dist/types-CJrHMDV9.mjs.map +1 -0
- package/dist/types-ClbVc2rc.d.mts +117 -0
- package/dist/types-ClbVc2rc.d.mts.map +1 -0
- package/dist/types-D91N16Ym.d.mts +242 -0
- package/dist/types-D91N16Ym.d.mts.map +1 -0
- package/dist/types-DfLp_ibQ.d.mts +178 -0
- package/dist/types-DfLp_ibQ.d.mts.map +1 -0
- package/dist/types-DfjDgEoJ.d.mts +88 -0
- package/dist/types-DfjDgEoJ.d.mts.map +1 -0
- package/dist/types-Dp5Z9VBr.mjs +23 -0
- package/dist/types-Dp5Z9VBr.mjs.map +1 -0
- package/dist/types-hriBJTsU.d.mts +129 -0
- package/dist/types-hriBJTsU.d.mts.map +1 -0
- package/dist/types-rn6UuLL8.d.mts +184 -0
- package/dist/types-rn6UuLL8.d.mts.map +1 -0
- package/dist/whatsapp-CFp7ryR4.mjs +101 -0
- package/dist/whatsapp-CFp7ryR4.mjs.map +1 -0
- package/dist/youtube-Bs0fdY7H.mjs +98 -0
- package/dist/youtube-Bs0fdY7H.mjs.map +1 -0
- 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
|