mirror of
https://github.com/yusufipk/YTChatHub.git
synced 2026-09-11 10:56:17 +00:00
feat: Add combined start script, correct backend path, and allow nullable poll event.
This commit is contained in:
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user