- use Prisma.JsonValue and Prisma input types throughout - add proper type guards for array and object checks - replace any with Record<string, unknown> where appropriate - all API/RSS routes now have zero any type errors
218 lines
4.9 KiB
TypeScript
218 lines
4.9 KiB
TypeScript
import type { RequestHandler } from './$types'
|
|
import type { Prisma } from '@prisma/client'
|
|
import { prisma } from '$lib/server/database'
|
|
import {
|
|
jsonResponse,
|
|
errorResponse,
|
|
getPaginationParams,
|
|
getPaginationMeta,
|
|
checkAdminAuth
|
|
} from '$lib/server/api-utils'
|
|
import { logger } from '$lib/server/logger'
|
|
|
|
// GET /api/media - List all media with pagination and filters
|
|
export const GET: RequestHandler = async (event) => {
|
|
// Check authentication
|
|
if (!checkAdminAuth(event)) {
|
|
return errorResponse('Unauthorized', 401)
|
|
}
|
|
|
|
try {
|
|
const { page, limit } = getPaginationParams(event.url)
|
|
const skip = (page - 1) * limit
|
|
|
|
// Get filter parameters
|
|
const mimeType = event.url.searchParams.get('mimeType')
|
|
const unused = event.url.searchParams.get('unused') === 'true'
|
|
const search = event.url.searchParams.get('search')
|
|
const publishedFilter = event.url.searchParams.get('publishedFilter')
|
|
const sort = event.url.searchParams.get('sort') || 'newest'
|
|
const albumId = event.url.searchParams.get('albumId')
|
|
|
|
// Build where clause
|
|
const whereConditions: Prisma.MediaWhereInput[] = []
|
|
|
|
// Handle mime type filtering
|
|
if (mimeType && mimeType !== 'all') {
|
|
switch (mimeType) {
|
|
case 'image':
|
|
// JPG, PNG images (excluding GIF which is in video)
|
|
whereConditions.push({
|
|
OR: [
|
|
{ mimeType: { startsWith: 'image/jpeg' } },
|
|
{ mimeType: { startsWith: 'image/jpg' } },
|
|
{ mimeType: { startsWith: 'image/png' } },
|
|
{ mimeType: { startsWith: 'image/webp' } }
|
|
]
|
|
})
|
|
break
|
|
case 'video':
|
|
// MP4, MOV, GIF
|
|
whereConditions.push({
|
|
OR: [{ mimeType: { startsWith: 'video/' } }, { mimeType: { equals: 'image/gif' } }]
|
|
})
|
|
break
|
|
case 'audio':
|
|
// WAV, MP3, M4A
|
|
whereConditions.push({
|
|
mimeType: { startsWith: 'audio/' }
|
|
})
|
|
break
|
|
case 'vector':
|
|
// SVG
|
|
whereConditions.push({
|
|
mimeType: { equals: 'image/svg+xml' }
|
|
})
|
|
break
|
|
}
|
|
}
|
|
|
|
if (unused) {
|
|
whereConditions.push({ usedIn: { equals: [] } })
|
|
}
|
|
|
|
if (search) {
|
|
whereConditions.push({ filename: { contains: search, mode: 'insensitive' } })
|
|
}
|
|
|
|
// Filter by album if specified
|
|
if (albumId) {
|
|
whereConditions.push({
|
|
albums: {
|
|
some: {
|
|
albumId: parseInt(albumId)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Handle published filter
|
|
if (publishedFilter && publishedFilter !== 'all') {
|
|
switch (publishedFilter) {
|
|
case 'unpublished':
|
|
// Media that is not used anywhere and not marked as photography
|
|
whereConditions.push({
|
|
AND: [
|
|
{ usedIn: { equals: [] } },
|
|
{ isPhotography: false },
|
|
{
|
|
usage: {
|
|
none: {}
|
|
}
|
|
},
|
|
{
|
|
albums: {
|
|
none: {}
|
|
}
|
|
}
|
|
]
|
|
})
|
|
break
|
|
case 'photos':
|
|
// Media marked as photography or used in albums
|
|
whereConditions.push({
|
|
OR: [
|
|
{ isPhotography: true },
|
|
{ usedIn: { array_contains: 'album' } },
|
|
{
|
|
albums: {
|
|
some: {}
|
|
}
|
|
}
|
|
]
|
|
})
|
|
break
|
|
case 'universe':
|
|
// Media used in blog posts or essays (check both usedIn and usage relation)
|
|
whereConditions.push({
|
|
OR: [
|
|
{ usedIn: { array_contains: 'post' } },
|
|
{ usedIn: { array_contains: 'essay' } },
|
|
{
|
|
usage: {
|
|
some: {
|
|
contentType: { in: ['post', 'essay'] }
|
|
}
|
|
}
|
|
}
|
|
]
|
|
})
|
|
break
|
|
}
|
|
}
|
|
|
|
// Combine all conditions with AND
|
|
const where =
|
|
whereConditions.length > 0
|
|
? whereConditions.length === 1
|
|
? whereConditions[0]
|
|
: { AND: whereConditions }
|
|
: {}
|
|
|
|
// Build orderBy clause based on sort parameter
|
|
let orderBy: Prisma.MediaOrderByWithRelationInput = { createdAt: 'desc' } // default to newest
|
|
|
|
switch (sort) {
|
|
case 'oldest':
|
|
orderBy = { createdAt: 'asc' }
|
|
break
|
|
case 'name-asc':
|
|
orderBy = { filename: 'asc' }
|
|
break
|
|
case 'name-desc':
|
|
orderBy = { filename: 'desc' }
|
|
break
|
|
case 'size-asc':
|
|
orderBy = { size: 'asc' }
|
|
break
|
|
case 'size-desc':
|
|
orderBy = { size: 'desc' }
|
|
break
|
|
case 'newest':
|
|
default:
|
|
orderBy = { createdAt: 'desc' }
|
|
break
|
|
}
|
|
|
|
// Get total count
|
|
const total = await prisma.media.count({ where })
|
|
|
|
// Get media items
|
|
const media = await prisma.media.findMany({
|
|
where,
|
|
orderBy,
|
|
skip,
|
|
take: limit,
|
|
select: {
|
|
id: true,
|
|
filename: true,
|
|
mimeType: true,
|
|
size: true,
|
|
url: true,
|
|
thumbnailUrl: true,
|
|
width: true,
|
|
height: true,
|
|
dominantColor: true,
|
|
colors: true,
|
|
aspectRatio: true,
|
|
usedIn: true,
|
|
isPhotography: true,
|
|
createdAt: true,
|
|
description: true,
|
|
exifData: true
|
|
}
|
|
})
|
|
|
|
const pagination = getPaginationMeta(total, page, limit)
|
|
|
|
logger.info('Media list retrieved', { total, page, limit })
|
|
|
|
return jsonResponse({
|
|
media,
|
|
pagination
|
|
})
|
|
} catch (error) {
|
|
logger.error('Failed to retrieve media', error as Error)
|
|
return errorResponse('Failed to retrieve media', 500)
|
|
}
|
|
}
|