mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 17:24:12 +00:00
feat: add downloadAndUploadImage utility for YouTube thumbnails
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0691031c36
commit
52a6bce815
2 changed files with 171 additions and 0 deletions
76
src/lib/utils/media-download.ts
Normal file
76
src/lib/utils/media-download.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import type { Payload } from 'payload'
|
||||
|
||||
interface DownloadOptions {
|
||||
url: string
|
||||
filename: string
|
||||
alt?: string
|
||||
tenantId?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads an image from a URL and creates a Payload Media document.
|
||||
*
|
||||
* - Checks for existing media with same filename first (deduplication)
|
||||
* - Returns the Media document ID on success, null on failure
|
||||
* - Never throws — errors are logged and null is returned
|
||||
*/
|
||||
export async function downloadAndUploadImage(
|
||||
payload: Payload,
|
||||
options: DownloadOptions,
|
||||
): Promise<number | null> {
|
||||
const { url, filename, alt, tenantId } = options
|
||||
|
||||
try {
|
||||
// Check for existing media with same filename
|
||||
const existing = await payload.find({
|
||||
collection: 'media',
|
||||
where: {
|
||||
filename: { equals: filename },
|
||||
...(tenantId ? { tenant: { equals: tenantId } } : {}),
|
||||
},
|
||||
limit: 1,
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
if (existing.docs[0]) {
|
||||
return existing.docs[0].id as number
|
||||
}
|
||||
|
||||
// Download the image with timeout
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), 10_000)
|
||||
|
||||
const response = await fetch(url, { signal: controller.signal })
|
||||
clearTimeout(timeout)
|
||||
|
||||
if (!response.ok) {
|
||||
console.log(`[media-download] HTTP ${response.status} for ${url}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
const buffer = Buffer.from(arrayBuffer)
|
||||
const contentType = response.headers.get('content-type') || 'image/jpeg'
|
||||
|
||||
// Create Payload Media document
|
||||
const media = await payload.create({
|
||||
collection: 'media',
|
||||
data: {
|
||||
alt: alt || filename.replace(/[-_]/g, ' ').replace(/\.\w+$/, ''),
|
||||
...(tenantId ? { tenant: tenantId } : {}),
|
||||
},
|
||||
file: {
|
||||
name: filename,
|
||||
data: buffer,
|
||||
mimetype: contentType,
|
||||
size: buffer.length,
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`[media-download] Created media ${media.id} from ${url}`)
|
||||
return media.id as number
|
||||
} catch (error) {
|
||||
console.log(`[media-download] Failed to download ${url}:`, (error as Error).message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
95
tests/unit/media-download.unit.spec.ts
Normal file
95
tests/unit/media-download.unit.spec.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// tests/unit/media-download.unit.spec.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { downloadAndUploadImage } from '../../src/lib/utils/media-download'
|
||||
|
||||
// Mock fetch globally
|
||||
const mockFetch = vi.fn()
|
||||
vi.stubGlobal('fetch', mockFetch)
|
||||
|
||||
// Mock payload instance
|
||||
const mockPayload = {
|
||||
find: vi.fn(),
|
||||
create: vi.fn(),
|
||||
}
|
||||
|
||||
describe('downloadAndUploadImage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should download image and create media document', async () => {
|
||||
// Mock successful fetch
|
||||
const mockBuffer = Buffer.from('fake-image-data')
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
headers: { get: (h: string) => (h === 'content-type' ? 'image/jpeg' : '1024') },
|
||||
arrayBuffer: () => Promise.resolve(mockBuffer.buffer),
|
||||
})
|
||||
|
||||
// Mock no existing duplicate
|
||||
mockPayload.find.mockResolvedValue({ docs: [], totalDocs: 0 })
|
||||
|
||||
// Mock media creation
|
||||
mockPayload.create.mockResolvedValue({ id: 42 })
|
||||
|
||||
const result = await downloadAndUploadImage(mockPayload as any, {
|
||||
url: 'https://img.youtube.com/vi/abc123/hqdefault.jpg',
|
||||
filename: 'abc123_thumbnail.jpg',
|
||||
alt: 'Video Thumbnail',
|
||||
})
|
||||
|
||||
expect(result).toBe(42)
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://img.youtube.com/vi/abc123/hqdefault.jpg',
|
||||
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
||||
)
|
||||
expect(mockPayload.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
collection: 'media',
|
||||
file: expect.objectContaining({
|
||||
name: 'abc123_thumbnail.jpg',
|
||||
mimetype: 'image/jpeg',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should return existing media ID if duplicate found', async () => {
|
||||
mockPayload.find.mockResolvedValue({
|
||||
docs: [{ id: 99 }],
|
||||
totalDocs: 1,
|
||||
})
|
||||
|
||||
const result = await downloadAndUploadImage(mockPayload as any, {
|
||||
url: 'https://img.youtube.com/vi/abc123/hqdefault.jpg',
|
||||
filename: 'abc123_thumbnail.jpg',
|
||||
})
|
||||
|
||||
expect(result).toBe(99)
|
||||
expect(mockFetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return null on fetch error', async () => {
|
||||
mockPayload.find.mockResolvedValue({ docs: [], totalDocs: 0 })
|
||||
mockFetch.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const result = await downloadAndUploadImage(mockPayload as any, {
|
||||
url: 'https://img.youtube.com/vi/bad/hqdefault.jpg',
|
||||
filename: 'bad_thumbnail.jpg',
|
||||
})
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null on non-ok response', async () => {
|
||||
mockPayload.find.mockResolvedValue({ docs: [], totalDocs: 0 })
|
||||
mockFetch.mockResolvedValue({ ok: false, status: 404 })
|
||||
|
||||
const result = await downloadAndUploadImage(mockPayload as any, {
|
||||
url: 'https://img.youtube.com/vi/notfound/hqdefault.jpg',
|
||||
filename: 'notfound_thumbnail.jpg',
|
||||
})
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue