mirror of
https://github.com/svemagie/indiekit-endpoint-youtube.git
synced 2026-04-02 15:54:59 +02:00
fix: dashboard controller support for multi-channel mode
The Indiekit backend UI was broken when using channels array because the dashboard controller only checked for channelId/channelHandle. Now uses getPrimaryChannel() helper to extract the first channel from either single-channel or multi-channel configuration. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,32 @@
|
||||
import { YouTubeClient } from "../youtube-client.js";
|
||||
|
||||
/**
|
||||
* Get normalized channels array from config
|
||||
* Supports both single channel (backward compat) and multiple channels
|
||||
*/
|
||||
function getChannelsFromConfig(youtubeConfig) {
|
||||
const { channelId, channelHandle, channels } = youtubeConfig;
|
||||
|
||||
// If channels array is provided, use it
|
||||
if (channels && Array.isArray(channels) && channels.length > 0) {
|
||||
return channels;
|
||||
}
|
||||
|
||||
// Fallback to single channel config (backward compatible)
|
||||
if (channelId || channelHandle) {
|
||||
return [{ id: channelId, handle: channelHandle, name: "Primary" }];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel controller
|
||||
*/
|
||||
export const channelController = {
|
||||
/**
|
||||
* Get channel info (JSON API)
|
||||
* Returns array of channels if multiple configured
|
||||
* @type {import("express").RequestHandler}
|
||||
*/
|
||||
async api(request, response) {
|
||||
@@ -16,28 +37,58 @@ export const channelController = {
|
||||
return response.status(500).json({ error: "Not configured" });
|
||||
}
|
||||
|
||||
const { apiKey, channelId, channelHandle, cacheTtl } = youtubeConfig;
|
||||
const { apiKey, cacheTtl } = youtubeConfig;
|
||||
const channelConfigs = getChannelsFromConfig(youtubeConfig);
|
||||
|
||||
if (!apiKey || (!channelId && !channelHandle)) {
|
||||
if (!apiKey || channelConfigs.length === 0) {
|
||||
return response.status(500).json({ error: "Invalid configuration" });
|
||||
}
|
||||
|
||||
const client = new YouTubeClient({
|
||||
apiKey,
|
||||
channelId,
|
||||
channelHandle,
|
||||
cacheTtl,
|
||||
// Fetch all channels in parallel
|
||||
const channelPromises = channelConfigs.map(async (channelConfig) => {
|
||||
const client = new YouTubeClient({
|
||||
apiKey,
|
||||
channelId: channelConfig.id,
|
||||
channelHandle: channelConfig.handle,
|
||||
cacheTtl,
|
||||
});
|
||||
|
||||
try {
|
||||
const channel = await client.getChannelInfo();
|
||||
return {
|
||||
...channel,
|
||||
configName: channelConfig.name,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[YouTube] Failed to fetch channel ${channelConfig.name || channelConfig.handle}:`,
|
||||
error.message
|
||||
);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const channel = await client.getChannelInfo();
|
||||
const channelsData = await Promise.all(channelPromises);
|
||||
const channels = channelsData.filter(Boolean);
|
||||
|
||||
response.json({
|
||||
channel,
|
||||
cached: true,
|
||||
});
|
||||
// Return single channel for backward compatibility when only one configured
|
||||
if (channelConfigs.length === 1) {
|
||||
response.json({
|
||||
channel: channels[0] || null,
|
||||
cached: true,
|
||||
});
|
||||
} else {
|
||||
response.json({
|
||||
channels,
|
||||
channel: channels[0] || null, // Primary channel for backward compat
|
||||
cached: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[YouTube] Channel API error:", error);
|
||||
response.status(500).json({ error: error.message });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export { getChannelsFromConfig };
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
import { YouTubeClient } from "../youtube-client.js";
|
||||
|
||||
/**
|
||||
* Get primary channel from config (for backward compatibility)
|
||||
* Multi-channel mode uses first channel for dashboard
|
||||
*/
|
||||
function getPrimaryChannel(config) {
|
||||
const { channelId, channelHandle, channels } = config;
|
||||
|
||||
// Multi-channel mode: use first channel
|
||||
if (channels && Array.isArray(channels) && channels.length > 0) {
|
||||
const first = channels[0];
|
||||
return { id: first.id, handle: first.handle };
|
||||
}
|
||||
|
||||
// Single channel mode (backward compatible)
|
||||
if (channelId || channelHandle) {
|
||||
return { id: channelId, handle: channelHandle };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard controller
|
||||
*/
|
||||
@@ -19,7 +40,7 @@ export const dashboardController = {
|
||||
});
|
||||
}
|
||||
|
||||
const { apiKey, channelId, channelHandle, cacheTtl, limits } = youtubeConfig;
|
||||
const { apiKey, cacheTtl, limits } = youtubeConfig;
|
||||
|
||||
if (!apiKey) {
|
||||
return response.render("youtube", {
|
||||
@@ -28,7 +49,9 @@ export const dashboardController = {
|
||||
});
|
||||
}
|
||||
|
||||
if (!channelId && !channelHandle) {
|
||||
const primaryChannel = getPrimaryChannel(youtubeConfig);
|
||||
|
||||
if (!primaryChannel) {
|
||||
return response.render("youtube", {
|
||||
title: response.locals.__("youtube.title"),
|
||||
error: { message: response.locals.__("youtube.error.noChannel") },
|
||||
@@ -37,8 +60,8 @@ export const dashboardController = {
|
||||
|
||||
const client = new YouTubeClient({
|
||||
apiKey,
|
||||
channelId,
|
||||
channelHandle,
|
||||
channelId: primaryChannel.id,
|
||||
channelHandle: primaryChannel.handle,
|
||||
cacheTtl,
|
||||
});
|
||||
|
||||
@@ -93,10 +116,16 @@ export const dashboardController = {
|
||||
return response.status(500).json({ error: "Not configured" });
|
||||
}
|
||||
|
||||
const primaryChannel = getPrimaryChannel(youtubeConfig);
|
||||
|
||||
if (!primaryChannel) {
|
||||
return response.status(500).json({ error: "No channel configured" });
|
||||
}
|
||||
|
||||
const client = new YouTubeClient({
|
||||
apiKey: youtubeConfig.apiKey,
|
||||
channelId: youtubeConfig.channelId,
|
||||
channelHandle: youtubeConfig.channelHandle,
|
||||
channelId: primaryChannel.id,
|
||||
channelHandle: primaryChannel.handle,
|
||||
});
|
||||
|
||||
// Clear cache and refetch
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { YouTubeClient } from "../youtube-client.js";
|
||||
import { getChannelsFromConfig } from "./channel.js";
|
||||
|
||||
/**
|
||||
* Live status controller
|
||||
@@ -8,6 +9,7 @@ export const liveController = {
|
||||
* Get live status (JSON API)
|
||||
* Uses efficient method (checking recent videos) by default
|
||||
* Use ?full=true for full search (costs 100 quota units)
|
||||
* Returns live status for all configured channels
|
||||
* @type {import("express").RequestHandler}
|
||||
*/
|
||||
async api(request, response) {
|
||||
@@ -18,44 +20,87 @@ export const liveController = {
|
||||
return response.status(500).json({ error: "Not configured" });
|
||||
}
|
||||
|
||||
const { apiKey, channelId, channelHandle, liveCacheTtl } = youtubeConfig;
|
||||
const { apiKey, liveCacheTtl } = youtubeConfig;
|
||||
const channelConfigs = getChannelsFromConfig(youtubeConfig);
|
||||
|
||||
if (!apiKey || (!channelId && !channelHandle)) {
|
||||
if (!apiKey || channelConfigs.length === 0) {
|
||||
return response.status(500).json({ error: "Invalid configuration" });
|
||||
}
|
||||
|
||||
const client = new YouTubeClient({
|
||||
apiKey,
|
||||
channelId,
|
||||
channelHandle,
|
||||
liveCacheTtl,
|
||||
const useFullSearch = request.query.full === "true";
|
||||
|
||||
// Fetch live status from all channels in parallel
|
||||
const livePromises = channelConfigs.map(async (channelConfig) => {
|
||||
const client = new YouTubeClient({
|
||||
apiKey,
|
||||
channelId: channelConfig.id,
|
||||
channelHandle: channelConfig.handle,
|
||||
liveCacheTtl,
|
||||
});
|
||||
|
||||
try {
|
||||
const liveStatus = useFullSearch
|
||||
? await client.getLiveStatus()
|
||||
: await client.getLiveStatusEfficient();
|
||||
|
||||
if (liveStatus) {
|
||||
return {
|
||||
channelConfigName: channelConfig.name,
|
||||
isLive: liveStatus.isLive || false,
|
||||
isUpcoming: liveStatus.isUpcoming || false,
|
||||
stream: {
|
||||
videoId: liveStatus.videoId,
|
||||
title: liveStatus.title,
|
||||
thumbnail: liveStatus.thumbnail,
|
||||
url: `https://www.youtube.com/watch?v=${liveStatus.videoId}`,
|
||||
scheduledStart: liveStatus.scheduledStart,
|
||||
actualStart: liveStatus.actualStart,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
channelConfigName: channelConfig.name,
|
||||
isLive: false,
|
||||
isUpcoming: false,
|
||||
stream: null,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[YouTube] Failed to fetch live status for ${channelConfig.name || channelConfig.handle}:`,
|
||||
error.message
|
||||
);
|
||||
return {
|
||||
channelConfigName: channelConfig.name,
|
||||
isLive: false,
|
||||
isUpcoming: false,
|
||||
stream: null,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Use full search only if explicitly requested
|
||||
const useFullSearch = request.query.full === "true";
|
||||
const liveStatus = useFullSearch
|
||||
? await client.getLiveStatus()
|
||||
: await client.getLiveStatusEfficient();
|
||||
const liveStatuses = await Promise.all(livePromises);
|
||||
|
||||
if (liveStatus) {
|
||||
// For single channel, return flat response (backward compatible)
|
||||
if (channelConfigs.length === 1) {
|
||||
const status = liveStatuses[0];
|
||||
response.json({
|
||||
isLive: liveStatus.isLive || false,
|
||||
isUpcoming: liveStatus.isUpcoming || false,
|
||||
stream: {
|
||||
videoId: liveStatus.videoId,
|
||||
title: liveStatus.title,
|
||||
thumbnail: liveStatus.thumbnail,
|
||||
url: `https://www.youtube.com/watch?v=${liveStatus.videoId}`,
|
||||
scheduledStart: liveStatus.scheduledStart,
|
||||
actualStart: liveStatus.actualStart,
|
||||
},
|
||||
isLive: status.isLive,
|
||||
isUpcoming: status.isUpcoming,
|
||||
stream: status.stream,
|
||||
cached: true,
|
||||
});
|
||||
} else {
|
||||
// For multiple channels, find any that are live
|
||||
const anyLive = liveStatuses.find((s) => s.isLive);
|
||||
const anyUpcoming = liveStatuses.find((s) => s.isUpcoming && !s.isLive);
|
||||
|
||||
response.json({
|
||||
isLive: false,
|
||||
isUpcoming: false,
|
||||
stream: null,
|
||||
// Backward compat: primary live status (prefer live over upcoming)
|
||||
isLive: !!anyLive,
|
||||
isUpcoming: !anyLive && !!anyUpcoming,
|
||||
stream: anyLive?.stream || anyUpcoming?.stream || null,
|
||||
// Multi-channel data
|
||||
liveStatuses,
|
||||
cached: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { YouTubeClient } from "../youtube-client.js";
|
||||
import { getChannelsFromConfig } from "./channel.js";
|
||||
|
||||
/**
|
||||
* Videos controller
|
||||
@@ -6,6 +7,7 @@ import { YouTubeClient } from "../youtube-client.js";
|
||||
export const videosController = {
|
||||
/**
|
||||
* Get latest videos (JSON API)
|
||||
* Returns videos from all configured channels
|
||||
* @type {import("express").RequestHandler}
|
||||
*/
|
||||
async api(request, response) {
|
||||
@@ -16,31 +18,73 @@ export const videosController = {
|
||||
return response.status(500).json({ error: "Not configured" });
|
||||
}
|
||||
|
||||
const { apiKey, channelId, channelHandle, cacheTtl, limits } = youtubeConfig;
|
||||
const { apiKey, cacheTtl, limits } = youtubeConfig;
|
||||
const channelConfigs = getChannelsFromConfig(youtubeConfig);
|
||||
|
||||
if (!apiKey || (!channelId && !channelHandle)) {
|
||||
if (!apiKey || channelConfigs.length === 0) {
|
||||
return response.status(500).json({ error: "Invalid configuration" });
|
||||
}
|
||||
|
||||
const client = new YouTubeClient({
|
||||
apiKey,
|
||||
channelId,
|
||||
channelHandle,
|
||||
cacheTtl,
|
||||
});
|
||||
|
||||
const maxResults = Math.min(
|
||||
parseInt(request.query.limit, 10) || limits?.videos || 10,
|
||||
50
|
||||
);
|
||||
|
||||
const videos = await client.getLatestVideos(maxResults);
|
||||
// Fetch videos from all channels in parallel
|
||||
const videosPromises = channelConfigs.map(async (channelConfig) => {
|
||||
const client = new YouTubeClient({
|
||||
apiKey,
|
||||
channelId: channelConfig.id,
|
||||
channelHandle: channelConfig.handle,
|
||||
cacheTtl,
|
||||
});
|
||||
|
||||
response.json({
|
||||
videos,
|
||||
count: videos.length,
|
||||
cached: true,
|
||||
try {
|
||||
const videos = await client.getLatestVideos(maxResults);
|
||||
// Add channel info to each video
|
||||
return videos.map((video) => ({
|
||||
...video,
|
||||
channelConfigName: channelConfig.name,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[YouTube] Failed to fetch videos for ${channelConfig.name || channelConfig.handle}:`,
|
||||
error.message
|
||||
);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const videosArrays = await Promise.all(videosPromises);
|
||||
|
||||
// For single channel, return flat array (backward compatible)
|
||||
if (channelConfigs.length === 1) {
|
||||
const videos = videosArrays[0] || [];
|
||||
response.json({
|
||||
videos,
|
||||
count: videos.length,
|
||||
cached: true,
|
||||
});
|
||||
} else {
|
||||
// For multiple channels, return grouped by channel
|
||||
const videosByChannel = {};
|
||||
channelConfigs.forEach((config, index) => {
|
||||
videosByChannel[config.name || config.handle] = videosArrays[index] || [];
|
||||
});
|
||||
|
||||
// Also provide flat array sorted by date
|
||||
const allVideos = videosArrays
|
||||
.flat()
|
||||
.sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt))
|
||||
.slice(0, maxResults);
|
||||
|
||||
response.json({
|
||||
videos: allVideos, // Backward compat: flat array
|
||||
videosByChannel,
|
||||
count: allVideos.length,
|
||||
cached: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[YouTube] Videos API error:", error);
|
||||
response.status(500).json({ error: error.message });
|
||||
|
||||
Reference in New Issue
Block a user