mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cceae11cc | ||
|
|
a4c5bfcbf7 | ||
|
|
6d2ab5f958 | ||
|
|
d3cb3a5881 | ||
|
|
111ef8be43 | ||
|
|
35feb772d7 |
@@ -87,7 +87,10 @@ class FredyRuntime {
|
|||||||
return listings.map(this._providerConfig.normalize);
|
return listings.map(this._providerConfig.normalize);
|
||||||
}
|
}
|
||||||
_filter(listings) {
|
_filter(listings) {
|
||||||
return listings.filter(this._providerConfig.filter);
|
//only return those where all the fields have been found
|
||||||
|
const keys = Object.keys(this._providerConfig.crawlFields);
|
||||||
|
const filteredListings = listings.filter((item) => keys.every((key) => key in item));
|
||||||
|
return filteredListings.filter(this._providerConfig.filter);
|
||||||
}
|
}
|
||||||
_findNew(listings) {
|
_findNew(listings) {
|
||||||
const newListings = listings.filter((o) => getKnownListings(this._jobKey, this._providerId)[o.id] == null);
|
const newListings = listings.filter((o) => getKnownListings(this._jobKey, this._providerId)[o.id] == null);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const send = ({ serviceName, newListings, notificationConfig, jobKey }) =
|
|||||||
const jobName = job == null ? jobKey : job.name;
|
const jobName = job == null ? jobKey : job.name;
|
||||||
const promises = newListings.map((newListing) => {
|
const promises = newListings.map((newListing) => {
|
||||||
const title = `${jobName} at ${serviceName}: ${newListing.title}`;
|
const title = `${jobName} at ${serviceName}: ${newListing.title}`;
|
||||||
const message = `Address: ${newListing.address}\nSize: ${newListing.size}\nPrice: ${newListing.price}\Link: ${newListing.link}`;
|
const message = `Address: ${newListing.address}\nSize: ${newListing.size}\nPrice: ${newListing.price}\nink: ${newListing.link}`;
|
||||||
return fetch(server, {
|
return fetch(server, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|||||||
25
lib/notification/adapter/sqlite.js
Normal file
25
lib/notification/adapter/sqlite.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { markdown2Html } from '../../services/markdown.js';
|
||||||
|
import Database from 'better-sqlite3';
|
||||||
|
export const send = ({ serviceName, newListings, jobKey }) => {
|
||||||
|
const db = new Database('db/listings.db');
|
||||||
|
const fields = ['serviceName', 'jobKey', 'id', 'size', 'rooms', 'price', 'address', 'title', 'link', 'description'];
|
||||||
|
db.prepare(`CREATE TABLE IF NOT EXISTS listing (${fields.join(' TEXT, ')} TEXT);`).run();
|
||||||
|
const insert = db.prepare(`INSERT INTO listing (${fields.join(', ')}) VALUES (@${fields.join(', @')})`);
|
||||||
|
newListings.map((listing) => {
|
||||||
|
let insertListing = {};
|
||||||
|
fields.map((field) => {
|
||||||
|
insertListing[field] = listing[field];
|
||||||
|
});
|
||||||
|
insertListing.serviceName = serviceName;
|
||||||
|
insertListing.jobKey = jobKey;
|
||||||
|
insert.run(insertListing);
|
||||||
|
});
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
export const config = {
|
||||||
|
id: 'sqlite',
|
||||||
|
name: 'Sqlite',
|
||||||
|
description: 'This adapter stores listings in a local sqlite3 database.',
|
||||||
|
config: {},
|
||||||
|
readme: markdown2Html('lib/notification/adapter/sqlite.md'),
|
||||||
|
};
|
||||||
7
lib/notification/adapter/sqlite.md
Normal file
7
lib/notification/adapter/sqlite.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
### Sqlite Adapter
|
||||||
|
This adapter stores search results in a sqlite database located in db/listings.db. This file can be used for further analysis later on.
|
||||||
|
|
||||||
|
Fields are:
|
||||||
|
```
|
||||||
|
['serviceName', 'jobKey', 'id', 'size', 'rooms', 'price', 'address', 'title', 'link', 'description']
|
||||||
|
```
|
||||||
@@ -1,18 +1,40 @@
|
|||||||
import utils from '../utils.js';
|
import utils, { buildHash } from '../utils.js';
|
||||||
let appliedBlackList = [];
|
let appliedBlackList = [];
|
||||||
|
|
||||||
function normalize(o) {
|
function normalize(o) {
|
||||||
let size = `${o.size.replace(' Wohnfläche ', '').trim()}`;
|
let size = `${o.size.replace(' Wohnfläche ', '').trim()}`;
|
||||||
if (o.rooms != null) {
|
if (o.rooms != null) {
|
||||||
size += ` / / ${o.rooms.trim()}`;
|
size += ` / / ${o.rooms.trim()}`;
|
||||||
}
|
}
|
||||||
const link = `https://www.1a-immobilienmarkt.de/expose/${o.id}.html`;
|
const link = `https://www.1a-immobilienmarkt.de/expose/${o.id}.html`;
|
||||||
return Object.assign(o, { size, link });
|
const price = normalizePrice(o.price);
|
||||||
|
const id = buildHash(o.id, price);
|
||||||
|
return Object.assign(o, { id, price, size, link });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* einsAImmobilien sometimes use a weird pricing label such as `775.700,00 EUR Kaufpreis ab 2.475 € mtl`.
|
||||||
|
* Make sure to extract only the actual price out of the string.
|
||||||
|
* @param price
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
function normalizePrice(price) {
|
||||||
|
if (price == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const regex = /(\d{1,3}(?:\.\d{3})*,\d{2})\s?(EUR|€)/g;
|
||||||
|
const result = price.match(regex);
|
||||||
|
if (result == null || result.length === 0) {
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
return result[0];
|
||||||
}
|
}
|
||||||
function applyBlacklist(o) {
|
function applyBlacklist(o) {
|
||||||
const titleNotBlacklisted = !utils.isOneOf(o.title, appliedBlackList);
|
const titleNotBlacklisted = !utils.isOneOf(o.title, appliedBlackList);
|
||||||
const descNotBlacklisted = !utils.isOneOf(o.description, appliedBlackList);
|
const descNotBlacklisted = !utils.isOneOf(o.description, appliedBlackList);
|
||||||
return titleNotBlacklisted && descNotBlacklisted;
|
return titleNotBlacklisted && descNotBlacklisted;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
url: null,
|
url: null,
|
||||||
crawlContainer: '.tabelle',
|
crawlContainer: '.tabelle',
|
||||||
@@ -23,7 +45,6 @@ const config = {
|
|||||||
size: '.tabelle .inner_object_data .data_boxes div:nth-child(1)',
|
size: '.tabelle .inner_object_data .data_boxes div:nth-child(1)',
|
||||||
rooms: '.tabelle .inner_object_data .data_boxes div:nth-child(2)',
|
rooms: '.tabelle .inner_object_data .data_boxes div:nth-child(2)',
|
||||||
title: '.tabelle .inner_object_data .tabelle_inhalt_titel_black | removeNewline | trim',
|
title: '.tabelle .inner_object_data .tabelle_inhalt_titel_black | removeNewline | trim',
|
||||||
description: '.tabelle .inner_object_data .objekt_beschreibung | removeNewline | trim',
|
|
||||||
},
|
},
|
||||||
normalize: normalize,
|
normalize: normalize,
|
||||||
filter: applyBlacklist,
|
filter: applyBlacklist,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import utils from '../utils.js';
|
import utils, {buildHash} from '../utils.js';
|
||||||
let appliedBlackList = [];
|
let appliedBlackList = [];
|
||||||
function shortenLink(link) {
|
function shortenLink(link) {
|
||||||
return link.substring(0, link.indexOf('?'));
|
return link.substring(0, link.indexOf('?'));
|
||||||
@@ -7,12 +7,12 @@ function parseId(shortenedLink) {
|
|||||||
return shortenedLink.substring(shortenedLink.lastIndexOf('/') + 1);
|
return shortenedLink.substring(shortenedLink.lastIndexOf('/') + 1);
|
||||||
}
|
}
|
||||||
function normalize(o) {
|
function normalize(o) {
|
||||||
const id = parseId(shortenLink(o.link));
|
|
||||||
const size = o.size || 'N/A m²';
|
const size = o.size || 'N/A m²';
|
||||||
const price = o.price || 'N/A €';
|
const price = o.price || 'N/A €';
|
||||||
const title = o.title || 'No title available';
|
const title = o.title || 'No title available';
|
||||||
const address = o.address || 'No address available';
|
const address = o.address || 'No address available';
|
||||||
const link = shortenLink(o.link);
|
const link = shortenLink(o.link);
|
||||||
|
const id = buildHash(parseId(shortenLink(o.link)), o.price);
|
||||||
return Object.assign(o, { id, price, size, title, address, link });
|
return Object.assign(o, { id, price, size, title, address, link });
|
||||||
}
|
}
|
||||||
function applyBlacklist(o) {
|
function applyBlacklist(o) {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import utils from '../utils.js';
|
import utils, {buildHash} from '../utils.js';
|
||||||
let appliedBlackList = [];
|
let appliedBlackList = [];
|
||||||
function normalize(o) {
|
function normalize(o) {
|
||||||
const id = o.id.substring(o.id.lastIndexOf('/') + 1, o.id.length);
|
|
||||||
const size = o.size != null ? o.size.replace('Wohnfläche ', '') : 'N/A m²';
|
const size = o.size != null ? o.size.replace('Wohnfläche ', '') : 'N/A m²';
|
||||||
const price = o.price.replace('Kaufpreis ', '');
|
const price = o.price.replace('Kaufpreis ', '');
|
||||||
const address = o.address.split(' • ')[o.address.split(' • ').length - 1];
|
const address = o.address.split(' • ')[o.address.split(' • ').length - 1];
|
||||||
const title = o.title || 'No title available';
|
const title = o.title || 'No title available';
|
||||||
const link = o.id;
|
const link = o.id;
|
||||||
|
const id = buildHash(o.id.substring(o.id.lastIndexOf('/') + 1, o.id.length), price);
|
||||||
return Object.assign(o, { id, address, price, size, title, link });
|
return Object.assign(o, { id, address, price, size, title, link });
|
||||||
}
|
}
|
||||||
function applyBlacklist(o) {
|
function applyBlacklist(o) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import utils from '../utils.js';
|
import utils, {buildHash} from '../utils.js';
|
||||||
let appliedBlackList = [];
|
let appliedBlackList = [];
|
||||||
function nullOrEmpty(val) {
|
function nullOrEmpty(val) {
|
||||||
return val == null || val.length === 0;
|
return val == null || val.length === 0;
|
||||||
@@ -7,7 +7,8 @@ function normalize(o) {
|
|||||||
const title = nullOrEmpty(o.title) ? 'NO TITLE FOUND' : o.title.replace('NEU', '');
|
const title = nullOrEmpty(o.title) ? 'NO TITLE FOUND' : o.title.replace('NEU', '');
|
||||||
const address = nullOrEmpty(o.address) ? 'NO ADDRESS FOUND' : (o.address || '').replace(/\(.*\),.*$/, '').trim();
|
const address = nullOrEmpty(o.address) ? 'NO ADDRESS FOUND' : (o.address || '').replace(/\(.*\),.*$/, '').trim();
|
||||||
const link = nullOrEmpty(o.link) ? 'NO LINK' : `https://www.immobilienscout24.de${o.link.substring(o.link.indexOf('/expose'))}`;
|
const link = nullOrEmpty(o.link) ? 'NO LINK' : `https://www.immobilienscout24.de${o.link.substring(o.link.indexOf('/expose'))}`;
|
||||||
return Object.assign(o, { title, address, link });
|
const id = buildHash(o.id, o.price);
|
||||||
|
return Object.assign(o, { id, title, address, link });
|
||||||
}
|
}
|
||||||
function applyBlacklist(o) {
|
function applyBlacklist(o) {
|
||||||
return !utils.isOneOf(o.title, appliedBlackList);
|
return !utils.isOneOf(o.title, appliedBlackList);
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import utils from '../utils.js';
|
import utils, {buildHash} from '../utils.js';
|
||||||
|
|
||||||
let appliedBlackList = [];
|
let appliedBlackList = [];
|
||||||
|
|
||||||
function normalize(o) {
|
function normalize(o) {
|
||||||
const id = o.id.substring(o.id.indexOf('-') + 1, o.id.length);
|
|
||||||
const size = o.size || 'N/A m²';
|
const size = o.size || 'N/A m²';
|
||||||
const price = (o.price || '--- €').replace('Preis auf Anfrage', '--- €');
|
const price = (o.price || '--- €').replace('Preis auf Anfrage', '--- €');
|
||||||
const title = o.title || 'No title available';
|
const title = o.title || 'No title available';
|
||||||
const link = `https://immo.swp.de/immobilien/${id}`;
|
const immoId = o.id.substring(o.id.indexOf('-') + 1, o.id.length);
|
||||||
|
const link = `https://immo.swp.de/immobilien/${immoId}`;
|
||||||
const description = o.description;
|
const description = o.description;
|
||||||
|
const id = buildHash(immoId, price);
|
||||||
return Object.assign(o, {id, price, size, title, link, description});
|
return Object.assign(o, {id, price, size, title, link, description});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,30 @@
|
|||||||
import utils from '../utils.js';
|
import utils, { buildHash } from '../utils.js';
|
||||||
|
|
||||||
let appliedBlackList = [];
|
let appliedBlackList = [];
|
||||||
|
|
||||||
function normalize(o) {
|
function normalize(o) {
|
||||||
return o;
|
const id = buildHash(o.id, o.price);
|
||||||
|
return Object.assign(o, { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyBlacklist(o) {
|
function applyBlacklist(o) {
|
||||||
const titleNotBlacklisted = !utils.isOneOf(o.title, appliedBlackList);
|
const titleNotBlacklisted = !utils.isOneOf(o.title, appliedBlackList);
|
||||||
const descNotBlacklisted = !utils.isOneOf(o.description, appliedBlackList);
|
const descNotBlacklisted = !utils.isOneOf(o.description, appliedBlackList);
|
||||||
return titleNotBlacklisted && descNotBlacklisted;
|
return titleNotBlacklisted && descNotBlacklisted;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
url: null,
|
url: null,
|
||||||
crawlContainer: "div[class^='EstateItem-']",
|
crawlContainer:
|
||||||
sortByDateParam: 'sd=DESC&sf=TIMESTAMP',
|
'div[data-testid="serp-card-testid"]:not(div[data-testid="serp-enlargementlist-testid"] div[data-testid="serp-card-testid"])',
|
||||||
|
sortByDateParam: 'order=DateDesc',
|
||||||
crawlFields: {
|
crawlFields: {
|
||||||
id: 'a@id',
|
id: 'a@id',
|
||||||
price: "div[class^='KeyFacts-'] [data-test='price'] | removeNewline | trim",
|
price: 'div[data-testid="cardmfe-price-testid"] | removeNewline | trim',
|
||||||
size: "div[class^='KeyFacts-'] [data-test='area'] | removeNewline | trim",
|
size: 'div[data-testid="cardmfe-keyfacts-testid"] | removeNewline | trim',
|
||||||
title: "div[class^='FactsMain-'] h2",
|
title: '.css-1cbj9xw',
|
||||||
link: 'a@href',
|
link: 'a@href',
|
||||||
address: "div[class^='estateFacts-'] span | removeNewline | trim",
|
address: 'div[data-testid="cardmfe-description-box-address"] | removeNewline | trim',
|
||||||
},
|
},
|
||||||
paginate: '#pnlPaging #nlbPlus@href',
|
paginate: '#pnlPaging #nlbPlus@href',
|
||||||
normalize: normalize,
|
normalize: normalize,
|
||||||
|
|||||||
@@ -1,44 +1,49 @@
|
|||||||
import utils from '../utils.js';
|
import utils, {buildHash} from '../utils.js';
|
||||||
|
|
||||||
let appliedBlackList = [];
|
let appliedBlackList = [];
|
||||||
let appliedBlacklistedDistricts = [];
|
let appliedBlacklistedDistricts = [];
|
||||||
|
|
||||||
function normalize(o) {
|
function normalize(o) {
|
||||||
const size = o.size || '--- m²';
|
const size = o.size || '--- m²';
|
||||||
return Object.assign(o, { size });
|
const id = buildHash(o.id, o.price);
|
||||||
|
return Object.assign(o, {id, size});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyBlacklist(o) {
|
function applyBlacklist(o) {
|
||||||
const titleNotBlacklisted = !utils.isOneOf(o.title, appliedBlackList);
|
const titleNotBlacklisted = !utils.isOneOf(o.title, appliedBlackList);
|
||||||
const descNotBlacklisted = !utils.isOneOf(o.description, appliedBlackList);
|
const descNotBlacklisted = !utils.isOneOf(o.description, appliedBlackList);
|
||||||
const isBlacklistedDistrict =
|
const isBlacklistedDistrict =
|
||||||
appliedBlacklistedDistricts.length === 0 ? false : utils.isOneOf(o.description, appliedBlacklistedDistricts);
|
appliedBlacklistedDistricts.length === 0 ? false : utils.isOneOf(o.description, appliedBlacklistedDistricts);
|
||||||
return !isBlacklistedDistrict && titleNotBlacklisted && descNotBlacklisted;
|
return !isBlacklistedDistrict && titleNotBlacklisted && descNotBlacklisted;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
url: null,
|
url: null,
|
||||||
crawlContainer: '#srchrslt-adtable .ad-listitem ',
|
crawlContainer: '#srchrslt-adtable .ad-listitem ',
|
||||||
//sort by date is standard oO
|
//sort by date is standard oO
|
||||||
sortByDateParam: null,
|
sortByDateParam: null,
|
||||||
crawlFields: {
|
crawlFields: {
|
||||||
id: '.aditem@data-adid | int',
|
id: '.aditem@data-adid | int',
|
||||||
price: '.aditem-main--middle--price-shipping--price | removeNewline | trim',
|
price: '.aditem-main--middle--price-shipping--price | removeNewline | trim',
|
||||||
size: '.aditem-main .text-module-end span:nth-child(2) | removeNewline | trim',
|
size: '.aditem-main .text-module-end span:nth-child(2) | removeNewline | trim',
|
||||||
title: '.aditem-main .text-module-begin a | removeNewline | trim',
|
title: '.aditem-main .text-module-begin a | removeNewline | trim',
|
||||||
link: '.aditem-main .text-module-begin a@href | removeNewline | trim',
|
link: '.aditem-main .text-module-begin a@href | removeNewline | trim',
|
||||||
description: '.aditem-main p:not(.text-module-end) | removeNewline | trim',
|
description: '.aditem-main p:not(.text-module-end) | removeNewline | trim',
|
||||||
address: '.aditem-main--top--left | trim | removeNewline',
|
address: '.aditem-main--top--left | trim | removeNewline',
|
||||||
},
|
},
|
||||||
paginate: '#srchrslt-pagination .pagination-next@href',
|
paginate: '#srchrslt-pagination .pagination-next@href',
|
||||||
normalize: normalize,
|
normalize: normalize,
|
||||||
filter: applyBlacklist,
|
filter: applyBlacklist,
|
||||||
};
|
};
|
||||||
export const metaInformation = {
|
export const metaInformation = {
|
||||||
name: 'Ebay Kleinanzeigen',
|
name: 'Ebay Kleinanzeigen',
|
||||||
baseUrl: 'https://www.kleinanzeigen.de/',
|
baseUrl: 'https://www.kleinanzeigen.de/',
|
||||||
id: 'kleinanzeigen',
|
id: 'kleinanzeigen',
|
||||||
};
|
};
|
||||||
export const init = (sourceConfig, blacklist, blacklistedDistricts) => {
|
export const init = (sourceConfig, blacklist, blacklistedDistricts) => {
|
||||||
config.enabled = sourceConfig.enabled;
|
config.enabled = sourceConfig.enabled;
|
||||||
config.url = sourceConfig.url;
|
config.url = sourceConfig.url;
|
||||||
appliedBlacklistedDistricts = blacklistedDistricts || [];
|
appliedBlacklistedDistricts = blacklistedDistricts || [];
|
||||||
appliedBlackList = blacklist || [];
|
appliedBlackList = blacklist || [];
|
||||||
};
|
};
|
||||||
export { config };
|
export {config};
|
||||||
|
|||||||
@@ -1,38 +1,44 @@
|
|||||||
import utils from '../utils.js';
|
import utils, {buildHash} from '../utils.js';
|
||||||
|
|
||||||
let appliedBlackList = [];
|
let appliedBlackList = [];
|
||||||
|
|
||||||
function nullOrEmpty(val) {
|
function nullOrEmpty(val) {
|
||||||
return val == null || val.length === 0;
|
return val == null || val.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalize(o) {
|
function normalize(o) {
|
||||||
const link = nullOrEmpty(o.link) ? 'NO LINK' : `https://www.neubaukompass.de${o.link.substring(o.link.indexOf('/neubau'))}`;
|
const link = nullOrEmpty(o.link) ? 'NO LINK' : `https://www.neubaukompass.de${o.link.substring(o.link.indexOf('/neubau'))}`;
|
||||||
return {...o, link};
|
const id = buildHash(o.id, o.price);
|
||||||
|
return Object.assign(o, {id, link});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyBlacklist(o) {
|
function applyBlacklist(o) {
|
||||||
return !utils.isOneOf(o.title, appliedBlackList);
|
return !utils.isOneOf(o.title, appliedBlackList);
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
url: null,
|
url: null,
|
||||||
crawlContainer: '.nbk-container >div article',
|
crawlContainer: '.nbk-container >div article',
|
||||||
sortByDateParam: 'Sortierung=Id&Richtung=DESC',
|
sortByDateParam: 'Sortierung=Id&Richtung=DESC',
|
||||||
crawlFields: {
|
crawlFields: {
|
||||||
id: '@id',
|
id: '@id',
|
||||||
title: 'a.nbk-truncate@title | removeNewline | trim',
|
title: 'a.nbk-truncate@title | removeNewline | trim',
|
||||||
link: 'a.nbk-truncate@href',
|
link: 'a.nbk-truncate@href',
|
||||||
address: 'p.nbk-truncate | removeNewline | trim',
|
address: 'p.nbk-truncate | removeNewline | trim',
|
||||||
price: 'p.nbk-mb-0 | removeNewline | trim',
|
price: 'p.nbk-mb-0 | removeNewline | trim',
|
||||||
},
|
},
|
||||||
paginate: '.numbered-pager__bottom .numbered-pager--info li:nth-child(2) a@href',
|
paginate: '.numbered-pager__bottom .numbered-pager--info li:nth-child(2) a@href',
|
||||||
normalize: normalize,
|
normalize: normalize,
|
||||||
filter: applyBlacklist,
|
filter: applyBlacklist,
|
||||||
};
|
};
|
||||||
export const init = (sourceConfig, blacklist) => {
|
export const init = (sourceConfig, blacklist) => {
|
||||||
config.enabled = sourceConfig.enabled;
|
config.enabled = sourceConfig.enabled;
|
||||||
config.url = sourceConfig.url;
|
config.url = sourceConfig.url;
|
||||||
appliedBlackList = blacklist || [];
|
appliedBlackList = blacklist || [];
|
||||||
};
|
};
|
||||||
export const metaInformation = {
|
export const metaInformation = {
|
||||||
name: 'Neubau Kompass',
|
name: 'Neubau Kompass',
|
||||||
baseUrl: 'https://www.neubaukompass.de/',
|
baseUrl: 'https://www.neubaukompass.de/',
|
||||||
id: 'neubauKompass',
|
id: 'neubauKompass',
|
||||||
};
|
};
|
||||||
export { config };
|
export {config};
|
||||||
|
|||||||
@@ -1,36 +1,41 @@
|
|||||||
import utils from '../utils.js';
|
import utils, {buildHash} from '../utils.js';
|
||||||
|
|
||||||
let appliedBlackList = [];
|
let appliedBlackList = [];
|
||||||
|
|
||||||
function normalize(o) {
|
function normalize(o) {
|
||||||
return o;
|
const id = buildHash(o.id, o.price);
|
||||||
|
return Object.assign(o, {id});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyBlacklist(o) {
|
function applyBlacklist(o) {
|
||||||
const titleNotBlacklisted = !utils.isOneOf(o.title, appliedBlackList);
|
const titleNotBlacklisted = !utils.isOneOf(o.title, appliedBlackList);
|
||||||
const descNotBlacklisted = !utils.isOneOf(o.description, appliedBlackList);
|
const descNotBlacklisted = !utils.isOneOf(o.description, appliedBlackList);
|
||||||
return o.id != null && titleNotBlacklisted && descNotBlacklisted;
|
return o.id != null && titleNotBlacklisted && descNotBlacklisted;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
url: null,
|
url: null,
|
||||||
crawlContainer: '#main_column .wgg_card',
|
crawlContainer: '#main_column .wgg_card',
|
||||||
sortByDateParam: 'sort_column=0&sort_order=0',
|
sortByDateParam: 'sort_column=0&sort_order=0',
|
||||||
crawlFields: {
|
crawlFields: {
|
||||||
id: '@data-id',
|
id: '@data-id',
|
||||||
details: '.row .noprint .col-xs-11 |removeNewline |trim',
|
details: '.row .noprint .col-xs-11 |removeNewline |trim',
|
||||||
price: '.middle .col-xs-3 |removeNewline |trim',
|
price: '.middle .col-xs-3 |removeNewline |trim',
|
||||||
size: '.middle .text-right |removeNewline |trim',
|
size: '.middle .text-right |removeNewline |trim',
|
||||||
title: '.truncate_title a |removeNewline |trim',
|
title: '.truncate_title a |removeNewline |trim',
|
||||||
link: '.truncate_title a@href',
|
link: '.truncate_title a@href',
|
||||||
},
|
},
|
||||||
normalize: normalize,
|
normalize: normalize,
|
||||||
filter: applyBlacklist,
|
filter: applyBlacklist,
|
||||||
};
|
};
|
||||||
export const init = (sourceConfig, blacklist) => {
|
export const init = (sourceConfig, blacklist) => {
|
||||||
config.enabled = sourceConfig.enabled;
|
config.enabled = sourceConfig.enabled;
|
||||||
config.url = sourceConfig.url;
|
config.url = sourceConfig.url;
|
||||||
appliedBlackList = blacklist || [];
|
appliedBlackList = blacklist || [];
|
||||||
};
|
};
|
||||||
export const metaInformation = {
|
export const metaInformation = {
|
||||||
name: 'Wg gesucht',
|
name: 'Wg gesucht',
|
||||||
baseUrl: 'https://www.wg-gesucht.de/',
|
baseUrl: 'https://www.wg-gesucht.de/',
|
||||||
id: 'wgGesucht',
|
id: 'wgGesucht',
|
||||||
};
|
};
|
||||||
export { config };
|
export {config};
|
||||||
|
|||||||
86
lib/utils.js
86
lib/utils.js
@@ -1,51 +1,69 @@
|
|||||||
import { dirname } from 'node:path';
|
import {dirname} from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import {fileURLToPath} from 'node:url';
|
||||||
import { readFile } from 'fs/promises';
|
import {readFile} from 'fs/promises';
|
||||||
|
import {createHash} from 'crypto';
|
||||||
|
|
||||||
function isOneOf(word, arr) {
|
function isOneOf(word, arr) {
|
||||||
if (arr == null || arr.length === 0) {
|
if (arr == null || arr.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const expression = String.raw`\b(${arr.join('|')})\b`;
|
const expression = String.raw`\b(${arr.join('|')})\b`;
|
||||||
const blacklist = new RegExp(expression, 'ig');
|
const blacklist = new RegExp(expression, 'ig');
|
||||||
return blacklist.test(word);
|
return blacklist.test(word);
|
||||||
}
|
}
|
||||||
|
|
||||||
function nullOrEmpty(val) {
|
function nullOrEmpty(val) {
|
||||||
return val == null || val.length === 0;
|
return val == null || val.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeStringToMs(timeString, now) {
|
function timeStringToMs(timeString, now) {
|
||||||
const d = new Date(now);
|
const d = new Date(now);
|
||||||
const parts = timeString.split(':');
|
const parts = timeString.split(':');
|
||||||
d.setHours(parts[0]);
|
d.setHours(parts[0]);
|
||||||
d.setMinutes(parts[1]);
|
d.setMinutes(parts[1]);
|
||||||
d.setSeconds(0);
|
d.setSeconds(0);
|
||||||
return d.getTime();
|
return d.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
function duringWorkingHoursOrNotSet(config, now) {
|
function duringWorkingHoursOrNotSet(config, now) {
|
||||||
const { workingHours } = config;
|
const {workingHours} = config;
|
||||||
if (workingHours == null || nullOrEmpty(workingHours.from) || nullOrEmpty(workingHours.to)) {
|
if (workingHours == null || nullOrEmpty(workingHours.from) || nullOrEmpty(workingHours.to)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const toDate = timeStringToMs(workingHours.to, now);
|
const toDate = timeStringToMs(workingHours.to, now);
|
||||||
const fromDate = timeStringToMs(workingHours.from, now);
|
const fromDate = timeStringToMs(workingHours.from, now);
|
||||||
return fromDate <= now && toDate >= now;
|
return fromDate <= now && toDate >= now;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDirName() {
|
function getDirName() {
|
||||||
return dirname(fileURLToPath(import.meta.url));
|
return dirname(fileURLToPath(import.meta.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHash(...inputs) {
|
||||||
|
if (inputs == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const cleaned = inputs.filter(i => i != null && i.length > 0);
|
||||||
|
if (cleaned.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return createHash('sha256')
|
||||||
|
.update(cleaned.join(','))
|
||||||
|
.digest('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = JSON.parse(await readFile(new URL('../conf/config.json', import.meta.url)));
|
const config = JSON.parse(await readFile(new URL('../conf/config.json', import.meta.url)));
|
||||||
|
|
||||||
export { isOneOf };
|
export {isOneOf};
|
||||||
export { nullOrEmpty };
|
export {nullOrEmpty};
|
||||||
export { duringWorkingHoursOrNotSet };
|
export {duringWorkingHoursOrNotSet};
|
||||||
export { getDirName };
|
export {getDirName};
|
||||||
export { config };
|
export {config};
|
||||||
|
export {buildHash};
|
||||||
export default {
|
export default {
|
||||||
isOneOf,
|
isOneOf,
|
||||||
nullOrEmpty,
|
nullOrEmpty,
|
||||||
duringWorkingHoursOrNotSet,
|
duringWorkingHoursOrNotSet,
|
||||||
getDirName,
|
getDirName,
|
||||||
config,
|
config,
|
||||||
};
|
};
|
||||||
|
|||||||
40
package.json
40
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fredy",
|
"name": "fredy",
|
||||||
"version": "9.0.0",
|
"version": "10.2.0",
|
||||||
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
@@ -55,24 +55,24 @@
|
|||||||
"Firefox ESR"
|
"Firefox ESR"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@douyinfe/semi-ui": "2.62.1",
|
"@douyinfe/semi-ui": "2.68.3",
|
||||||
"@rematch/core": "2.2.0",
|
"@rematch/core": "2.2.0",
|
||||||
"@rematch/loading": "2.1.2",
|
"@rematch/loading": "2.1.2",
|
||||||
"@sendgrid/mail": "8.1.3",
|
"@sendgrid/mail": "8.1.4",
|
||||||
"@vitejs/plugin-react": "4.3.1",
|
"@vitejs/plugin-react": "4.3.3",
|
||||||
"better-sqlite3": "8.6.0",
|
"better-sqlite3": "^11.5.0",
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.3",
|
||||||
"cookie-session": "2.1.0",
|
"cookie-session": "2.1.0",
|
||||||
"handlebars": "4.7.8",
|
"handlebars": "4.7.8",
|
||||||
"highcharts": "11.4.6",
|
"highcharts": "11.4.8",
|
||||||
"highcharts-react-official": "3.2.1",
|
"highcharts-react-official": "3.2.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"lowdb": "6.0.1",
|
"lowdb": "6.0.1",
|
||||||
"markdown": "^0.5.0",
|
"markdown": "^0.5.0",
|
||||||
"nanoid": "5.0.7",
|
"nanoid": "5.0.8",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"node-mailjet": "6.0.5",
|
"node-mailjet": "6.0.6",
|
||||||
"query-string": "8.2.0",
|
"query-string": "9.1.1",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-redux": "9.1.2",
|
"react-redux": "9.1.2",
|
||||||
@@ -81,27 +81,27 @@
|
|||||||
"redux": "5.0.1",
|
"redux": "5.0.1",
|
||||||
"redux-thunk": "3.1.0",
|
"redux-thunk": "3.1.0",
|
||||||
"restana": "4.9.9",
|
"restana": "4.9.9",
|
||||||
"serve-static": "1.15.0",
|
"serve-static": "1.16.2",
|
||||||
"slack": "11.0.2",
|
"slack": "11.0.2",
|
||||||
"string-similarity": "^4.0.4",
|
"string-similarity": "^4.0.4",
|
||||||
"vite": "5.3.4",
|
"vite": "5.4.10",
|
||||||
"x-ray": "2.3.4"
|
"x-ray": "2.3.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.24.9",
|
"@babel/core": "7.26.0",
|
||||||
"@babel/eslint-parser": "7.24.8",
|
"@babel/eslint-parser": "7.25.9",
|
||||||
"@babel/preset-env": "7.24.8",
|
"@babel/preset-env": "7.26.0",
|
||||||
"@babel/preset-react": "7.24.7",
|
"@babel/preset-react": "7.25.9",
|
||||||
"chai": "5.1.1",
|
"chai": "5.1.2",
|
||||||
"eslint": "8.56.0",
|
"eslint": "8.56.0",
|
||||||
"eslint-config-prettier": "8.8.0",
|
"eslint-config-prettier": "8.8.0",
|
||||||
"eslint-plugin-react": "7.35.0",
|
"eslint-plugin-react": "7.37.2",
|
||||||
"esmock": "2.6.7",
|
"esmock": "2.6.9",
|
||||||
"history": "5.3.0",
|
"history": "5.3.0",
|
||||||
"husky": "4.3.8",
|
"husky": "4.3.8",
|
||||||
"less": "4.2.0",
|
"less": "4.2.0",
|
||||||
"lint-staged": "13.2.2",
|
"lint-staged": "13.2.2",
|
||||||
"mocha": "10.7.0",
|
"mocha": "10.8.2",
|
||||||
"prettier": "3.3.3",
|
"prettier": "3.3.3",
|
||||||
"redux-logger": "3.0.6"
|
"redux-logger": "3.0.6"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ describe('#einsAImmobilien testsuite()', () => {
|
|||||||
expect(notificationObj.serviceName).to.equal('einsAImmobilien');
|
expect(notificationObj.serviceName).to.equal('einsAImmobilien');
|
||||||
notificationObj.payload.forEach((notify) => {
|
notificationObj.payload.forEach((notify) => {
|
||||||
/** check the actual structure **/
|
/** check the actual structure **/
|
||||||
expect(notify.id).to.be.a('number');
|
expect(notify.id).to.be.a('string');
|
||||||
expect(notify.price).to.be.a('string');
|
expect(notify.price).to.be.a('string');
|
||||||
expect(notify.size).to.be.a('string');
|
expect(notify.size).to.be.a('string');
|
||||||
expect(notify.title).to.be.a('string');
|
expect(notify.title).to.be.a('string');
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ describe('#kleinanzeigen testsuite()', () => {
|
|||||||
expect(notificationObj.serviceName).to.equal('kleinanzeigen');
|
expect(notificationObj.serviceName).to.equal('kleinanzeigen');
|
||||||
notificationObj.payload.forEach((notify) => {
|
notificationObj.payload.forEach((notify) => {
|
||||||
/** check the actual structure **/
|
/** check the actual structure **/
|
||||||
expect(notify.id).to.be.a('number');
|
expect(notify.id).to.be.a('string');
|
||||||
expect(notify.title).to.be.a('string');
|
expect(notify.title).to.be.a('string');
|
||||||
expect(notify.link).to.be.a('string');
|
expect(notify.link).to.be.a('string');
|
||||||
expect(notify.address).to.be.a('string');
|
expect(notify.address).to.be.a('string');
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ describe('#neubauKompass testsuite()', () => {
|
|||||||
similarityCache.stopCacheCleanup();
|
similarityCache.stopCacheCleanup();
|
||||||
});
|
});
|
||||||
provider.init(providerConfig.neubauKompass, [], []);
|
provider.init(providerConfig.neubauKompass, [], []);
|
||||||
it.only('should test neubauKompass provider', async () => {
|
it('should test neubauKompass provider', async () => {
|
||||||
const Fredy = await mockFredy();
|
const Fredy = await mockFredy();
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
if (!scrapingAnt.isScrapingAntApiKeySet()) {
|
if (!scrapingAnt.isScrapingAntApiKeySet()) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"immowelt": {
|
"immowelt": {
|
||||||
"url": "https://www.immowelt.de/liste/duesseldorf/wohnungen/kaufen?d=true&rmi=3&sd=DESC&sf=TIMESTAMP&sp=1",
|
"url": "https://www.immowelt.de/classified-search?distributionTypes=Buy,Buy_Auction,Compulsory_Auction&estateTypes=House,Apartment&locations=AD08DE2350",
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"immoscout": {
|
"immoscout": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"url": "https://www.immowelt.de/liste/40589/wohnungen/mieten?d=true&sd=DESC&sf=PRIMARY_PRICE_AMOUNT&sp=1",
|
"url": "https://www.immowelt.de/classified-search?distributionTypes=Buy,Buy_Auction,Compulsory_Auction&estateTypes=House,Apartment&locations=AD08DE2350",
|
||||||
"shouldBecome": "https://www.immowelt.de/liste/40589/wohnungen/mieten?d=true&sd=DESC&sf=TIMESTAMP&sp=1",
|
"shouldBecome": "https://www.immowelt.de/classified-search?distributionTypes=Buy,Buy_Auction,Compulsory_Auction&estateTypes=House,Apartment&locations=AD08DE2350&order=DateDesc",
|
||||||
"id": "immowelt"
|
"id": "immowelt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
16
test/utils/utils.test.js
Normal file
16
test/utils/utils.test.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { expect } from 'chai';
|
||||||
|
import {buildHash} from '../../lib/utils.js';
|
||||||
|
|
||||||
|
describe('utilsCheck', () => {
|
||||||
|
describe('#utilsCheck()', () => {
|
||||||
|
it('should be null when null input', () => {
|
||||||
|
expect(buildHash(null)).to.be.null;
|
||||||
|
});
|
||||||
|
it('should be null when null empty', () => {
|
||||||
|
expect(buildHash('')).to.be.null;
|
||||||
|
});
|
||||||
|
it('should return a value', () => {
|
||||||
|
expect(buildHash('bla', '', null)).to.be.a.string;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user