feat: add leaderboard rank extraction and display in chat and overlay components, including CSS styling for leaderboard badge

This commit is contained in:
Yusuf İpek
2025-10-12 21:48:05 +03:00
parent 1678117d16
commit d3185b43c3
5 changed files with 43 additions and 4 deletions
+26 -4
View File
@@ -65,9 +65,9 @@ export async function bootstrapInnertube(videoId: string): Promise<IngestionCont
liveChat.on('chat-update', (action: any) => {
// 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
};
}
+5
View File
@@ -616,6 +616,11 @@ function ChatItem({ message, isSelected, onSelect, onLinkClick, isPreviouslySele
<span className="chatItem__membership">{message.membershipLevel}:</span>
)}
<span className="chatItem__author">{message.author}</span>
{message.leaderboardRank && (
<span className="badge badge--leaderboard" title={`#${message.leaderboardRank} on leaderboard`}>
👑 #{message.leaderboardRank}
</span>
)}
{message.badges && message.badges.map((badge, i) => (
badge.imageUrl ? (
<img key={i} src={proxyImageUrl(badge.imageUrl)} alt={badge.label} className="badge badge--image" title={badge.label} />
+6
View File
@@ -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;
+5
View File
@@ -170,6 +170,11 @@ export default function OverlayPage() {
<div>
<div className="overlay__authorLine">
<span className="overlay__author">{displayMessage.author}</span>
{displayMessage.leaderboardRank && (
<span className="overlay__badge overlay__badge--leaderboard" title={`#${displayMessage.leaderboardRank} on leaderboard`}>
👑 #{displayMessage.leaderboardRank}
</span>
)}
{displayMessage.badges && displayMessage.badges.map((badge, i) => (
badge.imageUrl ? (
<img key={i} src={proxyImageUrl(badge.imageUrl)} alt={badge.label} className="overlay__badge overlay__badge--image" title={badge.label} />
+1
View File
@@ -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.)
};