From 19b412a82a25b7728f6c75c01c2ce33a8f977443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C4=B0pek?= Date: Sun, 28 Dec 2025 18:46:52 +0300 Subject: [PATCH] feat: Add combined `start` script, correct backend path, and allow nullable `poll` event. --- backend/src/ingestion/youtubei.ts | 50 +++++++++++++++---------------- package.json | 5 ++-- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/backend/src/ingestion/youtubei.ts b/backend/src/ingestion/youtubei.ts index 156bbf1..1595205 100644 --- a/backend/src/ingestion/youtubei.ts +++ b/backend/src/ingestion/youtubei.ts @@ -17,7 +17,7 @@ export type ContinuationState = { export type ChatEventEmitter = EventEmitter<{ message: (message: ChatMessage) => void; - poll: (poll: Poll) => void; + poll: (poll: Poll | null) => void; error: (error: unknown) => void; }>; @@ -70,7 +70,7 @@ export async function bootstrapInnertube(videoId: string): Promise= 1e15), divide by 1000 to get milliseconds @@ -207,7 +207,7 @@ function resolveTimestamp(timestamp: number | string | undefined): string { // Fallback for smaller numbers millis = numeric; } - + return new Date(millis).toISOString(); } @@ -216,13 +216,13 @@ function resolveTimestamp(timestamp: number | string | undefined): string { function extractBadges(item: any): Badge[] { const badges: Badge[] = []; - + if (!item.author?.badges) return badges; for (const badge of item.author.badges) { const label = badge.tooltip ?? badge.label ?? ''; const imageUrl = badge.custom_thumbnail?.[0]?.url; - + if (label.toLowerCase().includes('moderator')) { badges.push({ type: 'moderator', label, imageUrl }); } else if (label.toLowerCase().includes('member')) { @@ -239,14 +239,14 @@ function extractBadges(item: any): Badge[] { 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 ?? ''; const imageUrl = badge.custom_thumbnail?.[0]?.url; - + if (label.toLowerCase().includes('moderator') || iconType === 'MODERATOR') { badges.push({ type: 'moderator', label, imageUrl }); } else if (label.toLowerCase().includes('member')) { @@ -266,7 +266,7 @@ function extractBadgesFromHeader(header: any): Badge[] { function extractLeaderboardRank(item: any): number | undefined { // Check for before_content_buttons array (where leaderboard badge appears) if (!Array.isArray(item.before_content_buttons)) return undefined; - + for (const button of item.before_content_buttons) { // Look for CROWN icon (leaderboard indicator) if (button.icon_name === 'CROWN' && button.title) { @@ -277,7 +277,7 @@ function extractLeaderboardRank(item: any): number | undefined { } } } - + return undefined; } @@ -337,7 +337,7 @@ function extractSuperChatInfo(item: any): SuperChatInfo | undefined { amount = amountText; } } - + // If we still don't have an amount, use a default if (!amount) { amount = 'Super Chat'; @@ -358,7 +358,7 @@ function extractSuperChatInfo(item: any): SuperChatInfo | undefined { // Extract super sticker image URL if present let stickerUrl: string | undefined; let stickerAlt: string | undefined; - + if (Array.isArray(item.sticker) && item.sticker.length > 0) { // Prefer larger image (first in array is usually largest) const stickerThumb = item.sticker[0]; @@ -373,7 +373,7 @@ function extractSuperChatInfo(item: any): SuperChatInfo | undefined { } stickerUrl = url; } - + // Extract accessibility label for alt text if (item.sticker_accessibility_label) { stickerAlt = String(item.sticker_accessibility_label); @@ -400,16 +400,16 @@ 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 === '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') || + 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); @@ -426,7 +426,7 @@ function normalizeAction(action: any): ChatMessage | null { const isModerator = badges.some(b => b.type === 'moderator'); const isMember = badges.some(b => b.type === 'member'); const isVerified = badges.some(b => b.type === 'verified'); - + // Extract membership level for new members, upgrades, and milestones let membershipLevel: string | undefined; if (isMembership) { @@ -452,7 +452,7 @@ function normalizeAction(action: any): ChatMessage | null { } else if (isPaid) { membershipLevel = undefined; } - + // Extract gift count for gift purchases let giftCount: number | undefined; if (isGiftPurchase) { @@ -463,21 +463,21 @@ function normalizeAction(action: any): ChatMessage | null { giftCount = parseInt(countMatch[1], 10); } } - + // Extract channel ID - for gifts it's in author_external_channel_id - const authorChannelId = isGiftPurchase - ? item.author_external_channel_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; - + // Build text with fallback: if no user message, show header subtext/primary for membership events const resolvedText = resolveMessageText(item); const membershipFallbackText = isMembership diff --git a/package.json b/package.json index 7f29b3e..8047083 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "build": "pnpm build:backend && pnpm build:client", "build:backend": "tsc -p backend/tsconfig.build.json", "build:client": "next build client", - "start:backend": "node backend/dist/index.js", + "start": "concurrently \"pnpm start:backend\" \"pnpm start:client\"", + "start:backend": "node backend/dist/backend/src/index.js", "start:client": "next start client", "lint": "next lint client" }, @@ -37,4 +38,4 @@ "tsx": "^4.19.1", "typescript": "^5.9.3" } -} +} \ No newline at end of file