244 lines
10 KiB
JavaScript
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();
|