diff --git a/backend/src/ingestion/youtubei.ts b/backend/src/ingestion/youtubei.ts index 343e562..7c965c3 100644 --- a/backend/src/ingestion/youtubei.ts +++ b/backend/src/ingestion/youtubei.ts @@ -65,9 +65,9 @@ export async function bootstrapInnertube(videoId: string): Promise { // Log the complete raw action data from YouTube (commented out for production) - console.log('=== YOUTUBE RAW MESSAGE DATA ==='); - console.log('Action type:', action?.type); - console.log('Complete action object:', JSON.stringify(action, null, 2)); + //console.log('=== YOUTUBE RAW MESSAGE DATA ==='); + //console.log('Action type:', action?.type); + //console.log('Complete action object:', JSON.stringify(action, null, 2)); const normalized = normalizeAction(action); if (normalized) { @@ -233,6 +233,24 @@ function extractBadgesFromHeader(header: any): Badge[] { return badges; } +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) { + // Title is like "#3", "#1", etc. + const match = String(button.title).match(/#(\d+)/); + if (match && match[1]) { + return parseInt(match[1], 10); + } + } + } + + return undefined; +} + function extractSuperChatInfo(item: any): SuperChatInfo | undefined { const superTypes = new Set([ 'LiveChatPaidMessage', @@ -439,6 +457,9 @@ function normalizeAction(action: any): ChatMessage | null { // Use timestamp_usec (microseconds) as it's more accurate than timestamp (milliseconds) const timestampToUse = item.timestamp_usec ?? item.timestamp; + // Extract leaderboard rank if present + const leaderboardRank = extractLeaderboardRank(item); + return { id: String(item.id ?? item.timestamp_usec ?? Date.now()), author: authorName, @@ -455,7 +476,8 @@ function normalizeAction(action: any): ChatMessage | null { membershipGift: isMembership, membershipGiftPurchase: isGiftPurchase, membershipLevel, - giftCount + giftCount, + leaderboardRank }; } diff --git a/client/app/dashboard/page.tsx b/client/app/dashboard/page.tsx index 8f5a3c8..6c07efc 100644 --- a/client/app/dashboard/page.tsx +++ b/client/app/dashboard/page.tsx @@ -616,6 +616,11 @@ function ChatItem({ message, isSelected, onSelect, onLinkClick, isPreviouslySele {message.membershipLevel}: )} {message.author} + {message.leaderboardRank && ( + + 👑 #{message.leaderboardRank} + + )} {message.badges && message.badges.map((badge, i) => ( badge.imageUrl ? ( {badge.label} diff --git a/client/app/globals.css b/client/app/globals.css index f018c9c..cd7c61b 100644 --- a/client/app/globals.css +++ b/client/app/globals.css @@ -784,6 +784,12 @@ main { color: #93c5fd; } +.badge--leaderboard { + background: rgba(251, 191, 36, 0.2); + color: #fcd34d; + font-weight: 600; +} + .memberItem { display: flex; flex-direction: column; diff --git a/client/app/overlay/page.tsx b/client/app/overlay/page.tsx index 51aa4a5..c3673e2 100644 --- a/client/app/overlay/page.tsx +++ b/client/app/overlay/page.tsx @@ -170,6 +170,11 @@ export default function OverlayPage() {
{displayMessage.author} + {displayMessage.leaderboardRank && ( + + 👑 #{displayMessage.leaderboardRank} + + )} {displayMessage.badges && displayMessage.badges.map((badge, i) => ( badge.imageUrl ? ( {badge.label} diff --git a/shared/chat.ts b/shared/chat.ts index 7f92cdc..3c8d374 100644 --- a/shared/chat.ts +++ b/shared/chat.ts @@ -36,4 +36,5 @@ export type ChatMessage = { membershipGiftPurchase?: boolean; membershipLevel?: string; // e.g., "New member", "Member (6 months)", etc. giftCount?: number; // Number of memberships gifted + leaderboardRank?: number; // YouTube leaderboard rank (1, 2, 3, etc.) };