import { prisma } from './prisma' import { getVideoDownloadAvailability, getVideoPreviewUnavailableMessage, type VideoDownloadAvailability, type VideoPolicyOptions, } from './video-download-policy' const VIDEO_MEDIA_EXTENSIONS = ['.mp4', '.webm', '.ogg'] type VideoMaterialMatch = { id: string materialType: string | null createdAt: Date | string | null } export type BlockedVideoPreviewMatch = { material: VideoMaterialMatch availability: VideoDownloadAvailability message: string } function safeDecode(value: string) { try { return decodeURIComponent(value) } catch { return value } } function normalizePathSlashes(value: string) { return value.replace(/\\/g, '/').replace(/\/+/g, '/') } function getPathnameCandidate(value: string) { try { return new URL(value).pathname } catch { return value } } function getFileNameCandidate(value: string) { const pathname = normalizePathSlashes(getPathnameCandidate(value)).split(/[?#]/)[0] const fileName = pathname.split('/').filter(Boolean).pop() || '' return safeDecode(fileName) } export function isVideoMediaPath(pathname: string) { const normalizedPath = safeDecode(String(pathname || '')).toLowerCase().split(/[?#]/)[0] return VIDEO_MEDIA_EXTENSIONS.some((extension) => normalizedPath.endsWith(extension)) } export function buildVideoUrlCandidates(values: Array) { const candidates = new Set() for (const value of values) { if (!value) continue const rawValue = String(value).trim() if (!rawValue) continue const decodedValue = safeDecode(rawValue) candidates.add(rawValue) candidates.add(decodedValue) const pathname = normalizePathSlashes(getPathnameCandidate(decodedValue)).split(/[?#]/)[0] if (pathname) { const decodedPathname = safeDecode(pathname) candidates.add(pathname) candidates.add(decodedPathname) candidates.add(decodedPathname.replace(/^\/+/, '')) if (decodedPathname.startsWith('/api/uploads/')) { const uploadRelativePath = decodedPathname.replace(/^\/api\/uploads\/+/, '') candidates.add(uploadRelativePath) candidates.add(`uploads/${uploadRelativePath}`) candidates.add(`/uploads/${uploadRelativePath}`) } if (decodedPathname.startsWith('/uploads/')) { const uploadRelativePath = decodedPathname.replace(/^\/uploads\/+/, '') candidates.add(uploadRelativePath) candidates.add(`uploads/${uploadRelativePath}`) candidates.add(`/uploads/${uploadRelativePath}`) } } } return Array.from(candidates).filter(Boolean) } function buildExactUrlWhere(candidates: string[]) { return [ { videoPreviewClipUrl: { in: candidates } }, { videoPreview540Url: { in: candidates } }, { videoSourceUrl: { in: candidates } }, { zipFilePath: { in: candidates } }, { zipFileName: { in: candidates } }, ] } function buildFileNameWhere(fileNames: string[]) { return fileNames.flatMap((fileName) => [ { videoPreviewClipUrl: { contains: fileName } }, { videoPreview540Url: { contains: fileName } }, { videoSourceUrl: { contains: fileName } }, { zipFilePath: { contains: fileName } }, { zipFileName: { contains: fileName } }, ]) } async function findVideoMaterialByUrlCandidates( candidates: string[], ): Promise { if (candidates.length === 0) { return null } const select = { id: true, materialType: true, createdAt: true, } as const const exactMatch = await prisma.material.findFirst({ where: { deletedAt: null, OR: buildExactUrlWhere(candidates), }, select, }) if (exactMatch) { return exactMatch } const fileNames = Array.from( new Set( candidates .map(getFileNameCandidate) .filter((fileName) => fileName.length >= 12 && isVideoMediaPath(fileName)), ), ) if (fileNames.length === 0) { return null } return prisma.material.findFirst({ where: { deletedAt: null, OR: buildFileNameWhere(fileNames), }, select, }) } export async function findBlockedVideoPreviewByUrl( values: Array, now: Date = new Date(), options?: VideoPolicyOptions, ): Promise { const candidates = buildVideoUrlCandidates(values) const material = await findVideoMaterialByUrlCandidates(candidates) if (!material) { return null } const availability = getVideoDownloadAvailability( material.materialType, material.createdAt, now, options, ) if (availability.allowed) { return null } return { material, availability, message: getVideoPreviewUnavailableMessage(availability), } }