diff --git a/src/lib/integrations/youtube/YouTubeClient.ts b/src/lib/integrations/youtube/YouTubeClient.ts index c1b74b9..88e097e 100644 --- a/src/lib/integrations/youtube/YouTubeClient.ts +++ b/src/lib/integrations/youtube/YouTubeClient.ts @@ -201,6 +201,37 @@ export class YouTubeClient { } } + /** + * Video-Statistiken für mehrere Videos abrufen (Batch) + * YouTube API erlaubt max. 50 IDs pro Request + */ + async getVideoStatistics(videoIds: string[]): Promise> { + if (videoIds.length === 0) return [] + + try { + const response = await this.youtube.videos.list({ + part: ['statistics'], + id: videoIds, + maxResults: 50, + }) + + return (response.data.items || []).map((item) => ({ + id: item.id!, + views: parseInt(item.statistics?.viewCount || '0', 10), + likes: parseInt(item.statistics?.likeCount || '0', 10), + comments: parseInt(item.statistics?.commentCount || '0', 10), + })) + } catch (error) { + console.error('Error fetching video statistics:', error) + throw error + } + } + /** * Kanal-Statistiken abrufen */ diff --git a/tests/unit/youtube/youtube-client-stats.unit.spec.ts b/tests/unit/youtube/youtube-client-stats.unit.spec.ts new file mode 100644 index 0000000..693c361 --- /dev/null +++ b/tests/unit/youtube/youtube-client-stats.unit.spec.ts @@ -0,0 +1,200 @@ +/** + * YouTubeClient.getVideoStatistics Unit Tests + * + * Tests the batch video statistics retrieval method that fetches + * view counts, like counts, and comment counts for multiple videos. + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockVideosList = vi.fn() + +vi.mock('googleapis', () => { + class MockOAuth2 { + setCredentials(): void { + // no-op + } + } + + return { + google: { + auth: { + OAuth2: MockOAuth2, + }, + youtube: () => ({ + videos: { + list: mockVideosList, + }, + commentThreads: { list: vi.fn() }, + comments: { + insert: vi.fn(), + setModerationStatus: vi.fn(), + delete: vi.fn(), + }, + channels: { list: vi.fn() }, + }), + }, + } +}) + +const mockPayload = {} as import('payload').Payload + +describe('YouTubeClient.getVideoStatistics', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should return parsed statistics for multiple videos', async () => { + mockVideosList.mockResolvedValueOnce({ + data: { + items: [ + { + id: 'vid1', + statistics: { + viewCount: '1500', + likeCount: '120', + commentCount: '45', + }, + }, + { + id: 'vid2', + statistics: { + viewCount: '3200', + likeCount: '250', + commentCount: '80', + }, + }, + ], + }, + }) + + const { YouTubeClient } = await import( + '@/lib/integrations/youtube/YouTubeClient' + ) + + const client = new YouTubeClient( + { + clientId: 'test-id', + clientSecret: 'test-secret', + accessToken: 'test-token', + refreshToken: 'test-refresh', + }, + mockPayload, + ) + + const result = await client.getVideoStatistics(['vid1', 'vid2']) + + expect(result).toEqual([ + { id: 'vid1', views: 1500, likes: 120, comments: 45 }, + { id: 'vid2', views: 3200, likes: 250, comments: 80 }, + ]) + + expect(mockVideosList).toHaveBeenCalledWith({ + part: ['statistics'], + id: ['vid1', 'vid2'], + maxResults: 50, + }) + }) + + it('should return empty array for empty input without calling API', async () => { + const { YouTubeClient } = await import( + '@/lib/integrations/youtube/YouTubeClient' + ) + + const client = new YouTubeClient( + { + clientId: 'test-id', + clientSecret: 'test-secret', + accessToken: 'test-token', + refreshToken: 'test-refresh', + }, + mockPayload, + ) + + const result = await client.getVideoStatistics([]) + + expect(result).toEqual([]) + expect(mockVideosList).not.toHaveBeenCalled() + }) + + it('should default missing statistics to zero', async () => { + mockVideosList.mockResolvedValueOnce({ + data: { + items: [ + { + id: 'vid3', + statistics: {}, + }, + ], + }, + }) + + const { YouTubeClient } = await import( + '@/lib/integrations/youtube/YouTubeClient' + ) + + const client = new YouTubeClient( + { + clientId: 'test-id', + clientSecret: 'test-secret', + accessToken: 'test-token', + refreshToken: 'test-refresh', + }, + mockPayload, + ) + + const result = await client.getVideoStatistics(['vid3']) + + expect(result).toEqual([ + { id: 'vid3', views: 0, likes: 0, comments: 0 }, + ]) + }) + + it('should handle null items in API response', async () => { + mockVideosList.mockResolvedValueOnce({ + data: { + items: null, + }, + }) + + const { YouTubeClient } = await import( + '@/lib/integrations/youtube/YouTubeClient' + ) + + const client = new YouTubeClient( + { + clientId: 'test-id', + clientSecret: 'test-secret', + accessToken: 'test-token', + refreshToken: 'test-refresh', + }, + mockPayload, + ) + + const result = await client.getVideoStatistics(['vid-missing']) + + expect(result).toEqual([]) + }) + + it('should propagate API errors', async () => { + mockVideosList.mockRejectedValueOnce(new Error('API quota exceeded')) + + const { YouTubeClient } = await import( + '@/lib/integrations/youtube/YouTubeClient' + ) + + const client = new YouTubeClient( + { + clientId: 'test-id', + clientSecret: 'test-secret', + accessToken: 'test-token', + refreshToken: 'test-refresh', + }, + mockPayload, + ) + + await expect( + client.getVideoStatistics(['vid1']), + ).rejects.toThrow('API quota exceeded') + }) +})