my-youtube-api 1.0.0 → 1.0.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.
- package/dist/errors/youtube-api-errors.d.ts +30 -0
- package/dist/errors/youtube-api-errors.js +59 -0
- package/dist/youtube-handlers/youtube-video-poster.d.ts +39 -3
- package/dist/youtube-handlers/youtube-video-poster.js +291 -10
- package/dist/youtube-handlers copy/youtube-account-handler.d.ts +21 -0
- package/dist/youtube-handlers copy/youtube-account-handler.js +33 -0
- package/dist/youtube-handlers copy/youtube-channel-handler.d.ts +65 -0
- package/dist/youtube-handlers copy/youtube-channel-handler.js +193 -0
- package/dist/youtube-handlers copy/youtube-channel-handler2.d.ts +65 -0
- package/dist/youtube-handlers copy/youtube-channel-handler2.js +177 -0
- package/dist/youtube-handlers copy/youtube-video-handler.d.ts +30 -0
- package/dist/youtube-handlers copy/youtube-video-handler.js +55 -0
- package/dist/youtube-handlers copy/youtube-video-poster.d.ts +23 -0
- package/dist/youtube-handlers copy/youtube-video-poster.js +47 -0
- package/package.json +1 -1
- package/src/errors/youtube-api-errors.ts +72 -0
- package/src/youtube-handlers/youtube-video-poster.ts +361 -12
- package/src/youtube-handlers copy/youtube-account-handler.ts +51 -0
- package/src/youtube-handlers copy/youtube-channel-handler.ts +275 -0
- package/src/youtube-handlers copy/youtube-channel-handler2.ts +249 -0
- package/src/youtube-handlers copy/youtube-video-handler.ts +87 -0
- package/src/youtube-handlers copy/youtube-video-poster.ts +65 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
|
|
3
|
+
interface VideoAnalytics {
|
|
4
|
+
videoId: string;
|
|
5
|
+
title: string;
|
|
6
|
+
views: number;
|
|
7
|
+
likes: number;
|
|
8
|
+
comments: number;
|
|
9
|
+
publishedAt: string;
|
|
10
|
+
thumbnail: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ChannelAnalytics {
|
|
14
|
+
channelId: string;
|
|
15
|
+
title: string;
|
|
16
|
+
description: string;
|
|
17
|
+
thumbnail: string;
|
|
18
|
+
subscriberCount: number;
|
|
19
|
+
viewCount: number;
|
|
20
|
+
videoCount: number;
|
|
21
|
+
hiddenSubscriberCount: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
interface AudienceAnalytics {
|
|
26
|
+
views: number;
|
|
27
|
+
estimatedMinutesWatched: number;
|
|
28
|
+
averageViewDuration: number;
|
|
29
|
+
subscribersGained: number;
|
|
30
|
+
subscribersLost: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
interface VideoAnalytics {
|
|
35
|
+
videoId: string;
|
|
36
|
+
title: string;
|
|
37
|
+
views: number;
|
|
38
|
+
likes: number;
|
|
39
|
+
comments: number;
|
|
40
|
+
publishedAt: string;
|
|
41
|
+
thumbneil?: string;
|
|
42
|
+
videoType: "short" | "regular";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface ChannelRevenue {
|
|
46
|
+
estimatedRevenue: number; // in USD
|
|
47
|
+
currency: string;
|
|
48
|
+
startDate: string;
|
|
49
|
+
endDate: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/*
|
|
53
|
+
. Enable YouTube Analytics API
|
|
54
|
+
|
|
55
|
+
Go to your Google Cloud Console → APIs & Services → Library → Search for and enable:
|
|
56
|
+
|
|
57
|
+
YouTube Analytics API
|
|
58
|
+
|
|
59
|
+
YouTube Reporting API
|
|
60
|
+
|
|
61
|
+
for report and analytics
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
export class YoutubeChannelHandler {
|
|
65
|
+
private access_token: string;
|
|
66
|
+
private channel_id: string;
|
|
67
|
+
|
|
68
|
+
constructor(access_token: string, channel_id: string) {
|
|
69
|
+
this.access_token = access_token;
|
|
70
|
+
this.channel_id = channel_id;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Fetches basic channel statistics and information
|
|
75
|
+
* @returns Channel analytics including subscriber count, view count, and video count
|
|
76
|
+
*/
|
|
77
|
+
async fetchChannelAnalytics(): Promise<ChannelAnalytics> {
|
|
78
|
+
const response = await axios.get("https://www.googleapis.com/youtube/v3/channels", {
|
|
79
|
+
params: {
|
|
80
|
+
part: "snippet,statistics,contentDetails",
|
|
81
|
+
mine: true // This gets the authenticated user's channel
|
|
82
|
+
},
|
|
83
|
+
headers: { Authorization: `Bearer ${this.access_token}` },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const channel = response.data.items[0];
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
channelId: channel.id,
|
|
90
|
+
title: channel.snippet.title,
|
|
91
|
+
description: channel.snippet.description,
|
|
92
|
+
thumbnail: channel.snippet.thumbnails?.high?.url,
|
|
93
|
+
subscriberCount: parseInt(channel.statistics.subscriberCount || '0'),
|
|
94
|
+
viewCount: parseInt(channel.statistics.viewCount || '0'),
|
|
95
|
+
videoCount: parseInt(channel.statistics.videoCount || '0'),
|
|
96
|
+
hiddenSubscriberCount: channel.statistics.hiddenSubscriberCount || false
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Fetches analytics for all videos in the channel
|
|
102
|
+
* @param maxResults Maximum number of videos to fetch (default: 50, max: 50)
|
|
103
|
+
* @returns Array of video analytics including views, likes, and comments
|
|
104
|
+
*/
|
|
105
|
+
async fetchVideos(maxResults: number = 50): Promise<VideoAnalytics[]> {
|
|
106
|
+
// 1️⃣ Get uploads playlist ID
|
|
107
|
+
const channelRes = await axios.get("https://www.googleapis.com/youtube/v3/channels", {
|
|
108
|
+
params: { part: "contentDetails", id: this.channel_id },
|
|
109
|
+
headers: { Authorization: `Bearer ${this.access_token}` },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const uploadsPlaylistId = channelRes.data.items[0]?.contentDetails?.relatedPlaylists?.uploads;
|
|
113
|
+
if (!uploadsPlaylistId) return [];
|
|
114
|
+
|
|
115
|
+
// 2️⃣ Fetch videos from the uploads playlist
|
|
116
|
+
const videosRes = await axios.get("https://www.googleapis.com/youtube/v3/playlistItems", {
|
|
117
|
+
params: {
|
|
118
|
+
part: "snippet,contentDetails",
|
|
119
|
+
playlistId: uploadsPlaylistId,
|
|
120
|
+
maxResults: Math.min(maxResults, 50),
|
|
121
|
+
},
|
|
122
|
+
headers: { Authorization: `Bearer ${this.access_token}` },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const videoIds = videosRes.data.items.map((v: any) => v.contentDetails.videoId);
|
|
126
|
+
if (videoIds.length === 0) return [];
|
|
127
|
+
|
|
128
|
+
// 3️⃣ Fetch analytics & contentDetails for each video
|
|
129
|
+
const analyticsRes = await axios.get("https://www.googleapis.com/youtube/v3/videos", {
|
|
130
|
+
params: {
|
|
131
|
+
part: "snippet,statistics,contentDetails",
|
|
132
|
+
id: videoIds.join(","),
|
|
133
|
+
maxResults: Math.min(maxResults, 50),
|
|
134
|
+
},
|
|
135
|
+
headers: { Authorization: `Bearer ${this.access_token}` },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return analyticsRes.data.items.map((video: any) => {
|
|
139
|
+
// Convert ISO 8601 duration to seconds
|
|
140
|
+
const duration = video.contentDetails?.duration || "PT0S";
|
|
141
|
+
const match = duration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
|
|
142
|
+
const seconds =
|
|
143
|
+
(parseInt(match?.[1] || "0") * 3600) +
|
|
144
|
+
(parseInt(match?.[2] || "0") * 60) +
|
|
145
|
+
parseInt(match?.[3] || "0");
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
videoId: video.id,
|
|
149
|
+
title: video.snippet.title,
|
|
150
|
+
views: parseInt(video.statistics.viewCount || 0),
|
|
151
|
+
likes: parseInt(video.statistics.likeCount || 0),
|
|
152
|
+
comments: parseInt(video.statistics.commentCount || 0),
|
|
153
|
+
publishedAt: video.snippet.publishedAt,
|
|
154
|
+
thumbnail: video.snippet.thumbnails?.high?.url,
|
|
155
|
+
videoType: seconds < 60 ? "short" : "regular",
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Fetches audience analytics and engagement metrics for the channel
|
|
162
|
+
* @returns Audience analytics including view duration and subscriber changes
|
|
163
|
+
*/
|
|
164
|
+
async fetchAudienceAnalytics(): Promise<AudienceAnalytics> {
|
|
165
|
+
try {
|
|
166
|
+
|
|
167
|
+
// 1️⃣ Compute proper date range (last 30 days)
|
|
168
|
+
const endDate = new Date();
|
|
169
|
+
const startDate = new Date();
|
|
170
|
+
startDate.setDate(endDate.getDate() - 30);
|
|
171
|
+
|
|
172
|
+
const formatDate = (date: Date) =>
|
|
173
|
+
date.toISOString().split("T")[0]; // YYYY-MM-DD
|
|
174
|
+
|
|
175
|
+
// 2️⃣ Call YouTube Analytics API
|
|
176
|
+
const response = await axios.get(
|
|
177
|
+
"https://youtubeanalytics.googleapis.com/v2/reports",
|
|
178
|
+
{
|
|
179
|
+
params: {
|
|
180
|
+
ids: `channel==${this.channel_id}`,
|
|
181
|
+
startDate: formatDate(startDate),
|
|
182
|
+
endDate: formatDate(endDate),
|
|
183
|
+
metrics:
|
|
184
|
+
"views,estimatedMinutesWatched,averageViewDuration,subscribersGained,subscribersLost",
|
|
185
|
+
dimensions: "day",
|
|
186
|
+
},
|
|
187
|
+
headers: { Authorization: `Bearer ${this.access_token}` },
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const rows: any[] = response.data.rows || [];
|
|
192
|
+
|
|
193
|
+
if (rows.length === 0) {
|
|
194
|
+
return {
|
|
195
|
+
views: 0,
|
|
196
|
+
estimatedMinutesWatched: 0,
|
|
197
|
+
averageViewDuration: 0,
|
|
198
|
+
subscribersGained: 0,
|
|
199
|
+
subscribersLost: 0,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 3️⃣ Sum up all daily metrics
|
|
204
|
+
const totals = rows.reduce(
|
|
205
|
+
(acc: any, row: any[]) => ({
|
|
206
|
+
views: acc.views + Number(row[1] || 0),
|
|
207
|
+
estimatedMinutesWatched:
|
|
208
|
+
acc.estimatedMinutesWatched + Number(row[2] || 0),
|
|
209
|
+
averageViewDuration: acc.averageViewDuration + Number(row[3] || 0),
|
|
210
|
+
subscribersGained: acc.subscribersGained + Number(row[4] || 0),
|
|
211
|
+
subscribersLost: acc.subscribersLost + Number(row[5] || 0),
|
|
212
|
+
}),
|
|
213
|
+
{
|
|
214
|
+
views: 0,
|
|
215
|
+
estimatedMinutesWatched: 0,
|
|
216
|
+
averageViewDuration: 0,
|
|
217
|
+
subscribersGained: 0,
|
|
218
|
+
subscribersLost: 0,
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// 4️⃣ Return final analytics
|
|
223
|
+
return {
|
|
224
|
+
views: totals.views,
|
|
225
|
+
estimatedMinutesWatched: totals.estimatedMinutesWatched,
|
|
226
|
+
averageViewDuration: totals.averageViewDuration / rows.length, // Average of averages
|
|
227
|
+
subscribersGained: totals.subscribersGained,
|
|
228
|
+
subscribersLost: totals.subscribersLost,
|
|
229
|
+
};
|
|
230
|
+
} catch (err) {
|
|
231
|
+
console.error("Error fetching audience analytics:", err);
|
|
232
|
+
return {
|
|
233
|
+
views: 0,
|
|
234
|
+
estimatedMinutesWatched: 0,
|
|
235
|
+
averageViewDuration: 0,
|
|
236
|
+
subscribersGained: 0,
|
|
237
|
+
subscribersLost: 0,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async fetchChannelRevenue(startDate: string = "2023-01-01", endDate: string = new Date().toISOString().split("T")[0]): Promise<ChannelRevenue> {
|
|
243
|
+
try {
|
|
244
|
+
const response = await axios.get("https://youtubeanalytics.googleapis.com/v2/reports", {
|
|
245
|
+
params: {
|
|
246
|
+
ids: `channel==${this.channel_id}`,
|
|
247
|
+
startDate,
|
|
248
|
+
endDate,
|
|
249
|
+
metrics: "estimatedRevenue",
|
|
250
|
+
dimensions: "day", // optional: can remove to get total
|
|
251
|
+
currency: "USD"
|
|
252
|
+
},
|
|
253
|
+
headers: {
|
|
254
|
+
Authorization: `Bearer ${this.access_token}`
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Sum revenue if using "day" dimension
|
|
259
|
+
const totalRevenue = response.data.rows
|
|
260
|
+
? response.data.rows.reduce((acc: number, row: any[]) => acc + parseFloat(row[1] || 0), 0)
|
|
261
|
+
: 0;
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
estimatedRevenue: totalRevenue,
|
|
265
|
+
currency: "USD",
|
|
266
|
+
startDate,
|
|
267
|
+
endDate
|
|
268
|
+
};
|
|
269
|
+
} catch (err: any) {
|
|
270
|
+
console.error("Failed to fetch channel revenue:", err.response?.data || err.message);
|
|
271
|
+
return { estimatedRevenue: 0, currency: "USD", startDate, endDate };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
|
|
3
|
+
interface GrowthMetrics {
|
|
4
|
+
subscribers: number;
|
|
5
|
+
totalVideoViews: number;
|
|
6
|
+
estimatedRevenue: number;
|
|
7
|
+
videosCount: number;
|
|
8
|
+
period: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SubscriberBalance {
|
|
12
|
+
subscribersGained: number;
|
|
13
|
+
subscribersLost: number;
|
|
14
|
+
netSubscribers: number;
|
|
15
|
+
videosPublished: number;
|
|
16
|
+
period: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface VideoPerformance {
|
|
20
|
+
videoId: string;
|
|
21
|
+
title: string;
|
|
22
|
+
publishedAt: string;
|
|
23
|
+
views: number;
|
|
24
|
+
watchTime: number; // in minutes
|
|
25
|
+
avgViewDuration: number; // in seconds
|
|
26
|
+
likes: number;
|
|
27
|
+
dislikes: number;
|
|
28
|
+
comments: number;
|
|
29
|
+
shares: number;
|
|
30
|
+
thumbnail: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface PublishedVideosStats {
|
|
34
|
+
totalViews: number;
|
|
35
|
+
totalLikes: number;
|
|
36
|
+
totalDislikes: number;
|
|
37
|
+
totalComments: number;
|
|
38
|
+
totalShares: number;
|
|
39
|
+
totalVideos: number;
|
|
40
|
+
averageEngagementRate: number;
|
|
41
|
+
period: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class YoutubeAnalyticsHandler {
|
|
45
|
+
private access_token: string;
|
|
46
|
+
private channel_id: string;
|
|
47
|
+
|
|
48
|
+
constructor(access_token: string, channel_id: string) {
|
|
49
|
+
this.access_token = access_token;
|
|
50
|
+
this.channel_id = channel_id;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fetches growth metrics for the channel over a specified period
|
|
55
|
+
* @param period Time period for growth analysis (7days, 30days, 90days, 365days)
|
|
56
|
+
* @returns Growth metrics including subscribers, views, revenue, and video count
|
|
57
|
+
* . Enable YouTube Analytics API
|
|
58
|
+
|
|
59
|
+
Go to your Google Cloud Console → APIs & Services → Library → Search for and enable:
|
|
60
|
+
|
|
61
|
+
YouTube Analytics API
|
|
62
|
+
|
|
63
|
+
YouTube Reporting API
|
|
64
|
+
*/
|
|
65
|
+
async fetchGrowthMetrics(period: string = "30days"): Promise<GrowthMetrics> {
|
|
66
|
+
const endDate = "today";
|
|
67
|
+
const startDate = this.getStartDate(period);
|
|
68
|
+
|
|
69
|
+
// Fetch analytics data
|
|
70
|
+
const [analyticsResponse, channelResponse] = await Promise.all([
|
|
71
|
+
axios.get("https://youtubeanalytics.googleapis.com/v2/reports", {
|
|
72
|
+
params: {
|
|
73
|
+
ids: `channel==${this.channel_id}`,
|
|
74
|
+
startDate,
|
|
75
|
+
endDate,
|
|
76
|
+
metrics: "subscribersGained,views,estimatedRevenue",
|
|
77
|
+
dimensions: "day"
|
|
78
|
+
},
|
|
79
|
+
headers: { Authorization: `Bearer ${this.access_token}` },
|
|
80
|
+
}),
|
|
81
|
+
this.fetchChannelBasicStats()
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const rows = analyticsResponse.data.rows || [];
|
|
85
|
+
|
|
86
|
+
const totals = rows.reduce((acc: any, row: any[]) => ({
|
|
87
|
+
subscribersGained: acc.subscribersGained + (row[1] || 0),
|
|
88
|
+
views: acc.views + (row[2] || 0),
|
|
89
|
+
estimatedRevenue: acc.estimatedRevenue + (row[3] || 0)
|
|
90
|
+
}), { subscribersGained: 0, views: 0, estimatedRevenue: 0 });
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
subscribers: totals.subscribersGained,
|
|
94
|
+
totalVideoViews: totals.views,
|
|
95
|
+
estimatedRevenue: totals.estimatedRevenue,
|
|
96
|
+
videosCount: channelResponse.videoCount,
|
|
97
|
+
period: `${startDate} to ${endDate}`
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Fetches aggregated statistics for all published videos in a period
|
|
105
|
+
* @param period Time period for analysis (7days, 30days, 90days)
|
|
106
|
+
* @returns Combined statistics for all published videos
|
|
107
|
+
*/
|
|
108
|
+
async fetchPublishedVideosStats(period: string = "30days"): Promise<PublishedVideosStats> {
|
|
109
|
+
const endDate = "today";
|
|
110
|
+
const startDate = this.getStartDate(period);
|
|
111
|
+
|
|
112
|
+
const videos = await this.fetchVideosPublishedInPeriod(startDate, endDate);
|
|
113
|
+
|
|
114
|
+
if (videos.length === 0) {
|
|
115
|
+
return {
|
|
116
|
+
totalViews: 0,
|
|
117
|
+
totalLikes: 0,
|
|
118
|
+
totalDislikes: 0,
|
|
119
|
+
totalComments: 0,
|
|
120
|
+
totalShares: 0,
|
|
121
|
+
totalVideos: 0,
|
|
122
|
+
averageEngagementRate: 0,
|
|
123
|
+
period: `${startDate} to ${endDate}`
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const videoIds = videos.map(v => v.videoId);
|
|
128
|
+
const videoStats = await this.fetchVideosDetailedStats(videoIds);
|
|
129
|
+
|
|
130
|
+
const totals = videoStats.reduce((acc: any, video: VideoPerformance) => ({
|
|
131
|
+
totalViews: acc.totalViews + video.views,
|
|
132
|
+
totalLikes: acc.totalLikes + video.likes,
|
|
133
|
+
totalDislikes: acc.totalDislikes + video.dislikes,
|
|
134
|
+
totalComments: acc.totalComments + video.comments,
|
|
135
|
+
totalShares: acc.totalShares + video.shares
|
|
136
|
+
}), { totalViews: 0, totalLikes: 0, totalDislikes: 0, totalComments: 0, totalShares: 0 });
|
|
137
|
+
|
|
138
|
+
const averageEngagementRate = ((totals.totalLikes + totals.totalComments + totals.totalShares) / totals.totalViews) * 100;
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
totalViews: totals.totalViews,
|
|
142
|
+
totalLikes: totals.totalLikes,
|
|
143
|
+
totalDislikes: totals.totalDislikes,
|
|
144
|
+
totalComments: totals.totalComments,
|
|
145
|
+
totalShares: totals.totalShares,
|
|
146
|
+
totalVideos: videos.length,
|
|
147
|
+
averageEngagementRate: isNaN(averageEngagementRate) ? 0 : averageEngagementRate,
|
|
148
|
+
period: `${startDate} to ${endDate}`
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Fetches detailed list of videos with comprehensive performance metrics
|
|
154
|
+
* @param maxResults Maximum number of videos to return (default: 50, max: 50)
|
|
155
|
+
* @returns Array of video performance metrics
|
|
156
|
+
*/
|
|
157
|
+
async fetchVideosList(maxResults: number = 50): Promise<VideoPerformance[]> {
|
|
158
|
+
// Get uploads playlist ID
|
|
159
|
+
const channelRes = await axios.get("https://www.googleapis.com/youtube/v3/channels", {
|
|
160
|
+
params: { part: "contentDetails", id: this.channel_id },
|
|
161
|
+
headers: { Authorization: `Bearer ${this.access_token}` },
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const uploadsPlaylistId = channelRes.data.items[0]?.contentDetails?.relatedPlaylists?.uploads;
|
|
165
|
+
if (!uploadsPlaylistId) return [];
|
|
166
|
+
|
|
167
|
+
// Fetch videos from uploads playlist
|
|
168
|
+
const videosRes = await axios.get("https://www.googleapis.com/youtube/v3/playlistItems", {
|
|
169
|
+
params: {
|
|
170
|
+
part: "snippet,contentDetails",
|
|
171
|
+
playlistId: uploadsPlaylistId,
|
|
172
|
+
maxResults: Math.min(maxResults, 50)
|
|
173
|
+
},
|
|
174
|
+
headers: { Authorization: `Bearer ${this.access_token}` },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const videoIds = videosRes.data.items.map((v: any) => v.contentDetails.videoId);
|
|
178
|
+
if (videoIds.length === 0) return [];
|
|
179
|
+
|
|
180
|
+
// Fetch detailed statistics for each video
|
|
181
|
+
return await this.fetchVideosDetailedStats(videoIds);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ============ HELPER METHODS ============
|
|
185
|
+
|
|
186
|
+
private async fetchChannelBasicStats(): Promise<{ videoCount: number }> {
|
|
187
|
+
const response = await axios.get("https://www.googleapis.com/youtube/v3/channels", {
|
|
188
|
+
params: {
|
|
189
|
+
part: "statistics",
|
|
190
|
+
id: this.channel_id
|
|
191
|
+
},
|
|
192
|
+
headers: { Authorization: `Bearer ${this.access_token}` },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
videoCount: parseInt(response.data.items[0]?.statistics?.videoCount || 0)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private async fetchVideosPublishedInPeriod(startDate: string, endDate: string): Promise<any[]> {
|
|
201
|
+
const videos = await this.fetchVideosList(100); // Get more videos for period filtering
|
|
202
|
+
|
|
203
|
+
return videos.filter(video => {
|
|
204
|
+
const publishedDate = new Date(video.publishedAt).toISOString().split('T')[0];
|
|
205
|
+
return publishedDate >= startDate && publishedDate <= endDate;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private async fetchVideosDetailedStats(videoIds: string[]): Promise<VideoPerformance[]> {
|
|
210
|
+
const response = await axios.get("https://www.googleapis.com/youtube/v3/videos", {
|
|
211
|
+
params: {
|
|
212
|
+
part: "snippet,statistics,contentDetails",
|
|
213
|
+
id: videoIds.join(','),
|
|
214
|
+
maxResults: videoIds.length
|
|
215
|
+
},
|
|
216
|
+
headers: { Authorization: `Bearer ${this.access_token}` },
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return response.data.items.map((video: any) => ({
|
|
220
|
+
videoId: video.id,
|
|
221
|
+
title: video.snippet.title,
|
|
222
|
+
publishedAt: video.snippet.publishedAt,
|
|
223
|
+
views: parseInt(video.statistics.viewCount || 0),
|
|
224
|
+
watchTime: parseInt(video.statistics.watchTime || 0) / 60, // Convert to minutes
|
|
225
|
+
avgViewDuration: parseInt(video.statistics.averageViewDuration || 0),
|
|
226
|
+
likes: parseInt(video.statistics.likeCount || 0),
|
|
227
|
+
dislikes: parseInt(video.statistics.dislikeCount || 0),
|
|
228
|
+
comments: parseInt(video.statistics.commentCount || 0),
|
|
229
|
+
shares: parseInt(video.statistics.shareCount || 0),
|
|
230
|
+
thumbnail: video.snippet.thumbnails?.high?.url
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private getStartDate(period: string): string {
|
|
235
|
+
const days = parseInt(period);
|
|
236
|
+
if (!isNaN(days)) {
|
|
237
|
+
return `${days}daysAgo`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const periodMap: { [key: string]: string } = {
|
|
241
|
+
"7days": "7daysAgo",
|
|
242
|
+
"30days": "30daysAgo",
|
|
243
|
+
"90days": "90daysAgo",
|
|
244
|
+
"365days": "365daysAgo"
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
return periodMap[period] || "30daysAgo";
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
interface CommentReply {
|
|
5
|
+
replyId: string;
|
|
6
|
+
author: string;
|
|
7
|
+
text: string;
|
|
8
|
+
likeCount: number;
|
|
9
|
+
publishedAt: string;
|
|
10
|
+
}
|
|
11
|
+
interface CommentAnalyticsWithReplies {
|
|
12
|
+
commentId: string;
|
|
13
|
+
videoId: string;
|
|
14
|
+
author: string;
|
|
15
|
+
text: string;
|
|
16
|
+
likeCount: number;
|
|
17
|
+
publishedAt: string;
|
|
18
|
+
replyCount: number;
|
|
19
|
+
replies: CommentReply[];
|
|
20
|
+
}
|
|
21
|
+
export class YoutubeVideoHandler {
|
|
22
|
+
|
|
23
|
+
private video_id: string;
|
|
24
|
+
private access_token: string;
|
|
25
|
+
|
|
26
|
+
constructor(access_token: string, video_id: string) {
|
|
27
|
+
this.access_token = access_token;
|
|
28
|
+
this.video_id = video_id;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Fetches comments analytics for a specific video or all channel videos
|
|
33
|
+
* @param videoId Optional video ID - if not provided, fetches comments from all channel videos
|
|
34
|
+
* @param maxResults Maximum number of comments to fetch per video (default: 20, max: 100)
|
|
35
|
+
* @returns Array of comment analytics including like counts and reply counts
|
|
36
|
+
*/
|
|
37
|
+
async fetchCommentsAnalytics(maxResults: number = 20): Promise<CommentAnalyticsWithReplies[]> {
|
|
38
|
+
const targetVideoIds: string[] = [this.video_id];
|
|
39
|
+
const allComments: CommentAnalyticsWithReplies[] = [];
|
|
40
|
+
|
|
41
|
+
for (const vid of targetVideoIds) {
|
|
42
|
+
try {
|
|
43
|
+
const response = await axios.get("https://www.googleapis.com/youtube/v3/commentThreads", {
|
|
44
|
+
params: {
|
|
45
|
+
part: "snippet,replies",
|
|
46
|
+
videoId: vid,
|
|
47
|
+
maxResults: Math.min(maxResults, 100),
|
|
48
|
+
order: "relevance"
|
|
49
|
+
},
|
|
50
|
+
headers: { Authorization: `Bearer ${this.access_token}` },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
for (const item of response.data.items) {
|
|
54
|
+
const topComment = item.snippet.topLevelComment.snippet;
|
|
55
|
+
|
|
56
|
+
const replies: CommentReply[] = (item.replies?.comments || []).map((rep: any) => ({
|
|
57
|
+
replyId: rep.id,
|
|
58
|
+
author: rep.snippet.authorDisplayName,
|
|
59
|
+
text: rep.snippet.textDisplay,
|
|
60
|
+
likeCount: rep.snippet.likeCount,
|
|
61
|
+
publishedAt: rep.snippet.publishedAt
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
allComments.push({
|
|
65
|
+
commentId: item.id,
|
|
66
|
+
videoId: vid,
|
|
67
|
+
author: topComment.authorDisplayName,
|
|
68
|
+
text: topComment.textDisplay,
|
|
69
|
+
likeCount: topComment.likeCount,
|
|
70
|
+
publishedAt: topComment.publishedAt,
|
|
71
|
+
replyCount: item.snippet.totalReplyCount,
|
|
72
|
+
replies
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.warn(`Failed to fetch comments for video ${vid}:`, error);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return allComments;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { google, youtube_v3 } from "googleapis";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import { Readable } from "stream";
|
|
4
|
+
|
|
5
|
+
export interface VideoMetadata {
|
|
6
|
+
title: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
tags?: string[];
|
|
9
|
+
categoryId?: string;
|
|
10
|
+
privacyStatus?: "public" | "private" | "unlisted";
|
|
11
|
+
madeForKids?: boolean;
|
|
12
|
+
license?: "youtube" | "creativeCommon";
|
|
13
|
+
embeddable?: boolean;
|
|
14
|
+
publicStatsViewable?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class YoutubeVideoPoster {
|
|
18
|
+
private youtube: youtube_v3.Youtube;
|
|
19
|
+
|
|
20
|
+
constructor(private access_token: string) {
|
|
21
|
+
this.youtube = google.youtube({ version: "v3", auth: this.access_token });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Upload a video from a Cloudinary URL to YouTube
|
|
26
|
+
* @param videoUrl URL of the video on Cloudinary
|
|
27
|
+
* @param metadata Video metadata
|
|
28
|
+
*/
|
|
29
|
+
public async uploadFromCloudUrl(videoUrl: string, metadata: VideoMetadata): Promise<youtube_v3.Schema$Video> {
|
|
30
|
+
try {
|
|
31
|
+
// Fetch video as stream from Cloudinary
|
|
32
|
+
const response = await axios.get(videoUrl, { responseType: "stream" });
|
|
33
|
+
const videoStream = response.data as Readable;
|
|
34
|
+
|
|
35
|
+
// Prepare metadata payload
|
|
36
|
+
const metadataPayload: youtube_v3.Params$Resource$Videos$Insert = {
|
|
37
|
+
part: ["snippet", "status"],
|
|
38
|
+
requestBody: {
|
|
39
|
+
snippet: {
|
|
40
|
+
title: metadata.title.substring(0, 100),
|
|
41
|
+
description: (metadata.description || "").substring(0, 5000),
|
|
42
|
+
tags: (metadata.tags || []).slice(0, 5),
|
|
43
|
+
categoryId: metadata.categoryId || "22"
|
|
44
|
+
},
|
|
45
|
+
status: {
|
|
46
|
+
privacyStatus: metadata.privacyStatus || "private",
|
|
47
|
+
embeddable: metadata.embeddable !== false,
|
|
48
|
+
publicStatsViewable: metadata.publicStatsViewable !== false,
|
|
49
|
+
license: metadata.license || "youtube",
|
|
50
|
+
selfDeclaredMadeForKids: metadata.madeForKids || false
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
media: { body: videoStream }
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Upload video
|
|
57
|
+
const res = await this.youtube.videos.insert(metadataPayload);
|
|
58
|
+
return res.data;
|
|
59
|
+
|
|
60
|
+
} catch (error: any) {
|
|
61
|
+
console.error("YouTube upload failed:", error.response?.data || error.message);
|
|
62
|
+
throw new Error(`YouTube upload failed: ${error.response?.data?.error?.message || error.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|