{message.text}
+ {message.text && ( +{message.text}
+ )} + > + ) : ( + <> +{message.text}
+ )} + > )}diff --git a/backend/src/index.ts b/backend/src/index.ts index 8822b6f..11aae49 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -9,7 +9,7 @@ const MAX_MESSAGES = 500; export async function startBackend() { const fastify = Fastify({ logger: { - level: 'info' + level: 'warn', // Only show warnings and errors, not every request } }); @@ -38,7 +38,6 @@ export async function startBackend() { ingestion = await bootstrapInnertube(parsedLiveId); console.log(`[Backend] ✓ YouTube chat connected successfully`); ingestion.emitter.on('message', (message) => { - console.log(`[Chat] ${message.author}: ${message.text}`); store.push(message); if (store.length > MAX_MESSAGES) { store.splice(0, store.length - MAX_MESSAGES); @@ -106,7 +105,6 @@ export async function startBackend() { console.log(`[Backend] ✓ YouTube chat connected successfully`); ingestion.emitter.on('message', (message) => { - console.log(`[Chat] ${message.author}: ${message.text}`); store.push(message); if (store.length > MAX_MESSAGES) { store.splice(0, store.length - MAX_MESSAGES); diff --git a/backend/src/ingestion/youtubei.ts b/backend/src/ingestion/youtubei.ts index b3113b1..c1edbda 100644 --- a/backend/src/ingestion/youtubei.ts +++ b/backend/src/ingestion/youtubei.ts @@ -164,13 +164,15 @@ function extractSuperChatInfo(item: any): SuperChatInfo | undefined { 'liveChatPaidStickerRenderer' ]); const typeName = String(item.type || item.item_type || item.renderer || '').trim(); - const isSuper = superTypes.has(typeName) || !!(item.purchase_amount_text || item.purchaseAmountText); + const isSuper = superTypes.has(typeName) || !!(item.purchase_amount); if (!isSuper) return undefined; let amountText = ''; const candidates = [ - item.purchase_amount_text, + item.purchase_amount, // Correct property name from youtubei.js docs + item.purchase_amount_text, // Fallback item.purchaseAmountText, + item.header?.purchase_amount, item.header?.purchase_amount_text, item.header?.purchaseAmountText, item.amount, @@ -183,6 +185,9 @@ function extractSuperChatInfo(item: any): SuperChatInfo | undefined { if (typeof v === 'number') return String(v); if (typeof v.simpleText === 'string') return v.simpleText; if (Array.isArray(v.runs)) return v.runs.map((r: any) => r.text ?? '').join(''); + if (typeof v.toString === 'function' && v.toString !== Object.prototype.toString) { + return v.toString(); + } return ''; } @@ -191,22 +196,27 @@ function extractSuperChatInfo(item: any): SuperChatInfo | undefined { if (amountText) break; } - let amount = 'Super Chat'; + let amount = ''; let currency = ''; if (amountText) { // Regex to capture currency symbol/code and amount - // Handles: $5.00, €5,00, 5,00 €, 5.00 USD, TRY5.00 - const match = amountText.match(/([\$\€\£\¥\₹\₺A-Z]+)?\s*([\d,\.,]+)\s*([\$\€\£\¥\₹\₺A-Z]+)?/); + // Handles: $5.00, €5,00, 5,00 €, 5.00 USD, TRY5.00, TRY 55, etc. + const match = amountText.match(/([\$\€\£\¥\₹\₺]|[A-Z]{2,3})?\s*([\d,\.]+)\s*([\$\€\£\¥\₹\₺]|[A-Z]{2,3})?/); if (match) { - // Prefer currency symbol before the number, fallback to after + // Prefer currency symbol/code before the number, fallback to after currency = match[1] || match[3] || ''; amount = match[2]; } else { - // Fallback for cases where only the number is present + // Fallback: use the whole text if no pattern matches amount = amountText; } } + + // If we still don't have an amount, use a default + if (!amount) { + amount = 'Super Chat'; + } let rawColor = item.body_background_color ?? item.bodyBackgroundColor ?? item.headerBackgroundColor; let color = '#1e3a8a'; @@ -237,13 +247,38 @@ function normalizeAction(action: any): ChatMessage | null { // Normalize various live chat events const itemType = String(item.type || '').trim(); + const messageText = resolveMessageText(item).toLowerCase(); + const isText = itemType === 'LiveChatTextMessage'; const isPaid = !!extractSuperChatInfo(item); const isMembership = itemType === 'LiveChatMembershipItem'; - const isGiftPurchase = itemType === 'LiveChatGiftMembershipsPurchase' || itemType === 'LiveChatSponsorshipsGiftRedemptionAnnouncement'; - const isGiftReceived = itemType === 'LiveChatGiftMembershipReceived'; + const isGiftPurchase = itemType === 'LiveChatSponsorshipsGiftPurchaseAnnouncement'; + const isGiftReceived = itemType === 'LiveChatSponsorshipsGiftRedemptionAnnouncement'; + + // Check if the message text indicates it's a gift recipient message + const isGiftRecipientMessage = + messageText.includes('received a gift membership') || + messageText.includes('received a membership gift') || + messageText.includes('received a gift') || + /received\s+a\s+.*membership.*by/i.test(messageText); - if (isText || isPaid || isMembership || isGiftPurchase || isGiftReceived) { + // Log all gift-related messages for debugging + if (isGiftPurchase || isGiftReceived || isGiftRecipientMessage) { + console.log(`[Gift Debug] Type: ${itemType}, Author: ${item.author?.name}, Text: "${messageText}", Purchase: ${isGiftPurchase}, Received: ${isGiftReceived}, TextPattern: ${isGiftRecipientMessage}`); + } + + // Ignore gift received messages - we only care about the purchaser + if (isGiftReceived || isGiftRecipientMessage) { + console.log(`[Gift] Filtering out recipient message from: ${item.author?.name}`); + return null; + } + + // Log gift purchase messages for debugging + if (isGiftPurchase) { + console.log(`[Chat] Gift purchase detected - Author: ${item.author?.name}, Type: ${itemType}, Text: ${messageText}`); + } + + if (isText || isPaid || isMembership || isGiftPurchase) { const badges = extractBadges(item); const isModerator = badges.some(b => b.type === 'moderator'); const isMember = badges.some(b => b.type === 'member'); @@ -251,7 +286,7 @@ function normalizeAction(action: any): ChatMessage | null { // Extract membership level for new members let membershipLevel: string | undefined; - if (isMembership || isGiftPurchase || isGiftReceived || isPaid) { + if (isMembership || isGiftPurchase || isPaid) { membershipLevel = item.header_subtext?.toString() || item.header_primary_text?.toString() || (isPaid ? undefined : 'New member'); @@ -263,8 +298,9 @@ function normalizeAction(action: any): ChatMessage | null { const text = resolveMessageText(item); const headerText = item.header_primary_text?.toString() || item.header_subtext?.toString() || ''; const combinedText = text + ' ' + headerText; - // Try to extract number from text like "Gifted 5 memberships" or "5 memberships" - const countMatch = combinedText.match(/(\d+)\s*(?:membership|memberships|member|members)/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) { giftCount = parseInt(countMatch[1], 10); } diff --git a/client/app/dashboard/page.tsx b/client/app/dashboard/page.tsx index db4814c..d41ae42 100644 --- a/client/app/dashboard/page.tsx +++ b/client/app/dashboard/page.tsx @@ -435,15 +435,15 @@ function ChatItem({ message, isSelected, onSelect }: ChatItemProps) { {badge.type === 'verified' && '✓'} ))} + {message.superChat && ( + + {message.superChat.currency}{message.superChat.currency ? ' ' : ''}{message.superChat.amount} + + )} - {message.superChat && ( -
{message.text}
); @@ -463,7 +463,7 @@ function MemberItem({ message, isSelected, onSelect }: ChatItemProps) { {message.author} {message.membershipGiftPurchase && message.giftCount - ? `Gifted ${message.giftCount} membership${message.giftCount > 1 ? 's' : ''}` + ? `Sent ${message.giftCount} gift membership${message.giftCount > 1 ? 's' : ''}` : message.membershipLevel || 'New member'} diff --git a/client/app/globals.css b/client/app/globals.css index 82392fa..7867e44 100644 --- a/client/app/globals.css +++ b/client/app/globals.css @@ -686,6 +686,17 @@ main { align-self: flex-start; /* Align to the left */ } +.chatItem__superchat-inline { + padding: 0.3rem 0.6rem; + border-radius: 6px; + font-weight: 700; + font-size: 0.85rem; + color: #fff; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); + margin-left: 0.5rem; + display: inline-block; +} + .chatItem__membership { padding: 0.6rem 1rem; background: linear-gradient(135deg, #10b981, #059669); @@ -944,6 +955,53 @@ main { align-self: flex-start; } +.overlay__superchat-header { + padding: 1rem 1.5rem; + border-radius: 12px 12px 0 0; + color: #fff; + display: flex; + align-items: center; + gap: 0.5rem; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4); +} + +.overlay__superchat-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + object-fit: cover; + border: 2px solid rgba(255, 255, 255, 0.3); +} + +.overlay__superchat-name { + font-size: 1.6rem; + font-weight: 700; +} + +.overlay__superchat-separator { + font-size: 1.4rem; + font-weight: 600; + opacity: 0.9; +} + +.overlay__superchat-amount { + font-size: 1.6rem; + font-weight: 700; +} + +.overlay__superchat-text { + padding: 1rem 1.5rem; + font-size: 1.1rem; + line-height: 1.6; + color: #e5e7eb; + font-weight: 400; + text-align: left; + word-break: break-word; + background: rgba(0, 0, 0, 0.3); + border-radius: 0 0 12px 12px; + margin: 0; +} + .overlay__membership { background: linear-gradient(135deg, #10b981, #059669); } diff --git a/client/app/overlay/page.tsx b/client/app/overlay/page.tsx index 5ed4197..d9757cd 100644 --- a/client/app/overlay/page.tsx +++ b/client/app/overlay/page.tsx @@ -67,39 +67,54 @@ export default function OverlayPage() {{message.text}
+ {message.text && ( +{message.text}
+ )} + > + ) : ( + <> +{message.text}
+ )} + > )}