@rmdes/indiekit-endpoint-youtube 1.1.0 → 1.2.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/lib/controllers/dashboard.js +94 -30
- package/package.json +1 -1
- package/views/youtube.njk +110 -0
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
import { YouTubeClient } from "../youtube-client.js";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Get all channels from config (supports both single and multi-channel modes)
|
|
5
|
+
* @param {object} config - YouTube configuration
|
|
6
|
+
* @returns {Array<{id?: string, handle?: string, name?: string}>}
|
|
7
|
+
*/
|
|
8
|
+
function getAllChannels(config) {
|
|
9
|
+
const { channelId, channelHandle, channels } = config;
|
|
10
|
+
|
|
11
|
+
// Multi-channel mode
|
|
12
|
+
if (channels && Array.isArray(channels) && channels.length > 0) {
|
|
13
|
+
return channels.map((ch) => ({
|
|
14
|
+
id: ch.id,
|
|
15
|
+
handle: ch.handle,
|
|
16
|
+
name: ch.name,
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Single channel mode (backward compatible)
|
|
21
|
+
if (channelId || channelHandle) {
|
|
22
|
+
return [{ id: channelId, handle: channelHandle }];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get primary channel from config (for backward compatibility)
|
|
30
|
+
* Multi-channel mode uses first channel for dashboard
|
|
31
|
+
*/
|
|
32
|
+
function getPrimaryChannel(config) {
|
|
33
|
+
const channels = getAllChannels(config);
|
|
34
|
+
return channels.length > 0 ? channels[0] : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
3
37
|
/**
|
|
4
38
|
* Dashboard controller
|
|
5
39
|
*/
|
|
@@ -19,7 +53,7 @@ export const dashboardController = {
|
|
|
19
53
|
});
|
|
20
54
|
}
|
|
21
55
|
|
|
22
|
-
const { apiKey,
|
|
56
|
+
const { apiKey, cacheTtl, limits } = youtubeConfig;
|
|
23
57
|
|
|
24
58
|
if (!apiKey) {
|
|
25
59
|
return response.render("youtube", {
|
|
@@ -28,36 +62,53 @@ export const dashboardController = {
|
|
|
28
62
|
});
|
|
29
63
|
}
|
|
30
64
|
|
|
31
|
-
|
|
65
|
+
const allChannels = getAllChannels(youtubeConfig);
|
|
66
|
+
|
|
67
|
+
if (allChannels.length === 0) {
|
|
32
68
|
return response.render("youtube", {
|
|
33
69
|
title: response.locals.__("youtube.title"),
|
|
34
70
|
error: { message: response.locals.__("youtube.error.noChannel") },
|
|
35
71
|
});
|
|
36
72
|
}
|
|
37
73
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
channelId,
|
|
41
|
-
channelHandle,
|
|
42
|
-
cacheTtl,
|
|
43
|
-
});
|
|
74
|
+
// Fetch data for all configured channels
|
|
75
|
+
const channelsData = [];
|
|
44
76
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
client.getChannelInfo(),
|
|
52
|
-
client.getLatestVideos(limits?.videos || 6),
|
|
53
|
-
client.getLiveStatusEfficient(),
|
|
54
|
-
]);
|
|
55
|
-
} catch (apiError) {
|
|
56
|
-
console.error("[YouTube] API error:", apiError.message);
|
|
57
|
-
return response.render("youtube", {
|
|
58
|
-
title: response.locals.__("youtube.title"),
|
|
59
|
-
error: { message: response.locals.__("youtube.error.connection") },
|
|
77
|
+
for (const channelConfig of allChannels) {
|
|
78
|
+
const client = new YouTubeClient({
|
|
79
|
+
apiKey,
|
|
80
|
+
channelId: channelConfig.id,
|
|
81
|
+
channelHandle: channelConfig.handle,
|
|
82
|
+
cacheTtl,
|
|
60
83
|
});
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const [channel, videos, liveStatus] = await Promise.all([
|
|
87
|
+
client.getChannelInfo(),
|
|
88
|
+
client.getLatestVideos(limits?.videos || 6),
|
|
89
|
+
client.getLiveStatusEfficient(),
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
channelsData.push({
|
|
93
|
+
name: channelConfig.name || channel.title,
|
|
94
|
+
channel,
|
|
95
|
+
videos: videos.slice(0, limits?.videos || 6),
|
|
96
|
+
liveStatus,
|
|
97
|
+
isLive: liveStatus?.isLive || false,
|
|
98
|
+
isUpcoming: liveStatus?.isUpcoming || false,
|
|
99
|
+
});
|
|
100
|
+
} catch (apiError) {
|
|
101
|
+
console.error(
|
|
102
|
+
`[YouTube] API error for channel ${channelConfig.name || channelConfig.id || channelConfig.handle}:`,
|
|
103
|
+
apiError.message,
|
|
104
|
+
);
|
|
105
|
+
// Continue with other channels even if one fails
|
|
106
|
+
channelsData.push({
|
|
107
|
+
name:
|
|
108
|
+
channelConfig.name || channelConfig.id || channelConfig.handle,
|
|
109
|
+
error: apiError.message,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
61
112
|
}
|
|
62
113
|
|
|
63
114
|
// Determine public frontend URL
|
|
@@ -65,13 +116,20 @@ export const dashboardController = {
|
|
|
65
116
|
? youtubeEndpoint.replace(/api$/, "")
|
|
66
117
|
: "/youtube";
|
|
67
118
|
|
|
119
|
+
// For backward compatibility, also expose first channel's data at top level
|
|
120
|
+
const primaryData = channelsData[0] || {};
|
|
121
|
+
|
|
68
122
|
response.render("youtube", {
|
|
69
123
|
title: response.locals.__("youtube.title"),
|
|
70
|
-
channel
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
124
|
+
// Multi-channel data
|
|
125
|
+
channelsData,
|
|
126
|
+
isMultiChannel: allChannels.length > 1,
|
|
127
|
+
// Backward compatible single-channel data (first channel)
|
|
128
|
+
channel: primaryData.channel,
|
|
129
|
+
videos: primaryData.videos,
|
|
130
|
+
liveStatus: primaryData.liveStatus,
|
|
131
|
+
isLive: primaryData.isLive || false,
|
|
132
|
+
isUpcoming: primaryData.isUpcoming || false,
|
|
75
133
|
publicUrl,
|
|
76
134
|
mountPath: request.baseUrl,
|
|
77
135
|
});
|
|
@@ -93,10 +151,16 @@ export const dashboardController = {
|
|
|
93
151
|
return response.status(500).json({ error: "Not configured" });
|
|
94
152
|
}
|
|
95
153
|
|
|
154
|
+
const primaryChannel = getPrimaryChannel(youtubeConfig);
|
|
155
|
+
|
|
156
|
+
if (!primaryChannel) {
|
|
157
|
+
return response.status(500).json({ error: "No channel configured" });
|
|
158
|
+
}
|
|
159
|
+
|
|
96
160
|
const client = new YouTubeClient({
|
|
97
161
|
apiKey: youtubeConfig.apiKey,
|
|
98
|
-
channelId:
|
|
99
|
-
channelHandle:
|
|
162
|
+
channelId: primaryChannel.id,
|
|
163
|
+
channelHandle: primaryChannel.handle,
|
|
100
164
|
});
|
|
101
165
|
|
|
102
166
|
// Clear cache and refetch
|
package/package.json
CHANGED
package/views/youtube.njk
CHANGED
|
@@ -178,7 +178,117 @@
|
|
|
178
178
|
|
|
179
179
|
{% if error %}
|
|
180
180
|
{{ prose({ text: error.message }) }}
|
|
181
|
+
{% elif isMultiChannel and channelsData %}
|
|
182
|
+
{# Multi-channel mode: show all channels #}
|
|
183
|
+
{% for chData in channelsData %}
|
|
184
|
+
<div class="yt-channel-section" style="{% if not loop.first %}margin-top: 3rem; padding-top: 2rem; border-top: 1px solid var(--color-border, #e5e5e5);{% endif %}">
|
|
185
|
+
{# Channel Header #}
|
|
186
|
+
{% if chData.error %}
|
|
187
|
+
<div class="yt-channel" style="border: 1px solid #ff6b6b;">
|
|
188
|
+
<div class="yt-channel__info">
|
|
189
|
+
<h2 class="yt-channel__name">{{ chData.name }}</h2>
|
|
190
|
+
<p style="color: #ff6b6b; margin: 0;">Error: {{ chData.error }}</p>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
{% elif chData.channel %}
|
|
194
|
+
<div class="yt-channel">
|
|
195
|
+
{% if chData.channel.thumbnail %}
|
|
196
|
+
<img src="{{ chData.channel.thumbnail }}" alt="" class="yt-channel__avatar">
|
|
197
|
+
{% endif %}
|
|
198
|
+
<div class="yt-channel__info">
|
|
199
|
+
<h2 class="yt-channel__name">{{ chData.channel.title }}</h2>
|
|
200
|
+
<div class="yt-channel__stats">
|
|
201
|
+
<span>{{ chData.channel.subscriberCount }} {{ __("youtube.subscribers") }}</span>
|
|
202
|
+
<span>{{ chData.channel.videoCount }} videos</span>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
{% if chData.isLive %}
|
|
206
|
+
<span class="yt-live-badge yt-live-badge--live">
|
|
207
|
+
<span class="yt-live-dot"></span>
|
|
208
|
+
{{ __("youtube.live") }}
|
|
209
|
+
</span>
|
|
210
|
+
{% elif chData.isUpcoming %}
|
|
211
|
+
<span class="yt-live-badge yt-live-badge--upcoming">
|
|
212
|
+
{{ __("youtube.upcoming") }}
|
|
213
|
+
</span>
|
|
214
|
+
{% else %}
|
|
215
|
+
<span class="yt-live-badge yt-live-badge--offline">
|
|
216
|
+
{{ __("youtube.offline") }}
|
|
217
|
+
</span>
|
|
218
|
+
{% endif %}
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
{# Live Stream (if live) #}
|
|
222
|
+
{% if chData.liveStatus and (chData.liveStatus.isLive or chData.liveStatus.isUpcoming) %}
|
|
223
|
+
<section class="yt-section">
|
|
224
|
+
<h3>{% if chData.liveStatus.isLive %}{{ __("youtube.live") }}{% else %}{{ __("youtube.upcoming") }}{% endif %}</h3>
|
|
225
|
+
<div class="yt-live-stream">
|
|
226
|
+
{% if chData.liveStatus.thumbnail %}
|
|
227
|
+
<img src="{{ chData.liveStatus.thumbnail }}" alt="" class="yt-live-stream__thumb">
|
|
228
|
+
{% endif %}
|
|
229
|
+
<div class="yt-live-stream__info">
|
|
230
|
+
<h4 class="yt-live-stream__title">
|
|
231
|
+
<a href="https://www.youtube.com/watch?v={{ chData.liveStatus.videoId }}" target="_blank" rel="noopener">
|
|
232
|
+
{{ chData.liveStatus.title }}
|
|
233
|
+
</a>
|
|
234
|
+
</h4>
|
|
235
|
+
{{ button({
|
|
236
|
+
href: "https://www.youtube.com/watch?v=" + chData.liveStatus.videoId,
|
|
237
|
+
text: __("youtube.watchNow"),
|
|
238
|
+
target: "_blank"
|
|
239
|
+
}) }}
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
</section>
|
|
243
|
+
{% endif %}
|
|
244
|
+
|
|
245
|
+
{# Latest Videos #}
|
|
246
|
+
<section class="yt-section">
|
|
247
|
+
<h3>{{ __("youtube.videos") }}</h3>
|
|
248
|
+
{% if chData.videos and chData.videos.length > 0 %}
|
|
249
|
+
<ul class="yt-video-grid">
|
|
250
|
+
{% for video in chData.videos %}
|
|
251
|
+
<li class="yt-video">
|
|
252
|
+
<div class="yt-video__thumb-wrapper">
|
|
253
|
+
<a href="{{ video.url }}" target="_blank" rel="noopener">
|
|
254
|
+
<img src="{{ video.thumbnail }}" alt="" class="yt-video__thumb" loading="lazy">
|
|
255
|
+
</a>
|
|
256
|
+
{% if video.durationFormatted and not video.isLive %}
|
|
257
|
+
<span class="yt-video__duration">{{ video.durationFormatted }}</span>
|
|
258
|
+
{% elif video.isLive %}
|
|
259
|
+
<span class="yt-video__duration" style="background:#ff0000">LIVE</span>
|
|
260
|
+
{% endif %}
|
|
261
|
+
</div>
|
|
262
|
+
<div class="yt-video__info">
|
|
263
|
+
<h4 class="yt-video__title">
|
|
264
|
+
<a href="{{ video.url }}" target="_blank" rel="noopener">{{ video.title }}</a>
|
|
265
|
+
</h4>
|
|
266
|
+
<div class="yt-video__meta">
|
|
267
|
+
{{ video.viewCount }} {{ __("youtube.views") }}
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</li>
|
|
271
|
+
{% endfor %}
|
|
272
|
+
</ul>
|
|
273
|
+
{% else %}
|
|
274
|
+
{{ prose({ text: __("youtube.noVideos") }) }}
|
|
275
|
+
{% endif %}
|
|
276
|
+
</section>
|
|
277
|
+
|
|
278
|
+
{# Link to YouTube channel #}
|
|
279
|
+
<div class="yt-public-link" style="margin-top: 1rem;">
|
|
280
|
+
<p>{{ __("youtube.widget.description") }}</p>
|
|
281
|
+
{{ button({
|
|
282
|
+
href: "https://www.youtube.com/channel/" + chData.channel.id,
|
|
283
|
+
text: __("youtube.widget.view"),
|
|
284
|
+
target: "_blank"
|
|
285
|
+
}) }}
|
|
286
|
+
</div>
|
|
287
|
+
{% endif %}
|
|
288
|
+
</div>
|
|
289
|
+
{% endfor %}
|
|
181
290
|
{% else %}
|
|
291
|
+
{# Single channel mode (backward compatible) #}
|
|
182
292
|
{# Channel Header #}
|
|
183
293
|
{% if channel %}
|
|
184
294
|
<div class="yt-channel">
|