feat: Add combined start script, correct backend path, and allow nullable poll event.

This commit is contained in:
Yusuf İpek
2025-12-28 18:46:52 +03:00
parent 0ae6014886
commit 19b412a82a
2 changed files with 28 additions and 27 deletions
+25 -25
View File
@@ -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<IngestionCont
console.log('[Ingestion] Fetching video info...');
const info = await client.getInfo(videoId);
console.log('[Ingestion] Getting live chat...');
const liveChat = info.getLiveChat();
@@ -191,7 +191,7 @@ function resolveTimestamp(timestamp: number | string | undefined): string {
}
const numeric = typeof timestamp === 'string' ? Number(timestamp) : timestamp;
if (Number.isFinite(numeric)) {
// YouTube timestamps are in microseconds (16 digits) or milliseconds (13 digits)
// If it's microseconds (>= 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
+3 -2
View File
@@ -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"
}
}
}