mirror of
https://github.com/yusufipk/YTChatHub.git
synced 2026-09-11 19:06:14 +00:00
feat: add debug logging and fix gift membership badge extraction
This commit is contained in:
@@ -38,6 +38,12 @@ export async function startBackend() {
|
|||||||
ingestion = await bootstrapInnertube(parsedLiveId);
|
ingestion = await bootstrapInnertube(parsedLiveId);
|
||||||
console.log(`[Backend] ✓ YouTube chat connected successfully`);
|
console.log(`[Backend] ✓ YouTube chat connected successfully`);
|
||||||
ingestion.emitter.on('message', (message) => {
|
ingestion.emitter.on('message', (message) => {
|
||||||
|
// Debug: Log all message attributes
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
console.log('[Message Debug] Full message object:');
|
||||||
|
console.log(JSON.stringify(message, null, 2));
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
|
||||||
store.push(message);
|
store.push(message);
|
||||||
if (store.length > MAX_MESSAGES) {
|
if (store.length > MAX_MESSAGES) {
|
||||||
store.splice(0, store.length - MAX_MESSAGES);
|
store.splice(0, store.length - MAX_MESSAGES);
|
||||||
@@ -105,6 +111,12 @@ export async function startBackend() {
|
|||||||
console.log(`[Backend] ✓ YouTube chat connected successfully`);
|
console.log(`[Backend] ✓ YouTube chat connected successfully`);
|
||||||
|
|
||||||
ingestion.emitter.on('message', (message) => {
|
ingestion.emitter.on('message', (message) => {
|
||||||
|
// Debug: Log all message attributes
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
console.log('[Message Debug] Full message object:');
|
||||||
|
console.log(JSON.stringify(message, null, 2));
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
|
||||||
store.push(message);
|
store.push(message);
|
||||||
if (store.length > MAX_MESSAGES) {
|
if (store.length > MAX_MESSAGES) {
|
||||||
store.splice(0, store.length - MAX_MESSAGES);
|
store.splice(0, store.length - MAX_MESSAGES);
|
||||||
|
|||||||
@@ -156,6 +156,31 @@ function extractBadges(item: any): Badge[] {
|
|||||||
return badges;
|
return badges;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractBadgesFromHeader(header: any): Badge[] {
|
||||||
|
const badges: Badge[] = [];
|
||||||
|
|
||||||
|
if (!header?.author_badges) return badges;
|
||||||
|
|
||||||
|
for (const badge of header.author_badges) {
|
||||||
|
const label = badge.tooltip ?? '';
|
||||||
|
const iconType = badge.icon_type ?? '';
|
||||||
|
|
||||||
|
if (label.toLowerCase().includes('moderator') || iconType === 'MODERATOR') {
|
||||||
|
badges.push({ type: 'moderator', label });
|
||||||
|
} else if (label.toLowerCase().includes('member')) {
|
||||||
|
badges.push({ type: 'member', label });
|
||||||
|
} else if (label.toLowerCase().includes('verified') || iconType === 'VERIFIED') {
|
||||||
|
badges.push({ type: 'verified', label });
|
||||||
|
} else if (iconType === 'OWNER') {
|
||||||
|
badges.push({ type: 'custom', label: label || 'Owner' });
|
||||||
|
} else if (label) {
|
||||||
|
badges.push({ type: 'custom', label });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return badges;
|
||||||
|
}
|
||||||
|
|
||||||
function extractSuperChatInfo(item: any): SuperChatInfo | undefined {
|
function extractSuperChatInfo(item: any): SuperChatInfo | undefined {
|
||||||
const superTypes = new Set([
|
const superTypes = new Set([
|
||||||
'LiveChatPaidMessage',
|
'LiveChatPaidMessage',
|
||||||
@@ -245,6 +270,13 @@ function normalizeAction(action: any): ChatMessage | null {
|
|||||||
const item = action.item;
|
const item = action.item;
|
||||||
if (!item) return null;
|
if (!item) return null;
|
||||||
|
|
||||||
|
// Debug: Log raw item from YouTube API
|
||||||
|
console.log('╔════════════════════════════════════════════════════════════════╗');
|
||||||
|
console.log('║ RAW INNERTUBE API ITEM ║');
|
||||||
|
console.log('╚════════════════════════════════════════════════════════════════╝');
|
||||||
|
console.log(JSON.stringify(item, null, 2));
|
||||||
|
console.log('╚════════════════════════════════════════════════════════════════╝');
|
||||||
|
|
||||||
// Normalize various live chat events
|
// Normalize various live chat events
|
||||||
const itemType = String(item.type || '').trim();
|
const itemType = String(item.type || '').trim();
|
||||||
const messageText = resolveMessageText(item).toLowerCase();
|
const messageText = resolveMessageText(item).toLowerCase();
|
||||||
@@ -279,40 +311,56 @@ function normalizeAction(action: any): ChatMessage | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isText || isPaid || isMembership || isGiftPurchase) {
|
if (isText || isPaid || isMembership || isGiftPurchase) {
|
||||||
const badges = extractBadges(item);
|
// For gift purchases, author info is in header
|
||||||
|
const authorSource = isGiftPurchase ? item.header : item;
|
||||||
|
const badges = isGiftPurchase ? extractBadgesFromHeader(item.header) : extractBadges(item);
|
||||||
const isModerator = badges.some(b => b.type === 'moderator');
|
const isModerator = badges.some(b => b.type === 'moderator');
|
||||||
const isMember = badges.some(b => b.type === 'member');
|
const isMember = badges.some(b => b.type === 'member');
|
||||||
const isVerified = badges.some(b => b.type === 'verified');
|
const isVerified = badges.some(b => b.type === 'verified');
|
||||||
|
|
||||||
// Extract membership level for new members
|
// Extract membership level for new members and milestones
|
||||||
let membershipLevel: string | undefined;
|
let membershipLevel: string | undefined;
|
||||||
if (isMembership || isGiftPurchase || isPaid) {
|
if (isMembership) {
|
||||||
membershipLevel = item.header_subtext?.toString() ||
|
// For membership items, header_primary_text contains milestone info like "Member for 9 months"
|
||||||
|
membershipLevel = item.header_primary_text?.text ||
|
||||||
item.header_primary_text?.toString() ||
|
item.header_primary_text?.toString() ||
|
||||||
(isPaid ? undefined : 'New member');
|
'New member';
|
||||||
|
} else if (isGiftPurchase) {
|
||||||
|
// For gift purchases, we don't need membership level
|
||||||
|
membershipLevel = undefined;
|
||||||
|
} else if (isPaid) {
|
||||||
|
membershipLevel = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract gift count for gift purchases
|
// Extract gift count for gift purchases
|
||||||
let giftCount: number | undefined;
|
let giftCount: number | undefined;
|
||||||
if (isGiftPurchase) {
|
if (isGiftPurchase) {
|
||||||
const text = resolveMessageText(item);
|
const primaryText = item.header?.primary_text?.text || '';
|
||||||
const headerText = item.header_primary_text?.toString() || item.header_subtext?.toString() || '';
|
// Extract number from "Sent 1 Yusuf İpek gift memberships"
|
||||||
const combinedText = text + ' ' + headerText;
|
const countMatch = primaryText.match(/sent\s+(\d+)\s+/i);
|
||||||
|
|
||||||
// Try to extract number from text like "Gifted 5 memberships", "Sent 5 gift memberships", or "5 memberships"
|
|
||||||
const countMatch = combinedText.match(/(?:sent|gifted)?\s*(\d+)\s*(?:gift\s*)?(?:membership|memberships|member|members)/i);
|
|
||||||
if (countMatch) {
|
if (countMatch) {
|
||||||
giftCount = parseInt(countMatch[1], 10);
|
giftCount = parseInt(countMatch[1], 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract channel ID from author object (item.author.id contains the YouTube channel ID)
|
// Extract channel ID - for gifts it's in author_external_channel_id
|
||||||
const authorChannelId = item.author?.id;
|
const authorChannelId = isGiftPurchase
|
||||||
|
? item.author_external_channel_id
|
||||||
|
: item.author?.id;
|
||||||
|
|
||||||
|
// Extract author name and photo
|
||||||
|
const authorName = isGiftPurchase
|
||||||
|
? (item.header?.author_name?.text || 'Unknown')
|
||||||
|
: String(item.author?.name ?? 'Unknown');
|
||||||
|
|
||||||
|
const authorPhoto = isGiftPurchase
|
||||||
|
? item.header?.author_photo?.[0]?.url
|
||||||
|
: item.author?.thumbnails?.[0]?.url;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: String(item.id ?? item.timestamp_usec ?? Date.now()),
|
id: String(item.id ?? item.timestamp_usec ?? Date.now()),
|
||||||
author: String(item.author?.name ?? 'Unknown'),
|
author: authorName,
|
||||||
authorPhoto: item.author?.thumbnails?.[0]?.url,
|
authorPhoto,
|
||||||
authorChannelId: authorChannelId ? String(authorChannelId) : undefined,
|
authorChannelId: authorChannelId ? String(authorChannelId) : undefined,
|
||||||
text: resolveMessageText(item),
|
text: resolveMessageText(item),
|
||||||
publishedAt: resolveTimestamp(item.timestamp ?? item.timestamp_usec),
|
publishedAt: resolveTimestamp(item.timestamp ?? item.timestamp_usec),
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
3. Client dashboard fetches chat data via REST and pushes selection updates via REST.
|
3. Client dashboard fetches chat data via REST and pushes selection updates via REST.
|
||||||
4. Overlay page consumes a Server-Sent Events stream to stay in sync with the selected message.
|
4. Overlay page consumes a Server-Sent Events stream to stay in sync with the selected message.
|
||||||
- **Realtime Delivery:** SSE for one-directional updates to OBS browser source; leave room to switch to WebSockets if we need bidirectional control later.
|
- **Realtime Delivery:** SSE for one-directional updates to OBS browser source; leave room to switch to WebSockets if we need bidirectional control later.
|
||||||
- **Configuration:** `.env.local` (or process env) supplies `YOUTUBE_LIVE_ID` and optional Innertube overrides; backend picks mock mode automatically when unset.
|
|
||||||
|
|
||||||
## Key Patterns & Practices
|
## Key Patterns & Practices
|
||||||
- Abstract ingestion behind a module (`backend/src/ingestion/youtubei.ts`) so alternate providers (official API, headless browser) can be swapped in quickly.
|
- Abstract ingestion behind a module (`backend/src/ingestion/youtubei.ts`) so alternate providers (official API, headless browser) can be swapped in quickly.
|
||||||
|
|||||||
Reference in New Issue
Block a user