feat(youtube): add getVideoStatistics to YouTubeClient

Add batch video statistics retrieval method that fetches view counts,
like counts, and comment counts for up to 50 videos per request.
Includes unit tests covering normal operation, empty input, missing
statistics defaults, null API response, and error propagation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martin Porwoll 2026-02-14 13:21:33 +00:00
parent 097bc5225c
commit 065e75b014
2 changed files with 231 additions and 0 deletions

View file

@ -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<Array<{
id: string
views: number
likes: number
comments: number
}>> {
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
*/

View file

@ -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')
})
})