fix: YouTube channel ID (UC...) handling + handle-to-channelId resolution

discover-channels: extractHandle('youtube') now detects UC* channel IDs
and returns them without @ prefix (previously @UC... caused verify fail)

verifyHandles: verifyYouTube uses cleanHandle for UC* check, requests
part=id,snippet for richer data

collect-channel-data: if channelId missing but handle present, resolves
via forHandle/forUsername lookup or direct UC* detection before skipping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
claude/bold-hawking
Haewon Kam 2026-04-04 01:00:21 +09:00
parent 163751410f
commit df8f84c3b9
3 changed files with 21 additions and 4 deletions

View File

@ -66,11 +66,11 @@ async function verifyYouTube(handle: string, apiKey: string): Promise<VerifiedCh
} }
// Try as channel ID directly (starts with UC) // Try as channel ID directly (starts with UC)
if (handle.startsWith('UC')) { if (cleanHandle.startsWith('UC')) {
const res = await fetch(`${YT_BASE}/channels?part=id&id=${handle}&key=${apiKey}`); const res = await fetch(`${YT_BASE}/channels?part=id,snippet&id=${cleanHandle}&key=${apiKey}`);
const data = await res.json(); const data = await res.json();
if (data.items?.[0]) { if (data.items?.[0]) {
return { handle, verified: true, channelId: handle, url: `https://youtube.com/channel/${handle}` }; return { handle: cleanHandle, verified: true, channelId: cleanHandle, url: `https://youtube.com/channel/${cleanHandle}` };
} }
} }

View File

@ -109,7 +109,22 @@ Deno.serve(async (req) => {
if (YOUTUBE_API_KEY && ytVerified?.verified) { if (YOUTUBE_API_KEY && ytVerified?.verified) {
tasks.push((async () => { tasks.push((async () => {
const YT = "https://www.googleapis.com/youtube/v3"; const YT = "https://www.googleapis.com/youtube/v3";
const channelId = (ytVerified?.channelId as string) || ""; let channelId = (ytVerified?.channelId as string) || "";
// If no channelId, try to resolve from handle
if (!channelId && ytVerified?.handle) {
const h = (ytVerified.handle as string).replace(/^@/, '');
if (h.startsWith('UC')) {
channelId = h;
} else {
for (const param of ['forHandle', 'forUsername']) {
const lookupRes = await fetch(`${YT}/channels?part=id&${param}=${h}&key=${YOUTUBE_API_KEY}`);
const lookupData = await lookupRes.json();
channelId = lookupData.items?.[0]?.id || '';
if (channelId) break;
}
}
}
if (!channelId) return; if (!channelId) return;
const chRes = await fetch(`${YT}/channels?part=snippet,statistics,brandingSettings&id=${channelId}&key=${YOUTUBE_API_KEY}`); const chRes = await fetch(`${YT}/channels?part=snippet,statistics,brandingSettings&id=${channelId}&key=${YOUTUBE_API_KEY}`);

View File

@ -221,6 +221,8 @@ Deno.serve(async (req) => {
h = h.replace(/^@/, ''); h = h.replace(/^@/, '');
// Reject if it looks like a non-YouTube URL // Reject if it looks like a non-YouTube URL
if (h.includes('http') || h.includes('/') || h.includes('.com')) return null; if (h.includes('http') || h.includes('/') || h.includes('.com')) return null;
// Channel IDs start with UC — don't add @ prefix
if (/^UC[a-zA-Z0-9_-]{20,}$/.test(h)) return h;
if (/^[a-zA-Z0-9._-]+$/.test(h) && h.length >= 2) return `@${h}`; if (/^[a-zA-Z0-9._-]+$/.test(h) && h.length >= 2) return `@${h}`;
return null; return null;
} }