const fs = require('fs'); const path = require('path'); const { Client, Databases, Storage, ID, Query } = require('node-appwrite'); const stringSimilarity = require('string-similarity'); const sharp = require('sharp'); require('dotenv').config({ path: '.env.local' }); let InputFile; try { InputFile = require('node-appwrite/file').InputFile; } catch (e) { InputFile = null; } const client = new Client() .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT) .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID) .setKey(process.env.APPWRITE_API_KEY); const databases = new Databases(client); const storage = new Storage(client); const DB_ID = process.env.APPWRITE_DATABASE_ID; const BUCKET_ID = process.env.NEXT_PUBLIC_APPWRITE_MEDIA_BUCKET_ID; const DIR_PENDIENTES = '/home/carlos/Documentos/Almeira Web/imagenes pendientes'; const DIR_PROCESADOS = path.join(DIR_PENDIENTES, 'Procesados'); if (!fs.existsSync(DIR_PROCESADOS)) { fs.mkdirSync(DIR_PROCESADOS); } function normalizeText(text) { if (!text) return ''; return text .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/[^a-z0-9]/g, '') .trim(); } function cleanFilenameTitle(filename) { const ext = path.extname(filename); let base = path.basename(filename, ext); return base.replace(/\(1\)/g, '').trim(); } async function main() { try { console.log('=== INICIANDO PROCESAMIENTO DE IMÁGENES PENDIENTES ==='); console.log(`Endpoint: ${process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT}`); console.log(`Proyecto: ${process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID}`); console.log(`Base de datos: ${DB_ID}`); console.log(`Bucket de medios: ${BUCKET_ID}\n`); // 1. Obtener mapeo dinámico de colecciones const collections = await databases.listCollections(DB_ID); const colMap = {}; collections.collections.forEach(c => { colMap[c.name.toLowerCase()] = c.$id; }); const postsCollectionId = colMap['posts'] || 'posts'; const mediaCollectionId = colMap['media'] || 'media'; // 2. Leer archivos en "imagenes pendientes" const files = fs.readdirSync(DIR_PENDIENTES).filter(file => { const ext = path.extname(file).toLowerCase(); return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.jfif'].includes(ext); }); if (files.length === 0) { console.log('No se encontraron imágenes pendientes en el directorio.'); return; } console.log(`Se encontraron ${files.length} imágenes para procesar.`); // 3. Cargar todos los posts de la base de datos console.log('Cargando publicaciones de la base de datos...'); const posts = []; let cursor = null; while (true) { const queries = [Query.limit(100)]; if (cursor) queries.push(Query.cursorAfter(cursor)); const res = await databases.listDocuments(DB_ID, postsCollectionId, queries); posts.push(...res.documents); if (res.documents.length < 100) break; cursor = res.documents[res.documents.length - 1].$id; } console.log(`Se cargaron ${posts.length} publicaciones de la base de datos.`); const postTitles = posts.map(p => p.titulo || ''); for (let i = 0; i < files.length; i++) { const file = files[i]; const filePath = path.join(DIR_PENDIENTES, file); const cleanTitle = cleanFilenameTitle(file); const normCleanTitle = normalizeText(cleanTitle); console.log(`\n[${i+1}/${files.length}] Procesando archivo: "${file}"`); // Buscar coincidencia en posts let bestPost = null; let bestScore = 0; // Primero buscar coincidencia exacta normalizada for (const post of posts) { const normPostTitle = normalizeText(post.titulo); if (normPostTitle === normCleanTitle) { bestPost = post; bestScore = 1.0; break; } } // Si no hay exacta, buscar por similitud de cadena if (bestScore < 1.0 && postTitles.length > 0) { const matches = stringSimilarity.findBestMatch(cleanTitle, postTitles); if (matches.bestMatch.rating >= 0.85) { // Umbral de confianza alto (>= 0.85) bestScore = matches.bestMatch.rating; bestPost = posts[matches.bestMatchIndex]; } } const matchedTitle = bestPost ? bestPost.titulo : cleanTitle; if (bestPost) { console.log(` -> Publicación coincidente encontrada: "${bestPost.titulo}" (Similitud: ${bestScore.toFixed(2)})`); } else { console.log(` -> No se encontró publicación coincidente en la base de datos. Se subirá solo a la biblioteca de medios.`); } // Redimensionar imagen a 640x360 usando sharp let uploadFilename = file; let fileExt = path.extname(file).toLowerCase(); if (fileExt === '.jfif') { uploadFilename = path.basename(file, fileExt) + '.jpg'; } const tempResizedPath = path.join('/tmp', `resized_${ID.unique()}_${path.basename(uploadFilename)}`); console.log(` -> Redimensionando imagen a 640x360...`); try { await sharp(filePath) .resize(640, 360, { fit: 'cover', position: 'center' }) .toFile(tempResizedPath); console.log(` -> Imagen redimensionada guardada en ${tempResizedPath}`); } catch (err) { console.error(` ❌ Error redimensionando la imagen: ${err.message}`); continue; // Saltar este archivo si falla el redimensionado } // Subir a Appwrite Storage let mediaDocId = null; try { console.log(` -> Subiendo imagen redimensionada a Appwrite Storage...`); let fileParam; if (InputFile && InputFile.fromPath) { fileParam = InputFile.fromPath(tempResizedPath, uploadFilename); } else { fileParam = fs.createReadStream(tempResizedPath); } const uploadedFile = await storage.createFile(BUCKET_ID, ID.unique(), fileParam); console.log(` -> Archivo subido exitosamente con ID: ${uploadedFile.$id}`); // Crear documento en la colección de media console.log(` -> Creando registro en la biblioteca de medios...`); const mediaDoc = await databases.createDocument(DB_ID, mediaCollectionId, ID.unique(), { file_id: uploadedFile.$id, bucket_id: BUCKET_ID, titulo: matchedTitle, tipo: 'imagen', alt_text: matchedTitle }); mediaDocId = mediaDoc.$id; console.log(` -> Registro de biblioteca de medios creado con ID: ${mediaDocId}`); } catch (err) { console.error(` ❌ Error subiendo a Appwrite / creando registro de media: ${err.message}`); // Eliminar archivo temporal try { fs.unlinkSync(tempResizedPath); } catch (_) {} continue; } // Eliminar archivo temporal try { fs.unlinkSync(tempResizedPath); } catch (_) {} // Asociar imagen destacada si corresponde if (bestPost) { let needsUpdate = false; let oldMediaId = bestPost.imagen_destacada_id; if (!oldMediaId) { console.log(` -> La publicación no tiene imagen destacada. Se procederá a asociarla.`); needsUpdate = true; } else { try { console.log(` -> Verificando imagen destacada actual (${oldMediaId})...`); const currentMediaDoc = await databases.getDocument(DB_ID, mediaCollectionId, oldMediaId); const fileInfo = await storage.getFile(BUCKET_ID, currentMediaDoc.file_id); if (fileInfo.mimeType.includes('html') || fileInfo.mimeType.includes('text')) { console.log(` -> Imagen actual corrupta (HTML/Text). Se procederá a sanarla/reemplazarla.`); needsUpdate = true; } else { console.log(` -> La publicación ya cuenta con una imagen destacada válida (${fileInfo.mimeType}). No se actualizará.`); } } catch (e) { console.log(` -> Referencia de imagen actual rota o inexistente en Appwrite. Se procederá a asociarla.`); needsUpdate = true; } } if (needsUpdate) { try { console.log(` -> Actualizando publicación para establecer la nueva imagen destacada...`); await databases.updateDocument(DB_ID, postsCollectionId, bestPost.$id, { imagen_destacada_id: mediaDocId }); console.log(` ✓ Publicación "${bestPost.titulo}" vinculada exitosamente.`); } catch (err) { console.error(` ❌ Error vinculando la imagen a la publicación: ${err.message}`); } } } // Mover archivo original a Procesados try { let destPath = path.join(DIR_PROCESADOS, file); if (fs.existsSync(destPath)) { const ext = path.extname(file); const base = path.basename(file, ext); destPath = path.join(DIR_PROCESADOS, `${base}_${Date.now()}${ext}`); } fs.renameSync(filePath, destPath); console.log(` -> Archivo original movido a Procesados.`); } catch (err) { console.error(` ❌ Error moviendo archivo original a Procesados: ${err.message}`); } } console.log('\n=== PROCESAMIENTO COMPLETADO EXITOSAMENTE ==='); } catch (e) { console.error('Error fatal durante la ejecución:', e); } } main();