Add two new MCP tools for extracting video comments: - ytdlp_get_video_comments: Extract comments as structured JSON with author info, likes, timestamps, and reply threading - ytdlp_get_video_comments_summary: Get human-readable summary of top comments Features: - Support for sorting by "top" (most liked) or "new" (newest first) - Configurable comment limit (1-100 comments) - Includes author verification status, pinned comments, and uploader replies - Comprehensive error handling for disabled comments, private videos, etc. - Comprehensive test suite
180 lines
6.7 KiB
TypeScript
180 lines
6.7 KiB
TypeScript
// @ts-nocheck
|
|
// @jest-environment node
|
|
import { describe, test, expect, beforeAll } from '@jest/globals';
|
|
import { getVideoComments, getVideoCommentsSummary } from '../modules/comments.js';
|
|
import type { CommentsResponse } from '../modules/comments.js';
|
|
import { CONFIG } from '../config.js';
|
|
|
|
// Set Python environment
|
|
process.env.PYTHONPATH = '';
|
|
process.env.PYTHONHOME = '';
|
|
|
|
describe('Video Comments Extraction', () => {
|
|
// Using a popular video that should have comments enabled
|
|
const testUrl = 'https://www.youtube.com/watch?v=jNQXAC9IVRw';
|
|
|
|
describe('getVideoComments', () => {
|
|
test('should extract comments from YouTube video', async () => {
|
|
const commentsJson = await getVideoComments(testUrl, 5, 'top', CONFIG);
|
|
const data: CommentsResponse = JSON.parse(commentsJson);
|
|
|
|
// Verify response structure
|
|
expect(data).toHaveProperty('count');
|
|
expect(data).toHaveProperty('has_more');
|
|
expect(data).toHaveProperty('comments');
|
|
expect(Array.isArray(data.comments)).toBe(true);
|
|
expect(data.count).toBeGreaterThan(0);
|
|
expect(data.count).toBeLessThanOrEqual(5);
|
|
}, 60000);
|
|
|
|
test('should return comments with expected fields', async () => {
|
|
const commentsJson = await getVideoComments(testUrl, 3, 'top', CONFIG);
|
|
const data: CommentsResponse = JSON.parse(commentsJson);
|
|
|
|
if (data.comments.length > 0) {
|
|
const comment = data.comments[0];
|
|
|
|
// These fields should typically be present
|
|
expect(comment).toHaveProperty('text');
|
|
expect(comment).toHaveProperty('author');
|
|
|
|
// Verify text is a string
|
|
if (comment.text !== undefined) {
|
|
expect(typeof comment.text).toBe('string');
|
|
}
|
|
if (comment.author !== undefined) {
|
|
expect(typeof comment.author).toBe('string');
|
|
}
|
|
}
|
|
}, 60000);
|
|
|
|
test('should respect maxComments parameter', async () => {
|
|
const commentsJson = await getVideoComments(testUrl, 3, 'top', CONFIG);
|
|
const data: CommentsResponse = JSON.parse(commentsJson);
|
|
|
|
expect(data.comments.length).toBeLessThanOrEqual(3);
|
|
}, 60000);
|
|
|
|
test('should support different sort orders', async () => {
|
|
// Just verify both sort orders work without error
|
|
const topComments = await getVideoComments(testUrl, 2, 'top', CONFIG);
|
|
const topData: CommentsResponse = JSON.parse(topComments);
|
|
expect(topData).toHaveProperty('comments');
|
|
|
|
const newComments = await getVideoComments(testUrl, 2, 'new', CONFIG);
|
|
const newData: CommentsResponse = JSON.parse(newComments);
|
|
expect(newData).toHaveProperty('comments');
|
|
}, 90000);
|
|
|
|
test('should throw error for invalid URL', async () => {
|
|
await expect(getVideoComments('invalid-url', 5, 'top', CONFIG)).rejects.toThrow();
|
|
});
|
|
|
|
test('should throw error for unsupported URL', async () => {
|
|
await expect(getVideoComments('https://example.com/video', 5, 'top', CONFIG)).rejects.toThrow();
|
|
}, 30000);
|
|
});
|
|
|
|
describe('getVideoCommentsSummary', () => {
|
|
test('should generate human-readable summary', async () => {
|
|
const summary = await getVideoCommentsSummary(testUrl, 5, CONFIG);
|
|
|
|
expect(typeof summary).toBe('string');
|
|
expect(summary.length).toBeGreaterThan(0);
|
|
|
|
// Should contain header
|
|
expect(summary).toContain('Video Comments');
|
|
|
|
// Should have formatted content
|
|
expect(summary).toContain('Author:');
|
|
}, 60000);
|
|
|
|
test('should respect maxComments parameter', async () => {
|
|
const summary = await getVideoCommentsSummary(testUrl, 3, CONFIG);
|
|
|
|
// Count occurrences of "Author:" to verify number of comments
|
|
const authorMatches = summary.match(/Author:/g);
|
|
if (authorMatches) {
|
|
expect(authorMatches.length).toBeLessThanOrEqual(3);
|
|
}
|
|
}, 60000);
|
|
|
|
test('should throw error for invalid URL', async () => {
|
|
await expect(getVideoCommentsSummary('invalid-url', 5, CONFIG)).rejects.toThrow();
|
|
});
|
|
|
|
test('should handle videos with different comment counts', async () => {
|
|
const summary = await getVideoCommentsSummary(testUrl, 10, CONFIG);
|
|
|
|
// Summary should be a valid string
|
|
expect(typeof summary).toBe('string');
|
|
expect(summary.trim().length).toBeGreaterThan(0);
|
|
}, 60000);
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
test('should provide helpful error message for unavailable video', async () => {
|
|
const unavailableUrl = 'https://www.youtube.com/watch?v=invalid_video_id_xyz123';
|
|
|
|
await expect(getVideoComments(unavailableUrl, 5, 'top', CONFIG)).rejects.toThrow();
|
|
}, 30000);
|
|
|
|
test('should handle unsupported URLs gracefully', async () => {
|
|
const unsupportedUrl = 'https://example.com/not-a-video';
|
|
|
|
await expect(getVideoComments(unsupportedUrl, 5, 'top', CONFIG)).rejects.toThrow();
|
|
}, 30000);
|
|
});
|
|
|
|
describe('Comment Fields', () => {
|
|
test('should include author information when available', async () => {
|
|
const commentsJson = await getVideoComments(testUrl, 5, 'top', CONFIG);
|
|
const data: CommentsResponse = JSON.parse(commentsJson);
|
|
|
|
if (data.comments.length > 0) {
|
|
const comment = data.comments[0];
|
|
|
|
// Author fields
|
|
if (comment.author !== undefined) {
|
|
expect(typeof comment.author).toBe('string');
|
|
}
|
|
if (comment.author_id !== undefined) {
|
|
expect(typeof comment.author_id).toBe('string');
|
|
}
|
|
}
|
|
}, 60000);
|
|
|
|
test('should include engagement metrics when available', async () => {
|
|
const commentsJson = await getVideoComments(testUrl, 5, 'top', CONFIG);
|
|
const data: CommentsResponse = JSON.parse(commentsJson);
|
|
|
|
if (data.comments.length > 0) {
|
|
// At least one top comment should have like_count
|
|
const hasLikes = data.comments.some(c =>
|
|
c.like_count !== undefined && typeof c.like_count === 'number'
|
|
);
|
|
// This is optional - some comments may not have likes
|
|
expect(hasLikes || data.comments.length > 0).toBe(true);
|
|
}
|
|
}, 60000);
|
|
|
|
test('should handle boolean flags correctly', async () => {
|
|
const commentsJson = await getVideoComments(testUrl, 10, 'top', CONFIG);
|
|
const data: CommentsResponse = JSON.parse(commentsJson);
|
|
|
|
for (const comment of data.comments) {
|
|
// Boolean flags should be boolean or undefined
|
|
if (comment.is_pinned !== undefined) {
|
|
expect(typeof comment.is_pinned).toBe('boolean');
|
|
}
|
|
if (comment.author_is_uploader !== undefined) {
|
|
expect(typeof comment.author_is_uploader).toBe('boolean');
|
|
}
|
|
if (comment.author_is_verified !== undefined) {
|
|
expect(typeof comment.author_is_verified).toBe('boolean');
|
|
}
|
|
}
|
|
}, 60000);
|
|
});
|
|
});
|