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<{ export type ChatEventEmitter = EventEmitter<{
message: (message: ChatMessage) => void; message: (message: ChatMessage) => void;
poll: (poll: Poll) => void; poll: (poll: Poll | null) => void;
error: (error: unknown) => void; error: (error: unknown) => void;
}>; }>;
@@ -70,7 +70,7 @@ export async function bootstrapInnertube(videoId: string): Promise<IngestionCont
console.log('[Ingestion] Fetching video info...'); console.log('[Ingestion] Fetching video info...');
const info = await client.getInfo(videoId); const info = await client.getInfo(videoId);
console.log('[Ingestion] Getting live chat...'); console.log('[Ingestion] Getting live chat...');
const liveChat = info.getLiveChat(); const liveChat = info.getLiveChat();
@@ -191,7 +191,7 @@ function resolveTimestamp(timestamp: number | string | undefined): string {
} }
const numeric = typeof timestamp === 'string' ? Number(timestamp) : timestamp; const numeric = typeof timestamp === 'string' ? Number(timestamp) : timestamp;
if (Number.isFinite(numeric)) { if (Number.isFinite(numeric)) {
// YouTube timestamps are in microseconds (16 digits) or milliseconds (13 digits) // YouTube timestamps are in microseconds (16 digits) or milliseconds (13 digits)
// If it's microseconds (>= 1e15), divide by 1000 to get milliseconds // 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 // Fallback for smaller numbers
millis = numeric; millis = numeric;
} }
return new Date(millis).toISOString(); return new Date(millis).toISOString();
} }
@@ -216,13 +216,13 @@ function resolveTimestamp(timestamp: number | string | undefined): string {
function extractBadges(item: any): Badge[] { function extractBadges(item: any): Badge[] {
const badges: Badge[] = []; const badges: Badge[] = [];
if (!item.author?.badges) return badges; if (!item.author?.badges) return badges;
for (const badge of item.author.badges) { for (const badge of item.author.badges) {
const label = badge.tooltip ?? badge.label ?? ''; const label = badge.tooltip ?? badge.label ?? '';
const imageUrl = badge.custom_thumbnail?.[0]?.url; const imageUrl = badge.custom_thumbnail?.[0]?.url;
if (label.toLowerCase().includes('moderator')) { if (label.toLowerCase().includes('moderator')) {
badges.push({ type: 'moderator', label, imageUrl }); badges.push({ type: 'moderator', label, imageUrl });
} else if (label.toLowerCase().includes('member')) { } else if (label.toLowerCase().includes('member')) {
@@ -239,14 +239,14 @@ function extractBadges(item: any): Badge[] {
function extractBadgesFromHeader(header: any): Badge[] { function extractBadgesFromHeader(header: any): Badge[] {
const badges: Badge[] = []; const badges: Badge[] = [];
if (!header?.author_badges) return badges; if (!header?.author_badges) return badges;
for (const badge of header.author_badges) { for (const badge of header.author_badges) {
const label = badge.tooltip ?? ''; const label = badge.tooltip ?? '';
const iconType = badge.icon_type ?? ''; const iconType = badge.icon_type ?? '';
const imageUrl = badge.custom_thumbnail?.[0]?.url; const imageUrl = badge.custom_thumbnail?.[0]?.url;
if (label.toLowerCase().includes('moderator') || iconType === 'MODERATOR') { if (label.toLowerCase().includes('moderator') || iconType === 'MODERATOR') {
badges.push({ type: 'moderator', label, imageUrl }); badges.push({ type: 'moderator', label, imageUrl });
} else if (label.toLowerCase().includes('member')) { } else if (label.toLowerCase().includes('member')) {
@@ -266,7 +266,7 @@ function extractBadgesFromHeader(header: any): Badge[] {
function extractLeaderboardRank(item: any): number | undefined { function extractLeaderboardRank(item: any): number | undefined {
// Check for before_content_buttons array (where leaderboard badge appears) // Check for before_content_buttons array (where leaderboard badge appears)
if (!Array.isArray(item.before_content_buttons)) return undefined; if (!Array.isArray(item.before_content_buttons)) return undefined;
for (const button of item.before_content_buttons) { for (const button of item.before_content_buttons) {
// Look for CROWN icon (leaderboard indicator) // Look for CROWN icon (leaderboard indicator)
if (button.icon_name === 'CROWN' && button.title) { if (button.icon_name === 'CROWN' && button.title) {
@@ -277,7 +277,7 @@ function extractLeaderboardRank(item: any): number | undefined {
} }
} }
} }
return undefined; return undefined;
} }
@@ -337,7 +337,7 @@ function extractSuperChatInfo(item: any): SuperChatInfo | undefined {
amount = amountText; amount = amountText;
} }
} }
// If we still don't have an amount, use a default // If we still don't have an amount, use a default
if (!amount) { if (!amount) {
amount = 'Super Chat'; amount = 'Super Chat';
@@ -358,7 +358,7 @@ function extractSuperChatInfo(item: any): SuperChatInfo | undefined {
// Extract super sticker image URL if present // Extract super sticker image URL if present
let stickerUrl: string | undefined; let stickerUrl: string | undefined;
let stickerAlt: string | undefined; let stickerAlt: string | undefined;
if (Array.isArray(item.sticker) && item.sticker.length > 0) { if (Array.isArray(item.sticker) && item.sticker.length > 0) {
// Prefer larger image (first in array is usually largest) // Prefer larger image (first in array is usually largest)
const stickerThumb = item.sticker[0]; const stickerThumb = item.sticker[0];
@@ -373,7 +373,7 @@ function extractSuperChatInfo(item: any): SuperChatInfo | undefined {
} }
stickerUrl = url; stickerUrl = url;
} }
// Extract accessibility label for alt text // Extract accessibility label for alt text
if (item.sticker_accessibility_label) { if (item.sticker_accessibility_label) {
stickerAlt = String(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 // Normalize various live chat events
const itemType = String(item.type || '').trim(); const itemType = String(item.type || '').trim();
const messageText = resolveMessageText(item).toLowerCase(); const messageText = resolveMessageText(item).toLowerCase();
const isText = itemType === 'LiveChatTextMessage'; const isText = itemType === 'LiveChatTextMessage';
const isPaid = !!extractSuperChatInfo(item); const isPaid = !!extractSuperChatInfo(item);
const isMembership = itemType === 'LiveChatMembershipItem'; const isMembership = itemType === 'LiveChatMembershipItem';
const isGiftPurchase = itemType === 'LiveChatSponsorshipsGiftPurchaseAnnouncement'; const isGiftPurchase = itemType === 'LiveChatSponsorshipsGiftPurchaseAnnouncement';
const isGiftReceived = itemType === 'LiveChatSponsorshipsGiftRedemptionAnnouncement'; const isGiftReceived = itemType === 'LiveChatSponsorshipsGiftRedemptionAnnouncement';
// Check if the message text indicates it's a gift recipient message // Check if the message text indicates it's a gift recipient message
const isGiftRecipientMessage = const isGiftRecipientMessage =
messageText.includes('received a gift membership') || messageText.includes('received a gift membership') ||
messageText.includes('received a membership gift') || messageText.includes('received a membership gift') ||
messageText.includes('received a gift') || messageText.includes('received a gift') ||
/received\s+a\s+.*membership.*by/i.test(messageText); /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 isModerator = badges.some(b => b.type === 'moderator');
const isMember = badges.some(b => b.type === 'member'); const isMember = badges.some(b => b.type === 'member');
const isVerified = badges.some(b => b.type === 'verified'); const isVerified = badges.some(b => b.type === 'verified');
// Extract membership level for new members, upgrades, and milestones // Extract membership level for new members, upgrades, and milestones
let membershipLevel: string | undefined; let membershipLevel: string | undefined;
if (isMembership) { if (isMembership) {
@@ -452,7 +452,7 @@ function normalizeAction(action: any): ChatMessage | null {
} else if (isPaid) { } else if (isPaid) {
membershipLevel = undefined; membershipLevel = undefined;
} }
// Extract gift count for gift purchases // Extract gift count for gift purchases
let giftCount: number | undefined; let giftCount: number | undefined;
if (isGiftPurchase) { if (isGiftPurchase) {
@@ -463,21 +463,21 @@ function normalizeAction(action: any): ChatMessage | null {
giftCount = parseInt(countMatch[1], 10); giftCount = parseInt(countMatch[1], 10);
} }
} }
// Extract channel ID - for gifts it's in author_external_channel_id // Extract channel ID - for gifts it's in author_external_channel_id
const authorChannelId = isGiftPurchase const authorChannelId = isGiftPurchase
? item.author_external_channel_id ? item.author_external_channel_id
: item.author?.id; : item.author?.id;
// Extract author name and photo // Extract author name and photo
const authorName = isGiftPurchase const authorName = isGiftPurchase
? (item.header?.author_name?.text || 'Unknown') ? (item.header?.author_name?.text || 'Unknown')
: String(item.author?.name ?? 'Unknown'); : String(item.author?.name ?? 'Unknown');
const authorPhoto = isGiftPurchase const authorPhoto = isGiftPurchase
? item.header?.author_photo?.[0]?.url ? item.header?.author_photo?.[0]?.url
: item.author?.thumbnails?.[0]?.url; : item.author?.thumbnails?.[0]?.url;
// Build text with fallback: if no user message, show header subtext/primary for membership events // Build text with fallback: if no user message, show header subtext/primary for membership events
const resolvedText = resolveMessageText(item); const resolvedText = resolveMessageText(item);
const membershipFallbackText = isMembership const membershipFallbackText = isMembership
+3 -2
View File
@@ -12,7 +12,8 @@
"build": "pnpm build:backend && pnpm build:client", "build": "pnpm build:backend && pnpm build:client",
"build:backend": "tsc -p backend/tsconfig.build.json", "build:backend": "tsc -p backend/tsconfig.build.json",
"build:client": "next build client", "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", "start:client": "next start client",
"lint": "next lint client" "lint": "next lint client"
}, },
@@ -37,4 +38,4 @@
"tsx": "^4.19.1", "tsx": "^4.19.1",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
} }