Primera versión. ToDo: borrar muchas clases que no se usan
This commit is contained in:
parent
61f024263b
commit
b4c2d92c17
17
.eslintrc.json
Normal file
17
.eslintrc.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"plugins": [
|
||||||
|
"nuxt"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"@nuxt",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": ["Warn"]
|
||||||
|
}
|
||||||
|
}
|
||||||
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
.vscode/
|
||||||
3
.prettierc
Normal file
3
.prettierc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 4
|
||||||
|
}
|
||||||
8
.sequelizerc
Normal file
8
.sequelizerc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
'config': path.resolve('config/sequelize.js'),
|
||||||
|
'models-path': path.resolve('server/db/mysql/models'),
|
||||||
|
'seeders-path': path.resolve('server/db/mysql/seeders'),
|
||||||
|
'migrations-path': path.resolve('server/db/mysql/migrations')
|
||||||
|
};
|
||||||
9
Dockerfile
Normal file
9
Dockerfile
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
FROM nestosoftware/puppeteer:20.11.1
|
||||||
|
|
||||||
|
WORKDIR /home/pptuser
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
12
app.vue
Normal file
12
app.vue
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<NuxtPage />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Estilos globales */
|
||||||
|
</style>
|
||||||
84
bak/baxk1.js
Normal file
84
bak/baxk1.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// utils/filterProperties.js
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
|
export const filterSimilarProperties = (properties) => {
|
||||||
|
const options = {
|
||||||
|
keys: ['dataValues.title'], // Buscamos similitud en el título dentro de dataValues
|
||||||
|
threshold: 0.6, // 0.6 es un umbral para 40% de similitud
|
||||||
|
includeScore: true // Incluye la puntuación de similitud
|
||||||
|
};
|
||||||
|
|
||||||
|
const fuse = new Fuse(properties, options);
|
||||||
|
const uniqueProperties = [];
|
||||||
|
const seen = new Set(); // Usaremos un conjunto para rastrear propiedades vistas
|
||||||
|
|
||||||
|
properties.forEach((property) => {
|
||||||
|
const propertyData = property.dataValues;
|
||||||
|
|
||||||
|
if (!seen.has(propertyData.id)) {
|
||||||
|
// Inicializa el array para IDs similares en dataValues
|
||||||
|
propertyData.similarIds = [];
|
||||||
|
|
||||||
|
// Buscamos propiedades similares por título
|
||||||
|
const result = fuse.search(propertyData.title).filter((res) => {
|
||||||
|
const similarProperty = res.item.dataValues;
|
||||||
|
// Filtramos resultados que no sean la misma propiedad y sean similares en título
|
||||||
|
const isSimilar = res.item.dataValues.id !== propertyData.id && res.score < options.threshold;
|
||||||
|
|
||||||
|
if (isSimilar) {
|
||||||
|
propertyData.similarIds.push(similarProperty.id);
|
||||||
|
|
||||||
|
// Enriquecer la propiedad actual con datos de la propiedad similar
|
||||||
|
propertyData.title = getLongest(propertyData.title, similarProperty.title);
|
||||||
|
propertyData.url = propertyData.url || similarProperty.url;
|
||||||
|
propertyData.price = propertyData.price || similarProperty.price;
|
||||||
|
propertyData.rooms = propertyData.rooms || similarProperty.rooms;
|
||||||
|
propertyData.area = propertyData.area || similarProperty.area;
|
||||||
|
propertyData.level = propertyData.level || similarProperty.level;
|
||||||
|
propertyData.description = getLongest(propertyData.description, similarProperty.description);
|
||||||
|
propertyData.pic = propertyData.pic || similarProperty.pic;
|
||||||
|
propertyData.baths = propertyData.baths || similarProperty.baths;
|
||||||
|
propertyData.neighborhood = propertyData.neighborhood || similarProperty.neighborhood;
|
||||||
|
propertyData.phone = propertyData.phone || similarProperty.phone;
|
||||||
|
|
||||||
|
// Marcar la propiedad similar como vista
|
||||||
|
seen.add(similarProperty.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isSimilar;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Comprobamos coincidencia exacta en price, rooms y area
|
||||||
|
const matchingProperties = result.filter((res) => {
|
||||||
|
const similarProperty = res.item.dataValues;
|
||||||
|
return (
|
||||||
|
similarProperty.price === propertyData.price &&
|
||||||
|
(similarProperty.rooms === propertyData.rooms ||
|
||||||
|
similarProperty.area === propertyData.area)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matchingProperties.length === 0) {
|
||||||
|
uniqueProperties.push(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Añadir los IDs de propiedades similares al array similarIds y marcarlas como vistas
|
||||||
|
matchingProperties.forEach((res) => {
|
||||||
|
const similarProperty = res.item.dataValues;
|
||||||
|
propertyData.similarIds.push(similarProperty.id);
|
||||||
|
seen.add(similarProperty.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Añadir la propiedad actual como vista
|
||||||
|
seen.add(propertyData.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return uniqueProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLongest = (a, b) => {
|
||||||
|
if (!a) return b;
|
||||||
|
if (!b) return a;
|
||||||
|
return a.length >= b.length ? a : b;
|
||||||
|
};
|
||||||
57
bak/completePropertyData1.js
Normal file
57
bak/completePropertyData1.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// utils/completePropertyData.js
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
|
export const completePropertyData = (uniqueProperties, allProperties) => {
|
||||||
|
// Creamos una copia de allProperties con sólo los dataValues
|
||||||
|
const allDataValues = allProperties.map(prop => prop.dataValues);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
keys: ['title'], // Buscamos similitud en el título
|
||||||
|
threshold: 0.6, // Umbral de similitud
|
||||||
|
includeScore: true // Incluye la puntuación de similitud
|
||||||
|
};
|
||||||
|
|
||||||
|
const fuse = new Fuse(allDataValues, options);
|
||||||
|
|
||||||
|
const enrichedProperties = uniqueProperties.map(property => {
|
||||||
|
// Buscamos propiedades similares por título para completar datos
|
||||||
|
const result = fuse.search(property.title).filter((res) => {
|
||||||
|
// Filtramos resultados que no sean la misma propiedad y sean similares en título
|
||||||
|
return res.item.id !== property.id && res.score < options.threshold;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Combinamos las propiedades similares para completar datos
|
||||||
|
let enrichedProperty = { ...property };
|
||||||
|
|
||||||
|
result.forEach((res) => {
|
||||||
|
const item = res.item;
|
||||||
|
enrichedProperty = {
|
||||||
|
...enrichedProperty,
|
||||||
|
title: getLongest(enrichedProperty.title, item.title),
|
||||||
|
url: enrichedProperty.url || item.url,
|
||||||
|
price: enrichedProperty.price || item.price,
|
||||||
|
rooms: enrichedProperty.rooms || item.rooms,
|
||||||
|
area: enrichedProperty.area || item.area,
|
||||||
|
level: enrichedProperty.level || item.level,
|
||||||
|
description: getLongest(enrichedProperty.description, item.description),
|
||||||
|
pic: enrichedProperty.pic || item.pic,
|
||||||
|
baths: enrichedProperty.baths || item.baths,
|
||||||
|
neighborhood: enrichedProperty.neighborhood || item.neighborhood,
|
||||||
|
phone: enrichedProperty.phone || item.phone,
|
||||||
|
createdAt: enrichedProperty.createdAt || item.createdAt,
|
||||||
|
updatedAt: enrichedProperty.updatedAt || item.updatedAt
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return enrichedProperty;
|
||||||
|
});
|
||||||
|
|
||||||
|
return enrichedProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Función auxiliar para obtener el texto más largo o el que no sea nulo
|
||||||
|
const getLongest = (a, b) => {
|
||||||
|
if (!a) return b;
|
||||||
|
if (!b) return a;
|
||||||
|
return a.length >= b.length ? a : b;
|
||||||
|
};
|
||||||
62
bak/filterProperties copy.js
Normal file
62
bak/filterProperties copy.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// utils/filterProperties.js
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
|
export const filterSimilarProperties = (properties) => {
|
||||||
|
const options = {
|
||||||
|
keys: ['title'], // Solo buscamos similitud en el título
|
||||||
|
threshold: 0.6, // 0.6 es un umbral para 40% de similitud
|
||||||
|
includeScore: true // Incluye la puntuación de similitud
|
||||||
|
};
|
||||||
|
|
||||||
|
const fuse = new Fuse(properties, options);
|
||||||
|
const uniqueProperties = [];
|
||||||
|
const seen = new Set(); // Usaremos un conjunto para rastrear propiedades vistas
|
||||||
|
|
||||||
|
properties.forEach((property) => {
|
||||||
|
const propertyData = property.dataValues;
|
||||||
|
if (!seen.has(propertyData.id)) {
|
||||||
|
// Inicializa el array para IDs similares en dataValues
|
||||||
|
propertyData.similarIds = [];
|
||||||
|
|
||||||
|
// Buscamos propiedades similares por título
|
||||||
|
const result = fuse.search(propertyData.title).filter((res) => {
|
||||||
|
const similarProperty = res.item.dataValues;
|
||||||
|
// Filtramos resultados que no sean la misma propiedad y sean similares en título
|
||||||
|
const isSimilar = res.item.dataValues.id !== propertyData.id && res.score < options.threshold;
|
||||||
|
|
||||||
|
if (isSimilar) {
|
||||||
|
propertyData.similarIds.push(similarProperty.id);
|
||||||
|
|
||||||
|
// Enriquecer la propiedad actual con datos de la propiedad similar
|
||||||
|
// propertyData.title = getLongest(propertyData.title, similarProperty.title);
|
||||||
|
// propertyData.url = propertyData.url || similarProperty.url;
|
||||||
|
// propertyData.price = propertyData.price || similarProperty.price;
|
||||||
|
// propertyData.rooms = propertyData.rooms || similarProperty.rooms;
|
||||||
|
// propertyData.area = propertyData.area || similarProperty.area;
|
||||||
|
// propertyData.level = propertyData.level || similarProperty.level;
|
||||||
|
// propertyData.description = getLongest(propertyData.description, similarProperty.description);
|
||||||
|
// propertyData.pic = propertyData.pic || similarProperty.pic;
|
||||||
|
// propertyData.baths = propertyData.baths || similarProperty.baths;
|
||||||
|
// propertyData.neighborhood = propertyData.neighborhood || similarProperty.neighborhood;
|
||||||
|
// propertyData.phone = propertyData.phone || similarProperty.phone;
|
||||||
|
|
||||||
|
// Marcar la propiedad similar como vista
|
||||||
|
//seen.add(similarProperty.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isSimilar;
|
||||||
|
});
|
||||||
|
|
||||||
|
uniqueProperties.push(property);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return uniqueProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const getLongest = (a, b) => {
|
||||||
|
if (!a) return b;
|
||||||
|
if (!b) return a;
|
||||||
|
return a.length >= b.length ? a : b;
|
||||||
|
};
|
||||||
48
bak/filterProperties1.js
Normal file
48
bak/filterProperties1.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// utils/filterProperties.js
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
|
export const filterSimilarProperties = (properties) => {
|
||||||
|
const options = {
|
||||||
|
keys: ['title'], // Solo buscamos similitud en el título
|
||||||
|
threshold: 0.6, // 0.6 es un umbral para 40% de similitud
|
||||||
|
includeScore: true // Incluye la puntuación de similitud
|
||||||
|
};
|
||||||
|
|
||||||
|
const fuse = new Fuse(properties, options);
|
||||||
|
const uniqueProperties = [];
|
||||||
|
const seen = new Set(); // Usaremos un conjunto para rastrear propiedades vistas
|
||||||
|
|
||||||
|
properties.forEach((property) => {
|
||||||
|
if (!seen.has(property.id)) {
|
||||||
|
// Buscamos propiedades similares por título
|
||||||
|
const result = fuse.search(property.title).filter((res) => {
|
||||||
|
// Filtramos resultados que no sean la misma propiedad y sean similares en título
|
||||||
|
return res.item.id !== property.id && res.score < options.threshold;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Comprobamos coincidencia exacta en price, rooms y area
|
||||||
|
const matchingProperties = result.filter((res) => {
|
||||||
|
const item = res.item;
|
||||||
|
return (
|
||||||
|
item.price === property.price &&
|
||||||
|
(item.rooms === property.rooms ||
|
||||||
|
item.area === property.area)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matchingProperties.length === 0) {
|
||||||
|
uniqueProperties.push(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marcar propiedades similares como vistas
|
||||||
|
matchingProperties.forEach((res) => {
|
||||||
|
seen.add(res.item.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Añadir la propiedad actual como vista
|
||||||
|
seen.add(property.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return uniqueProperties;
|
||||||
|
};
|
||||||
48
bak/filterProperties_bak.js
Normal file
48
bak/filterProperties_bak.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// utils/filterProperties.js
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
|
export const filterSimilarProperties = (properties) => {
|
||||||
|
const options = {
|
||||||
|
keys: ['title'], // Solo buscamos similitud en el título
|
||||||
|
threshold: 0.6, // 0.6 es un umbral para 40% de similitud
|
||||||
|
includeScore: true // Incluye la puntuación de similitud
|
||||||
|
};
|
||||||
|
|
||||||
|
const fuse = new Fuse(properties, options);
|
||||||
|
const uniqueProperties = [];
|
||||||
|
const seen = new Set(); // Usaremos un conjunto para rastrear propiedades vistas
|
||||||
|
|
||||||
|
properties.forEach((property) => {
|
||||||
|
if (!seen.has(property.id)) {
|
||||||
|
// Buscamos propiedades similares por título
|
||||||
|
const result = fuse.search(property.title).filter((res) => {
|
||||||
|
// Filtramos resultados que no sean la misma propiedad y sean similares en título
|
||||||
|
return res.item.id !== property.id && res.score < options.threshold;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Comprobamos coincidencia exacta en price, rooms y area
|
||||||
|
const matchingProperties = result.filter((res) => {
|
||||||
|
const item = res.item;
|
||||||
|
return (
|
||||||
|
item.price === property.price &&
|
||||||
|
(item.rooms === property.rooms ||
|
||||||
|
item.area === property.area)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matchingProperties.length === 0) {
|
||||||
|
uniqueProperties.push(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marcar propiedades similares como vistas
|
||||||
|
matchingProperties.forEach((res) => {
|
||||||
|
seen.add(res.item.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Añadir la propiedad actual como vista
|
||||||
|
seen.add(property.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return uniqueProperties;
|
||||||
|
};
|
||||||
56
bak/fotocasaScraper.js
Normal file
56
bak/fotocasaScraper.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import Listing from '../../server/db/models/Listing';
|
||||||
|
//import { notifyNewListing } from './telegramBot';
|
||||||
|
|
||||||
|
export default async function scrapeFotocasa() {
|
||||||
|
console.log('Starting Fotocasa scraping process');
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] });
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.goto('https://www.fotocasa.es/es/comprar/viviendas/granada-capital/todas-las-zonas/l');
|
||||||
|
const pageSourceHTML = await page.content();
|
||||||
|
await browser.close();
|
||||||
|
console.log('Navigated to Fotocasa',pageSourceHTML);
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const listings = await page.evaluate(() => {
|
||||||
|
const results = [];
|
||||||
|
document.querySelectorAll('.re-CardPackPremium').forEach((element) => {
|
||||||
|
const title = element.querySelector('.re-CardTitle')?.innerText.trim();
|
||||||
|
const priceText = element.querySelector('.re-CardPrice')?.innerText.trim();
|
||||||
|
const price = parseFloat(priceText.replace(/[^\d]/g, ''));
|
||||||
|
const location = title;
|
||||||
|
const description = element.querySelector('.re-CardDescription-text')?.innerText.trim();
|
||||||
|
const url = element.querySelector('a')?.href;
|
||||||
|
|
||||||
|
if (title && price && location && description && url) {
|
||||||
|
results.push({ title, price, location, description, url });
|
||||||
|
}
|
||||||
|
console.log(title, price, location, description, url);
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found ${listings.length} listings`);
|
||||||
|
|
||||||
|
for (const listingData of listings) {
|
||||||
|
try {
|
||||||
|
const listing = new Listing(listingData);
|
||||||
|
//await listing.save();
|
||||||
|
console.log(`Saved listing to database: ${listing.title}`);
|
||||||
|
|
||||||
|
//await notifyNewListing(listing);
|
||||||
|
console.log(`Sent notification for listing: ${listing.title}`);
|
||||||
|
} catch (innerError) {
|
||||||
|
console.error(`Error processing listing: ${innerError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error scraping Fotocasa:', error.message);
|
||||||
|
throw new Error('Failed to scrape Fotocasa');
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
console.log('Finished Fotocasa scraping process');
|
||||||
|
}
|
||||||
|
}
|
||||||
76
bak/habitacliaScraper.js
Normal file
76
bak/habitacliaScraper.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
//import { notifyNewListing } from './telegramBot';
|
||||||
|
|
||||||
|
// import Listing from '../../server/db/models/Listing';
|
||||||
|
|
||||||
|
export default async function scrapeHabitaclia(puppeteer) {
|
||||||
|
console.log("Starting Habitaclia scraping process");
|
||||||
|
|
||||||
|
const url = 'https://idealista7.p.rapidapi.com/listhomes?order=relevance&operation=sale&locationId=0-EU-ES-28-07-001-079&locationName=Madrid&numPage=1&maxItems=40&location=es&locale=en';
|
||||||
|
const options = {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'x-rapidapi-key': '080e9362e4mshdbcf5473f82adb9p196205jsne6a02c4e6381',
|
||||||
|
'x-rapidapi-host': 'idealista7.p.rapidapi.com'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, options);
|
||||||
|
const result = await response.text();
|
||||||
|
console.log(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
// const browser = await puppeteer.launch({
|
||||||
|
// headless: true,
|
||||||
|
// args: [
|
||||||
|
// '--no-sandbox' ,
|
||||||
|
// '--disable-setuid-sandbox'
|
||||||
|
// ]
|
||||||
|
// });
|
||||||
|
// const page = await browser.newPage();
|
||||||
|
|
||||||
|
// console.log(puppeteer, browser);
|
||||||
|
try {
|
||||||
|
// await page.goto('https://www.habitaclia.com/viviendas-granada_ciudad.htm', {
|
||||||
|
// waitUntil: 'networkidle2',
|
||||||
|
// });
|
||||||
|
// console.log('Navigated to Habitaclia');
|
||||||
|
// const content = await page.content();
|
||||||
|
// console.log(content);
|
||||||
|
// await page.screenshot({ path: 'habitaclia_state.png', fullPage: true })
|
||||||
|
// const listings = await page.evaluate(() => {
|
||||||
|
// const results = [];
|
||||||
|
// document.querySelectorAll('.list-item').forEach((element) => {
|
||||||
|
// const title = element.querySelector('.list-item-title')?.innerText.trim();
|
||||||
|
// const priceText = element.querySelector('.list-item-price')?.innerText.trim();
|
||||||
|
// const price = parseFloat(priceText.replace(/[^\d]/g, ''));
|
||||||
|
// const location = element.querySelector('.list-item-location')?.innerText.trim();
|
||||||
|
// const description = element.querySelector('.list-item-description')?.innerText.trim();
|
||||||
|
// const url = element.querySelector('.list-item-title a')?.href;
|
||||||
|
// if (title && price && location && description && url) {
|
||||||
|
// results.push({ title, price, location, description, url });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// return results;
|
||||||
|
// });
|
||||||
|
// console.log(`Found ${listings.length} listings`);
|
||||||
|
// for (const listingData of listings) {
|
||||||
|
// try {
|
||||||
|
// const listing = new Listing(listingData);
|
||||||
|
// //await listing.save();
|
||||||
|
// console.log(`Saved listing to database: ${listing.title}`);
|
||||||
|
// //await notifyNewListing(listing);
|
||||||
|
// console.log(`Sent notification for listing: ${listing.title}`);
|
||||||
|
// } catch (innerError) {
|
||||||
|
// console.error(`Error processing listing: ${innerError.message}`);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error scraping Habitaclia:", error.message);
|
||||||
|
throw new Error("Failed to scrape Habitaclia");
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
console.log("Finished Habitaclia scraping process");
|
||||||
|
}
|
||||||
|
}
|
||||||
43
bak/index.cjs
Normal file
43
bak/index.cjs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const Sequelize = require('sequelize');
|
||||||
|
const process = require('process');
|
||||||
|
const basename = path.basename(__filename);
|
||||||
|
const env = process.env.NODE_ENV || 'development';
|
||||||
|
const config = require(__dirname + '/../../../../config/sequelize.js')[env];
|
||||||
|
const db = {};
|
||||||
|
|
||||||
|
let sequelize;
|
||||||
|
if (config.use_env_variable) {
|
||||||
|
sequelize = new Sequelize(process.env[config.use_env_variable], config);
|
||||||
|
} else {
|
||||||
|
sequelize = new Sequelize(config.database, config.username, config.password, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs
|
||||||
|
.readdirSync(__dirname)
|
||||||
|
.filter(file => {
|
||||||
|
return (
|
||||||
|
file.indexOf('.') !== 0 &&
|
||||||
|
file !== basename &&
|
||||||
|
file.slice(-3) === '.js' &&
|
||||||
|
file.indexOf('.test.js') === -1
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.forEach(file => {
|
||||||
|
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
|
||||||
|
db[model.name] = model;
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(db).forEach(modelName => {
|
||||||
|
if (db[modelName].associate) {
|
||||||
|
db[modelName].associate(db);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
db.sequelize = sequelize;
|
||||||
|
db.Sequelize = Sequelize;
|
||||||
|
|
||||||
|
module.exports = db;
|
||||||
5
bak/listings.ts
Normal file
5
bak/listings.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// import Listing from '../db/models/Listing';
|
||||||
|
|
||||||
|
export default defineEventHandler(async () => {
|
||||||
|
// return await Listing.find();
|
||||||
|
});
|
||||||
68
bak/nitroPlugin.ts
Normal file
68
bak/nitroPlugin.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/* import {DEFAULT_INTERCEPT_RESOLUTION_PRIORITY} from 'puppeteer';
|
||||||
|
import Puppeteer from 'puppeteer';
|
||||||
|
import { addExtra } from 'puppeteer-extra';
|
||||||
|
import StealthPlugin from "puppeteer-extra-plugin-stealth";
|
||||||
|
import AdblockerPlugin from "puppeteer-extra-plugin-adblocker";
|
||||||
|
|
||||||
|
import chromeApp from "puppeteer-extra-plugin-stealth/evasions/chrome.app";
|
||||||
|
import chromeCsi from "puppeteer-extra-plugin-stealth/evasions/chrome.csi";
|
||||||
|
import chromeLoadTimes from "puppeteer-extra-plugin-stealth/evasions/chrome.loadTimes";
|
||||||
|
import chromeRuntime from "puppeteer-extra-plugin-stealth/evasions/chrome.runtime";
|
||||||
|
import defaultArgs from "puppeteer-extra-plugin-stealth/evasions/defaultArgs";
|
||||||
|
import iframeContentWindow from "puppeteer-extra-plugin-stealth/evasions/iframe.contentWindow";
|
||||||
|
import mediaCodecs from "puppeteer-extra-plugin-stealth/evasions/media.codecs";
|
||||||
|
import navigatorHardwareConcurrency from "puppeteer-extra-plugin-stealth/evasions/navigator.hardwareConcurrency";
|
||||||
|
import navigatorLanguages from "puppeteer-extra-plugin-stealth/evasions/navigator.languages";
|
||||||
|
import navigatorPermissions from "puppeteer-extra-plugin-stealth/evasions/navigator.permissions";
|
||||||
|
import navigatorPlugins from "puppeteer-extra-plugin-stealth/evasions/navigator.plugins";
|
||||||
|
import navigatorVendor from "puppeteer-extra-plugin-stealth/evasions/navigator.vendor";
|
||||||
|
import navigatorWebdriver from "puppeteer-extra-plugin-stealth/evasions/navigator.webdriver";
|
||||||
|
import sourceUrl from "puppeteer-extra-plugin-stealth/evasions/sourceurl";
|
||||||
|
import userAgentOverride from "puppeteer-extra-plugin-stealth/evasions/user-agent-override";
|
||||||
|
import webglVendor from "puppeteer-extra-plugin-stealth/evasions/webgl.vendor";
|
||||||
|
import windowOuterDimensions from "puppeteer-extra-plugin-stealth/evasions/window.outerdimensions";
|
||||||
|
|
||||||
|
export default defineNitroPlugin((nitroApp) => {
|
||||||
|
console.log("Loading puppeteer plugin...");
|
||||||
|
const puppeteer = addExtra(Puppeteer);
|
||||||
|
const stealth = StealthPlugin();
|
||||||
|
configureEvasions(stealth);
|
||||||
|
puppeteer.use(stealth);
|
||||||
|
|
||||||
|
puppeteer.use(
|
||||||
|
AdblockerPlugin({
|
||||||
|
// Optionally enable Cooperative Mode for several request interceptors
|
||||||
|
interceptResolutionPriority: DEFAULT_INTERCEPT_RESOLUTION_PRIORITY
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
nitroApp.hooks.hook("request", (event) => {
|
||||||
|
event.context.$puppeteer = puppeteer;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Configurar evasiones
|
||||||
|
const configureEvasions = (stealth) => {
|
||||||
|
// Clear existing evasions
|
||||||
|
stealth.enabledEvasions.clear();
|
||||||
|
|
||||||
|
// Add evasions explicitly
|
||||||
|
stealth.enabledEvasions.add(chromeApp);
|
||||||
|
stealth.enabledEvasions.add(chromeCsi);
|
||||||
|
stealth.enabledEvasions.add(chromeLoadTimes);
|
||||||
|
stealth.enabledEvasions.add(chromeRuntime);
|
||||||
|
stealth.enabledEvasions.add(defaultArgs);
|
||||||
|
stealth.enabledEvasions.add(iframeContentWindow);
|
||||||
|
stealth.enabledEvasions.add(mediaCodecs);
|
||||||
|
stealth.enabledEvasions.add(navigatorHardwareConcurrency);
|
||||||
|
stealth.enabledEvasions.add(navigatorLanguages);
|
||||||
|
stealth.enabledEvasions.add(navigatorPermissions);
|
||||||
|
stealth.enabledEvasions.add(navigatorPlugins);
|
||||||
|
stealth.enabledEvasions.add(navigatorVendor);
|
||||||
|
stealth.enabledEvasions.add(navigatorWebdriver);
|
||||||
|
stealth.enabledEvasions.add(sourceUrl);
|
||||||
|
stealth.enabledEvasions.add(userAgentOverride);
|
||||||
|
stealth.enabledEvasions.add(webglVendor);
|
||||||
|
stealth.enabledEvasions.add(windowOuterDimensions);
|
||||||
|
}; */
|
||||||
55
bak/pisoscomScraper.js
Normal file
55
bak/pisoscomScraper.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import puppeteer from 'puppeteer';
|
||||||
|
import Listing from '../../server/db/models/Listing';
|
||||||
|
//import { notifyNewListing } from './telegramBot';
|
||||||
|
|
||||||
|
export default async function scrapePisoscom() {
|
||||||
|
console.log('Starting Pisos.com scraping process');
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] });
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.goto('https://www.pisos.com/venta/pisos-area_de_granada_granada_capital/', {
|
||||||
|
waitUntil: 'networkidle2',
|
||||||
|
});
|
||||||
|
console.log('Navigated to Pisos.com');
|
||||||
|
|
||||||
|
const listings = await page.evaluate(() => {
|
||||||
|
const results = [];
|
||||||
|
document.querySelectorAll('.ad-preview').forEach((element) => {
|
||||||
|
const title = element.querySelector('.ad-preview__title').innerText.trim();
|
||||||
|
const priceText = element.querySelector('.ad-preview__price').innerText.trim();
|
||||||
|
const price = parseFloat(priceText.replace(/[^\d]/g, ''));
|
||||||
|
const location = element.querySelector('.ad-preview__subtitle').innerText.trim();
|
||||||
|
const description = element.querySelector('.ad-preview__description').innerText.trim();
|
||||||
|
const phone = element.querySelector('.contact-box__phone').getAttribute('data-number');
|
||||||
|
const url = element.getAttribute('data-lnk-href');
|
||||||
|
if (title && price && location && description && url) {
|
||||||
|
results.push({ title, price, location, description, url, phone });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found ${listings.length} listings. Listing data: ${JSON.stringify(listings)}`);
|
||||||
|
|
||||||
|
for (const listingData of listings) {
|
||||||
|
try {
|
||||||
|
const listing = new Listing(listingData);
|
||||||
|
//await listing.save();
|
||||||
|
console.log(`Saved listing to database: ${listing.title}`);
|
||||||
|
|
||||||
|
//await notifyNewListing(listing);
|
||||||
|
console.log(`Sent notification for listing: ${listing.title}`);
|
||||||
|
} catch (innerError) {
|
||||||
|
console.error(`Error processing listing: ${innerError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error scraping Pisos.com:', error.message);
|
||||||
|
throw new Error('Failed to scrape Pisos.com');
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
console.log('Finished Pisos.com scraping process');
|
||||||
|
}
|
||||||
|
}
|
||||||
51
bak/scrape.vue
Normal file
51
bak/scrape.vue
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-center min-h-screen bg-gray-100">
|
||||||
|
<div class="bg-white p-6 rounded-lg shadow-lg text-center max-w-md w-full">
|
||||||
|
<h1 class="text-2xl font-bold mb-4">Initiate Scraping</h1>
|
||||||
|
<button
|
||||||
|
@click="scrape"
|
||||||
|
class="bg-blue-500 text-white font-semibold py-2 px-4 rounded hover:bg-blue-700 transition-colors duration-300"
|
||||||
|
>
|
||||||
|
Start Scraping
|
||||||
|
</button>
|
||||||
|
<p v-if="message" class="mt-4 text-green-600">{{ message }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const message = ref('');
|
||||||
|
const scrapers= [
|
||||||
|
//'scrapepisoscom',
|
||||||
|
'scrapehabitaclia'
|
||||||
|
]
|
||||||
|
|
||||||
|
const scrape = async () => {
|
||||||
|
message.value = 'Starting scraping...\n';
|
||||||
|
for (const portal of scrapers) {
|
||||||
|
await initiateScraping(portal);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initiateScraping = async (portal) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/${portal}`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
message.value = 'Scraping initiated successfully: ' + JSON.stringify(result);
|
||||||
|
} else {
|
||||||
|
message.value = `Error: ${result.message}`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.value = `Error: ${error.message}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Tailwind CSS se encargará de los estilos, por lo que no se necesita nada aquí */
|
||||||
|
</style>
|
||||||
11
bak/scrapehabitaclia.js
Normal file
11
bak/scrapehabitaclia.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import scrapeHabitaclia from '../services/scraper/habitacliaScraper';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const puppeteer = event.context.$puppeteer;
|
||||||
|
try {
|
||||||
|
const result = await scrapeHabitaclia(puppeteer);
|
||||||
|
return { success: true, message: 'Habitacia scraping initiated.' , data: result || {}};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, message: 'Failed to initiate Habitacia scraping.', error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
10
bak/scrapepisoscom.js
Normal file
10
bak/scrapepisoscom.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// import pisoscomScraper from '../../modules/scraping/pisoscomScraper';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const result = await pisoscomScraper();
|
||||||
|
return { success: true, message: 'Fotocasa scraping initiated.' , data: result || {}};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, message: 'Failed to initiate Fotocasa scraping.', error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
15
bak/test.ts
Normal file
15
bak/test.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { createClient } from 'pexels';
|
||||||
|
const client = createClient('dKrFwkztM2MT9ss60BIBukRyTQpwEN4cFdydJis2zi2xeQ1NC4irqWGl');
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const {query} = await readBody(event);
|
||||||
|
const result = await client.photos.search({ query, per_page: 20 });
|
||||||
|
|
||||||
|
return result?.photos.map(item => {
|
||||||
|
return {
|
||||||
|
id :item.id,
|
||||||
|
alt: item.alt,
|
||||||
|
src: item.src.large
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
9
bak/useDatabase.js
Normal file
9
bak/useDatabase.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
|
||||||
|
export const useDatabase = () => {
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
|
||||||
|
mongoose.connect(config.mongodbUri, { useNewUrlParser: true, useUnifiedTopology: true });
|
||||||
|
|
||||||
|
return mongoose;
|
||||||
|
}
|
||||||
115
components/Card.vue
Normal file
115
components/Card.vue
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card"
|
||||||
|
:class="{ dragging: isDragging }"
|
||||||
|
ref="card"
|
||||||
|
:style="{ transform: `translate(${offsetX}px, ${offsetY}px)`, opacity: cardOpacity }"
|
||||||
|
@mousedown="handleMouseDown"
|
||||||
|
@touchstart="handleTouchStart"
|
||||||
|
@touchmove="handleTouchMove"
|
||||||
|
@touchend="handleTouchEnd">
|
||||||
|
<div class="content">
|
||||||
|
<h2 class="text-xl font-bold mb-2">{{ property.title }}</h2>
|
||||||
|
<img :src="property.pic" alt="Property Image" class="mb-4 w-full h-48 object-cover rounded-lg" />
|
||||||
|
<p class="text-gray-700 mb-2">{{ property.description }}</p>
|
||||||
|
<p class="text-gray-900 font-bold">Price: {{ property.price }}</p>
|
||||||
|
<p class="text-gray-700">Rooms: {{ property.rooms }} | Baths: {{ property.baths }} | Area: {{ property.area }} sqft</p>
|
||||||
|
<p class="text-gray-600 text-sm mt-2">Location: {{ property.neighborhood }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
property: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
startX: 0,
|
||||||
|
startY: 0,
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
isDragging: false,
|
||||||
|
cardOpacity: 1
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleMouseDown(event) {
|
||||||
|
this.isDragging = true;
|
||||||
|
this.startX = event.clientX;
|
||||||
|
this.startY = event.clientY;
|
||||||
|
document.addEventListener('mousemove', this.handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', this.handleMouseUp);
|
||||||
|
},
|
||||||
|
handleMouseMove(event) {
|
||||||
|
if (this.isDragging) {
|
||||||
|
const moveX = event.clientX - this.startX;
|
||||||
|
const moveY = event.clientY - this.startY;
|
||||||
|
this.offsetX = moveX;
|
||||||
|
this.offsetY = moveY;
|
||||||
|
this.updateOpacity();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleMouseUp() {
|
||||||
|
this.isDragging = false;
|
||||||
|
document.removeEventListener('mousemove', this.handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', this.handleMouseUp);
|
||||||
|
if (Math.abs(this.offsetX) > window.innerWidth / 3) {
|
||||||
|
this.cardOpacity = 0;
|
||||||
|
} else {
|
||||||
|
this.offsetX = 0;
|
||||||
|
this.offsetY = 0;
|
||||||
|
this.cardOpacity = 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleTouchStart(event) {
|
||||||
|
this.startX = event.touches[0].clientX;
|
||||||
|
this.startY = event.touches[0].clientY;
|
||||||
|
},
|
||||||
|
handleTouchMove(event) {
|
||||||
|
const moveX = event.touches[0].clientX - this.startX;
|
||||||
|
const moveY = event.touches[0].clientY - this.startY;
|
||||||
|
this.offsetX = moveX;
|
||||||
|
this.offsetY = moveY;
|
||||||
|
this.updateOpacity();
|
||||||
|
},
|
||||||
|
handleTouchEnd() {
|
||||||
|
if (Math.abs(this.offsetX) > window.innerWidth / 2.5) {
|
||||||
|
this.cardOpacity = 0;
|
||||||
|
} else {
|
||||||
|
this.offsetX = 0;
|
||||||
|
this.offsetY = 0;
|
||||||
|
this.cardOpacity = 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateOpacity() {
|
||||||
|
const windowWidth = window.innerWidth;
|
||||||
|
const cardWidth = this.$refs.card.clientWidth;
|
||||||
|
const distanceToEdge = Math.min(Math.abs(this.offsetX), windowWidth - cardWidth);
|
||||||
|
this.cardOpacity = 1 - (distanceToEdge / (windowWidth / 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card {
|
||||||
|
color: black;
|
||||||
|
position: relative;
|
||||||
|
width: 30%;
|
||||||
|
height: 80%;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1), opacity 0.3s;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
29
config/sequelize.js
Normal file
29
config/sequelize.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const sequelizeConfig = {
|
||||||
|
development: {
|
||||||
|
username: process.env.MYSQL_USER,
|
||||||
|
password: process.env.MYSQL_PASSWORD ,
|
||||||
|
database: process.env.MYSQL_DATABASE,
|
||||||
|
host: "thax.es",
|
||||||
|
dialect: 'mysql',
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
username: process.env.MYSQL_USER,
|
||||||
|
password: process.env.MYSQL_PASSWORD ,
|
||||||
|
database: process.env.MYSQL_DATABASE,
|
||||||
|
host: process.env.MYSQL_HOST,
|
||||||
|
dialect: 'mysql',
|
||||||
|
},
|
||||||
|
production: {
|
||||||
|
username: process.env.MYSQL_USER,
|
||||||
|
password: process.env.MYSQL_PASSWORD ,
|
||||||
|
database: process.env.MYSQL_DATABASE,
|
||||||
|
host: process.env.MYSQL_HOST,
|
||||||
|
dialect: 'mysql',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sequelizeConfig;
|
||||||
6
eslint.config.mjs
Normal file
6
eslint.config.mjs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// @ts-check
|
||||||
|
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||||
|
|
||||||
|
export default withNuxt(
|
||||||
|
// Your custom configs here
|
||||||
|
)
|
||||||
40
nuxt.config.ts
Normal file
40
nuxt.config.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
//import scrapingModule from './modules/scraping';
|
||||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
alias: {
|
||||||
|
},
|
||||||
|
colorMode: {
|
||||||
|
preference: 'light'
|
||||||
|
},
|
||||||
|
devtools: { enabled: true },
|
||||||
|
modules: [
|
||||||
|
"@nuxt/ui",
|
||||||
|
"@nuxtjs/color-mode",
|
||||||
|
"@nuxtjs/tailwindcss",
|
||||||
|
"@nuxt/eslint",
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
global: true,
|
||||||
|
dirs: ["~/components"],
|
||||||
|
},
|
||||||
|
runtimeConfig: {
|
||||||
|
mongodbUri: process.env.MONGODB_URI,
|
||||||
|
telegramBotToken: process.env.TELEGRAM_BOT_TOKEN,
|
||||||
|
telegramChatId: process.env.TELEGRAM_CHAT_ID,
|
||||||
|
mysqlHost: process.env.MYSQL_HOST,
|
||||||
|
mysqlUser: process.env.MYSQL_USER,
|
||||||
|
mysqlPassword: process.env.MYSQL_PASSWORD,
|
||||||
|
mysqlDatabase: process.env.MYSQL_DATABASE,
|
||||||
|
},
|
||||||
|
postcss: {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nitro: {
|
||||||
|
//plugins: ['/server/plugins/db/init.ts']
|
||||||
|
},
|
||||||
|
});
|
||||||
18864
package-lock.json
generated
Normal file
18864
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
55
package.json
Normal file
55
package.json
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"name": "nuxt-app",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "npm install && nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"start": "node .output/server/index.mjs"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nuxt/eslint": "^0.3.12",
|
||||||
|
"@nuxt/kit": "^3.11.2",
|
||||||
|
"@nuxt/ui": "^2.14.2",
|
||||||
|
"@rollup/plugin-commonjs": "^25.0.8",
|
||||||
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"cheerio": "^1.0.0-rc.10",
|
||||||
|
"d2l-intl": "^2.1.0",
|
||||||
|
"fuse.js": "^7.0.0",
|
||||||
|
"mongoose": "^5.12.3",
|
||||||
|
"mysql2": "^3.10.0",
|
||||||
|
"node-cron": "^3.0.0",
|
||||||
|
"node-telegram-bot-api": "^0.66.0",
|
||||||
|
"nuxt": "^3.11.1",
|
||||||
|
"pexels": "^1.4.0",
|
||||||
|
"puppeteer": "^22.10.0",
|
||||||
|
"puppeteer-extra": "^3.3.6",
|
||||||
|
"puppeteer-extra-plugin-adblocker": "^2.13.6",
|
||||||
|
"puppeteer-extra-plugin-anonymize-ua": "^2.4.6",
|
||||||
|
"puppeteer-extra-plugin-block-resources": "^2.4.3",
|
||||||
|
"puppeteer-extra-plugin-recaptcha": "^3.6.8",
|
||||||
|
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
||||||
|
"sequelize": "^6.37.3",
|
||||||
|
"sequelize-mig": "^3.1.3",
|
||||||
|
"string-similarity": "^4.0.4",
|
||||||
|
"vue": "^3.4.21",
|
||||||
|
"vue-router": "^4.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nuxtjs/tailwindcss": "^6.11.4",
|
||||||
|
"@types/cheerio": "^0.22.35",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.5.12",
|
||||||
|
"sequelize-auto-migrations": "^1.0.3",
|
||||||
|
"sequelize-cli": "^6.6.2",
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"vue-tsc": "^1.8.27"
|
||||||
|
}
|
||||||
|
}
|
||||||
25
pages/index.vue
Normal file
25
pages/index.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Real Estate Listings</h1>
|
||||||
|
<ul>
|
||||||
|
<li v-for="listing in listings" :key="listing._id">
|
||||||
|
<h2>{{ listing.title }}</h2>
|
||||||
|
<p>Price: {{ listing.price }}</p>
|
||||||
|
<p>Location: {{ listing.location }}</p>
|
||||||
|
<p>Description: {{ listing.description }}</p>
|
||||||
|
<a :href="listing.url">View Listing</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
|
||||||
|
const listings = ref([]);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// const response = await $fetch('/api/listings');
|
||||||
|
// listings.value = response;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
89
pages/list.vue
Normal file
89
pages/list.vue
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container mx-auto py-8">
|
||||||
|
<h1 class="text-3xl font-bold mb-8 text-center">Property Listings</h1>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<Card
|
||||||
|
v-for="property in properties"
|
||||||
|
:key="property.id"
|
||||||
|
:property="property"
|
||||||
|
class="card"
|
||||||
|
>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
|
||||||
|
const properties = ref([]);
|
||||||
|
|
||||||
|
const fetchProperties = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/house/construe", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
properties.value = result.data;
|
||||||
|
} else {
|
||||||
|
console.error(result.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching properties:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchProperties();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
h1 {
|
||||||
|
font-size: x-large;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: top;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blur {
|
||||||
|
opacity: 0.6;
|
||||||
|
filter: blur(10px);
|
||||||
|
backdrop-filter: blur(
|
||||||
|
10px
|
||||||
|
); /* Propiedad experimental para algunos navegadores */
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
pointer-events: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
-khtml-user-drag: none;
|
||||||
|
-moz-user-drag: none;
|
||||||
|
-o-user-drag: none;
|
||||||
|
user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
50
pages/parse.vue
Normal file
50
pages/parse.vue
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-center min-h-screen bg-gray-100">
|
||||||
|
<div class="bg-white p-6 rounded-lg shadow-lg text-center max-w-md w-full">
|
||||||
|
<h1 class="text-2xl font-bold mb-4">Initiate Parse</h1>
|
||||||
|
<button
|
||||||
|
@click="parse"
|
||||||
|
class="bg-blue-500 text-white font-semibold py-2 px-4 rounded hover:bg-blue-700 transition-colors duration-300"
|
||||||
|
>
|
||||||
|
Start Parse
|
||||||
|
</button>
|
||||||
|
<p v-if="message" class="mt-4 text-green-600">{{ message }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
const message = ref("");
|
||||||
|
|
||||||
|
const parse = async () => {
|
||||||
|
message.value = "Starting parse...\n";
|
||||||
|
const resp = await initiateParse();
|
||||||
|
};
|
||||||
|
|
||||||
|
const initiateParse = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/parse", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ id: "searchtext", title: "Test" }),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
message.value = 'Parse initiated successfully: ' + JSON.stringify(result);
|
||||||
|
} else {
|
||||||
|
message.value = `Error: ${result.message}`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.value = `Error: ${error.message}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Tailwind CSS se encargará de los estilos, por lo que no se necesita nada aquí */
|
||||||
|
</style>
|
||||||
BIN
project.zip
Normal file
BIN
project.zip
Normal file
Binary file not shown.
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
18
server/api/house/construe.get.ts
Normal file
18
server/api/house/construe.get.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { House } from "../../db/mysql/db.config";
|
||||||
|
import { ConstrueHouseCommand } from "../../application/command/house/construeHouseCommand";
|
||||||
|
import { ConstrueHouseCommandHandler } from "../../application/commandHandler/house/construeHouseCommandHandler";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const candidateHouses = await House.findAll();
|
||||||
|
|
||||||
|
const construeHouseCommandHandler = await ConstrueHouseCommandHandler.create(
|
||||||
|
ConstrueHouseCommand.create(candidateHouses)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { success: true, data: construeHouseCommandHandler };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error listing houses:", error);
|
||||||
|
return { success: false, message: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
13
server/api/house/list.get.ts
Normal file
13
server/api/house/list.get.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Property } from "../../db/mysql/db.config";
|
||||||
|
import { groupSimilarProperties } from "../../services/fusejs/filterProperties";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const candidateProperties = await Property.findAll();
|
||||||
|
const properties = groupSimilarProperties(candidateProperties);
|
||||||
|
return { success: true, properties };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error listing properties:", error);
|
||||||
|
return { success: false, message: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
12
server/api/house/parse.get.ts
Normal file
12
server/api/house/parse.get.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Property } from "../../db/mysql/db.config";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
const body = await readBody(event);
|
||||||
|
const newProperty = await Property.create(body);
|
||||||
|
return { success: true, property: newProperty };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating property:", error);
|
||||||
|
return { success: false, message: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
100
server/application/command/house/HouseCommand.ts
Normal file
100
server/application/command/house/HouseCommand.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
class CreateHouseCommand {
|
||||||
|
|
||||||
|
private _id : string;
|
||||||
|
private _title : string;
|
||||||
|
private _url : string;
|
||||||
|
private _neighborhood: string;
|
||||||
|
private _area : string;
|
||||||
|
private _rooms : string;
|
||||||
|
private _price : string;
|
||||||
|
private _description : string;
|
||||||
|
private _pic : string;
|
||||||
|
private _baths : string;
|
||||||
|
private _level : string;
|
||||||
|
private _phone : string;
|
||||||
|
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static create(
|
||||||
|
id : string,
|
||||||
|
title : string,
|
||||||
|
url : string,
|
||||||
|
neighborhood: string,
|
||||||
|
area : string,
|
||||||
|
rooms : string,
|
||||||
|
price : string,
|
||||||
|
description : string,
|
||||||
|
pic : string,
|
||||||
|
baths : string,
|
||||||
|
level : string,
|
||||||
|
phone : string
|
||||||
|
) {
|
||||||
|
const createHouseCommand = new CreateHouseCommand();
|
||||||
|
createHouseCommand._id = id;
|
||||||
|
createHouseCommand._title = title;
|
||||||
|
createHouseCommand._url = url;
|
||||||
|
createHouseCommand._neighborhood = neighborhood;
|
||||||
|
createHouseCommand._area = area;
|
||||||
|
createHouseCommand._rooms = rooms;
|
||||||
|
createHouseCommand._price = price;
|
||||||
|
createHouseCommand._description = description;
|
||||||
|
createHouseCommand._pic = pic;
|
||||||
|
createHouseCommand._baths = baths;
|
||||||
|
createHouseCommand._level = level;
|
||||||
|
createHouseCommand._phone = phone;
|
||||||
|
|
||||||
|
return createHouseCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
public id() {
|
||||||
|
return this._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public title() {
|
||||||
|
return this._title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public url() {
|
||||||
|
return this._url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public neighborhood() {
|
||||||
|
return this._neighborhood;
|
||||||
|
}
|
||||||
|
|
||||||
|
public area() {
|
||||||
|
return this._area;
|
||||||
|
}
|
||||||
|
|
||||||
|
public rooms() {
|
||||||
|
return this._rooms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public price() {
|
||||||
|
return this._price;
|
||||||
|
}
|
||||||
|
|
||||||
|
public description() {
|
||||||
|
return this._description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public pic() {
|
||||||
|
return this._pic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public baths() {
|
||||||
|
return this._baths;
|
||||||
|
}
|
||||||
|
|
||||||
|
public level() {
|
||||||
|
return this._level;
|
||||||
|
}
|
||||||
|
|
||||||
|
public phone() {
|
||||||
|
return this._phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
22
server/application/command/house/construeHouseCommand.ts
Normal file
22
server/application/command/house/construeHouseCommand.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export class ConstrueHouseCommand {
|
||||||
|
|
||||||
|
private _rawHouseList: object;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static create(
|
||||||
|
rawHouseList: object,
|
||||||
|
) {
|
||||||
|
const construeHouseCommand = new ConstrueHouseCommand();
|
||||||
|
construeHouseCommand._rawHouseList = rawHouseList;
|
||||||
|
|
||||||
|
return construeHouseCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
public rawHouseList() {
|
||||||
|
return this._rawHouseList;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,173 @@
|
|||||||
|
import { Op, sequelize, House as Hou } from "../../../db/mysql/db.config";
|
||||||
|
import { House } from "../../../domain/house";
|
||||||
|
import type { ConstrueHouseCommand } from "../../command/house/construeHouseCommand";
|
||||||
|
|
||||||
|
export class ConstrueHouseCommandHandler {
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static async create(construeHouseCommand: ConstrueHouseCommand) {
|
||||||
|
const houseCandidates: House[] = [];
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
construeHouseCommand.rawHouseList().forEach((houseCandidate: object) => {
|
||||||
|
houseCandidates.push(
|
||||||
|
House.create(
|
||||||
|
`${++i}`,
|
||||||
|
houseCandidate.title,
|
||||||
|
houseCandidate.url,
|
||||||
|
houseCandidate.neighborhood,
|
||||||
|
houseCandidate.area,
|
||||||
|
houseCandidate.rooms,
|
||||||
|
houseCandidate.price,
|
||||||
|
houseCandidate.description,
|
||||||
|
houseCandidate.pic,
|
||||||
|
houseCandidate.baths,
|
||||||
|
houseCandidate.level,
|
||||||
|
houseCandidate.phone
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
const proposedHouses = ConstrueHouseCommandHandler.findHouseByTitleAndExactPriceAndRoomsAndBaths(houseCandidates);
|
||||||
|
|
||||||
|
return proposedHouses;
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async findHouseByTitleAndExactPriceAndRoomsAndBaths(houseCandidates: any): Promise<String[]> {
|
||||||
|
const mergedHouses: any[] = [];
|
||||||
|
|
||||||
|
// Usamos un bucle for normal para manejar bien el await dentro del ciclo
|
||||||
|
for (let i = 0; i < houseCandidates.length; i++) {
|
||||||
|
const houseCandidate = houseCandidates[i];
|
||||||
|
const sanitizedTitle = houseCandidate.title.replace(/'/g, "\\'");
|
||||||
|
|
||||||
|
// Esperamos a que la búsqueda en la base de datos se resuelva
|
||||||
|
const matchingHouses = await Hou.findAll({
|
||||||
|
where: {
|
||||||
|
[Op.and]: [
|
||||||
|
{ price: houseCandidate.rawPrice },
|
||||||
|
{ rooms: houseCandidate.rooms },
|
||||||
|
{ area: houseCandidate.area },
|
||||||
|
{ baths: houseCandidate.baths },
|
||||||
|
sequelize.literal(`MATCH(title) AGAINST('${sanitizedTitle}' IN NATURAL LANGUAGE MODE)`)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultHouse = ConstrueHouseCommandHandler.mergeHouseProperties(matchingHouses);
|
||||||
|
|
||||||
|
// Si hay coincidencias, añadimos los resultados al array de casas encontradas
|
||||||
|
if (matchingHouses.length > 0) {
|
||||||
|
mergedHouses.push(resultHouse);
|
||||||
|
|
||||||
|
// Eliminamos todas las casas que coincidan con los resultados obtenidos
|
||||||
|
houseCandidates = houseCandidates.filter((candidate: any) => {
|
||||||
|
return !matchingHouses.some((matchedHouse: any) => {
|
||||||
|
// Aquí puedes definir los criterios para eliminar la casa original.
|
||||||
|
// Por ejemplo, comparando el ID, precio, habitaciones, etc.
|
||||||
|
return (
|
||||||
|
candidate.rawPrice === matchedHouse.price &&
|
||||||
|
candidate.rooms === matchedHouse.rooms &&
|
||||||
|
candidate.area === matchedHouse.area &&
|
||||||
|
candidate.baths === matchedHouse.baths
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Como hemos modificado el array, restamos 1 al índice
|
||||||
|
i = -1; // Reiniciamos el índice para volver a iterar el array actualizado
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedHouses;
|
||||||
|
|
||||||
|
// const sanitizedTitle = house.title.replace(/'/g, "\\'");
|
||||||
|
|
||||||
|
// const matchingHouses = Hou.findAll({
|
||||||
|
// where: {
|
||||||
|
// [Op.and]: [
|
||||||
|
// {price: house.rawPrice},
|
||||||
|
// {rooms: house.rooms},
|
||||||
|
// {area: house.area},
|
||||||
|
// {baths: house.baths},
|
||||||
|
// sequelize.literal(`MATCH(title) AGAINST('${sanitizedTitle}' IN NATURAL LANGUAGE MODE)`)
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return matchingHouses;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static mergeHouseProperties(houses: any[]): any {
|
||||||
|
if (!Array.isArray(houses)) {
|
||||||
|
throw new Error("El parámetro 'houses' debe ser un array.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedHouse = {
|
||||||
|
id: ConstrueHouseCommandHandler.getLongestString(houses.map(h => h.id)) || Math.floor(Math.random() * 100000000).toString().padStart(8, '0'),
|
||||||
|
title: ConstrueHouseCommandHandler.getLongestString(houses.map(h => h.title)),
|
||||||
|
url: ConstrueHouseCommandHandler.joinByComma(houses.map(h => h.url)),
|
||||||
|
neighborhood: ConstrueHouseCommandHandler.getLongestString(houses.map(h => h.neighborhood)),
|
||||||
|
area: houses[0]?.area || 0, // Asumo que el área es la misma en todos los duplicados
|
||||||
|
rooms: houses[0]?.rooms || 0, // Asumo que el número de habitaciones es el mismo
|
||||||
|
price: houses[0]?.price || 0, // Asumo que el precio es el mismo
|
||||||
|
description: ConstrueHouseCommandHandler.getLongestString(houses.map(h => h.description)),
|
||||||
|
pic: ConstrueHouseCommandHandler.joinByComma(houses.map(h => h.pic)),
|
||||||
|
baths: houses[0]?.baths || 0, // Asumo que el número de baños es el mismo
|
||||||
|
level: ConstrueHouseCommandHandler.getLongestString(houses.map(h => h.level)),
|
||||||
|
phone: ConstrueHouseCommandHandler.joinByComma(houses.map(h => h.phone)) // Concatenar los teléfonos
|
||||||
|
};
|
||||||
|
|
||||||
|
return mergedHouse;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getLongestString(strings: string[]): string {
|
||||||
|
const preSelectedString = strings
|
||||||
|
.filter(str => str && !str.startsWith('<a href=')) // Ignora las cadenas que comienzan con '<a href='
|
||||||
|
.reduce((longest, current) => current && current.length > longest.length ? current : longest, "");
|
||||||
|
|
||||||
|
const alternativeString = strings.reduce((longest, current) => current && current.length > longest.length ? current : longest, "");
|
||||||
|
|
||||||
|
return preSelectedString !== "" ? preSelectedString : ConstrueHouseCommandHandler.parseLinks(alternativeString);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static joinByComma(items: string[]): string {
|
||||||
|
return items.filter(Boolean).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static parseLinks(htmlString) {
|
||||||
|
// Regex para identificar las etiquetas <a> con el href y el contenido interno
|
||||||
|
const anchorTagRegex = /<a href="\/comprar\/(.*?)18.*?"[^>]*>(.*?)<\/a>/g;
|
||||||
|
let match;
|
||||||
|
let result = "";
|
||||||
|
|
||||||
|
while ((match = anchorTagRegex.exec(htmlString)) !== null) {
|
||||||
|
let urlPart = match[1]; // La parte que queremos desde el href
|
||||||
|
let innerText = match[2].trim(); // El texto dentro de las etiquetas <a>
|
||||||
|
|
||||||
|
// Si innerText está vacío, tomamos la parte de la URL
|
||||||
|
if (!innerText) {
|
||||||
|
result= ConstrueHouseCommandHandler.snakeToSpaceCase(urlPart);
|
||||||
|
} else {
|
||||||
|
result = innerText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static snakeToSpaceCase(snakeCaseString) {
|
||||||
|
// Reemplazar tanto los guiones bajos (_) como los guiones medios (-) por espacios
|
||||||
|
let spaceCaseString = snakeCaseString.replace(/[_-]/g, ' ');
|
||||||
|
|
||||||
|
// Capitalizar la primera letra de cada palabra
|
||||||
|
spaceCaseString = spaceCaseString.split(' ').map(word => {
|
||||||
|
return word.charAt(0).toUpperCase() + word.slice(1);
|
||||||
|
}).join(' ');
|
||||||
|
|
||||||
|
return spaceCaseString;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
9
server/db/mongo/index.js
Normal file
9
server/db/mongo/index.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
|
||||||
|
export default async () => {
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
|
||||||
|
await mongoose.connect(config.mongodbUri, { useNewUrlParser: true, useUnifiedTopology: true });
|
||||||
|
|
||||||
|
return mongoose;
|
||||||
|
}
|
||||||
20
server/db/mongo/models/Property.js
Normal file
20
server/db/mongo/models/Property.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import mongoose from "mongoose";
|
||||||
|
|
||||||
|
const propertySchema = new mongoose.Schema({
|
||||||
|
id: { type: String, required: true, unique: true },
|
||||||
|
title: { type: String, required: true },
|
||||||
|
url: { type: String, required: true },
|
||||||
|
price: { type: String, required: true },
|
||||||
|
rooms: { type: Number, required: true },
|
||||||
|
area: { type: Number, required: true },
|
||||||
|
level: { type: String, required: true },
|
||||||
|
description: { type: String },
|
||||||
|
pic: { type: String },
|
||||||
|
baths: { type: Number, required: true },
|
||||||
|
neighborhood: { type: String, required: true },
|
||||||
|
phone: { type: String, required: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const Property = mongoose.model("Property", propertySchema);
|
||||||
|
|
||||||
|
export default Property;
|
||||||
27
server/db/mysql/db.config.js
Normal file
27
server/db/mysql/db.config.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Sequelize, Op } from 'sequelize';
|
||||||
|
import sequelizeConfig from '~/config/sequelize';
|
||||||
|
import defineProperty from "./models/Property";
|
||||||
|
import defineHouse from "./models/House";
|
||||||
|
import defineHome from "./models/Home";
|
||||||
|
|
||||||
|
const environmentConfig = sequelizeConfig["production"];
|
||||||
|
|
||||||
|
const sequelize = new Sequelize(
|
||||||
|
environmentConfig.database,
|
||||||
|
environmentConfig.username,
|
||||||
|
environmentConfig.password,
|
||||||
|
{
|
||||||
|
host: environmentConfig.host,
|
||||||
|
dialect: environmentConfig.dialect,
|
||||||
|
logging: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Property = defineProperty(sequelize);
|
||||||
|
const House = defineHouse(sequelize);
|
||||||
|
const Home = defineHome(sequelize);
|
||||||
|
|
||||||
|
sequelize.sync()
|
||||||
|
.then(() => console.log('Database & tables created!'))
|
||||||
|
.catch((error) => console.error('Error syncing database:', error));
|
||||||
|
|
||||||
|
export { sequelize, Op, House, Home, Property };
|
||||||
13
server/db/mysql/index.js
Normal file
13
server/db/mysql/index.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import {sequelize} from "./db.config";
|
||||||
|
|
||||||
|
async function connectToDatabase() {
|
||||||
|
try {
|
||||||
|
await sequelize.authenticate();
|
||||||
|
console.log('Connection to MySQL has been established successfully.');
|
||||||
|
return sequelize;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Unable to connect to the MySQL database:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connectToDatabase;
|
||||||
103
server/db/mysql/migrations/20240614190129_noname.cjs
Normal file
103
server/db/mysql/migrations/20240614190129_noname.cjs
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
const Sequelize = require("sequelize");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions summary:
|
||||||
|
*
|
||||||
|
* createTable() => "properties", deps: []
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
const info = {
|
||||||
|
revision: 1,
|
||||||
|
name: "noname",
|
||||||
|
created: "2024-06-14T19:01:29.478Z",
|
||||||
|
comment: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrationCommands = (transaction) => [
|
||||||
|
{
|
||||||
|
fn: "createTable",
|
||||||
|
params: [
|
||||||
|
"properties",
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
field: "id",
|
||||||
|
primaryKey: true,
|
||||||
|
unique: true,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
title: { type: Sequelize.STRING, field: "title", allowNull: true },
|
||||||
|
url: { type: Sequelize.STRING, field: "url", allowNull: true },
|
||||||
|
price: { type: Sequelize.STRING, field: "price", allowNull: true },
|
||||||
|
rooms: { type: Sequelize.INTEGER, field: "rooms", allowNull: true },
|
||||||
|
area: { type: Sequelize.INTEGER, field: "area", allowNull: true },
|
||||||
|
level: { type: Sequelize.STRING, field: "level", allowNull: true },
|
||||||
|
description: {
|
||||||
|
type: Sequelize.TEXT,
|
||||||
|
field: "description",
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
pic: { type: Sequelize.TEXT, field: "pic", allowNull: true },
|
||||||
|
baths: { type: Sequelize.INTEGER, field: "baths", allowNull: true },
|
||||||
|
neighborhood: {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
field: "neighborhood",
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
phone: { type: Sequelize.STRING, field: "phone", allowNull: true },
|
||||||
|
createdAt: {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
field: "createdAt",
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
field: "updatedAt",
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ transaction },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const rollbackCommands = (transaction) => [
|
||||||
|
{
|
||||||
|
fn: "dropTable",
|
||||||
|
params: ["properties", { transaction }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const pos = 0;
|
||||||
|
const useTransaction = true;
|
||||||
|
|
||||||
|
const execute = (queryInterface, sequelize, _commands) => {
|
||||||
|
let index = pos;
|
||||||
|
const run = (transaction) => {
|
||||||
|
const commands = _commands(transaction);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const next = () => {
|
||||||
|
if (index < commands.length) {
|
||||||
|
const command = commands[index];
|
||||||
|
console.log(`[#${index}] execute: ${command.fn}`);
|
||||||
|
index++;
|
||||||
|
queryInterface[command.fn](...command.params).then(next, reject);
|
||||||
|
} else resolve();
|
||||||
|
};
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (useTransaction) return queryInterface.sequelize.transaction(run);
|
||||||
|
return run(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
pos,
|
||||||
|
useTransaction,
|
||||||
|
up: (queryInterface, sequelize) =>
|
||||||
|
execute(queryInterface, sequelize, migrationCommands),
|
||||||
|
down: (queryInterface, sequelize) =>
|
||||||
|
execute(queryInterface, sequelize, rollbackCommands),
|
||||||
|
info,
|
||||||
|
};
|
||||||
86
server/db/mysql/migrations/_current.json
Normal file
86
server/db/mysql/migrations/_current.json
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
"tables": {
|
||||||
|
"properties": {
|
||||||
|
"tableName": "properties",
|
||||||
|
"schema": {
|
||||||
|
"id": {
|
||||||
|
"allowNull": true,
|
||||||
|
"unique": true,
|
||||||
|
"primaryKey": true,
|
||||||
|
"field": "id",
|
||||||
|
"seqType": "Sequelize.STRING"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"allowNull": true,
|
||||||
|
"field": "title",
|
||||||
|
"seqType": "Sequelize.STRING"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"allowNull": true,
|
||||||
|
"field": "url",
|
||||||
|
"seqType": "Sequelize.STRING"
|
||||||
|
},
|
||||||
|
"price": {
|
||||||
|
"allowNull": true,
|
||||||
|
"field": "price",
|
||||||
|
"seqType": "Sequelize.STRING"
|
||||||
|
},
|
||||||
|
"rooms": {
|
||||||
|
"allowNull": true,
|
||||||
|
"field": "rooms",
|
||||||
|
"seqType": "Sequelize.INTEGER"
|
||||||
|
},
|
||||||
|
"area": {
|
||||||
|
"allowNull": true,
|
||||||
|
"field": "area",
|
||||||
|
"seqType": "Sequelize.INTEGER"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"allowNull": true,
|
||||||
|
"field": "level",
|
||||||
|
"seqType": "Sequelize.STRING"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"allowNull": true,
|
||||||
|
"field": "description",
|
||||||
|
"seqType": "Sequelize.TEXT"
|
||||||
|
},
|
||||||
|
"pic": {
|
||||||
|
"allowNull": true,
|
||||||
|
"field": "pic",
|
||||||
|
"seqType": "Sequelize.TEXT"
|
||||||
|
},
|
||||||
|
"baths": {
|
||||||
|
"allowNull": true,
|
||||||
|
"field": "baths",
|
||||||
|
"seqType": "Sequelize.INTEGER"
|
||||||
|
},
|
||||||
|
"neighborhood": {
|
||||||
|
"allowNull": true,
|
||||||
|
"field": "neighborhood",
|
||||||
|
"seqType": "Sequelize.STRING"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"allowNull": true,
|
||||||
|
"field": "phone",
|
||||||
|
"seqType": "Sequelize.STRING"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"allowNull": false,
|
||||||
|
"field": "createdAt",
|
||||||
|
"seqType": "Sequelize.DATE"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"allowNull": false,
|
||||||
|
"field": "updatedAt",
|
||||||
|
"seqType": "Sequelize.DATE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"path": "Z:\\webs\\hometify\\server\\db\\mysql\\migrations\\_current.json",
|
||||||
|
"backupPath": "Z:\\webs\\hometify\\server\\db\\mysql\\migrations\\_current_bak.json",
|
||||||
|
"exists": false,
|
||||||
|
"revision": 1
|
||||||
|
}
|
||||||
65
server/db/mysql/models/Home.js
Normal file
65
server/db/mysql/models/Home.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// server/models/Home.js
|
||||||
|
import { DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
export default function House (sequelize) {
|
||||||
|
return sequelize.define(
|
||||||
|
"Home",
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
unique: true,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
neighborhood: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
area: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
rooms: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
pic: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
baths: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
level: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
phone: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tableName: "home",
|
||||||
|
timestamps: false, // createdAt y updatedAt
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
64
server/db/mysql/models/House.js
Normal file
64
server/db/mysql/models/House.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// server/models/Property.js
|
||||||
|
import { DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
export default function House (sequelize) {
|
||||||
|
return sequelize.define(
|
||||||
|
"House",
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
unique: true,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
neighborhood: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
area: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
rooms: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
pic: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
baths: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
level: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
phone: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tableName: "house",
|
||||||
|
timestamps: false, // createdAt y updatedAt
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
64
server/db/mysql/models/Property.js
Normal file
64
server/db/mysql/models/Property.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// server/models/Property.js
|
||||||
|
import { DataTypes } from "sequelize";
|
||||||
|
|
||||||
|
export default function Property (sequelize) {
|
||||||
|
return sequelize.define(
|
||||||
|
"Property",
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
unique: true,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
rooms: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
area: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
level: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
pic: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
baths: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
neighborhood: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
phone: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tableName: "properties",
|
||||||
|
timestamps: true, // Incluye createdAt y updatedAt
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
63
server/db/mysql/models/index.js
Normal file
63
server/db/mysql/models/index.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Importar módulos necesarios
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath, pathToFileURL } from 'url';
|
||||||
|
import { Sequelize, DataTypes } from 'sequelize';
|
||||||
|
import process from 'process';
|
||||||
|
import configFile from '../../../../config/sequelize.js';
|
||||||
|
|
||||||
|
// Definir __filename y __dirname en ES Modules
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
// Variables necesarias
|
||||||
|
const basename = path.basename(__filename);
|
||||||
|
const env = process.env.NODE_ENV || 'development';
|
||||||
|
const config = configFile[env];
|
||||||
|
const db = {};
|
||||||
|
|
||||||
|
// Configuración de Sequelize
|
||||||
|
let sequelize;
|
||||||
|
if (config.use_env_variable) {
|
||||||
|
sequelize = new Sequelize(process.env[config.use_env_variable], config);
|
||||||
|
} else {
|
||||||
|
sequelize = new Sequelize(config.database, config.username, config.password, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Función para cargar modelos de forma asíncrona
|
||||||
|
const loadModels = async () => {
|
||||||
|
const files = fs.readdirSync(__dirname).filter(file => {
|
||||||
|
return (
|
||||||
|
file.indexOf('.') !== 0 &&
|
||||||
|
file !== basename &&
|
||||||
|
file.slice(-3) === '.js' &&
|
||||||
|
file.indexOf('.test.js') === -1
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const modelPath = path.join(__dirname, file);
|
||||||
|
const modelURL = pathToFileURL(modelPath).href; // Convertir a una URL válida con esquema file://
|
||||||
|
const { default: modelDef } = await import(modelURL);
|
||||||
|
const model = modelDef(sequelize, DataTypes);
|
||||||
|
db[model.name] = model;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ejecutar la carga de modelos
|
||||||
|
await loadModels();
|
||||||
|
|
||||||
|
// Asociar modelos
|
||||||
|
Object.keys(db).forEach(modelName => {
|
||||||
|
if (db[modelName].associate) {
|
||||||
|
db[modelName].associate(db);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Exportar la instancia de Sequelize y modelos
|
||||||
|
db.sequelize = sequelize;
|
||||||
|
db.Sequelize = Sequelize;
|
||||||
|
|
||||||
|
export default db;
|
||||||
6
server/db/mysql/repository/HomeRepository.ts
Normal file
6
server/db/mysql/repository/HomeRepository.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { sequelize, Op, Home } from "../db.config";
|
||||||
|
|
||||||
|
async function insertOrUpdateHome(home: any) {
|
||||||
|
await Home.create(home);
|
||||||
|
console.log('Nueva casa insertada:', home.id);
|
||||||
|
}
|
||||||
18
server/db/mysql/repository/HouseRepository.ts
Normal file
18
server/db/mysql/repository/HouseRepository.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { sequelize, Op, House } from "../db.config";
|
||||||
|
|
||||||
|
export async function findHouseByTitleAndExactPriceAndRoomsAndBaths(house: any) {
|
||||||
|
//const sanitizedTitle = house.title.replace(/'/g, "\\'");
|
||||||
|
|
||||||
|
return "pollas";
|
||||||
|
|
||||||
|
const matchingHouses = await House.findAll({
|
||||||
|
where: {
|
||||||
|
price: house.price,
|
||||||
|
rooms: house.rooms,
|
||||||
|
baths: house.baths,
|
||||||
|
[Op.and]: sequelize.literal(`MATCH(title) AGAINST('${sanitizedTitle}' IN NATURAL LANGUAGE MODE)`)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return matchingHouses;
|
||||||
|
}
|
||||||
107
server/domain/house.ts
Normal file
107
server/domain/house.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
export class House {
|
||||||
|
_id: string;
|
||||||
|
_title: string | null;
|
||||||
|
_url: string | null;
|
||||||
|
_neighborhood: string | null;
|
||||||
|
_area: number | null;
|
||||||
|
_rooms: number | null;
|
||||||
|
_price: number | null;
|
||||||
|
_rawPrice: string | null;
|
||||||
|
_description: string| null;
|
||||||
|
_pic: string | null;
|
||||||
|
_baths: number | null;
|
||||||
|
_level: string | null;
|
||||||
|
_phone: string | null;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static create(
|
||||||
|
id : string,
|
||||||
|
title : string,
|
||||||
|
url : string,
|
||||||
|
neighborhood: string,
|
||||||
|
area : number | string,
|
||||||
|
rooms : number | string,
|
||||||
|
price : number | string,
|
||||||
|
description : string,
|
||||||
|
pic : string,
|
||||||
|
baths : number | string,
|
||||||
|
level : string,
|
||||||
|
phone : string
|
||||||
|
) {
|
||||||
|
const house = new House();
|
||||||
|
house._id = id;
|
||||||
|
house._title = title?.trim() || "";
|
||||||
|
house._url = url?.trim() || "";
|
||||||
|
house._neighborhood = neighborhood?.trim() || "";
|
||||||
|
house._area = area ? parseInt(area.toString()) : 0;
|
||||||
|
house._rooms = rooms ? parseInt(rooms.toString()) : 0;
|
||||||
|
house._price = price ? parseInt(price.toString()?.replace(/\./g,'')) : 0;
|
||||||
|
house._rawPrice = price.toString() || "";
|
||||||
|
house._description = description?.trim() || "";
|
||||||
|
house._pic = pic?.trim() || "";
|
||||||
|
house._baths = baths ? parseInt(baths.toString()) : 0;
|
||||||
|
house._level = level?.trim() || "";
|
||||||
|
house._phone = phone?.trim() || "";
|
||||||
|
|
||||||
|
return house;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Método para verificar coincidencia exacta en habitaciones, baños y precio
|
||||||
|
matchesExact(other: House): boolean {
|
||||||
|
return this.rooms === other.rooms && this.baths === other.baths && this.price === other.price;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get id() {
|
||||||
|
return this._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get title() {
|
||||||
|
return this._title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get url() {
|
||||||
|
return this._url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get neighborhood() {
|
||||||
|
return this._neighborhood;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get area() {
|
||||||
|
return this._area;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get rooms() {
|
||||||
|
return this._rooms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get price() {
|
||||||
|
return this._price;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get rawPrice() {
|
||||||
|
return this._rawPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get description() {
|
||||||
|
return this._description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get pic() {
|
||||||
|
return this._pic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get baths() {
|
||||||
|
return this._baths;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get level() {
|
||||||
|
return this._level;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get phone() {
|
||||||
|
return this._phone;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
server/plugins/db/init.ts
Normal file
19
server/plugins/db/init.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { defineEventHandler } from "h3";
|
||||||
|
import connectToDatabase from "~/server/db/mysql";
|
||||||
|
import Property from "~/server/db/mysql/models/Property";
|
||||||
|
|
||||||
|
// Inicializar Sequelize con las configuraciones de Nuxt
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const sequelize = await connectToDatabase();
|
||||||
|
|
||||||
|
// Definir el modelo Property
|
||||||
|
Property(sequelize);
|
||||||
|
|
||||||
|
// Sincronizar la base de datos
|
||||||
|
try {
|
||||||
|
await sequelize?.sync({ alter: false }); // O usa { alter: true } para no perder datos existentes.
|
||||||
|
console.log("Database synchronized successfully.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error synchronizing database:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
12
server/services/cron/cron.js
Normal file
12
server/services/cron/cron.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
//import cron from "node-cron";
|
||||||
|
//import fotocasaScraper from "./fotocasaScraper";
|
||||||
|
// Import other scrapers similarly
|
||||||
|
//import { notifyNewListing } from './telegramBot'; // Ensure the bot is initialized
|
||||||
|
|
||||||
|
export default function (moduleOptions) {
|
||||||
|
// Schedule scraping tasks
|
||||||
|
//cron.schedule("0 0 * * *", () => {
|
||||||
|
//fotocasaScraper();
|
||||||
|
// Call other scrapers similarly
|
||||||
|
//});
|
||||||
|
}
|
||||||
100
server/services/fusejs/filterProperties.js
Normal file
100
server/services/fusejs/filterProperties.js
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
// utils/filterProperties.js
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
|
export const filterSimilarProperties = (properties) => {
|
||||||
|
const options = {
|
||||||
|
keys: ['_title'], // Solo buscamos similitud en el título
|
||||||
|
threshold: 0.55, // 0.6 es un umbral para 40% de similitud
|
||||||
|
includeScore: true // Incluye la puntuación de similitud
|
||||||
|
};
|
||||||
|
|
||||||
|
const fuse = new Fuse(properties, options);
|
||||||
|
const uniqueProperties = [];
|
||||||
|
const seen = new Set(); // Usaremos un conjunto para rastrear propiedades vistas
|
||||||
|
|
||||||
|
properties.forEach((property) => {
|
||||||
|
const propertyData = property;
|
||||||
|
if (!seen.has(propertyData._id)) {
|
||||||
|
// Inicializa el array para IDs similares en dataValues
|
||||||
|
propertyData.similarIds = [];
|
||||||
|
|
||||||
|
// Buscamos propiedades similares por título
|
||||||
|
const result = fuse.search(propertyData._title).filter((res) => {
|
||||||
|
const similarProperty = res.item;
|
||||||
|
// Filtramos resultados que no sean la misma propiedad y sean similares en título
|
||||||
|
const isSimilar = res.item._id !== propertyData._id && res.score < options.threshold;
|
||||||
|
|
||||||
|
if (isSimilar) {
|
||||||
|
propertyData.similarIds.push(similarProperty._id);
|
||||||
|
|
||||||
|
// Enriquecer la propiedad actual con datos de la propiedad similar
|
||||||
|
// propertyData.title = getLongest(propertyData.title, similarProperty.title);
|
||||||
|
// propertyData.url = propertyData.url || similarProperty.url;
|
||||||
|
// propertyData.price = propertyData.price || similarProperty.price;
|
||||||
|
// propertyData.rooms = propertyData.rooms || similarProperty.rooms;
|
||||||
|
// propertyData.area = propertyData.area || similarProperty.area;
|
||||||
|
// propertyData.level = propertyData.level || similarProperty.level;
|
||||||
|
// propertyData.description = getLongest(propertyData.description, similarProperty.description);
|
||||||
|
// propertyData.pic = propertyData.pic || similarProperty.pic;
|
||||||
|
// propertyData.baths = propertyData.baths || similarProperty.baths;
|
||||||
|
// propertyData.neighborhood = propertyData.neighborhood || similarProperty.neighborhood;
|
||||||
|
// propertyData.phone = propertyData.phone || similarProperty.phone;
|
||||||
|
|
||||||
|
// Marcar la propiedad similar como vista
|
||||||
|
//seen.add(similarProperty.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isSimilar;
|
||||||
|
});
|
||||||
|
|
||||||
|
uniqueProperties.push(property);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return uniqueProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const groupSimilarProperties = (properties) => {
|
||||||
|
const filteredProperties = filterSimilarProperties(properties);
|
||||||
|
const groups = [];
|
||||||
|
const visited = new Set();
|
||||||
|
|
||||||
|
filteredProperties.forEach((property) => {
|
||||||
|
const propertyId = property._id;
|
||||||
|
if (!visited.has(propertyId)) {
|
||||||
|
const group = [property];
|
||||||
|
visited.add(propertyId);
|
||||||
|
|
||||||
|
const queue = [...property.similarIds];
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const currentId = queue.shift();
|
||||||
|
if (!visited.has(currentId)) {
|
||||||
|
const similarProperty = filteredProperties.find(prop => prop._id === currentId);
|
||||||
|
if (similarProperty) {
|
||||||
|
group.push(similarProperty);
|
||||||
|
visited.add(currentId);
|
||||||
|
queue.push(...similarProperty.similarIds.filter(id => !visited.has(id)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
groups.push(group);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert groups to desired output format
|
||||||
|
const groupedProperties = groups.reduce((acc, group, index) => {
|
||||||
|
acc[index] = group;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return groupedProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const getLongest = (a, b) => {
|
||||||
|
if (!a) return b;
|
||||||
|
if (!b) return a;
|
||||||
|
return a.length >= b.length ? a : b;
|
||||||
|
};
|
||||||
66
server/services/fuzzySearch/fuzzySearchService.ts
Normal file
66
server/services/fuzzySearch/fuzzySearchService.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import stringSimilarity from 'string-similarity';
|
||||||
|
|
||||||
|
export class FuzzySearchService {
|
||||||
|
_data: any = [];
|
||||||
|
_threshold: number;
|
||||||
|
|
||||||
|
private constructor(data, threshold) {
|
||||||
|
this._data = data;
|
||||||
|
this._threshold = threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static create(data, threshold = 0.7) {
|
||||||
|
return new FuzzySearchService(data, threshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
matchSimilarProperty(property) {
|
||||||
|
const groupedData: Map<string, unknown[]> = new Map();
|
||||||
|
|
||||||
|
this.data.forEach((obj) => {
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
for (const [key, group] of groupedData.entries()) {
|
||||||
|
const representativeObj: any = group[0];
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.isPropertySimilar(representativeObj[property], obj[property])
|
||||||
|
) {
|
||||||
|
group.push(obj);
|
||||||
|
groupedData.set(key, group);
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
// Crea una nueva clave en el mapa para un nuevo grupo
|
||||||
|
groupedData.set(obj.id, [obj]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return groupedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
matchExactProperty(property) {
|
||||||
|
// Implement your exact property matching logic here
|
||||||
|
// Return the matched property
|
||||||
|
}
|
||||||
|
|
||||||
|
matchExactProperties(properties) {
|
||||||
|
// Implement your exact properties matching logic here
|
||||||
|
// Return the matched properties
|
||||||
|
}
|
||||||
|
|
||||||
|
isPropertySimilar(property, otherProperty: string): boolean {
|
||||||
|
const similarity = stringSimilarity.compareTwoStrings(property, otherProperty);
|
||||||
|
return similarity >= this.threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
get data() {
|
||||||
|
return this._data;
|
||||||
|
}
|
||||||
|
|
||||||
|
get threshold() {
|
||||||
|
return this._threshold;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
server/services/telegram/telegramBot.js
Normal file
21
server/services/telegram/telegramBot.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import TelegramBot from 'node-telegram-bot-api';
|
||||||
|
import Listing from '../../server/db/models/Listing';
|
||||||
|
|
||||||
|
const botToken = process.env.TELEGRAM_BOT_TOKEN;
|
||||||
|
const chatId = process.env.TELEGRAM_CHAT_ID;
|
||||||
|
const bot = new TelegramBot(botToken, { polling: true });
|
||||||
|
|
||||||
|
export const notifyNewListing = async (listing) => {
|
||||||
|
const message = `
|
||||||
|
New Listing:
|
||||||
|
Title: ${listing.title}
|
||||||
|
Price: ${listing.price}
|
||||||
|
Location: ${listing.location}
|
||||||
|
Description: ${listing.description}
|
||||||
|
URL: ${listing.url}
|
||||||
|
`;
|
||||||
|
|
||||||
|
await bot.sendMessage(chatId, message);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default bot;
|
||||||
8
server/tsconfig.json
Normal file
8
server/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "../.nuxt/tsconfig.server.json",
|
||||||
|
"include": [
|
||||||
|
"./node_modules/puppeteer-extra/*.d.ts",
|
||||||
|
"./node_modules/puppeteer-extra-*/*.d.ts",
|
||||||
|
"./node_modules/@types/puppeteer/index.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
10
tsconfig.json
Normal file
10
tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
|
|
||||||
|
"extends": "./.nuxt/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": [
|
||||||
|
"./types/index"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
8
types/index.d.ts
vendored
Normal file
8
types/index.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// types/index.d.ts
|
||||||
|
import 'nuxt'
|
||||||
|
|
||||||
|
declare module 'nuxt' {
|
||||||
|
interface Context {
|
||||||
|
$puppeteer: typeof import('puppeteer-extra')
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user