WIP blurash & sync card ?
All checks were successful
Deploy App / build (push) Successful in 26s
Deploy App / deploy (push) Successful in 18s

This commit is contained in:
valere
2026-02-21 09:07:45 +01:00
parent 543b513e08
commit aef705834b
8 changed files with 425 additions and 20 deletions

View File

@@ -14,6 +14,7 @@ export const cards = sqliteTable('cards', {
slug: text('slug').notNull(),
suit: text('suit').notNull(),
rank: text('rank').notNull(),
blurhash: text('blurhash').notNull(), // blurhash of the image
createdAt: int('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),

View File

@@ -1,6 +1,7 @@
import { eq, notInArray } from 'drizzle-orm'
import { useDB, schema } from '../db'
import { scanMusicFolder } from '../utils/fileScanner'
import { generateBlurhash } from '../utils/blurHash'
const { cards } = schema
@@ -21,27 +22,34 @@ export async function syncCardsWithDatabase(folderPath: string) {
const scannedEsids = new Set(scannedCards.map((t) => t.esid))
const cardsToDelete = existingCards.filter((t) => !scannedEsids.has(t.esid))
// 4. Insérer les nouvelles cards
// 4. Insérer les nouvelles cartes
if (cardsToInsert.length > 0) {
// Dans la fonction syncCardsWithDatabase
await db.insert(cards).values(
cardsToInsert.map((card) => ({
url_audio: card.url_audio,
url_image: card.url_image,
year: card.year,
month: card.month,
day: card.day,
hour: card.hour,
artist: card.artist,
title: card.title,
esid: card.esid,
slug: card.slug,
createdAt: card.createdAt,
suit: card.suit,
rank: card.rank
}))
// Générer tous les blurhash en parallèle
const cardsWithBlurhash = await Promise.all(
cardsToInsert.map(async (card) => {
const blurhash = await generateBlurhash(card.url_image)
return {
url_audio: card.url_audio,
url_image: card.url_image,
year: card.year,
month: card.month,
day: card.day,
hour: card.hour,
artist: card.artist,
title: card.title,
esid: card.esid,
slug: card.slug,
createdAt: card.createdAt,
suit: card.suit,
rank: card.rank,
blurhash: blurhash
}
})
)
console.log(`${cardsToInsert.length} cards ajoutées`)
// Insérer les cartes avec les blurhash déjà résolus
await db.insert(cards).values(cardsWithBlurhash)
console.log(`${cardsToInsert.length} cartes ajoutées`)
}
// 5. Supprimer les cards obsolètes avec une requête distincte pour chaque esid

114
server/utils/blurHash.ts Normal file
View File

@@ -0,0 +1,114 @@
// server/utils/blurHash.ts
import sharp from 'sharp'
import { encode } from 'blurhash'
import { $fetch } from 'ofetch'
import { mkdir, writeFile, unlink } from 'node:fs/promises'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { randomUUID } from 'node:crypto'
// Dossier temporaire pour les images téléchargées
const TMP_DIR = join(tmpdir(), 'evilspins-images')
async function ensureTmpDir() {
try {
await mkdir(TMP_DIR, { recursive: true })
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
}
}
async function downloadImage(url: string): Promise<string> {
await ensureTmpDir()
const tmpPath = join(TMP_DIR, `${randomUUID()}.jpg`)
try {
const response = await $fetch(url, {
responseType: 'arrayBuffer',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
Accept: 'image/*,*/*;q=0.8'
},
// Timeout plus long
timeout: 60000, // 60 secondes
// Désactiver la compression pour les images
headers: {
'Accept-Encoding': 'identity'
}
})
await writeFile(tmpPath, Buffer.from(response))
return tmpPath
} catch (error) {
// Nettoyer en cas d'erreur
try {
await unlink(tmpPath).catch(() => {})
} catch {
/* Ignorer les erreurs de suppression */
}
console.error(`Failed to download image from ${url}:`, error.message)
throw error
}
}
function createDefaultBlurhash() {
// Un blurhash plus discret
return 'L5H2EC=H00~q^-=wD6xuxvV@%KxZ'
}
export async function generateBlurhash(
input: Buffer | string,
componentX: number = 4,
componentY: number = 3
): Promise<string> {
let tmpPath: string | null = null
try {
if (typeof input === 'string') {
if (input.startsWith('http')) {
try {
tmpPath = await downloadImage(input)
input = tmpPath
} catch (error) {
console.warn(`Using default blurhash for ${input} due to download error`)
return createDefaultBlurhash()
}
}
// Vérifier si le fichier existe
try {
const image = sharp(input).resize(32, 32, { fit: 'inside' }).ensureAlpha()
const { data, info } = await image.raw().toBuffer({ resolveWithObject: true })
return encode(new Uint8ClampedArray(data), info.width, info.height, componentX, componentY)
} catch (error) {
console.warn(`Error processing image ${input}:`, error.message)
return createDefaultBlurhash()
}
} else {
// Si c'est déjà un Buffer
try {
const image = sharp(input).resize(32, 32, { fit: 'inside' }).ensureAlpha()
const { data, info } = await image.raw().toBuffer({ resolveWithObject: true })
return encode(new Uint8ClampedArray(data), info.width, info.height, componentX, componentY)
} catch (error) {
console.warn('Error processing image buffer:', error.message)
return createDefaultBlurhash()
}
}
} catch (error) {
console.error('Unexpected error in generateBlurhash:', error.message)
return createDefaultBlurhash()
} finally {
// Nettoyer le fichier temporaire s'il existe
if (tmpPath) {
try {
await unlink(tmpPath).catch(() => {})
} catch {
/* Ignorer les erreurs de suppression */
}
}
}
}