mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cceae11cc | ||
|
|
a4c5bfcbf7 | ||
|
|
6d2ab5f958 | ||
|
|
d3cb3a5881 | ||
|
|
111ef8be43 | ||
|
|
35feb772d7 | ||
|
|
1bf012f13e | ||
|
|
933dc3fc64 | ||
|
|
42c48fdceb | ||
|
|
f07aa0a06d | ||
|
|
92db8219b4 | ||
|
|
8ba3a53779 | ||
|
|
e7db4e23f5 | ||
|
|
06c4ebb975 | ||
|
|
b075e09ac2 | ||
|
|
f215ab53db | ||
|
|
4ed92b246f | ||
|
|
4a9b60633a | ||
|
|
2123c1024b | ||
|
|
35767e6774 | ||
|
|
bf77ba2667 | ||
|
|
827c7e7321 | ||
|
|
7b63dc72cb | ||
|
|
fd42b57010 | ||
|
|
f5917af8f3 | ||
|
|
a85400d570 | ||
|
|
8ce6668c78 | ||
|
|
2d8121a708 | ||
|
|
172c039c79 | ||
|
|
4ab1fd9294 | ||
|
|
50b3fde075 | ||
|
|
1a3fc6f94d | ||
|
|
26ed42230a | ||
|
|
6f4defdc1b | ||
|
|
f798aed342 | ||
|
|
27e098c244 | ||
|
|
37948be0d3 | ||
|
|
cc7bbb77c4 | ||
|
|
96da0b7892 | ||
|
|
72993312c7 | ||
|
|
17b4bad2e4 | ||
|
|
fbad4456d7 | ||
|
|
deec626feb | ||
|
|
88c6641485 | ||
|
|
f4eedda658 | ||
|
|
d2b80561f8 | ||
|
|
3bda88a075 | ||
|
|
86465e0076 | ||
|
|
d947dad488 | ||
|
|
23ef434fe1 | ||
|
|
5e6d92c5be | ||
|
|
4ba098e0b6 | ||
|
|
2d1a9a0452 | ||
|
|
6fbee3e7c6 | ||
|
|
46775c3662 | ||
|
|
1feb5bfda1 | ||
|
|
3ec9ed3b2a | ||
|
|
75a536d5ab |
1
.github/workflows/docker.yml
vendored
1
.github/workflows/docker.yml
vendored
@@ -44,3 +44,4 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
platforms: linux/amd64, linux/arm64
|
||||||
|
|||||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v2.5.1
|
uses: actions/setup-node@v2.5.1
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 20
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
- run: yarn install
|
- run: yarn install
|
||||||
- run: yarn run test
|
- run: yarn run test
|
||||||
|
|||||||
22
Dockerfile
22
Dockerfile
@@ -1,18 +1,20 @@
|
|||||||
# syntax=docker/dockerfile:1.3
|
FROM node:20
|
||||||
FROM node:16-alpine AS builder
|
|
||||||
COPY --chown=1000:1000 . /fredy
|
|
||||||
WORKDIR /fredy
|
WORKDIR /fredy
|
||||||
USER 1000
|
|
||||||
|
COPY . /fredy
|
||||||
|
|
||||||
RUN yarn install
|
RUN yarn install
|
||||||
|
|
||||||
|
RUN yarn global add pm2
|
||||||
|
|
||||||
RUN yarn run prod
|
RUN yarn run prod
|
||||||
|
|
||||||
FROM node:16-alpine
|
|
||||||
COPY --from=builder --chown=1000:1000 /fredy /fredy
|
|
||||||
RUN mkdir /db /conf && \
|
RUN mkdir /db /conf && \
|
||||||
chown 1000:1000 /db /conf && \
|
chown 1000:1000 /db /conf && \
|
||||||
|
chmod 777 -R /db/ && \
|
||||||
ln -s /db /fredy/db && ln -s /conf /fredy/conf
|
ln -s /db /fredy/db && ln -s /conf /fredy/conf
|
||||||
|
|
||||||
EXPOSE 9998
|
EXPOSE 9998
|
||||||
USER 1000
|
|
||||||
VOLUME [ "/conf", "/db" ]
|
CMD pm2-runtime index.js
|
||||||
WORKDIR /fredy
|
|
||||||
CMD node index.js --no-daemon
|
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2021 Christian Kellner
|
Copyright (c) 2024 Christian Kellner
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -17,7 +17,7 @@ _Fredy_ is supported by JetBrains under Open Source Support Program
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
- Make sure to use Node.js 16 or above
|
- Make sure to use Node.js 20 or above
|
||||||
- Run the following commands:
|
- Run the following commands:
|
||||||
```ssh
|
```ssh
|
||||||
yarn (or npm install)
|
yarn (or npm install)
|
||||||
@@ -33,9 +33,6 @@ _Fredy_ will start with the default port, set to `9998`. You can access _Fredy_
|
|||||||
|
|
||||||
<img alt="Job Overview" width="30%" src="https://github.com/orangecoding/fredy/blob/master/doc/screenshot_3.png">
|
<img alt="Job Overview" width="30%" src="https://github.com/orangecoding/fredy/blob/master/doc/screenshot_3.png">
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
|
||||||
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## Understanding the fundamentals
|
## Understanding the fundamentals
|
||||||
There are 3 important parts in Fredy, that you need to understand to leverage the full power of _Fredy_.
|
There are 3 important parts in Fredy, that you need to understand to leverage the full power of _Fredy_.
|
||||||
@@ -81,15 +78,20 @@ yarn run test
|
|||||||
# Architecture
|
# Architecture
|
||||||

|

|
||||||
|
|
||||||
### Immoscout
|
### Immoscout / Immonet / NeubauKompass
|
||||||
I have added **experimental** support for Immoscout. Immoscout is somewhat special, because they have decided to secure their service from bots using Re-Capture. Finding a way around this is barely possible. For _Fredy_ to be able to bypass this check, I'm using a service called [ScrapingAnt](https://scrapingant.com/). The trick is to use a headless browser, rotating proxies and (once successfully validated) to re-send the cookies each time.
|
I have added **experimental** support for Immoscout, Immonet and NeubauKompass. They all are somewhat special, because they have decided to secure their service from bots using Re-Capture. Finding a way around this is barely possible. For _Fredy_ to be able to bypass this check, I'm using a service called [ScrapingAnt](https://scrapingant.com/). The trick is to use a headless browser, rotating proxies and (once successfully validated) to re-send the cookies each time.
|
||||||
|
|
||||||
To be able to use Immoscout, you need to create an account at ScrapingAnt. Configure the API key in the "General Settings" tab (visible when logged in as administrator).
|
To be able to use Immoscout / Immonet, you need to create an account at ScrapingAnt. Configure the API key in the "General Settings" tab (visible when logged in as administrator).
|
||||||
The rest will be handled by _Fredy_. Keep in mind, the support is experimental. There might be bugs and you might not always pass the re-capture check, but most of the time it works rather well :)
|
The rest will be handled by _Fredy_. Keep in mind, the support is experimental. There might be bugs and you might not always pass the re-capture check, but most of the time it works rather well :)
|
||||||
|
|
||||||
If you need more than the 1000 API calls allowed per month, I'd suggest opting for a paid account... ScrapingAnt loves OpenSource, therefore they have decided to give all _Fredy_ users a 10% discount by using the code **FREDY10** (Disclaimer: I do not earn any money for recommending their service).
|
If you need more than the 1000 API calls allowed per month, I'd suggest opting for a paid account... ScrapingAnt loves OpenSource, therefore they have decided to give all _Fredy_ users a 10% discount by using the code **FREDY10** (Disclaimer: I do not earn any money for recommending their service).
|
||||||
|
|
||||||
### Contribution guidelines
|
### 👐 Contributing
|
||||||
|
Thanks to all the people who already contributed!
|
||||||
|
|
||||||
|
<a href="https://github.com/orangecoding/fredy/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=orangecoding/fredy" />
|
||||||
|
</a>
|
||||||
|
|
||||||
See [Contributing](https://github.com/orangecoding/fredy/blob/master/CONTRIBUTING.md)
|
See [Contributing](https://github.com/orangecoding/fredy/blob/master/CONTRIBUTING.md)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
version: '3.3'
|
version: '3.8'
|
||||||
services:
|
services:
|
||||||
fredy:
|
fredy:
|
||||||
container_name: fredy
|
container_name: fredy
|
||||||
@@ -13,3 +13,4 @@ services:
|
|||||||
- ./db:/db
|
- ./db:/db
|
||||||
ports:
|
ports:
|
||||||
- 9998:9998
|
- 9998:9998
|
||||||
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -45,15 +45,15 @@ class FredyRuntime {
|
|||||||
_getListings(url) {
|
_getListings(url) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = this._providerId;
|
const id = this._providerId;
|
||||||
if (scrapingAnt.isImmoscout(id) && !scrapingAnt.isScrapingAntApiKeySet()) {
|
if (scrapingAnt.needScrapingAnt(id) && !scrapingAnt.isScrapingAntApiKeySet()) {
|
||||||
const error = 'Immoscout can only be used with if you have set an apikey for scrapingAnt.';
|
const error = 'Immoscout or Immonet can only be used with if you have set an apikey for scrapingAnt.';
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
console.log(error);
|
console.log(error);
|
||||||
/* eslint-enable no-console */
|
/* eslint-enable no-console */
|
||||||
reject(error);
|
reject(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const u = scrapingAnt.isImmoscout(id) ? scrapingAnt.transformUrlForScrapingAnt(url, id) : url;
|
const u = scrapingAnt.needScrapingAnt(id) ? scrapingAnt.transformUrlForScrapingAnt(url, id) : url;
|
||||||
try {
|
try {
|
||||||
if (this._providerConfig.paginate != null) {
|
if (this._providerConfig.paginate != null) {
|
||||||
xray(u, this._providerConfig.crawlContainer, [this._providerConfig.crawlFields])
|
xray(u, this._providerConfig.crawlContainer, [this._providerConfig.crawlFields])
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import files from 'serve-static';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getDirName } from '../utils.js';
|
import { getDirName } from '../utils.js';
|
||||||
const service = restana();
|
const service = restana();
|
||||||
const staticService = files(path.join(getDirName(), '../../ui/public'));
|
const staticService = files(path.join(getDirName(), '../ui/public'));
|
||||||
const PORT = config.port || 9998;
|
const PORT = config.port || 9998;
|
||||||
|
|
||||||
service.use(bodyParser.json());
|
service.use(bodyParser.json());
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobRouter.get('/processingTimes', async (req, res) => {
|
|||||||
let scrapingAntData = null;
|
let scrapingAntData = null;
|
||||||
if (config.scrapingAnt.apiKey != null && config.scrapingAnt.apiKey.length > 0) {
|
if (config.scrapingAnt.apiKey != null && config.scrapingAnt.apiKey.length > 0) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`https://api.scrapingant.com/v1/usage?x-api-key=${config.scrapingAnt.apiKey}`);
|
const response = await fetch(`https://api.scrapingant.com/v2/usage?x-api-key=${config.scrapingAnt.apiKey}`);
|
||||||
scrapingAntData = await response.json();
|
scrapingAntData = await response.json();
|
||||||
} catch (Exception) {
|
} catch (Exception) {
|
||||||
console.error('Could not query plan data from scraping ant.', Exception);
|
console.error('Could not query plan data from scraping ant.', Exception);
|
||||||
|
|||||||
36
lib/notification/adapter/apprise.js
Normal file
36
lib/notification/adapter/apprise.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { markdown2Html } from '../../services/markdown.js';
|
||||||
|
import { getJob } from '../../services/storage/jobStorage.js';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
|
||||||
|
export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => {
|
||||||
|
const { server } = notificationConfig.find((adapter) => adapter.id === config.id).fields;
|
||||||
|
const job = getJob(jobKey);
|
||||||
|
const jobName = job == null ? jobKey : job.name;
|
||||||
|
const promises = newListings.map((newListing) => {
|
||||||
|
const title = `${jobName} at ${serviceName}: ${newListing.title}`;
|
||||||
|
const message = `Address: ${newListing.address}\nSize: ${newListing.size}\nPrice: ${newListing.price}\nink: ${newListing.link}`;
|
||||||
|
return fetch(server, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
body: message,
|
||||||
|
title: title,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
};
|
||||||
|
export const config = {
|
||||||
|
id: 'apprise',
|
||||||
|
name: 'Apprise',
|
||||||
|
readme: markdown2Html('lib/notification/adapter/apprise.md'),
|
||||||
|
description: 'Fredy will send new listings to your Apprise instance.',
|
||||||
|
fields: {
|
||||||
|
server: {
|
||||||
|
type: 'text',
|
||||||
|
label: 'Server',
|
||||||
|
description: 'The server URL to send the notification to.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
3
lib/notification/adapter/apprise.md
Normal file
3
lib/notification/adapter/apprise.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
### Apprise Adapter
|
||||||
|
|
||||||
|
Refer to the [instructions](https://github.com/caronc/apprise-api#installation) on how to set up an Apprise instance and how to configure your preferred notification service.
|
||||||
@@ -9,7 +9,7 @@ const template = fs.readFileSync(path.resolve(__dirname + '/notification/emailTe
|
|||||||
const emailTemplate = Handlebars.compile(template);
|
const emailTemplate = Handlebars.compile(template);
|
||||||
export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => {
|
export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => {
|
||||||
const { apiPublicKey, apiPrivateKey, receiver, from } = notificationConfig.find(
|
const { apiPublicKey, apiPrivateKey, receiver, from } = notificationConfig.find(
|
||||||
(adapter) => adapter.id === 'mailJet'
|
(adapter) => adapter.id === config.id,
|
||||||
).fields;
|
).fields;
|
||||||
const to = receiver
|
const to = receiver
|
||||||
.trim()
|
.trim()
|
||||||
@@ -18,7 +18,7 @@ export const send = ({ serviceName, newListings, notificationConfig, jobKey }) =
|
|||||||
Email: r.trim(),
|
Email: r.trim(),
|
||||||
}));
|
}));
|
||||||
return mailjet
|
return mailjet
|
||||||
.connect(apiPublicKey, apiPrivateKey)
|
.apiConnect(apiPublicKey, apiPrivateKey)
|
||||||
.post('send', { version: 'v3.1' })
|
.post('send', { version: 'v3.1' })
|
||||||
.request({
|
.request({
|
||||||
Messages: [
|
Messages: [
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { markdown2Html } from '../../services/markdown.js';
|
|||||||
import { getJob } from '../../services/storage/jobStorage.js';
|
import { getJob } from '../../services/storage/jobStorage.js';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => {
|
export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => {
|
||||||
const { webhook, channel } = notificationConfig.find((adapter) => adapter.id === 'mattermost').fields;
|
const { webhook, channel } = notificationConfig.find((adapter) => adapter.id === config.id).fields;
|
||||||
const job = getJob(jobKey);
|
const job = getJob(jobKey);
|
||||||
const jobName = job == null ? jobKey : job.name;
|
const jobName = job == null ? jobKey : job.name;
|
||||||
let message = `### *${jobName}* (${serviceName}) found **${newListings.length}** new listings:\n\n`;
|
let message = `### *${jobName}* (${serviceName}) found **${newListings.length}** new listings:\n\n`;
|
||||||
message += `| Title | Address | Size | Price |\n|:----|:----|:----|:----|\n`;
|
message += `| Title | Address | Size | Price |\n|:----|:----|:----|:----|\n`;
|
||||||
message += newListings.map(
|
message += newListings.map(
|
||||||
(o) => `| [${o.title}](${o.link}) | ` + [o.address, o.size.replace(/2m/g, '$m^2$'), o.price].join(' | ') + ' |\n'
|
(o) => `| [${o.title}](${o.link}) | ` + [o.address, o.size.replace(/2m/g, '$m^2$'), o.price].join(' | ') + ' |\n',
|
||||||
);
|
);
|
||||||
return fetch(webhook, {
|
return fetch(webhook, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
51
lib/notification/adapter/ntfy.js
Normal file
51
lib/notification/adapter/ntfy.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { markdown2Html } from '../../services/markdown.js';
|
||||||
|
import { getJob } from '../../services/storage/jobStorage.js';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
|
||||||
|
export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => {
|
||||||
|
const { priority, server, topic } = notificationConfig.find((adapter) => adapter.id === config.id).fields;
|
||||||
|
const job = getJob(jobKey);
|
||||||
|
const jobName = job == null ? jobKey : job.name;
|
||||||
|
const promises = newListings.map((newListing) => {
|
||||||
|
const message = `Address: ${newListing.address} Size: ${newListing.size.replace(/2m/g, '$m^2$')} Price: ${
|
||||||
|
newListing.price
|
||||||
|
}`;
|
||||||
|
return fetch(server, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
topic: topic,
|
||||||
|
message: message,
|
||||||
|
title: newListing.title,
|
||||||
|
tags: [serviceName, jobName],
|
||||||
|
priority: parseInt(priority),
|
||||||
|
click: newListing.link,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
};
|
||||||
|
export const config = {
|
||||||
|
id: 'ntfy',
|
||||||
|
name: 'ntfy',
|
||||||
|
readme: markdown2Html('lib/notification/adapter/ntfy.md'),
|
||||||
|
description: 'Fredy will send new listings to your ntfy.',
|
||||||
|
fields: {
|
||||||
|
priority: {
|
||||||
|
type: 'number',
|
||||||
|
label: 'Priority',
|
||||||
|
description: 'The priority of the send notification.',
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
type: 'text',
|
||||||
|
label: 'Server-URL',
|
||||||
|
description: 'The server url to the send the notification to.',
|
||||||
|
},
|
||||||
|
topic: {
|
||||||
|
type: 'text',
|
||||||
|
label: 'topic',
|
||||||
|
description:
|
||||||
|
'The topic where fredy should send notifications to. The topic is a secret, only known to you, make sure it is something not generic.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
5
lib/notification/adapter/ntfy.md
Normal file
5
lib/notification/adapter/ntfy.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
### ntfy Adapter
|
||||||
|
|
||||||
|
For ntfy, you need to create a topic on your preferred ntfy instance. This is pretty easy. Please visit the steps in the [docs](https://docs.ntfy.sh/publish/) and follow the instructions.
|
||||||
|
|
||||||
|
As a result, you get the URL for configuration in fredy. In addition, the priority must be defined.
|
||||||
50
lib/notification/adapter/pushover.js
Normal file
50
lib/notification/adapter/pushover.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { markdown2Html } from '../../services/markdown.js';
|
||||||
|
import { getJob } from '../../services/storage/jobStorage.js';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
|
||||||
|
export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => {
|
||||||
|
const { token, user, device } = notificationConfig.find((adapter) => adapter.id === config.id).fields;
|
||||||
|
const job = getJob(jobKey);
|
||||||
|
const jobName = job == null ? jobKey : job.name;
|
||||||
|
const promises = newListings.map((newListing) => {
|
||||||
|
const title = `${jobName} at ${serviceName}: ${newListing.title}`;
|
||||||
|
const message = `Address: ${newListing.address}\nSize: ${newListing.size}\nPrice: ${newListing.price}\nLink: ${newListing.link}`;
|
||||||
|
return fetch('https://api.pushover.net/1/messages.json', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
token: token,
|
||||||
|
user: user,
|
||||||
|
message: message,
|
||||||
|
device: device,
|
||||||
|
title: title,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
id: 'pushover',
|
||||||
|
name: 'Pushover',
|
||||||
|
readme: markdown2Html('lib/notification/adapter/pushover.md'),
|
||||||
|
description: 'Fredy will send new listings to your mobile using Pushover.',
|
||||||
|
fields: {
|
||||||
|
token: {
|
||||||
|
type: 'text',
|
||||||
|
label: 'API token',
|
||||||
|
description: 'Your application\'s API token.',
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'text',
|
||||||
|
label: 'User key',
|
||||||
|
description: 'Your user/group key.',
|
||||||
|
},
|
||||||
|
device: {
|
||||||
|
type: 'text',
|
||||||
|
label: 'Device name',
|
||||||
|
description: 'The device name to send your notification to. Messages may be addressed to multiple specific devices by joining them with a comma.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
5
lib/notification/adapter/pushover.md
Normal file
5
lib/notification/adapter/pushover.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
### Pushover Adapter
|
||||||
|
|
||||||
|
Refer to the [instructions](https://support.pushover.net/i7-what-is-pushover-and-how-do-i-use-it) to set up your Pushover application.
|
||||||
|
|
||||||
|
After setting up the application, please enter both your newly created User key and API token.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import sgMail from '@sendgrid/mail';
|
import sgMail from '@sendgrid/mail';
|
||||||
import { markdown2Html } from '../../services/markdown.js';
|
import { markdown2Html } from '../../services/markdown.js';
|
||||||
export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => {
|
export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => {
|
||||||
const { apiKey, receiver, from, templateId } = notificationConfig.find((adapter) => adapter.id === 'sendGrid').fields;
|
const { apiKey, receiver, from, templateId } = notificationConfig.find((adapter) => adapter.id === config.id).fields;
|
||||||
sgMail.setApiKey(apiKey);
|
sgMail.setApiKey(apiKey);
|
||||||
const msg = {
|
const msg = {
|
||||||
templateId,
|
templateId,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Slack from 'slack';
|
|||||||
import { markdown2Html } from '../../services/markdown.js';
|
import { markdown2Html } from '../../services/markdown.js';
|
||||||
const msg = Slack.chat.postMessage;
|
const msg = Slack.chat.postMessage;
|
||||||
export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => {
|
export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => {
|
||||||
const { token, channel } = notificationConfig.find((adapter) => adapter.id === 'slack').fields;
|
const { token, channel } = notificationConfig.find((adapter) => adapter.id === config.id).fields;
|
||||||
return newListings.map((payload) =>
|
return newListings.map((payload) =>
|
||||||
msg({
|
msg({
|
||||||
token,
|
token,
|
||||||
@@ -35,7 +35,7 @@ export const send = ({ serviceName, newListings, notificationConfig, jobKey }) =
|
|||||||
ts: new Date().getTime() / 1000,
|
ts: new Date().getTime() / 1000,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export const config = {
|
export const config = {
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
### Sqlite Adapter
|
### 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.
|
||||||
|
|
||||||
This adapter stores search results in an sqlite database in db/listings.db
|
Fields are:
|
||||||
|
```
|
||||||
|
['serviceName', 'jobKey', 'id', 'size', 'rooms', 'price', 'address', 'title', 'link', 'description']
|
||||||
|
```
|
||||||
@@ -19,7 +19,7 @@ function shorten(str, len = 30) {
|
|||||||
return str.length > len ? str.substring(0, len) + '...' : str;
|
return str.length > len ? str.substring(0, len) + '...' : str;
|
||||||
}
|
}
|
||||||
export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => {
|
export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => {
|
||||||
const { token, chatId } = notificationConfig.find((adapter) => adapter.id === 'telegram').fields;
|
const { token, chatId } = notificationConfig.find((adapter) => adapter.id === config.id).fields;
|
||||||
const job = getJob(jobKey);
|
const job = getJob(jobKey);
|
||||||
const jobName = job == null ? jobKey : job.name;
|
const jobName = job == null ? jobKey : job.name;
|
||||||
//we have to split messages into chunk, because otherwise messages are going to become too big and will fail
|
//we have to split messages into chunk, because otherwise messages are going to become too big and will fail
|
||||||
@@ -30,7 +30,7 @@ export const send = ({ serviceName, newListings, notificationConfig, jobKey }) =
|
|||||||
(o) =>
|
(o) =>
|
||||||
`<a href='${o.link}'><b>${shorten(o.title.replace(/\*/g, ''), 45).trim()}</b></a>\n` +
|
`<a href='${o.link}'><b>${shorten(o.title.replace(/\*/g, ''), 45).trim()}</b></a>\n` +
|
||||||
[o.address, o.price, o.size].join(' | ') +
|
[o.address, o.price, o.size].join(' | ') +
|
||||||
'\n\n'
|
'\n\n',
|
||||||
);
|
);
|
||||||
/**
|
/**
|
||||||
* This is to not break the rate limit. It is to only send 1 message per second
|
* This is to not break the rate limit. It is to only send 1 message per second
|
||||||
|
|||||||
@@ -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,14 +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 = parseInt(o.id.substring(o.id.indexOf('_') + 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';
|
||||||
//normally we would just read the link from the source, but immonet decided to trick user by adding a click listener instead of
|
const link = o.id;
|
||||||
//a href to do some weird reporting. (Very user friendly for handicaped ppl... not)
|
const id = buildHash(o.id.substring(o.id.lastIndexOf('/') + 1, o.id.length), price);
|
||||||
const link = `https://www.immonet.de/angebot/${id}`;
|
|
||||||
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) {
|
||||||
@@ -18,14 +16,14 @@ function applyBlacklist(o) {
|
|||||||
}
|
}
|
||||||
const config = {
|
const config = {
|
||||||
url: null,
|
url: null,
|
||||||
crawlContainer: '#result-list-stage .item',
|
crawlContainer: '.content-wrapper-tiles .ng-star-inserted',
|
||||||
sortByDateParam: 'sortby=19',
|
sortByDateParam: 'sortby=19',
|
||||||
crawlFields: {
|
crawlFields: {
|
||||||
id: '@id',
|
id: '.card a@href',
|
||||||
price: 'div[id*="selPrice_"] | trim',
|
title: '.card h3 |trim',
|
||||||
size: 'div[id*="selArea_"] | trim',
|
price: '.card .has-font-300 .is-bold | trim',
|
||||||
title: '.item a img@title',
|
size: '.card .has-font-300 .ml-100 | trim',
|
||||||
address: '.item .box-25 .ellipsis .text-100 | removeNewline | trim',
|
address: '.card span:nth-child(2) | trim',
|
||||||
},
|
},
|
||||||
paginate: '#idResultList .margin-bottom-6.margin-bottom-sm-12 .panel a.pull-right@href',
|
paginate: '#idResultList .margin-bottom-6.margin-bottom-sm-12 .panel a.pull-right@href',
|
||||||
normalize: normalize,
|
normalize: normalize,
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -6,8 +6,9 @@ function nullOrEmpty(val) {
|
|||||||
function normalize(o) {
|
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 = `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);
|
||||||
@@ -20,7 +21,7 @@ const config = {
|
|||||||
id: '.result-list-entry@data-obid | int',
|
id: '.result-list-entry@data-obid | int',
|
||||||
price: '.result-list-entry .result-list-entry__criteria .grid-item:first-child dd | removeNewline | trim',
|
price: '.result-list-entry .result-list-entry__criteria .grid-item:first-child dd | removeNewline | trim',
|
||||||
size: '.result-list-entry .result-list-entry__criteria .grid-item:nth-child(2) dd | removeNewline | trim',
|
size: '.result-list-entry .result-list-entry__criteria .grid-item:nth-child(2) dd | removeNewline | trim',
|
||||||
title: '.result-list-entry .result-list-entry__brand-title-container h5 | removeNewline | trim',
|
title: '.result-list-entry .result-list-entry__brand-title-container h2 | removeNewline | trim',
|
||||||
link: '.result-list-entry .result-list-entry__brand-title-container@href',
|
link: '.result-list-entry .result-list-entry__brand-title-container@href',
|
||||||
address: '.result-list-entry .result-list-entry__map-link',
|
address: '.result-list-entry .result-list-entry__map-link',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,44 +1,48 @@
|
|||||||
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 address = o.address || 'No address available';
|
const immoId = o.id.substring(o.id.indexOf('-') + 1, o.id.length);
|
||||||
const title = o.title || 'No title available';
|
const link = `https://immo.swp.de/immobilien/${immoId}`;
|
||||||
const link = `https://immo.swp.de/immobilien/${id}`;
|
const description = o.description;
|
||||||
const description = o.description;
|
const id = buildHash(immoId, price);
|
||||||
return Object.assign(o, { id, address, price, size, title, link, description });
|
return Object.assign(o, {id, price, size, title, link, description});
|
||||||
}
|
}
|
||||||
|
|
||||||
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: '.js-serp-item',
|
crawlContainer: '.js-serp-item',
|
||||||
sortByDateParam: 's=most_recently_updated_first',
|
sortByDateParam: 's=most_recently_updated_first',
|
||||||
crawlFields: {
|
crawlFields: {
|
||||||
id: '@id',
|
id: '.js-bookmark-btn@data-id',
|
||||||
price: 'div.item__spec.item-spec-price | trim',
|
price: 'div.align-items-start div:first-child | trim',
|
||||||
size: 'div.item__spec.item-spec-area | trim',
|
size: 'div.align-items-start div:nth-child(3) | trim',
|
||||||
title: 'a.js-item-title-link@title',
|
title: '.card-title h2 | trim',
|
||||||
address: 'div.item__locality | removeNewline | trim',
|
link: '.ci-search-result__link@href',
|
||||||
description: 'div.item__main-info-points.clearfix p small | removeNewline | trim',
|
description: '.js-show-more-item-sm | removeNewline | trim',
|
||||||
},
|
},
|
||||||
paginate: 'li.page-item.pagination__item a.page-link@href',
|
paginate: 'li.page-item.pagination__item a.page-link@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: 'Immo Südwest Presse',
|
name: 'Immo Südwest Presse',
|
||||||
baseUrl: 'https://immo.swp.de/',
|
baseUrl: 'https://immo.swp.de/',
|
||||||
id: 'immoswp',
|
id: 'immoswp',
|
||||||
};
|
};
|
||||||
export { config };
|
export {config};
|
||||||
|
|||||||
@@ -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 | 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.ebay-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,34 +1,44 @@
|
|||||||
import utils from '../utils.js';
|
import utils, {buildHash} from '../utils.js';
|
||||||
|
|
||||||
let appliedBlackList = [];
|
let appliedBlackList = [];
|
||||||
|
|
||||||
|
function nullOrEmpty(val) {
|
||||||
|
return val == null || val.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
function normalize(o) {
|
function normalize(o) {
|
||||||
return o;
|
const link = nullOrEmpty(o.link) ? 'NO LINK' : `https://www.neubaukompass.de${o.link.substring(o.link.indexOf('/neubau'))}`;
|
||||||
|
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};
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import { config } from '../utils.js';
|
import { config } from '../utils.js';
|
||||||
import { makeUrlResidential } from './scrapingAnt.js';
|
import { makeUrlResidential } from './scrapingAnt.js';
|
||||||
|
import https from 'https';
|
||||||
//if ScrapingAnt got blocked, this http status is returned
|
//if ScrapingAnt got blocked, this http status is returned
|
||||||
const BLOCKED_HTTP_STATUS = 423;
|
const BLOCKED_HTTP_STATUS = 423;
|
||||||
const NOT_FOUND_HTTP_STATUS = 404;
|
const NOT_FOUND_HTTP_STATUS = 404;
|
||||||
const MAX_RETRIES_SCRAPING_ANT = 10;
|
const MAX_RETRIES_SCRAPING_ANT = 10;
|
||||||
const EXPECTED_STATUS_CODES = [BLOCKED_HTTP_STATUS, NOT_FOUND_HTTP_STATUS];
|
const EXPECTED_STATUS_CODES = [BLOCKED_HTTP_STATUS, NOT_FOUND_HTTP_STATUS];
|
||||||
|
const agent = new https.Agent({
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
});
|
||||||
|
|
||||||
function makeDriver(headers = {}) {
|
function makeDriver(headers = {}) {
|
||||||
let cookies = '';
|
let cookies = '';
|
||||||
async function scrapingAntDriver(context, callback, retryCounter = 0) {
|
async function scrapingAntDriver(context, callback, retryCounter = 0) {
|
||||||
@@ -19,13 +24,16 @@ function makeDriver(headers = {}) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const result = await response.text();
|
const result = await response.text();
|
||||||
|
if (EXPECTED_STATUS_CODES.includes(response.status)) {
|
||||||
|
throw new Error(`${response.status}`);
|
||||||
|
}
|
||||||
if (cookies.length === 0) {
|
if (cookies.length === 0) {
|
||||||
cookies = response.headers.raw()['set-cookie'] || [];
|
cookies = response.headers.raw()['set-cookie'] || [];
|
||||||
}
|
}
|
||||||
callback(null, result);
|
callback(null, result);
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
if (!EXPECTED_STATUS_CODES.includes(exception.response?.status)) {
|
if (!EXPECTED_STATUS_CODES.includes(exception.response?.status) && !EXPECTED_STATUS_CODES.includes(Number(exception.message))) {
|
||||||
console.error(`Error while trying to scrape data from scraping ant. Received error: ${exception.message}`);
|
console.error(`Error while trying to scrape data from scraping ant. Received error: ${exception.message}`);
|
||||||
callback(null, []);
|
callback(null, []);
|
||||||
return;
|
return;
|
||||||
@@ -41,9 +49,10 @@ function makeDriver(headers = {}) {
|
|||||||
/* eslint-enable no-console */
|
/* eslint-enable no-console */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The regular request driver is taking care of everyting, that doesn't need to be scraped by ScrapingAnt (which is
|
* The regular request driver is taking care of everyting, that doesn't need to be scraped by ScrapingAnt (which is
|
||||||
* everything != Immoscout as of writing this)
|
* everything != Immoscout & Immonet as of writing this)
|
||||||
*/
|
*/
|
||||||
return async function driver(context, callback) {
|
return async function driver(context, callback) {
|
||||||
if (context.url.toLowerCase().indexOf('scrapingant') !== -1) {
|
if (context.url.toLowerCase().indexOf('scrapingant') !== -1) {
|
||||||
@@ -55,6 +64,7 @@ function makeDriver(headers = {}) {
|
|||||||
...headers,
|
...headers,
|
||||||
Cookie: cookies,
|
Cookie: cookies,
|
||||||
},
|
},
|
||||||
|
agent,
|
||||||
});
|
});
|
||||||
const result = await response.text();
|
const result = await response.text();
|
||||||
callback(null, result);
|
callback(null, result);
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
import { metaInformation } from '../provider/immoscout.js';
|
import { metaInformation as immoScoutInfo } from '../provider/immoscout.js';
|
||||||
|
import { metaInformation as immoNetInfo } from '../provider/immonet.js';
|
||||||
|
import { metaInformation as neuBauCompassInfo } from '../provider/neubauKompass.js';
|
||||||
import { config } from '../utils.js';
|
import { config } from '../utils.js';
|
||||||
const isImmoscout = (id) => {
|
|
||||||
return id.toLowerCase() === metaInformation.id;
|
const additionalImmonetUrlParams = `&wait_for_selector=.content-wrapper-tiles&js_snippet=${Buffer.from(
|
||||||
|
'window.scrollTo(0,document.body.scrollHeight);'
|
||||||
|
).toString('base64')}`;
|
||||||
|
|
||||||
|
const needScrapingAnt = (id) => {
|
||||||
|
return id.toLowerCase() === immoScoutInfo.id || id.toLowerCase() === immoNetInfo.id || id.toLowerCase() === neuBauCompassInfo.id.toLowerCase();
|
||||||
};
|
};
|
||||||
export const transformUrlForScrapingAnt = (url, id) => {
|
export const transformUrlForScrapingAnt = (url, id) => {
|
||||||
if (isImmoscout(id)) {
|
let urlParams = '';
|
||||||
//only do calls to scrapingAnt when dealing with Immoscout
|
if (needScrapingAnt(id)) {
|
||||||
url = `https://api.scrapingant.com/v1/general?url=${encodeURIComponent(url)}&proxy_type=datacenter`;
|
if (id.toLowerCase() === immoNetInfo.id) {
|
||||||
|
urlParams = additionalImmonetUrlParams;
|
||||||
|
}
|
||||||
|
//only do calls to scrapingAnt when dealing with Immoscout/Immonet
|
||||||
|
url = `https://api.scrapingant.com/v2/general?url=${encodeURIComponent(url)}&proxy_type=datacenter${urlParams}`;
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
@@ -16,4 +27,4 @@ export const isScrapingAntApiKeySet = () => {
|
|||||||
export const makeUrlResidential = (url) => {
|
export const makeUrlResidential = (url) => {
|
||||||
return url.replace('datacenter', 'residential');
|
return url.replace('datacenter', 'residential');
|
||||||
};
|
};
|
||||||
export { isImmoscout };
|
export { needScrapingAnt };
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
import { LowSync } from 'lowdb';
|
import { LowSync } from 'lowdb';
|
||||||
export default class LowdashAdapter extends LowSync {
|
export default class LowdashAdapter extends LowSync {
|
||||||
constructor(adapter) {
|
constructor(adapter, defaultData = {}) {
|
||||||
super(adapter);
|
super(adapter, defaultData);
|
||||||
this.chain = lodash.chain(this).get('data');
|
this.chain = lodash.chain(this).get('data');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ import LowdashAdapter from './LowDashAdapter.js';
|
|||||||
|
|
||||||
const file = path.join(getDirName(), '../', 'db/jobs.json');
|
const file = path.join(getDirName(), '../', 'db/jobs.json');
|
||||||
const adapter = new JSONFileSync(file);
|
const adapter = new JSONFileSync(file);
|
||||||
const db = new LowdashAdapter(adapter);
|
const db = new LowdashAdapter(adapter, { jobs: [] });
|
||||||
|
|
||||||
db.read();
|
db.read();
|
||||||
|
|
||||||
db.data ||= { jobs: [] };
|
|
||||||
|
|
||||||
export const upsertJob = ({ jobId, name, blacklist = [], enabled = true, provider, notificationAdapter, userId }) => {
|
export const upsertJob = ({ jobId, name, blacklist = [], enabled = true, provider, notificationAdapter, userId }) => {
|
||||||
const currentJob =
|
const currentJob =
|
||||||
|
|||||||
@@ -5,12 +5,10 @@ import LowdashAdapter from './LowDashAdapter.js';
|
|||||||
|
|
||||||
const file = path.join(getDirName(), '../', 'db/jobListingData.json');
|
const file = path.join(getDirName(), '../', 'db/jobListingData.json');
|
||||||
const adapter = new JSONFileSync(file);
|
const adapter = new JSONFileSync(file);
|
||||||
const db = new LowdashAdapter(adapter);
|
const db = new LowdashAdapter(adapter, {});
|
||||||
|
|
||||||
db.read();
|
db.read();
|
||||||
|
|
||||||
db.data ||= {};
|
|
||||||
|
|
||||||
const buildKey = (jobKey, providerId, endpoint) => {
|
const buildKey = (jobKey, providerId, endpoint) => {
|
||||||
let key = `${jobKey}`;
|
let key = `${jobKey}`;
|
||||||
if (jobKey == null && endpoint == null) {
|
if (jobKey == null && endpoint == null) {
|
||||||
|
|||||||
@@ -6,24 +6,24 @@ import * as jobStorage from './jobStorage.js';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import LowdashAdapter from './LowDashAdapter.js';
|
import LowdashAdapter from './LowDashAdapter.js';
|
||||||
|
|
||||||
|
const defaultData = {
|
||||||
|
user: [
|
||||||
|
//you probably want to change the default password ;)
|
||||||
|
{
|
||||||
|
id: nanoid(),
|
||||||
|
lastLogin: Date.now(),
|
||||||
|
username: 'admin',
|
||||||
|
password: hasher.hash('admin'),
|
||||||
|
isAdmin: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const file = path.join(getDirName(), '../', 'db/users.json');
|
const file = path.join(getDirName(), '../', 'db/users.json');
|
||||||
const adapter = new JSONFileSync(file);
|
const adapter = new JSONFileSync(file);
|
||||||
const db = new LowdashAdapter(adapter);
|
const db = new LowdashAdapter(adapter, defaultData);
|
||||||
|
|
||||||
db.read();
|
db.read();
|
||||||
db.data ||= {
|
|
||||||
user: [
|
|
||||||
//you probably want to change the default password ;)
|
|
||||||
{
|
|
||||||
id: nanoid(),
|
|
||||||
lastLogin: Date.now(),
|
|
||||||
username: 'admin',
|
|
||||||
password: hasher.hash('admin'),
|
|
||||||
isAdmin: true,
|
|
||||||
isDemo: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUsers = (withPassword) => {
|
export const getUsers = (withPassword) => {
|
||||||
const jobs = jobStorage.getJobs();
|
const jobs = jobStorage.getJobs();
|
||||||
|
|||||||
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,
|
||||||
};
|
};
|
||||||
|
|||||||
74
package.json
74
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fredy",
|
"name": "fredy",
|
||||||
"version": "7.1.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",
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0",
|
"node": ">=20.0.0",
|
||||||
"npm": ">=7.0.0"
|
"npm": ">=7.0.0"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
@@ -55,54 +55,54 @@
|
|||||||
"Firefox ESR"
|
"Firefox ESR"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@douyinfe/semi-ui": "2.31.0",
|
"@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": "7.7.0",
|
"@sendgrid/mail": "8.1.4",
|
||||||
"@vitejs/plugin-react": "3.1.0",
|
"@vitejs/plugin-react": "4.3.3",
|
||||||
"better-sqlite3": "8.2.0",
|
"better-sqlite3": "^11.5.0",
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.3",
|
||||||
"cookie-session": "2.0.0",
|
"cookie-session": "2.1.0",
|
||||||
"handlebars": "4.7.7",
|
"handlebars": "4.7.8",
|
||||||
"highcharts": "10.3.3",
|
"highcharts": "11.4.8",
|
||||||
"highcharts-react-official": "3.2.0",
|
"highcharts-react-official": "3.2.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"lowdb": "5.1.0",
|
"lowdb": "6.0.1",
|
||||||
"markdown": "^0.5.0",
|
"markdown": "^0.5.0",
|
||||||
"nanoid": "4.0.1",
|
"nanoid": "5.0.8",
|
||||||
"node-fetch": "3.3.1",
|
"node-fetch": "3.3.2",
|
||||||
"node-mailjet": "6.0.2",
|
"node-mailjet": "6.0.6",
|
||||||
"query-string": "8.1.0",
|
"query-string": "9.1.1",
|
||||||
"react": "18.2.0",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.3.1",
|
||||||
"react-redux": "8.0.5",
|
"react-redux": "9.1.2",
|
||||||
"react-router": "5.2.1",
|
"react-router": "5.2.1",
|
||||||
"react-router-dom": "5.3.0",
|
"react-router-dom": "5.3.0",
|
||||||
"redux": "4.2.1",
|
"redux": "5.0.1",
|
||||||
"redux-thunk": "2.4.2",
|
"redux-thunk": "3.1.0",
|
||||||
"restana": "4.9.7",
|
"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": "4.2.0",
|
"vite": "5.4.10",
|
||||||
"x-ray": "2.3.4"
|
"x-ray": "2.3.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.21.3",
|
"@babel/core": "7.26.0",
|
||||||
"@babel/eslint-parser": "7.21.3",
|
"@babel/eslint-parser": "7.25.9",
|
||||||
"@babel/preset-env": "7.20.2",
|
"@babel/preset-env": "7.26.0",
|
||||||
"@babel/preset-react": "7.18.6",
|
"@babel/preset-react": "7.25.9",
|
||||||
"chai": "4.3.7",
|
"chai": "5.1.2",
|
||||||
"eslint": "8.36.0",
|
"eslint": "8.56.0",
|
||||||
"eslint-config-prettier": "8.7.0",
|
"eslint-config-prettier": "8.8.0",
|
||||||
"eslint-plugin-react": "7.32.2",
|
"eslint-plugin-react": "7.37.2",
|
||||||
"esmock": "2.1.0",
|
"esmock": "2.6.9",
|
||||||
"history": "5.3.0",
|
"history": "5.3.0",
|
||||||
"husky": "4.3.8",
|
"husky": "4.3.8",
|
||||||
"less": "4.1.3",
|
"less": "4.2.0",
|
||||||
"lint-staged": "13.2.0",
|
"lint-staged": "13.2.2",
|
||||||
"mocha": "10.2.0",
|
"mocha": "10.8.2",
|
||||||
"prettier": "2.8.5",
|
"prettier": "3.3.3",
|
||||||
"redux-logger": "3.0.6"
|
"redux-logger": "3.0.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
||||||
import { get } from '../mocks/mockNotification.js';
|
import { get } from '../mocks/mockNotification.js';
|
||||||
import { providerConfig, mockFredy } from '../utils.js';
|
import { providerConfig, mockFredy } from '../utils.js';
|
||||||
import chai from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as provider from '../../lib/provider/einsAImmobilien.js';
|
import * as provider from '../../lib/provider/einsAImmobilien.js';
|
||||||
|
|
||||||
const expect = chai.expect;
|
|
||||||
|
|
||||||
describe('#einsAImmobilien testsuite()', () => {
|
describe('#einsAImmobilien testsuite()', () => {
|
||||||
after(() => {
|
after(() => {
|
||||||
similarityCache.stopCacheCleanup();
|
similarityCache.stopCacheCleanup();
|
||||||
@@ -22,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');
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
||||||
import { get } from '../mocks/mockNotification.js';
|
import { get } from '../mocks/mockNotification.js';
|
||||||
import { providerConfig, mockFredy } from '../utils.js';
|
import { providerConfig, mockFredy } from '../utils.js';
|
||||||
import chai from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as provider from '../../lib/provider/immobilienDe.js';
|
import * as provider from '../../lib/provider/immobilienDe.js';
|
||||||
const expect = chai.expect;
|
|
||||||
describe('#immobilien.de testsuite()', () => {
|
describe('#immobilien.de testsuite()', () => {
|
||||||
after(() => {
|
after(() => {
|
||||||
similarityCache.stopCacheCleanup();
|
similarityCache.stopCacheCleanup();
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
||||||
import { get } from '../mocks/mockNotification.js';
|
import { get } from '../mocks/mockNotification.js';
|
||||||
import { mockFredy, providerConfig } from '../utils.js';
|
import { mockFredy, providerConfig } from '../utils.js';
|
||||||
import chai from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as provider from '../../lib/provider/immonet.js';
|
import * as provider from '../../lib/provider/immonet.js';
|
||||||
const expect = chai.expect;
|
import * as scrapingAnt from '../../lib/services/scrapingAnt.js';
|
||||||
|
|
||||||
describe('#immonet testsuite()', () => {
|
describe('#immonet testsuite()', () => {
|
||||||
after(() => {
|
after(() => {
|
||||||
similarityCache.stopCacheCleanup();
|
similarityCache.stopCacheCleanup();
|
||||||
@@ -12,6 +13,13 @@ describe('#immonet testsuite()', () => {
|
|||||||
it('should test immonet provider', async () => {
|
it('should test immonet provider', async () => {
|
||||||
const Fredy = await mockFredy();
|
const Fredy = await mockFredy();
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
|
if (!scrapingAnt.isScrapingAntApiKeySet()) {
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
console.info('Skipping Immonet test as ScrapingAnt Api Key is not set.');
|
||||||
|
/* eslint-enable no-console */
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'immonet', similarityCache);
|
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'immonet', similarityCache);
|
||||||
fredy.execute().then((listing) => {
|
fredy.execute().then((listing) => {
|
||||||
expect(listing).to.be.a('array');
|
expect(listing).to.be.a('array');
|
||||||
@@ -20,17 +28,17 @@ describe('#immonet testsuite()', () => {
|
|||||||
expect(notificationObj.serviceName).to.equal('immonet');
|
expect(notificationObj.serviceName).to.equal('immonet');
|
||||||
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');
|
||||||
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');
|
||||||
|
|
||||||
/** check the values if possible **/
|
/** check the values if possible **/
|
||||||
expect(notify.price).that.does.include('€');
|
expect(notify.price).that.does.include('€');
|
||||||
expect(notify.size).that.does.include('m²');
|
expect(notify.size).that.does.include('m²');
|
||||||
expect(notify.title).to.be.not.empty;
|
expect(notify.title).to.be.not.empty;
|
||||||
expect(notify.link).that.does.include('https://www.immonet.de');
|
|
||||||
expect(notify.address).to.be.not.empty;
|
expect(notify.address).to.be.not.empty;
|
||||||
});
|
});
|
||||||
resolve();
|
resolve();
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
||||||
import { get } from '../mocks/mockNotification.js';
|
import { get } from '../mocks/mockNotification.js';
|
||||||
import { mockFredy, providerConfig } from '../utils.js';
|
import { mockFredy, providerConfig } from '../utils.js';
|
||||||
import chai from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as provider from '../../lib/provider/immoscout.js';
|
import * as provider from '../../lib/provider/immoscout.js';
|
||||||
import * as scrapingAnt from '../../lib/services/scrapingAnt.js';
|
import * as scrapingAnt from '../../lib/services/scrapingAnt.js';
|
||||||
const expect = chai.expect;
|
|
||||||
describe('#immoscout testsuite()', () => {
|
describe('#immoscout testsuite()', () => {
|
||||||
after(() => {
|
after(() => {
|
||||||
similarityCache.stopCacheCleanup();
|
similarityCache.stopCacheCleanup();
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
||||||
import { get } from '../mocks/mockNotification.js';
|
import { get } from '../mocks/mockNotification.js';
|
||||||
import { mockFredy, providerConfig } from '../utils.js';
|
import { mockFredy, providerConfig } from '../utils.js';
|
||||||
import chai from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as provider from '../../lib/provider/immoswp.js';
|
import * as provider from '../../lib/provider/immoswp.js';
|
||||||
const expect = chai.expect;
|
|
||||||
describe('#immoswp testsuite()', () => {
|
describe('#immoswp testsuite()', () => {
|
||||||
after(() => {
|
after(() => {
|
||||||
similarityCache.stopCacheCleanup();
|
similarityCache.stopCacheCleanup();
|
||||||
@@ -25,12 +25,10 @@ describe('#immoswp testsuite()', () => {
|
|||||||
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');
|
||||||
expect(notify.link).to.be.a('string');
|
expect(notify.link).to.be.a('string');
|
||||||
expect(notify.address).to.be.a('string');
|
|
||||||
/** check the values if possible **/
|
/** check the values if possible **/
|
||||||
expect(notify.price).that.does.include('€');
|
expect(notify.price).that.does.include('€');
|
||||||
expect(notify.title).to.be.not.empty;
|
expect(notify.title).to.be.not.empty;
|
||||||
expect(notify.link).that.does.include('https://immo.swp.de');
|
expect(notify.link).that.does.include('https://immo.swp.de');
|
||||||
expect(notify.address).to.be.not.empty;
|
|
||||||
});
|
});
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
||||||
import { get } from '../mocks/mockNotification.js';
|
import { get } from '../mocks/mockNotification.js';
|
||||||
import { mockFredy, providerConfig } from '../utils.js';
|
import { mockFredy, providerConfig } from '../utils.js';
|
||||||
import chai from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as provider from '../../lib/provider/immowelt.js';
|
import * as provider from '../../lib/provider/immowelt.js';
|
||||||
const expect = chai.expect;
|
|
||||||
describe('#immowelt testsuite()', () => {
|
describe('#immowelt testsuite()', () => {
|
||||||
after(() => {
|
after(() => {
|
||||||
similarityCache.stopCacheCleanup();
|
similarityCache.stopCacheCleanup();
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
||||||
import { get } from '../mocks/mockNotification.js';
|
import { get } from '../mocks/mockNotification.js';
|
||||||
import { mockFredy, providerConfig } from '../utils.js';
|
import { mockFredy, providerConfig } from '../utils.js';
|
||||||
import chai from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as provider from '../../lib/provider/kleinanzeigen.js';
|
import * as provider from '../../lib/provider/kleinanzeigen.js';
|
||||||
const expect = chai.expect;
|
|
||||||
describe('#kleinanzeigen testsuite()', () => {
|
describe('#kleinanzeigen testsuite()', () => {
|
||||||
after(() => {
|
after(() => {
|
||||||
similarityCache.stopCacheCleanup();
|
similarityCache.stopCacheCleanup();
|
||||||
@@ -20,13 +20,13 @@ 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');
|
||||||
/** check the values if possible **/
|
/** check the values if possible **/
|
||||||
expect(notify.title).to.be.not.empty;
|
expect(notify.title).to.be.not.empty;
|
||||||
expect(notify.link).that.does.include('https://www.ebay-kleinanzeigen.de');
|
expect(notify.link).that.does.include('https://www.kleinanzeigen.de');
|
||||||
expect(notify.address).to.be.not.empty;
|
expect(notify.address).to.be.not.empty;
|
||||||
});
|
});
|
||||||
resolve();
|
resolve();
|
||||||
|
|||||||
@@ -1,36 +1,44 @@
|
|||||||
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
||||||
import { get } from '../mocks/mockNotification.js';
|
import {get} from '../mocks/mockNotification.js';
|
||||||
import { mockFredy, providerConfig } from '../utils.js';
|
import {mockFredy, providerConfig} from '../utils.js';
|
||||||
import chai from 'chai';
|
import {expect} from 'chai';
|
||||||
import * as provider from '../../lib/provider/neubauKompass.js';
|
import * as provider from '../../lib/provider/neubauKompass.js';
|
||||||
const expect = chai.expect;
|
import * as scrapingAnt from '../../lib/services/scrapingAnt.js';
|
||||||
|
|
||||||
describe('#neubauKompass testsuite()', () => {
|
describe('#neubauKompass testsuite()', () => {
|
||||||
after(() => {
|
after(() => {
|
||||||
similarityCache.stopCacheCleanup();
|
similarityCache.stopCacheCleanup();
|
||||||
});
|
});
|
||||||
provider.init(providerConfig.neubauKompass, [], []);
|
provider.init(providerConfig.neubauKompass, [], []);
|
||||||
it('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) => {
|
||||||
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'neubauKompass', similarityCache);
|
if (!scrapingAnt.isScrapingAntApiKeySet()) {
|
||||||
fredy.execute().then((listing) => {
|
/* eslint-disable no-console */
|
||||||
expect(listing).to.be.a('array');
|
console.info('Skipping Neubaukompass test as ScrapingAnt Api Key is not set.');
|
||||||
const notificationObj = get();
|
/* eslint-enable no-console */
|
||||||
expect(notificationObj.serviceName).to.equal('neubauKompass');
|
resolve();
|
||||||
notificationObj.payload.forEach((notify) => {
|
return;
|
||||||
expect(notify).to.be.a('object');
|
}
|
||||||
/** check the actual structure **/
|
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'neubauKompass', similarityCache);
|
||||||
expect(notify.id).to.be.a('string');
|
fredy.execute().then((listing) => {
|
||||||
expect(notify.title).to.be.a('string');
|
expect(listing).to.be.a('array');
|
||||||
expect(notify.link).to.be.a('string');
|
const notificationObj = get();
|
||||||
expect(notify.address).to.be.a('string');
|
expect(notificationObj.serviceName).to.equal('neubauKompass');
|
||||||
/** check the values if possible **/
|
notificationObj.payload.forEach((notify) => {
|
||||||
expect(notify.title).to.be.not.empty;
|
expect(notify).to.be.a('object');
|
||||||
expect(notify.link).that.does.include('https://www.neubaukompass.de');
|
/** check the actual structure **/
|
||||||
expect(notify.address).to.be.not.empty;
|
expect(notify.id).to.be.a('string');
|
||||||
});
|
expect(notify.title).to.be.a('string');
|
||||||
resolve();
|
expect(notify.link).to.be.a('string');
|
||||||
});
|
expect(notify.address).to.be.a('string');
|
||||||
|
/** check the values if possible **/
|
||||||
|
expect(notify.title).to.be.not.empty;
|
||||||
|
expect(notify.link).that.does.include('https://www.neubaukompass.de');
|
||||||
|
expect(notify.address).to.be.not.empty;
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,11 +9,11 @@
|
|||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"immonet": {
|
"immonet": {
|
||||||
"url": "https://www.immonet.de/immobiliensuche/sel.do?pageoffset=1&listsize=100&objecttype=1&locationname=Düsseldorf&acid=&actype=&district=8717&district=8718&district=8719&district=8720&district=8721&district=8723&district=8724&district=8725&district=8727&district=8728&district=8729&district=8730&district=8731&district=8732&district=8733&district=8737&district=8738&district=8741&district=8745&district=8747&district=8750&district=8752&district=8754&district=8755&district=8756&district=8759&district=8760&district=8761&district=8763&district=8764&district=8765&ajaxIsRadiusActive=false&sortby=19&suchart=1&radius=0&pcatmtypes=1_1&pCatMTypeStoragefield=&parentcat=1&marketingtype=1&fromprice=&toprice=420000&fromarea=90&toarea=&fromplotarea=&toplotarea=&fromrooms=3&torooms=&objectcat=225&objectcat=18&objectcat=17&objectcat=12&objectcat=16&objectcat=181&objectcat=14&objectcat=15&objectcat=226&objectcat=13&wbs=-1&fromyear=&toyear=",
|
"url": "https://www.immonet.de/immobiliensuche/beta?pageoffset=1&listsize=100&objecttype=1&locationname=D%C3%BCsseldorf&acid=&actype=&district=8717&district=8718&district=8719&district=8720&district=8721&district=8723&district=8724&district=8725&district=8727&district=8728&district=8729&district=8730&district=8731&district=8732&district=8733&district=8737&district=8738&district=8741&district=8745&district=8747&district=8750&district=8752&district=8754&district=8755&district=8756&district=8759&district=8760&district=8761&district=8763&district=8764&district=8765&ajaxIsRadiusActive=false&sortby=19&suchart=1&radius=0&pcatmtypes=1_1&pCatMTypeStoragefield=&parentcat=1&marketingtype=1&fromprice=&toprice=420000&fromarea=90&toarea=&fromplotarea=&toplotarea=&fromrooms=3&torooms=&objectcat=225&objectcat=18&objectcat=17&objectcat=12&objectcat=16&objectcat=181&objectcat=14&objectcat=15&objectcat=226&objectcat=13&wbs=-1&fromyear=&toyear=",
|
||||||
"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": {
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"kleinanzeigen": {
|
"kleinanzeigen": {
|
||||||
"url": "https://www.ebay-kleinanzeigen.de/s-immobilien/duesseldorf/anzeige:angebote/wohnung/k0c195l2068r5",
|
"url": "https://www.kleinanzeigen.de/s-immobilien/duesseldorf/anzeige:angebote/wohnung/k0c195l2068r5",
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"neubauKompass": {
|
"neubauKompass": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import utils from '../../lib/utils.js';
|
import utils from '../../lib/utils.js';
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import chai from 'chai';
|
import { expect } from 'chai';
|
||||||
const expect = chai.expect;
|
|
||||||
const fakeWorkingHoursConfig = (from, to) => ({
|
const fakeWorkingHoursConfig = (from, to) => ({
|
||||||
workingHours: {
|
workingHours: {
|
||||||
to,
|
to,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
||||||
import { get } from '../mocks/mockNotification.js';
|
import { get } from '../mocks/mockNotification.js';
|
||||||
import { mockFredy, providerConfig } from '../utils.js';
|
import { mockFredy, providerConfig } from '../utils.js';
|
||||||
import chai from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as provider from '../../lib/provider/wgGesucht.js';
|
import * as provider from '../../lib/provider/wgGesucht.js';
|
||||||
const expect = chai.expect;
|
|
||||||
describe('#wgGesucht testsuite()', () => {
|
describe('#wgGesucht testsuite()', () => {
|
||||||
after(() => {
|
after(() => {
|
||||||
similarityCache.stopCacheCleanup();
|
similarityCache.stopCacheCleanup();
|
||||||
@@ -23,7 +23,6 @@ describe('#wgGesucht testsuite()', () => {
|
|||||||
expect(notify.id).to.be.a('string');
|
expect(notify.id).to.be.a('string');
|
||||||
expect(notify.title).to.be.a('string');
|
expect(notify.title).to.be.a('string');
|
||||||
expect(notify.details).to.be.a('string');
|
expect(notify.details).to.be.a('string');
|
||||||
expect(notify.size).to.be.a('string');
|
|
||||||
expect(notify.price).to.be.a('string');
|
expect(notify.price).to.be.a('string');
|
||||||
expect(notify.link).to.be.a('string');
|
expect(notify.link).to.be.a('string');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import chai from 'chai';
|
import { expect } from 'chai';
|
||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
import mutator from '../../lib/services/queryStringMutator.js';
|
import mutator from '../../lib/services/queryStringMutator.js';
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
const expect = chai.expect;
|
|
||||||
|
|
||||||
const data = await readFile(new URL('./testData.json', import.meta.url));
|
const data = await readFile(new URL('./testData.json', import.meta.url));
|
||||||
|
|
||||||
const testData = JSON.parse(data);
|
const testData = JSON.parse(data);
|
||||||
|
|
||||||
let _provider = await Promise.all(
|
let _provider = await Promise.all(
|
||||||
fs.readdirSync('./lib/provider/').map(async (integPath) => await import(`../../lib/provider/${integPath}`))
|
fs.readdirSync('./lib/provider/').map(async (integPath) => await import(`../../lib/provider/${integPath}`)),
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import SimilarityCacheEntry from '../../lib/services/similarity-check/SimilarityCacheEntry.js';
|
import SimilarityCacheEntry from '../../lib/services/similarity-check/SimilarityCacheEntry.js';
|
||||||
import chai from 'chai';
|
import { expect } from 'chai';
|
||||||
const expect = chai.expect;
|
|
||||||
describe('similarityCheck', () => {
|
describe('similarityCheck', () => {
|
||||||
describe('#similarityCheck()', () => {
|
describe('#similarityCheck()', () => {
|
||||||
it('should be false', () => {
|
it('should be false', () => {
|
||||||
@@ -29,10 +29,10 @@ describe('similarityCheck', () => {
|
|||||||
it('should be false', () => {
|
it('should be false', () => {
|
||||||
const check = new SimilarityCacheEntry(0);
|
const check = new SimilarityCacheEntry(0);
|
||||||
check.setCacheEntry(
|
check.setCacheEntry(
|
||||||
'The index is known by several other names, especially Sørensen–Dice index,[3] Sørensen index and Dice\'s coefficient. Other variations include the "similarity coefficient" or "index", such as Dice similarity coefficient (DSC). Common alternate spellings for Sørensen are Sorenson, Soerenson and Sörenson, and all three can also be seen with the –sen ending.'
|
'The index is known by several other names, especially Sørensen–Dice index,[3] Sørensen index and Dice\'s coefficient. Other variations include the "similarity coefficient" or "index", such as Dice similarity coefficient (DSC). Common alternate spellings for Sørensen are Sorenson, Soerenson and Sörenson, and all three can also be seen with the –sen ending.',
|
||||||
);
|
);
|
||||||
check.setCacheEntry(
|
check.setCacheEntry(
|
||||||
'where |X| and |Y| are the cardinalities of the two sets (i.e. the number of elements in each set). The Sørensen index equals twice the number of elements common to both sets divided by the sum of the number of elements in each set.'
|
'where |X| and |Y| are the cardinalities of the two sets (i.e. the number of elements in each set). The Sørensen index equals twice the number of elements common to both sets divided by the sum of the number of elements in each set.',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -184,8 +184,7 @@ const GeneralSettings = function GeneralSettings() {
|
|||||||
more likely to fail, but they are cheaper. A call with a datacenter proxy cost 10 credits.
|
more likely to fail, but they are cheaper. A call with a datacenter proxy cost 10 credits.
|
||||||
<h4>Residential-Proxy</h4>
|
<h4>Residential-Proxy</h4>
|
||||||
High-quality proxy server located in one of the real people houses across the world. Datacenter
|
High-quality proxy server located in one of the real people houses across the world. Datacenter
|
||||||
proxies are faster and more likely to success, but they are more expensive. A call with a datacenter
|
proxies are faster and more likely to success, but they are more expensive.
|
||||||
proxy cost 250 credits.
|
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<b>
|
<b>
|
||||||
|
|||||||
@@ -45,9 +45,9 @@ export default function ProcessingTimes({ processingTimes }) {
|
|||||||
{format(new Date(processingTimes.scrapingAntData.end_date))}
|
{format(new Date(processingTimes.scrapingAntData.end_date))}
|
||||||
<br />
|
<br />
|
||||||
Credits: {processingTimes.scrapingAntData.remained_credits}/
|
Credits: {processingTimes.scrapingAntData.remained_credits}/
|
||||||
{processingTimes.scrapingAntData.plan_total_credits} (250 credits per call)
|
{processingTimes.scrapingAntData.plan_total_credits}
|
||||||
</p>
|
</p>
|
||||||
If you want to scrape Immoscout more often, you have to purchase a premium account of{' '}
|
If you want to scrape Immoscout or Immonet more often, you have to purchase a premium account of{' '}
|
||||||
<a href="https://scrapingant.com/" target="_blank" rel="noreferrer">
|
<a href="https://scrapingant.com/" target="_blank" rel="noreferrer">
|
||||||
ScrapingAnt
|
ScrapingAnt
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -25,9 +25,12 @@ const validate = (selectedAdapter) => {
|
|||||||
results.push('All fields are mandatory and must be set.');
|
results.push('All fields are mandatory and must be set.');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (uiElement.type === 'number' && (typeof uiElement.value !== 'number' || uiElement.value < 0)) {
|
if (uiElement.type === 'number') {
|
||||||
results.push('A number field cannot contain anything else and must be > 0.');
|
const numberValue = parseFloat(uiElement.value);
|
||||||
continue;
|
if(isNaN(numberValue) || numberValue < 0) {
|
||||||
|
results.push('A number field cannot contain anything else and must be > 0.');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (uiElement.type === 'boolean' && typeof uiElement.value !== 'boolean') {
|
if (uiElement.type === 'boolean' && typeof uiElement.value !== 'boolean') {
|
||||||
results.push('A boolean field cannot be of a different type.');
|
results.push('A boolean field cannot be of a different type.');
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export default function ProviderMutator({ onVisibilityChanged, visible = false,
|
|||||||
description={
|
description={
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
If you chose Immoscout as a provider, make sure to also add the scrapingAnt apiKey to the config.json.
|
If you chose Immoscout, Immonet or NeubauKompass as a provider, make sure to also add the scrapingAnt apiKey to the config.json.
|
||||||
(See readme)
|
(See readme)
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
Reference in New Issue
Block a user