almeira-web/import-images.js

244 lines
10 KiB
JavaScript

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();