From 5d96e2e3ffb1da9a2015590831d39901d154a73f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C4=B0pek?= Date: Sun, 5 Oct 2025 03:41:30 +0300 Subject: [PATCH] feat: enhance YouTube chat ingestion with support for gift memberships Expand YouTube chat ingestion to handle membership gift events including purchases and redemptions. Update SuperChat extraction to support multiple paid message types and improve field name handling. Display membership level in overlay for gift events. --- backend/src/ingestion/youtubei.ts | 56 +++++++++++++++++-------------- client/app/overlay/page.tsx | 2 +- memory-bank/activeContext.md | 4 +++ 3 files changed, 36 insertions(+), 26 deletions(-) diff --git a/backend/src/ingestion/youtubei.ts b/backend/src/ingestion/youtubei.ts index 04392fc..3b17832 100644 --- a/backend/src/ingestion/youtubei.ts +++ b/backend/src/ingestion/youtubei.ts @@ -152,30 +152,32 @@ function extractBadges(item: any): Badge[] { } function extractSuperChatInfo(item: any): SuperChatInfo | undefined { - if (item.type !== 'LiveChatPaidMessage') return undefined; + // Support multiple superchat-like types from Innertube + const superTypes = new Set([ + 'LiveChatPaidMessage', + 'LiveChatPaidSticker', + 'liveChatPaidMessageRenderer', + 'liveChatPaidStickerRenderer' + ]); + const typeName = String(item.type || item.item_type || item.renderer || '').trim(); + const isSuper = superTypes.has(typeName) || !!(item.purchase_amount_text || item.purchaseAmountText); + if (!isSuper) return undefined; // Try multiple possible field names for the amount let amount = ''; - if (item.purchase_amount_text) { - amount = typeof item.purchase_amount_text === 'string' - ? item.purchase_amount_text - : item.purchase_amount_text.simpleText || item.purchase_amount_text.toString(); - } else if (item.purchaseAmountText) { - amount = typeof item.purchaseAmountText === 'string' - ? item.purchaseAmountText - : item.purchaseAmountText.simpleText || item.purchaseAmountText.toString(); + const amt1 = item.purchase_amount_text; + const amt2 = item.purchaseAmountText; + if (amt1) { + amount = typeof amt1 === 'string' ? amt1 : (amt1.simpleText || amt1.toString()); + } else if (amt2) { + amount = typeof amt2 === 'string' ? amt2 : (amt2.simpleText || amt2.toString()); } else if (item.amount) { - amount = item.amount.toString(); + amount = String(item.amount); } - + // Try multiple possible field names for color - const color = item.body_background_color?.toString() - || item.bodyBackgroundColor?.toString() - || item.headerBackgroundColor?.toString() - || '#1e3a8a'; - - console.log('[SuperChat] Extracted:', { amount, color, rawItem: item }); - + const color = (item.body_background_color ?? item.bodyBackgroundColor ?? item.headerBackgroundColor ?? '#1e3a8a').toString(); + return { amount: amount || 'Super Chat', currency: item.currency ?? 'USD', @@ -191,11 +193,15 @@ function normalizeAction(action: any): ChatMessage | null { const item = action.item; if (!item) return null; - if ( - item.type === 'LiveChatTextMessage' || - item.type === 'LiveChatPaidMessage' || - item.type === 'LiveChatMembershipItem' - ) { + // Normalize various live chat events + const itemType = String(item.type || '').trim(); + const isText = itemType === 'LiveChatTextMessage'; + const isPaid = !!extractSuperChatInfo(item); + const isMembership = itemType === 'LiveChatMembershipItem'; + const isGiftPurchase = itemType === 'LiveChatGiftMembershipsPurchase' || itemType === 'LiveChatSponsorshipsGiftRedemptionAnnouncement'; + const isGiftReceived = itemType === 'LiveChatGiftMembershipReceived'; + + if (isText || isPaid || isMembership || isGiftPurchase || isGiftReceived) { const badges = extractBadges(item); const isModerator = badges.some(b => b.type === 'moderator'); const isMember = badges.some(b => b.type === 'member'); @@ -203,7 +209,7 @@ function normalizeAction(action: any): ChatMessage | null { // Extract membership level for new members let membershipLevel: string | undefined; - if (item.type === 'LiveChatMembershipItem') { + if (isMembership || isGiftPurchase || isGiftReceived) { membershipLevel = item.header_subtext?.toString() || item.header_primary_text?.toString() || 'New member'; @@ -220,7 +226,7 @@ function normalizeAction(action: any): ChatMessage | null { isMember, isVerified, superChat: extractSuperChatInfo(item), - membershipGift: item.type === 'LiveChatMembershipItem', + membershipGift: isMembership || isGiftPurchase || isGiftReceived, membershipLevel }; } diff --git a/client/app/overlay/page.tsx b/client/app/overlay/page.tsx index 3da09cf..945d6f3 100644 --- a/client/app/overlay/page.tsx +++ b/client/app/overlay/page.tsx @@ -91,7 +91,7 @@ export default function OverlayPage() { )} {message.membershipGift && (
- 🎁 New Member! + 🎁 {message.membershipLevel || 'New Member'}
)}

{message.text}

diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index 8879120..195b7a3 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -14,6 +14,10 @@ - Centered overlay message card content so live chat, superchat, and membership boxes align for OBS - Reworked dashboard grid so live chat runs full-height on the left and super chat/new member panels stack in a centered right column with tuned spacing and clear separation between the stacked cards - Moved the connection control into the header (inline, centered between logo/title and status) with a compact input width +- Fixed panel heights to fit within the viewport (no page scroll); grid rows split 1fr/1fr with internal lists scrolling so bottom edges are always visible +- Improved parsing so superchat amounts/colors show reliably; overlay and dashboard both display the amount +- Overlay membership banner now shows the membership level text +- Membership gifts are recognized and included in the New Members list ## Immediate Next Steps 1. Test with live YouTube stream to verify badge parsing and superchat detection