mirror of
https://github.com/nicetry247/offlineacademy.git
synced 2026-07-26 11:58:44 +00:00
88 lines
3.1 KiB
TypeScript
88 lines
3.1 KiB
TypeScript
import { readdir, stat } from 'fs/promises'
|
|
import { join, extname, basename } from 'path'
|
|
import { writeFile, mkdir } from 'fs/promises'
|
|
import { existsSync } from 'fs'
|
|
|
|
const LOCAL_THUMBNAIL_NAMES = ['cover', 'thumbnail']
|
|
const LOCAL_THUMBNAIL_EXTS = ['.jpg', '.jpeg', '.png', '.webp']
|
|
|
|
const COVER_GRADIENTS = [
|
|
'bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500',
|
|
'bg-gradient-to-br from-blue-500 via-cyan-500 to-teal-500',
|
|
'bg-gradient-to-br from-emerald-500 via-green-500 to-lime-500',
|
|
'bg-gradient-to-br from-orange-500 via-red-500 to-rose-500',
|
|
'bg-gradient-to-br from-violet-500 via-fuchsia-500 to-pink-500',
|
|
'bg-gradient-to-br from-sky-500 via-blue-500 to-indigo-500',
|
|
'bg-gradient-to-br from-amber-500 via-orange-500 to-red-500',
|
|
'bg-gradient-to-br from-teal-500 via-emerald-500 to-green-500',
|
|
]
|
|
|
|
export function getDeterministicGradient(title: string): string {
|
|
let hash = 0
|
|
for (let i = 0; i < title.length; i++) {
|
|
hash = ((hash << 5) - hash + title.charCodeAt(i)) | 0
|
|
}
|
|
const index = Math.abs(hash) % COVER_GRADIENTS.length
|
|
return COVER_GRADIENTS[index]
|
|
}
|
|
|
|
export async function getLocalCourseThumbnailPath(coursePath: string): Promise<string | null> {
|
|
try {
|
|
const entries = await readdir(coursePath, { withFileTypes: true })
|
|
const files = entries.filter(e => e.isFile())
|
|
|
|
for (const baseName of LOCAL_THUMBNAIL_NAMES) {
|
|
for (const ext of LOCAL_THUMBNAIL_EXTS) {
|
|
const match = files.find(f => f.name.toLowerCase() === `${baseName}${ext}`)
|
|
if (match) {
|
|
return join(coursePath, match.name)
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// coursePath may not exist yet or be unreadable
|
|
}
|
|
return null
|
|
}
|
|
|
|
export async function ensurePublicThumbnail(
|
|
courseSlug: string,
|
|
sourcePath: string,
|
|
): Promise<string | null> {
|
|
const ext = extname(sourcePath).toLowerCase()
|
|
if (!['.jpg', '.jpeg', '.png', '.webp'].includes(ext)) return null
|
|
|
|
const thumbDir = join(process.cwd(), 'public', 'thumbnails', 'courses')
|
|
if (!existsSync(thumbDir)) {
|
|
await mkdir(thumbDir, { recursive: true })
|
|
}
|
|
|
|
const targetName = `${courseSlug}-thumb${ext}`
|
|
const targetPath = join(thumbDir, targetName)
|
|
|
|
try {
|
|
const sourceStat = await stat(sourcePath)
|
|
const targetStat = await stat(targetPath).catch(() => null)
|
|
if (!targetStat || sourceStat.mtimeMs !== targetStat.mtimeMs || sourceStat.size !== targetStat.size) {
|
|
const { copyFile } = await import('fs/promises')
|
|
await copyFile(sourcePath, targetPath)
|
|
const targetStat2 = await stat(targetPath)
|
|
await stat(targetPath) // ensure exists
|
|
try {
|
|
// touch target mtime to match source so next run can diff by mtime
|
|
const { utimes } = await import('fs/promises')
|
|
await utimes(targetPath, new Date(sourceStat.mtimeMs), new Date(sourceStat.mtimeMs))
|
|
} catch {
|
|
// ignore utimes failure; content copy is what matters
|
|
}
|
|
}
|
|
return `/thumbnails/courses/${targetName}`
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function getThumbnailPriority(coursePath: string): 'local' | 'downloaded' | 'gradient' {
|
|
return 'local'
|
|
}
|