@rmdes/indiekit-endpoint-youtube 1.0.1 → 1.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/index.js +3 -0
- package/lib/controllers/channel.js +63 -12
- package/lib/controllers/live.js +71 -26
- package/lib/controllers/videos.js +58 -14
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -15,8 +15,11 @@ const publicRouter = express.Router();
|
|
|
15
15
|
const defaults = {
|
|
16
16
|
mountPath: "/youtube",
|
|
17
17
|
apiKey: process.env.YOUTUBE_API_KEY,
|
|
18
|
+
// Single channel (backward compatible)
|
|
18
19
|
channelId: process.env.YOUTUBE_CHANNEL_ID,
|
|
19
20
|
channelHandle: process.env.YOUTUBE_CHANNEL_HANDLE,
|
|
21
|
+
// Multiple channels support: array of {id, handle, name}
|
|
22
|
+
channels: null,
|
|
20
23
|
cacheTtl: 300_000, // 5 minutes
|
|
21
24
|
liveCacheTtl: 60_000, // 1 minute for live status
|
|
22
25
|
limits: {
|
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
import { YouTubeClient } from "../youtube-client.js";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Get normalized channels array from config
|
|
5
|
+
* Supports both single channel (backward compat) and multiple channels
|
|
6
|
+
*/
|
|
7
|
+
function getChannelsFromConfig(youtubeConfig) {
|
|
8
|
+
const { channelId, channelHandle, channels } = youtubeConfig;
|
|
9
|
+
|
|
10
|
+
// If channels array is provided, use it
|
|
11
|
+
if (channels && Array.isArray(channels) && channels.length > 0) {
|
|
12
|
+
return channels;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Fallback to single channel config (backward compatible)
|
|
16
|
+
if (channelId || channelHandle) {
|
|
17
|
+
return [{ id: channelId, handle: channelHandle, name: "Primary" }];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
3
23
|
/**
|
|
4
24
|
* Channel controller
|
|
5
25
|
*/
|
|
6
26
|
export const channelController = {
|
|
7
27
|
/**
|
|
8
28
|
* Get channel info (JSON API)
|
|
29
|
+
* Returns array of channels if multiple configured
|
|
9
30
|
* @type {import("express").RequestHandler}
|
|
10
31
|
*/
|
|
11
32
|
async api(request, response) {
|
|
@@ -16,28 +37,58 @@ export const channelController = {
|
|
|
16
37
|
return response.status(500).json({ error: "Not configured" });
|
|
17
38
|
}
|
|
18
39
|
|
|
19
|
-
const { apiKey,
|
|
40
|
+
const { apiKey, cacheTtl } = youtubeConfig;
|
|
41
|
+
const channelConfigs = getChannelsFromConfig(youtubeConfig);
|
|
20
42
|
|
|
21
|
-
if (!apiKey ||
|
|
43
|
+
if (!apiKey || channelConfigs.length === 0) {
|
|
22
44
|
return response.status(500).json({ error: "Invalid configuration" });
|
|
23
45
|
}
|
|
24
46
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
47
|
+
// Fetch all channels in parallel
|
|
48
|
+
const channelPromises = channelConfigs.map(async (channelConfig) => {
|
|
49
|
+
const client = new YouTubeClient({
|
|
50
|
+
apiKey,
|
|
51
|
+
channelId: channelConfig.id,
|
|
52
|
+
channelHandle: channelConfig.handle,
|
|
53
|
+
cacheTtl,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const channel = await client.getChannelInfo();
|
|
58
|
+
return {
|
|
59
|
+
...channel,
|
|
60
|
+
configName: channelConfig.name,
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(
|
|
64
|
+
`[YouTube] Failed to fetch channel ${channelConfig.name || channelConfig.handle}:`,
|
|
65
|
+
error.message
|
|
66
|
+
);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
30
69
|
});
|
|
31
70
|
|
|
32
|
-
const
|
|
71
|
+
const channelsData = await Promise.all(channelPromises);
|
|
72
|
+
const channels = channelsData.filter(Boolean);
|
|
33
73
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
74
|
+
// Return single channel for backward compatibility when only one configured
|
|
75
|
+
if (channelConfigs.length === 1) {
|
|
76
|
+
response.json({
|
|
77
|
+
channel: channels[0] || null,
|
|
78
|
+
cached: true,
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
response.json({
|
|
82
|
+
channels,
|
|
83
|
+
channel: channels[0] || null, // Primary channel for backward compat
|
|
84
|
+
cached: true,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
38
87
|
} catch (error) {
|
|
39
88
|
console.error("[YouTube] Channel API error:", error);
|
|
40
89
|
response.status(500).json({ error: error.message });
|
|
41
90
|
}
|
|
42
91
|
},
|
|
43
92
|
};
|
|
93
|
+
|
|
94
|
+
export { getChannelsFromConfig };
|
package/lib/controllers/live.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { YouTubeClient } from "../youtube-client.js";
|
|
2
|
+
import { getChannelsFromConfig } from "./channel.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Live status controller
|
|
@@ -8,6 +9,7 @@ export const liveController = {
|
|
|
8
9
|
* Get live status (JSON API)
|
|
9
10
|
* Uses efficient method (checking recent videos) by default
|
|
10
11
|
* Use ?full=true for full search (costs 100 quota units)
|
|
12
|
+
* Returns live status for all configured channels
|
|
11
13
|
* @type {import("express").RequestHandler}
|
|
12
14
|
*/
|
|
13
15
|
async api(request, response) {
|
|
@@ -18,44 +20,87 @@ export const liveController = {
|
|
|
18
20
|
return response.status(500).json({ error: "Not configured" });
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
const { apiKey,
|
|
23
|
+
const { apiKey, liveCacheTtl } = youtubeConfig;
|
|
24
|
+
const channelConfigs = getChannelsFromConfig(youtubeConfig);
|
|
22
25
|
|
|
23
|
-
if (!apiKey ||
|
|
26
|
+
if (!apiKey || channelConfigs.length === 0) {
|
|
24
27
|
return response.status(500).json({ error: "Invalid configuration" });
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const useFullSearch = request.query.full === "true";
|
|
31
|
+
|
|
32
|
+
// Fetch live status from all channels in parallel
|
|
33
|
+
const livePromises = channelConfigs.map(async (channelConfig) => {
|
|
34
|
+
const client = new YouTubeClient({
|
|
35
|
+
apiKey,
|
|
36
|
+
channelId: channelConfig.id,
|
|
37
|
+
channelHandle: channelConfig.handle,
|
|
38
|
+
liveCacheTtl,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const liveStatus = useFullSearch
|
|
43
|
+
? await client.getLiveStatus()
|
|
44
|
+
: await client.getLiveStatusEfficient();
|
|
45
|
+
|
|
46
|
+
if (liveStatus) {
|
|
47
|
+
return {
|
|
48
|
+
channelConfigName: channelConfig.name,
|
|
49
|
+
isLive: liveStatus.isLive || false,
|
|
50
|
+
isUpcoming: liveStatus.isUpcoming || false,
|
|
51
|
+
stream: {
|
|
52
|
+
videoId: liveStatus.videoId,
|
|
53
|
+
title: liveStatus.title,
|
|
54
|
+
thumbnail: liveStatus.thumbnail,
|
|
55
|
+
url: `https://www.youtube.com/watch?v=${liveStatus.videoId}`,
|
|
56
|
+
scheduledStart: liveStatus.scheduledStart,
|
|
57
|
+
actualStart: liveStatus.actualStart,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
channelConfigName: channelConfig.name,
|
|
63
|
+
isLive: false,
|
|
64
|
+
isUpcoming: false,
|
|
65
|
+
stream: null,
|
|
66
|
+
};
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(
|
|
69
|
+
`[YouTube] Failed to fetch live status for ${channelConfig.name || channelConfig.handle}:`,
|
|
70
|
+
error.message
|
|
71
|
+
);
|
|
72
|
+
return {
|
|
73
|
+
channelConfigName: channelConfig.name,
|
|
74
|
+
isLive: false,
|
|
75
|
+
isUpcoming: false,
|
|
76
|
+
stream: null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
32
79
|
});
|
|
33
80
|
|
|
34
|
-
|
|
35
|
-
const useFullSearch = request.query.full === "true";
|
|
36
|
-
const liveStatus = useFullSearch
|
|
37
|
-
? await client.getLiveStatus()
|
|
38
|
-
: await client.getLiveStatusEfficient();
|
|
81
|
+
const liveStatuses = await Promise.all(livePromises);
|
|
39
82
|
|
|
40
|
-
|
|
83
|
+
// For single channel, return flat response (backward compatible)
|
|
84
|
+
if (channelConfigs.length === 1) {
|
|
85
|
+
const status = liveStatuses[0];
|
|
41
86
|
response.json({
|
|
42
|
-
isLive:
|
|
43
|
-
isUpcoming:
|
|
44
|
-
stream:
|
|
45
|
-
videoId: liveStatus.videoId,
|
|
46
|
-
title: liveStatus.title,
|
|
47
|
-
thumbnail: liveStatus.thumbnail,
|
|
48
|
-
url: `https://www.youtube.com/watch?v=${liveStatus.videoId}`,
|
|
49
|
-
scheduledStart: liveStatus.scheduledStart,
|
|
50
|
-
actualStart: liveStatus.actualStart,
|
|
51
|
-
},
|
|
87
|
+
isLive: status.isLive,
|
|
88
|
+
isUpcoming: status.isUpcoming,
|
|
89
|
+
stream: status.stream,
|
|
52
90
|
cached: true,
|
|
53
91
|
});
|
|
54
92
|
} else {
|
|
93
|
+
// For multiple channels, find any that are live
|
|
94
|
+
const anyLive = liveStatuses.find((s) => s.isLive);
|
|
95
|
+
const anyUpcoming = liveStatuses.find((s) => s.isUpcoming && !s.isLive);
|
|
96
|
+
|
|
55
97
|
response.json({
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
98
|
+
// Backward compat: primary live status (prefer live over upcoming)
|
|
99
|
+
isLive: !!anyLive,
|
|
100
|
+
isUpcoming: !anyLive && !!anyUpcoming,
|
|
101
|
+
stream: anyLive?.stream || anyUpcoming?.stream || null,
|
|
102
|
+
// Multi-channel data
|
|
103
|
+
liveStatuses,
|
|
59
104
|
cached: true,
|
|
60
105
|
});
|
|
61
106
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { YouTubeClient } from "../youtube-client.js";
|
|
2
|
+
import { getChannelsFromConfig } from "./channel.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Videos controller
|
|
@@ -6,6 +7,7 @@ import { YouTubeClient } from "../youtube-client.js";
|
|
|
6
7
|
export const videosController = {
|
|
7
8
|
/**
|
|
8
9
|
* Get latest videos (JSON API)
|
|
10
|
+
* Returns videos from all configured channels
|
|
9
11
|
* @type {import("express").RequestHandler}
|
|
10
12
|
*/
|
|
11
13
|
async api(request, response) {
|
|
@@ -16,31 +18,73 @@ export const videosController = {
|
|
|
16
18
|
return response.status(500).json({ error: "Not configured" });
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
const { apiKey,
|
|
21
|
+
const { apiKey, cacheTtl, limits } = youtubeConfig;
|
|
22
|
+
const channelConfigs = getChannelsFromConfig(youtubeConfig);
|
|
20
23
|
|
|
21
|
-
if (!apiKey ||
|
|
24
|
+
if (!apiKey || channelConfigs.length === 0) {
|
|
22
25
|
return response.status(500).json({ error: "Invalid configuration" });
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
const client = new YouTubeClient({
|
|
26
|
-
apiKey,
|
|
27
|
-
channelId,
|
|
28
|
-
channelHandle,
|
|
29
|
-
cacheTtl,
|
|
30
|
-
});
|
|
31
|
-
|
|
32
28
|
const maxResults = Math.min(
|
|
33
29
|
parseInt(request.query.limit, 10) || limits?.videos || 10,
|
|
34
30
|
50
|
|
35
31
|
);
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
// Fetch videos from all channels in parallel
|
|
34
|
+
const videosPromises = channelConfigs.map(async (channelConfig) => {
|
|
35
|
+
const client = new YouTubeClient({
|
|
36
|
+
apiKey,
|
|
37
|
+
channelId: channelConfig.id,
|
|
38
|
+
channelHandle: channelConfig.handle,
|
|
39
|
+
cacheTtl,
|
|
40
|
+
});
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
try {
|
|
43
|
+
const videos = await client.getLatestVideos(maxResults);
|
|
44
|
+
// Add channel info to each video
|
|
45
|
+
return videos.map((video) => ({
|
|
46
|
+
...video,
|
|
47
|
+
channelConfigName: channelConfig.name,
|
|
48
|
+
}));
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error(
|
|
51
|
+
`[YouTube] Failed to fetch videos for ${channelConfig.name || channelConfig.handle}:`,
|
|
52
|
+
error.message
|
|
53
|
+
);
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
43
56
|
});
|
|
57
|
+
|
|
58
|
+
const videosArrays = await Promise.all(videosPromises);
|
|
59
|
+
|
|
60
|
+
// For single channel, return flat array (backward compatible)
|
|
61
|
+
if (channelConfigs.length === 1) {
|
|
62
|
+
const videos = videosArrays[0] || [];
|
|
63
|
+
response.json({
|
|
64
|
+
videos,
|
|
65
|
+
count: videos.length,
|
|
66
|
+
cached: true,
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
// For multiple channels, return grouped by channel
|
|
70
|
+
const videosByChannel = {};
|
|
71
|
+
channelConfigs.forEach((config, index) => {
|
|
72
|
+
videosByChannel[config.name || config.handle] = videosArrays[index] || [];
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Also provide flat array sorted by date
|
|
76
|
+
const allVideos = videosArrays
|
|
77
|
+
.flat()
|
|
78
|
+
.sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt))
|
|
79
|
+
.slice(0, maxResults);
|
|
80
|
+
|
|
81
|
+
response.json({
|
|
82
|
+
videos: allVideos, // Backward compat: flat array
|
|
83
|
+
videosByChannel,
|
|
84
|
+
count: allVideos.length,
|
|
85
|
+
cached: true,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
44
88
|
} catch (error) {
|
|
45
89
|
console.error("[YouTube] Videos API error:", error);
|
|
46
90
|
response.status(500).json({ error: error.message });
|
package/package.json
CHANGED