my-youtube-api 1.0.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/dist/error-utils.d.ts +16 -0
- package/dist/error-utils.js +29 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +12 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +40 -0
- package/dist/utils.d.ts +27 -0
- package/dist/utils.js +61 -0
- package/dist/youtube-handlers/youtube-account-handler.d.ts +21 -0
- package/dist/youtube-handlers/youtube-account-handler.js +33 -0
- package/dist/youtube-handlers/youtube-channel-handler.d.ts +65 -0
- package/dist/youtube-handlers/youtube-channel-handler.js +193 -0
- package/dist/youtube-handlers/youtube-channel-handler2.d.ts +65 -0
- package/dist/youtube-handlers/youtube-channel-handler2.js +177 -0
- package/dist/youtube-handlers/youtube-video-handler.d.ts +30 -0
- package/dist/youtube-handlers/youtube-video-handler.js +55 -0
- package/dist/youtube-handlers/youtube-video-poster.d.ts +23 -0
- package/dist/youtube-handlers/youtube-video-poster.js +47 -0
- package/my-youtube-api-1.0.0.tgz +0 -0
- package/package.json +26 -0
- package/src/error-utils.ts +38 -0
- package/src/index.ts +22 -0
- package/src/test.ts +50 -0
- package/src/utils.ts +85 -0
- package/src/youtube-handlers/youtube-account-handler.ts +51 -0
- package/src/youtube-handlers/youtube-channel-handler.ts +275 -0
- package/src/youtube-handlers/youtube-channel-handler2.ts +249 -0
- package/src/youtube-handlers/youtube-video-handler.ts +87 -0
- package/src/youtube-handlers/youtube-video-poster.ts +65 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
export class YoutubeAnalyticsHandler {
|
|
3
|
+
constructor(access_token, channel_id) {
|
|
4
|
+
this.access_token = access_token;
|
|
5
|
+
this.channel_id = channel_id;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Fetches growth metrics for the channel over a specified period
|
|
9
|
+
* @param period Time period for growth analysis (7days, 30days, 90days, 365days)
|
|
10
|
+
* @returns Growth metrics including subscribers, views, revenue, and video count
|
|
11
|
+
* . Enable YouTube Analytics API
|
|
12
|
+
|
|
13
|
+
Go to your Google Cloud Console → APIs & Services → Library → Search for and enable:
|
|
14
|
+
|
|
15
|
+
YouTube Analytics API
|
|
16
|
+
|
|
17
|
+
YouTube Reporting API
|
|
18
|
+
*/
|
|
19
|
+
async fetchGrowthMetrics(period = "30days") {
|
|
20
|
+
const endDate = "today";
|
|
21
|
+
const startDate = this.getStartDate(period);
|
|
22
|
+
// Fetch analytics data
|
|
23
|
+
const [analyticsResponse, channelResponse] = await Promise.all([
|
|
24
|
+
axios.get("https://youtubeanalytics.googleapis.com/v2/reports", {
|
|
25
|
+
params: {
|
|
26
|
+
ids: `channel==${this.channel_id}`,
|
|
27
|
+
startDate,
|
|
28
|
+
endDate,
|
|
29
|
+
metrics: "subscribersGained,views,estimatedRevenue",
|
|
30
|
+
dimensions: "day"
|
|
31
|
+
},
|
|
32
|
+
headers: { Authorization: `Bearer ${this.access_token}` },
|
|
33
|
+
}),
|
|
34
|
+
this.fetchChannelBasicStats()
|
|
35
|
+
]);
|
|
36
|
+
const rows = analyticsResponse.data.rows || [];
|
|
37
|
+
const totals = rows.reduce((acc, row) => ({
|
|
38
|
+
subscribersGained: acc.subscribersGained + (row[1] || 0),
|
|
39
|
+
views: acc.views + (row[2] || 0),
|
|
40
|
+
estimatedRevenue: acc.estimatedRevenue + (row[3] || 0)
|
|
41
|
+
}), { subscribersGained: 0, views: 0, estimatedRevenue: 0 });
|
|
42
|
+
return {
|
|
43
|
+
subscribers: totals.subscribersGained,
|
|
44
|
+
totalVideoViews: totals.views,
|
|
45
|
+
estimatedRevenue: totals.estimatedRevenue,
|
|
46
|
+
videosCount: channelResponse.videoCount,
|
|
47
|
+
period: `${startDate} to ${endDate}`
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Fetches aggregated statistics for all published videos in a period
|
|
52
|
+
* @param period Time period for analysis (7days, 30days, 90days)
|
|
53
|
+
* @returns Combined statistics for all published videos
|
|
54
|
+
*/
|
|
55
|
+
async fetchPublishedVideosStats(period = "30days") {
|
|
56
|
+
const endDate = "today";
|
|
57
|
+
const startDate = this.getStartDate(period);
|
|
58
|
+
const videos = await this.fetchVideosPublishedInPeriod(startDate, endDate);
|
|
59
|
+
if (videos.length === 0) {
|
|
60
|
+
return {
|
|
61
|
+
totalViews: 0,
|
|
62
|
+
totalLikes: 0,
|
|
63
|
+
totalDislikes: 0,
|
|
64
|
+
totalComments: 0,
|
|
65
|
+
totalShares: 0,
|
|
66
|
+
totalVideos: 0,
|
|
67
|
+
averageEngagementRate: 0,
|
|
68
|
+
period: `${startDate} to ${endDate}`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const videoIds = videos.map(v => v.videoId);
|
|
72
|
+
const videoStats = await this.fetchVideosDetailedStats(videoIds);
|
|
73
|
+
const totals = videoStats.reduce((acc, video) => ({
|
|
74
|
+
totalViews: acc.totalViews + video.views,
|
|
75
|
+
totalLikes: acc.totalLikes + video.likes,
|
|
76
|
+
totalDislikes: acc.totalDislikes + video.dislikes,
|
|
77
|
+
totalComments: acc.totalComments + video.comments,
|
|
78
|
+
totalShares: acc.totalShares + video.shares
|
|
79
|
+
}), { totalViews: 0, totalLikes: 0, totalDislikes: 0, totalComments: 0, totalShares: 0 });
|
|
80
|
+
const averageEngagementRate = ((totals.totalLikes + totals.totalComments + totals.totalShares) / totals.totalViews) * 100;
|
|
81
|
+
return {
|
|
82
|
+
totalViews: totals.totalViews,
|
|
83
|
+
totalLikes: totals.totalLikes,
|
|
84
|
+
totalDislikes: totals.totalDislikes,
|
|
85
|
+
totalComments: totals.totalComments,
|
|
86
|
+
totalShares: totals.totalShares,
|
|
87
|
+
totalVideos: videos.length,
|
|
88
|
+
averageEngagementRate: isNaN(averageEngagementRate) ? 0 : averageEngagementRate,
|
|
89
|
+
period: `${startDate} to ${endDate}`
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Fetches detailed list of videos with comprehensive performance metrics
|
|
94
|
+
* @param maxResults Maximum number of videos to return (default: 50, max: 50)
|
|
95
|
+
* @returns Array of video performance metrics
|
|
96
|
+
*/
|
|
97
|
+
async fetchVideosList(maxResults = 50) {
|
|
98
|
+
// Get uploads playlist ID
|
|
99
|
+
const channelRes = await axios.get("https://www.googleapis.com/youtube/v3/channels", {
|
|
100
|
+
params: { part: "contentDetails", id: this.channel_id },
|
|
101
|
+
headers: { Authorization: `Bearer ${this.access_token}` },
|
|
102
|
+
});
|
|
103
|
+
const uploadsPlaylistId = channelRes.data.items[0]?.contentDetails?.relatedPlaylists?.uploads;
|
|
104
|
+
if (!uploadsPlaylistId)
|
|
105
|
+
return [];
|
|
106
|
+
// Fetch videos from uploads playlist
|
|
107
|
+
const videosRes = await axios.get("https://www.googleapis.com/youtube/v3/playlistItems", {
|
|
108
|
+
params: {
|
|
109
|
+
part: "snippet,contentDetails",
|
|
110
|
+
playlistId: uploadsPlaylistId,
|
|
111
|
+
maxResults: Math.min(maxResults, 50)
|
|
112
|
+
},
|
|
113
|
+
headers: { Authorization: `Bearer ${this.access_token}` },
|
|
114
|
+
});
|
|
115
|
+
const videoIds = videosRes.data.items.map((v) => v.contentDetails.videoId);
|
|
116
|
+
if (videoIds.length === 0)
|
|
117
|
+
return [];
|
|
118
|
+
// Fetch detailed statistics for each video
|
|
119
|
+
return await this.fetchVideosDetailedStats(videoIds);
|
|
120
|
+
}
|
|
121
|
+
// ============ HELPER METHODS ============
|
|
122
|
+
async fetchChannelBasicStats() {
|
|
123
|
+
const response = await axios.get("https://www.googleapis.com/youtube/v3/channels", {
|
|
124
|
+
params: {
|
|
125
|
+
part: "statistics",
|
|
126
|
+
id: this.channel_id
|
|
127
|
+
},
|
|
128
|
+
headers: { Authorization: `Bearer ${this.access_token}` },
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
videoCount: parseInt(response.data.items[0]?.statistics?.videoCount || 0)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async fetchVideosPublishedInPeriod(startDate, endDate) {
|
|
135
|
+
const videos = await this.fetchVideosList(100); // Get more videos for period filtering
|
|
136
|
+
return videos.filter(video => {
|
|
137
|
+
const publishedDate = new Date(video.publishedAt).toISOString().split('T')[0];
|
|
138
|
+
return publishedDate >= startDate && publishedDate <= endDate;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
async fetchVideosDetailedStats(videoIds) {
|
|
142
|
+
const response = await axios.get("https://www.googleapis.com/youtube/v3/videos", {
|
|
143
|
+
params: {
|
|
144
|
+
part: "snippet,statistics,contentDetails",
|
|
145
|
+
id: videoIds.join(','),
|
|
146
|
+
maxResults: videoIds.length
|
|
147
|
+
},
|
|
148
|
+
headers: { Authorization: `Bearer ${this.access_token}` },
|
|
149
|
+
});
|
|
150
|
+
return response.data.items.map((video) => ({
|
|
151
|
+
videoId: video.id,
|
|
152
|
+
title: video.snippet.title,
|
|
153
|
+
publishedAt: video.snippet.publishedAt,
|
|
154
|
+
views: parseInt(video.statistics.viewCount || 0),
|
|
155
|
+
watchTime: parseInt(video.statistics.watchTime || 0) / 60, // Convert to minutes
|
|
156
|
+
avgViewDuration: parseInt(video.statistics.averageViewDuration || 0),
|
|
157
|
+
likes: parseInt(video.statistics.likeCount || 0),
|
|
158
|
+
dislikes: parseInt(video.statistics.dislikeCount || 0),
|
|
159
|
+
comments: parseInt(video.statistics.commentCount || 0),
|
|
160
|
+
shares: parseInt(video.statistics.shareCount || 0),
|
|
161
|
+
thumbnail: video.snippet.thumbnails?.high?.url
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
getStartDate(period) {
|
|
165
|
+
const days = parseInt(period);
|
|
166
|
+
if (!isNaN(days)) {
|
|
167
|
+
return `${days}daysAgo`;
|
|
168
|
+
}
|
|
169
|
+
const periodMap = {
|
|
170
|
+
"7days": "7daysAgo",
|
|
171
|
+
"30days": "30daysAgo",
|
|
172
|
+
"90days": "90daysAgo",
|
|
173
|
+
"365days": "365daysAgo"
|
|
174
|
+
};
|
|
175
|
+
return periodMap[period] || "30daysAgo";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
interface CommentReply {
|
|
2
|
+
replyId: string;
|
|
3
|
+
author: string;
|
|
4
|
+
text: string;
|
|
5
|
+
likeCount: number;
|
|
6
|
+
publishedAt: string;
|
|
7
|
+
}
|
|
8
|
+
interface CommentAnalyticsWithReplies {
|
|
9
|
+
commentId: string;
|
|
10
|
+
videoId: string;
|
|
11
|
+
author: string;
|
|
12
|
+
text: string;
|
|
13
|
+
likeCount: number;
|
|
14
|
+
publishedAt: string;
|
|
15
|
+
replyCount: number;
|
|
16
|
+
replies: CommentReply[];
|
|
17
|
+
}
|
|
18
|
+
export declare class YoutubeVideoHandler {
|
|
19
|
+
private video_id;
|
|
20
|
+
private access_token;
|
|
21
|
+
constructor(access_token: string, video_id: string);
|
|
22
|
+
/**
|
|
23
|
+
* Fetches comments analytics for a specific video or all channel videos
|
|
24
|
+
* @param videoId Optional video ID - if not provided, fetches comments from all channel videos
|
|
25
|
+
* @param maxResults Maximum number of comments to fetch per video (default: 20, max: 100)
|
|
26
|
+
* @returns Array of comment analytics including like counts and reply counts
|
|
27
|
+
*/
|
|
28
|
+
fetchCommentsAnalytics(maxResults?: number): Promise<CommentAnalyticsWithReplies[]>;
|
|
29
|
+
}
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
export class YoutubeVideoHandler {
|
|
3
|
+
constructor(access_token, video_id) {
|
|
4
|
+
this.access_token = access_token;
|
|
5
|
+
this.video_id = video_id;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Fetches comments analytics for a specific video or all channel videos
|
|
9
|
+
* @param videoId Optional video ID - if not provided, fetches comments from all channel videos
|
|
10
|
+
* @param maxResults Maximum number of comments to fetch per video (default: 20, max: 100)
|
|
11
|
+
* @returns Array of comment analytics including like counts and reply counts
|
|
12
|
+
*/
|
|
13
|
+
async fetchCommentsAnalytics(maxResults = 20) {
|
|
14
|
+
const targetVideoIds = [this.video_id];
|
|
15
|
+
const allComments = [];
|
|
16
|
+
for (const vid of targetVideoIds) {
|
|
17
|
+
try {
|
|
18
|
+
const response = await axios.get("https://www.googleapis.com/youtube/v3/commentThreads", {
|
|
19
|
+
params: {
|
|
20
|
+
part: "snippet,replies",
|
|
21
|
+
videoId: vid,
|
|
22
|
+
maxResults: Math.min(maxResults, 100),
|
|
23
|
+
order: "relevance"
|
|
24
|
+
},
|
|
25
|
+
headers: { Authorization: `Bearer ${this.access_token}` },
|
|
26
|
+
});
|
|
27
|
+
for (const item of response.data.items) {
|
|
28
|
+
const topComment = item.snippet.topLevelComment.snippet;
|
|
29
|
+
const replies = (item.replies?.comments || []).map((rep) => ({
|
|
30
|
+
replyId: rep.id,
|
|
31
|
+
author: rep.snippet.authorDisplayName,
|
|
32
|
+
text: rep.snippet.textDisplay,
|
|
33
|
+
likeCount: rep.snippet.likeCount,
|
|
34
|
+
publishedAt: rep.snippet.publishedAt
|
|
35
|
+
}));
|
|
36
|
+
allComments.push({
|
|
37
|
+
commentId: item.id,
|
|
38
|
+
videoId: vid,
|
|
39
|
+
author: topComment.authorDisplayName,
|
|
40
|
+
text: topComment.textDisplay,
|
|
41
|
+
likeCount: topComment.likeCount,
|
|
42
|
+
publishedAt: topComment.publishedAt,
|
|
43
|
+
replyCount: item.snippet.totalReplyCount,
|
|
44
|
+
replies
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.warn(`Failed to fetch comments for video ${vid}:`, error);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return allComments;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { youtube_v3 } from "googleapis";
|
|
2
|
+
export interface VideoMetadata {
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
tags?: string[];
|
|
6
|
+
categoryId?: string;
|
|
7
|
+
privacyStatus?: "public" | "private" | "unlisted";
|
|
8
|
+
madeForKids?: boolean;
|
|
9
|
+
license?: "youtube" | "creativeCommon";
|
|
10
|
+
embeddable?: boolean;
|
|
11
|
+
publicStatsViewable?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare class YoutubeVideoPoster {
|
|
14
|
+
private access_token;
|
|
15
|
+
private youtube;
|
|
16
|
+
constructor(access_token: string);
|
|
17
|
+
/**
|
|
18
|
+
* Upload a video from a Cloudinary URL to YouTube
|
|
19
|
+
* @param videoUrl URL of the video on Cloudinary
|
|
20
|
+
* @param metadata Video metadata
|
|
21
|
+
*/
|
|
22
|
+
uploadFromCloudUrl(videoUrl: string, metadata: VideoMetadata): Promise<youtube_v3.Schema$Video>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { google } from "googleapis";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
export class YoutubeVideoPoster {
|
|
4
|
+
constructor(access_token) {
|
|
5
|
+
this.access_token = access_token;
|
|
6
|
+
this.youtube = google.youtube({ version: "v3", auth: this.access_token });
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Upload a video from a Cloudinary URL to YouTube
|
|
10
|
+
* @param videoUrl URL of the video on Cloudinary
|
|
11
|
+
* @param metadata Video metadata
|
|
12
|
+
*/
|
|
13
|
+
async uploadFromCloudUrl(videoUrl, metadata) {
|
|
14
|
+
try {
|
|
15
|
+
// Fetch video as stream from Cloudinary
|
|
16
|
+
const response = await axios.get(videoUrl, { responseType: "stream" });
|
|
17
|
+
const videoStream = response.data;
|
|
18
|
+
// Prepare metadata payload
|
|
19
|
+
const metadataPayload = {
|
|
20
|
+
part: ["snippet", "status"],
|
|
21
|
+
requestBody: {
|
|
22
|
+
snippet: {
|
|
23
|
+
title: metadata.title.substring(0, 100),
|
|
24
|
+
description: (metadata.description || "").substring(0, 5000),
|
|
25
|
+
tags: (metadata.tags || []).slice(0, 5),
|
|
26
|
+
categoryId: metadata.categoryId || "22"
|
|
27
|
+
},
|
|
28
|
+
status: {
|
|
29
|
+
privacyStatus: metadata.privacyStatus || "private",
|
|
30
|
+
embeddable: metadata.embeddable !== false,
|
|
31
|
+
publicStatsViewable: metadata.publicStatsViewable !== false,
|
|
32
|
+
license: metadata.license || "youtube",
|
|
33
|
+
selfDeclaredMadeForKids: metadata.madeForKids || false
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
media: { body: videoStream }
|
|
37
|
+
};
|
|
38
|
+
// Upload video
|
|
39
|
+
const res = await this.youtube.videos.insert(metadataPayload);
|
|
40
|
+
return res.data;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.error("YouTube upload failed:", error.response?.data || error.message);
|
|
44
|
+
throw new Error(`YouTube upload failed: ${error.response?.data?.error?.message || error.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-youtube-api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"prepublishOnly": "npm run build",
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"test": "ts-node src/test.ts"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [],
|
|
11
|
+
"author": "",
|
|
12
|
+
"license": "ISC",
|
|
13
|
+
"description": "",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"axios": "^1.12.2",
|
|
18
|
+
"googleapis": "^160.0.0",
|
|
19
|
+
"ts-node": "^10.9.2"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/dotenv": "^6.1.1",
|
|
23
|
+
"@types/node": "^24.5.1",
|
|
24
|
+
"typescript": "^5.9.2"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const YOUTUBE_BASE_URL = "https://www.googleapis.com";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export const RUPLOAD_URL = 'https://rupload.facebook.com';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export enum FacebookErrorCode {
|
|
8
|
+
NO_ACCOUNT = 'NO_ACCOUNT',
|
|
9
|
+
ACCESS_TOKEN_EXPIRED = 'ACCESS_TOKEN_EXPIRED',
|
|
10
|
+
FAILED_GET_PAGES = 'FAILED_GET_PAGES',
|
|
11
|
+
ACCESS_TOKEN_INVALID = 'ACCESS_TOKEN_INVALID',
|
|
12
|
+
PAGE_NOT_FOUND = 'PAGE_NOT_FOUND',
|
|
13
|
+
PAGE_ACCESS_TOKEN_EXPIRE = 'PAGE_ACCESS_TOKEN_EXPIRE',
|
|
14
|
+
FAILED_FETCH_PAGES_FROM_API = 'FAILED_FETCH_PAGES_FROM_API'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const errorMessages: Record<FacebookErrorCode, string> = {
|
|
18
|
+
[FacebookErrorCode.NO_ACCOUNT]: 'Facebook account not connected',
|
|
19
|
+
[FacebookErrorCode.ACCESS_TOKEN_EXPIRED]: 'Facebook access token has expired',
|
|
20
|
+
[FacebookErrorCode.FAILED_GET_PAGES]: 'Failed to fetch Facebook pages',
|
|
21
|
+
[FacebookErrorCode.ACCESS_TOKEN_INVALID]: 'Facebook access token is invalid',
|
|
22
|
+
[FacebookErrorCode.PAGE_NOT_FOUND]: 'Facebook page not found',
|
|
23
|
+
[FacebookErrorCode.PAGE_ACCESS_TOKEN_EXPIRE]: 'Facebook page access token has expired',
|
|
24
|
+
[FacebookErrorCode.FAILED_FETCH_PAGES_FROM_API]: 'Failed to fetch pages from Facebook API'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
export class FacebookError extends Error {
|
|
29
|
+
constructor(
|
|
30
|
+
public code: FacebookErrorCode,
|
|
31
|
+
public originalError?: any
|
|
32
|
+
) {
|
|
33
|
+
super(errorMessages[code]);
|
|
34
|
+
this.name = 'FacebookError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { YoutubeAccountHandler } from "./youtube-handlers/youtube-account-handler";
|
|
2
|
+
import { YoutubeChannelHandler } from "./youtube-handlers/youtube-channel-handler";
|
|
3
|
+
import { YoutubeVideoHandler } from "./youtube-handlers/youtube-video-handler";
|
|
4
|
+
import { YoutubeVideoPoster } from "./youtube-handlers/youtube-video-poster";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
npm run build
|
|
8
|
+
npm pack
|
|
9
|
+
|
|
10
|
+
in nextjs go to this package/"my-youtube-api-1.0.0.tgz"
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { isValidAccessToken, fetchNewAccessToken } from './utils'
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
YoutubeAccountHandler,
|
|
17
|
+
YoutubeChannelHandler,
|
|
18
|
+
YoutubeVideoHandler,
|
|
19
|
+
YoutubeVideoPoster,
|
|
20
|
+
isValidAccessToken,
|
|
21
|
+
fetchNewAccessToken
|
|
22
|
+
};
|
package/src/test.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { YoutubeChannelHandler } from "./youtube-handlers/youtube-channel-handler";
|
|
2
|
+
import { YoutubeAnalyticsHandler } from "./youtube-handlers/youtube-channel-handler2";
|
|
3
|
+
import { YoutubeVideoHandler } from "./youtube-handlers/youtube-video-handler";
|
|
4
|
+
|
|
5
|
+
const access_token="ya29.a0AQQ_BDSZ2rCrjvxUbkkVtbVtWdstheMCiFoBLDwCLcOkcPVdej3wmFbraxVoVuhTNd875VIOA_hjWj7bPom8AJbRzfDL3roL3OIaLVP2egffiMo0lPScLLK4RuwA16JmdVeJadeNiMofUtfhqdwjz9JRHQ2DBhUYRwzwCAZzPTX9kUU4d4v43TePdZ9Pm1AoCNnGLEQaCgYKAQYSARUSFQHGX2MiEjdmyiMSsAh6MyI_w17C8Q0206";
|
|
6
|
+
const channel_id="UCfRRVg_MwvlFWqsvBUXrXcw";
|
|
7
|
+
const video_id="J-Rn69UHm78";
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
for nextjs:
|
|
12
|
+
in package json: "type": "module", "types": "dist/index.d.ts",
|
|
13
|
+
in tsconfig.json: "module": "ESNext",
|
|
14
|
+
for testing here:
|
|
15
|
+
in package json: remove "type": "module", and "type": "module"
|
|
16
|
+
in tsconfig.json: "module": "commonjs",
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
|
|
21
|
+
npm run build
|
|
22
|
+
npm pack
|
|
23
|
+
|
|
24
|
+
in nextjs go to this package/"my-facebook-api-1.0.0.tgz"
|
|
25
|
+
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
const youtubeChannelHandler = new YoutubeChannelHandler(access_token,channel_id);
|
|
30
|
+
|
|
31
|
+
/* youtubeChannelHandler.getChannelStats().then((res)=>{
|
|
32
|
+
console.log('================================================');
|
|
33
|
+
console.log(res,'wwwwwwwwwwwwwww');
|
|
34
|
+
|
|
35
|
+
})
|
|
36
|
+
*/
|
|
37
|
+
const youtubeAnalyticsHandlers = new YoutubeAnalyticsHandler(access_token,channel_id);
|
|
38
|
+
|
|
39
|
+
/* youtubeAnalyticsHandlers.fetchVideosList().then((res)=>{
|
|
40
|
+
console.log(res,'wwwwwwwwwwwwwwwwwwwwwwwwww');
|
|
41
|
+
|
|
42
|
+
}) */
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
const youvid = new YoutubeVideoHandler(access_token,video_id);
|
|
46
|
+
|
|
47
|
+
/* youvid.getYouTubeComments().then((res)=>{
|
|
48
|
+
console.log(res,res[0].replies,'eeeeeeeeeeeeeeeeeeeeeeeeeeeee');
|
|
49
|
+
|
|
50
|
+
}) */
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
interface AccessTokenType {
|
|
3
|
+
access_token: string;
|
|
4
|
+
refresh_token: string;
|
|
5
|
+
expires_in: number;
|
|
6
|
+
refresh_token_expires_in: number;
|
|
7
|
+
expires_at: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Checks whether a Google access token is still valid.
|
|
11
|
+
* @param accessToken - The access token to verify.
|
|
12
|
+
* @returns true if valid, false otherwise.
|
|
13
|
+
*/
|
|
14
|
+
export async function isValidAccessToken(accessToken: string | null): Promise<boolean> {
|
|
15
|
+
if (!accessToken) return false;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const response = await axios.get("https://www.googleapis.com/oauth2/v1/tokeninfo", {
|
|
19
|
+
params: { access_token: accessToken },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// The tokeninfo endpoint returns an error if the token is invalid,
|
|
23
|
+
// so if we get here, it's valid.
|
|
24
|
+
return response.status === 200;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return false; // expired or invalid token
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Refreshes an expired access token using a valid refresh token
|
|
34
|
+
* @param valid_refresh_token - The refresh token obtained during initial OAuth2 authentication
|
|
35
|
+
* @returns Promise resolving to AccessTokenType object containing new tokens and expiration information
|
|
36
|
+
* @throws {AxiosError} If the token refresh request fails (invalid refresh token, client credentials, etc.)
|
|
37
|
+
*
|
|
38
|
+
* @remarks
|
|
39
|
+
* All time-related values are returned in seconds:
|
|
40
|
+
* - expires_in: Lifetime of the access token in seconds (typically 3600)
|
|
41
|
+
* - expires_at: Unix timestamp in seconds when the access token will expire
|
|
42
|
+
* - refresh_token_expires_in: Lifetime of the refresh token in seconds (if provided by Google)
|
|
43
|
+
*/
|
|
44
|
+
export async function fetchNewAccessToken(
|
|
45
|
+
valid_refresh_token: string,
|
|
46
|
+
google_client_id: string,
|
|
47
|
+
google_client_secret: string
|
|
48
|
+
): Promise<AccessTokenType> {
|
|
49
|
+
try {
|
|
50
|
+
const params = new URLSearchParams({
|
|
51
|
+
client_id: google_client_id,
|
|
52
|
+
client_secret: google_client_secret,
|
|
53
|
+
grant_type: "refresh_token",
|
|
54
|
+
refresh_token: valid_refresh_token,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const response = await axios.post(
|
|
58
|
+
"https://oauth2.googleapis.com/token",
|
|
59
|
+
params.toString(),
|
|
60
|
+
{ headers: { "Content-Type": "application/x-www-form-urlencoded" } }
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const data = response.data;
|
|
64
|
+
const access_token = data.access_token;
|
|
65
|
+
const expires_in = data.expires_in;
|
|
66
|
+
const refresh_token = data.refresh_token ?? valid_refresh_token;
|
|
67
|
+
const refresh_token_expires_in = data.refresh_token_expires_in;
|
|
68
|
+
const expires_at = Math.floor(Date.now() / 1000) + expires_in;
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
access_token,
|
|
72
|
+
expires_at,
|
|
73
|
+
refresh_token_expires_in,
|
|
74
|
+
expires_in,
|
|
75
|
+
refresh_token,
|
|
76
|
+
};
|
|
77
|
+
} catch (error: any) {
|
|
78
|
+
console.error(
|
|
79
|
+
"❌ Failed to refresh access token:",
|
|
80
|
+
error.response?.data || error.message
|
|
81
|
+
);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { YOUTUBE_BASE_URL } from "../error-utils";
|
|
3
|
+
|
|
4
|
+
interface Channel {
|
|
5
|
+
channelId: string;
|
|
6
|
+
title: string;
|
|
7
|
+
description: string;
|
|
8
|
+
thumbnail: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export class YoutubeAccountHandler {
|
|
14
|
+
private account_access_token: string; //user_access_token
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a new YoutubeAccountHandler instance
|
|
18
|
+
* @param account_access_token - The current access token for YouTube API authentication
|
|
19
|
+
*/
|
|
20
|
+
constructor(account_access_token: string) {
|
|
21
|
+
this.account_access_token = account_access_token;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Fetches the list of YouTube channels associated with the authenticated account
|
|
26
|
+
* @returns Promise resolving to an array of Channel objects containing channel details
|
|
27
|
+
* @throws {AxiosError} If the API request fails due to authentication or other errors
|
|
28
|
+
*/
|
|
29
|
+
async fetchChannels(): Promise<Channel[]> {
|
|
30
|
+
const url = `${YOUTUBE_BASE_URL}/youtube/v3/channels`;
|
|
31
|
+
const params: Record<string, any> = {
|
|
32
|
+
part: "snippet,contentDetails,statistics",
|
|
33
|
+
mine: true,
|
|
34
|
+
};
|
|
35
|
+
const access_token = this.account_access_token;
|
|
36
|
+
const headers = { Authorization: `Bearer ${access_token}` }
|
|
37
|
+
|
|
38
|
+
const res = await axios.get(url, { params, headers });
|
|
39
|
+
|
|
40
|
+
const channels = res.data.items.map((ch: any) => ({
|
|
41
|
+
channelId: ch.id,
|
|
42
|
+
title: ch.snippet.title,
|
|
43
|
+
description: ch.snippet.description,
|
|
44
|
+
thumbnail: ch.snippet.thumbnails?.default?.url,
|
|
45
|
+
}))
|
|
46
|
+
|
|
47
|
+
return channels;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
}
|